global: snapshot

This commit is contained in:
nym21
2026-03-10 13:00:05 +01:00
parent b88f4762a5
commit a3238304f5
14 changed files with 358 additions and 138 deletions

View File

@@ -1,6 +1,6 @@
use brk_error::Result;
use brk_types::{Cents, Indexes};
use vecdb::Exit;
use vecdb::{Exit, VecIndex};
use super::super::{activity, cap, supply};
use super::Vecs;
@@ -22,6 +22,7 @@ impl Vecs {
let all_metrics = &distribution.utxo_cohorts.all.metrics;
let circulating_supply = &all_metrics.supply.total.btc.height;
let realized_price = &all_metrics.realized.price.cents.height;
let realized_cap = &all_metrics.realized.cap.cents.height;
self.vaulted_price.cents.height.compute_transform2(
starting_indexes.height,
@@ -93,6 +94,104 @@ impl Vecs {
&self.cointime_price.cents.height,
)?;
// transfer_price = cointime_price - vaulted_price
self.transfer_price.cents.height.compute_transform2(
starting_indexes.height,
&self.cointime_price.cents.height,
&self.vaulted_price.cents.height,
|(i, cointime, vaulted, ..)| {
(i, cointime.saturating_sub(vaulted))
},
exit,
)?;
self.transfer_price_ratio.compute_rest(
blocks,
prices,
starting_indexes,
exit,
&self.transfer_price.cents.height,
)?;
// balanced_price = (realized_price + transfer_price) / 2
self.balanced_price.cents.height.compute_transform2(
starting_indexes.height,
realized_price,
&self.transfer_price.cents.height,
|(i, realized, transfer, ..)| {
(i, (realized + transfer) / 2u64)
},
exit,
)?;
self.balanced_price_ratio.compute_rest(
blocks,
prices,
starting_indexes,
exit,
&self.balanced_price.cents.height,
)?;
// terminal_price = 21M × transfer_price / circulating_supply_btc
self.terminal_price.cents.height.compute_transform2(
starting_indexes.height,
&self.transfer_price.cents.height,
circulating_supply,
|(i, transfer, supply_btc, ..)| {
let supply = f64::from(supply_btc);
if supply == 0.0 {
(i, Cents::ZERO)
} else {
(i, Cents::from(f64::from(transfer) * 21_000_000.0 / supply))
}
},
exit,
)?;
self.terminal_price_ratio.compute_rest(
blocks,
prices,
starting_indexes,
exit,
&self.terminal_price.cents.height,
)?;
// cumulative_market_cap = Σ(market_cap) in dollars
self.cumulative_market_cap
.height
.compute_cumulative(
starting_indexes.height,
&all_metrics.supply.total.cents.height,
exit,
)?;
// delta_price = (realized_cap - average_cap) / circulating_supply
// average_cap = cumulative_market_cap / (height + 1)
self.delta_price.cents.height.compute_transform3(
starting_indexes.height,
realized_cap,
&self.cumulative_market_cap.height,
circulating_supply,
|(i, realized_cap_cents, cum_mcap_dollars, supply_btc, ..)| {
let supply = f64::from(supply_btc);
if supply == 0.0 {
return (i, Cents::ZERO);
}
let avg_cap_cents = f64::from(cum_mcap_dollars) * 100.0 / (i.to_usize() + 1) as f64;
let delta = (f64::from(realized_cap_cents) - avg_cap_cents) / supply;
(i, Cents::from(delta.max(0.0)))
},
exit,
)?;
self.delta_price_ratio.compute_rest(
blocks,
prices,
starting_indexes,
exit,
&self.delta_price.cents.height,
)?;
Ok(())
}
}

View File

@@ -5,7 +5,7 @@ use vecdb::Database;
use super::Vecs;
use crate::{
indexes,
internal::{RatioPerBlockExtended, Price},
internal::{ComputedPerBlock, RatioPerBlockExtended, Price},
};
impl Vecs {
@@ -34,6 +34,25 @@ impl Vecs {
let cointime_price_ratio =
RatioPerBlockExtended::forced_import(db, "cointime_price", version, indexes)?;
let transfer_price = Price::forced_import(db, "transfer_price", version, indexes)?;
let transfer_price_ratio =
RatioPerBlockExtended::forced_import(db, "transfer_price", version, indexes)?;
let balanced_price = Price::forced_import(db, "balanced_price", version, indexes)?;
let balanced_price_ratio =
RatioPerBlockExtended::forced_import(db, "balanced_price", version, indexes)?;
let terminal_price = Price::forced_import(db, "terminal_price", version, indexes)?;
let terminal_price_ratio =
RatioPerBlockExtended::forced_import(db, "terminal_price", version, indexes)?;
let delta_price = Price::forced_import(db, "delta_price", version, indexes)?;
let delta_price_ratio =
RatioPerBlockExtended::forced_import(db, "delta_price", version, indexes)?;
let cumulative_market_cap =
ComputedPerBlock::forced_import(db, "cumulative_market_cap", version, indexes)?;
Ok(Self {
vaulted_price,
vaulted_price_ratio,
@@ -43,6 +62,15 @@ impl Vecs {
true_market_mean_ratio,
cointime_price,
cointime_price_ratio,
transfer_price,
transfer_price_ratio,
balanced_price,
balanced_price_ratio,
terminal_price,
terminal_price_ratio,
delta_price,
delta_price_ratio,
cumulative_market_cap,
})
}
}

View File

@@ -1,5 +1,5 @@
use brk_traversable::Traversable;
use brk_types::Cents;
use brk_types::{Cents, Dollars};
use vecdb::{Rw, StorageMode};
use crate::internal::{ComputedPerBlock, RatioPerBlockExtended, Price};
@@ -14,4 +14,14 @@ pub struct Vecs<M: StorageMode = Rw> {
pub true_market_mean_ratio: RatioPerBlockExtended<M>,
pub cointime_price: Price<ComputedPerBlock<Cents, M>>,
pub cointime_price_ratio: RatioPerBlockExtended<M>,
pub transfer_price: Price<ComputedPerBlock<Cents, M>>,
pub transfer_price_ratio: RatioPerBlockExtended<M>,
pub balanced_price: Price<ComputedPerBlock<Cents, M>>,
pub balanced_price_ratio: RatioPerBlockExtended<M>,
pub terminal_price: Price<ComputedPerBlock<Cents, M>>,
pub terminal_price_ratio: RatioPerBlockExtended<M>,
pub delta_price: Price<ComputedPerBlock<Cents, M>>,
pub delta_price_ratio: RatioPerBlockExtended<M>,
pub cumulative_market_cap: ComputedPerBlock<Dollars, M>,
}

View File

@@ -66,7 +66,7 @@ use brk_error::Result;
use brk_types::{Cents, Height, Indexes, Version};
use vecdb::{AnyStoredVec, Exit, StorageMode};
use crate::{blocks, distribution::state::{CohortState, CostBasisData, CostBasisOps, CostBasisRaw, CoreRealizedState, MinimalRealizedState, RealizedOps, RealizedState}, prices};
use crate::{blocks, distribution::state::{WithoutCapital, WithCapital, CohortState, CostBasisData, CostBasisOps, CostBasisRaw, CoreRealizedState, MinimalRealizedState, RealizedOps, RealizedState}, prices};
pub trait CohortMetricsState {
type Realized: RealizedOps;
@@ -75,7 +75,7 @@ pub trait CohortMetricsState {
impl<M: StorageMode> CohortMetricsState for TypeCohortMetrics<M> {
type Realized = MinimalRealizedState;
type CostBasis = CostBasisData;
type CostBasis = CostBasisData<WithoutCapital>;
}
impl<M: StorageMode> CohortMetricsState for MinimalCohortMetrics<M> {
type Realized = MinimalRealizedState;
@@ -83,26 +83,26 @@ impl<M: StorageMode> CohortMetricsState for MinimalCohortMetrics<M> {
}
impl<M: StorageMode> CohortMetricsState for CoreCohortMetrics<M> {
type Realized = CoreRealizedState;
type CostBasis = CostBasisData;
type CostBasis = CostBasisData<WithoutCapital>;
}
impl<M: StorageMode> CohortMetricsState for BasicCohortMetrics<M> {
type Realized = RealizedState;
type CostBasis = CostBasisData;
type CostBasis = CostBasisData<WithCapital>;
}
impl<M: StorageMode> CohortMetricsState for ExtendedCohortMetrics<M> {
type Realized = RealizedState;
type CostBasis = CostBasisData;
type CostBasis = CostBasisData<WithCapital>;
}
impl<M: StorageMode> CohortMetricsState for ExtendedAdjustedCohortMetrics<M> {
type Realized = RealizedState;
type CostBasis = CostBasisData;
type CostBasis = CostBasisData<WithCapital>;
}
impl<M: StorageMode> CohortMetricsState for AllCohortMetrics<M> {
type Realized = RealizedState;
type CostBasis = CostBasisData;
type CostBasis = CostBasisData<WithCapital>;
}
pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState, CostBasis = CostBasisData> + Send + Sync {
pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState, CostBasis = CostBasisData<WithCapital>> + Send + Sync {
type ActivityVecs: ActivityLike;
type RealizedVecs: RealizedLike;
type UnrealizedVecs: UnrealizedLike;
@@ -142,7 +142,7 @@ pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState, CostBa
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState<RealizedState, CostBasisData>,
state: &mut CohortState<RealizedState, CostBasisData<WithCapital>>,
) -> Result<()> {
state.apply_pending();
let unrealized_state = state.compute_unrealized_state(height_price);
@@ -162,7 +162,7 @@ pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState, CostBa
.min(self.unrealized().min_stateful_height_len())
}
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData>) -> Result<()> {
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData<WithCapital>>) -> Result<()> {
self.supply_mut().truncate_push(height, state)?;
self.outputs_mut().truncate_push(height, state)?;
self.activity_mut().truncate_push(height, state)?;

View File

@@ -12,7 +12,7 @@ use vecdb::{
use crate::{
blocks,
distribution::state::{CohortState, CostBasisData, RealizedState},
distribution::state::{WithCapital, CohortState, CostBasisData, RealizedState},
internal::{
CentsUnsignedToDollars, ComputedPerBlock, ComputedPerBlockCumulative, FiatPerBlock,
FiatRollingDelta1m, FiatRollingDeltaExcept1m, LazyPerBlock, PercentPerBlock,
@@ -295,7 +295,7 @@ impl RealizedFull {
pub(crate) fn truncate_push(
&mut self,
height: Height,
state: &CohortState<RealizedState, CostBasisData>,
state: &CohortState<RealizedState, CostBasisData<WithCapital>>,
) -> Result<()> {
self.core.truncate_push(height, state)?;
self.profit

View File

@@ -12,7 +12,7 @@ use brk_error::Result;
use brk_types::{Height, Indexes};
use vecdb::Exit;
use crate::{blocks, distribution::state::{CohortState, CostBasisData, RealizedState}};
use crate::{blocks, distribution::state::{WithCapital, CohortState, CostBasisData, RealizedState}};
/// Polymorphic dispatch for realized metric types.
///
@@ -23,7 +23,7 @@ pub trait RealizedLike: Send + Sync {
fn as_core(&self) -> &RealizedCore;
fn as_core_mut(&mut self) -> &mut RealizedCore;
fn min_stateful_height_len(&self) -> usize;
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData>) -> Result<()>;
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData<WithCapital>>) -> Result<()>;
fn compute_rest_part1(&mut self, blocks: &blocks::Vecs, starting_indexes: &Indexes, exit: &Exit) -> Result<()>;
fn compute_from_stateful(
&mut self,
@@ -37,7 +37,7 @@ impl RealizedLike for RealizedCore {
fn as_core(&self) -> &RealizedCore { self }
fn as_core_mut(&mut self) -> &mut RealizedCore { self }
fn min_stateful_height_len(&self) -> usize { self.min_stateful_height_len() }
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData>) -> Result<()> {
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData<WithCapital>>) -> Result<()> {
self.truncate_push(height, state)
}
fn compute_rest_part1(&mut self, blocks: &blocks::Vecs, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
@@ -52,7 +52,7 @@ impl RealizedLike for RealizedFull {
fn as_core(&self) -> &RealizedCore { &self.core }
fn as_core_mut(&mut self) -> &mut RealizedCore { &mut self.core }
fn min_stateful_height_len(&self) -> usize { self.min_stateful_height_len() }
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData>) -> Result<()> {
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData<WithCapital>>) -> Result<()> {
self.truncate_push(height, state)
}
fn compute_rest_part1(&mut self, blocks: &blocks::Vecs, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use brk_error::Result;
use brk_types::{Age, Cents, CentsCompact, CentsSats, CentsSquaredSats, CostBasisSnapshot, Height, Sats, SupplyState};
use super::super::cost_basis::{CostBasisData, CostBasisOps, PendingDelta, RealizedOps, UnrealizedState};
use super::super::cost_basis::{Accumulate, CostBasisData, CostBasisOps, PendingDelta, RealizedOps, UnrealizedState};
pub struct SendPrecomputed {
pub sats: Sats,
@@ -282,8 +282,8 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
}
}
/// Methods only available with full CostBasisData (map + unrealized).
impl<R: RealizedOps> CohortState<R, CostBasisData> {
/// Methods only available with CostBasisData (map + unrealized).
impl<R: RealizedOps, S: Accumulate> CohortState<R, CostBasisData<S>> {
pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState {
self.cost_basis.compute_unrealized_state(height_price)
}

View File

@@ -11,7 +11,7 @@ use brk_types::{
use rustc_hash::FxHashMap;
use vecdb::{Bytes, unlikely};
use super::{CachedUnrealizedState, UnrealizedState};
use super::{Accumulate, CachedUnrealizedState, UnrealizedState};
/// Type alias for the price-to-sats map used in cost basis data.
pub(super) type CostBasisMap = BTreeMap<CentsCompact, Sats>;
@@ -261,19 +261,21 @@ impl CostBasisOps for CostBasisRaw {
/// Full cost basis tracking: BTreeMap distribution + raw scalars.
/// Composes `CostBasisRaw` for scalar tracking, adds map, pending, and cache.
/// Used by cohorts that need unrealized computation or Fenwick tree.
///
/// Generic over the accumulator `S`:
/// - `CachedStateRaw`: tracks all fields including invested capital + investor cap (128 bytes)
/// - `CachedStateCore`: tracks only supply + unrealized profit/loss (64 bytes, 1 cache line)
#[derive(Clone, Debug)]
pub struct CostBasisData {
pub struct CostBasisData<S: Accumulate> {
raw: CostBasisRaw,
map: Option<CostBasisDistribution>,
pending: FxHashMap<CentsCompact, PendingDelta>,
cache: Option<CachedUnrealizedState>,
cache: Option<CachedUnrealizedState<S>>,
rounding_digits: Option<i32>,
/// Monotonically increasing counter, bumped on each apply_pending with actual changes.
generation: u64,
}
impl CostBasisData {
impl<S: Accumulate> CostBasisData<S> {
#[inline]
fn round_price(&self, price: Cents) -> Cents {
match self.rounding_digits {
@@ -364,7 +366,7 @@ impl CostBasisData {
}
}
impl CostBasisOps for CostBasisData {
impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
fn create(path: &Path, name: &str) -> Self {
Self {
raw: CostBasisRaw::create(path, name),

View File

@@ -6,5 +6,7 @@ pub use data::*;
pub use realized::*;
pub use unrealized::UnrealizedState;
pub(crate) use unrealized::{Accumulate, WithoutCapital, WithCapital};
// Internal use only
pub(super) use unrealized::CachedUnrealizedState;

View File

@@ -37,63 +37,145 @@ impl UnrealizedState {
};
}
/// Internal cache state using u128 for raw cent*sat values.
/// This avoids rounding errors from premature division by ONE_BTC.
/// Division happens only when converting to UnrealizedState output.
/// Core cache state: supply + unrealized profit/loss only (64 bytes, 1 cache line).
#[derive(Debug, Default, Clone)]
struct CachedStateRaw {
pub struct WithoutCapital {
supply_in_profit: Sats,
supply_in_loss: Sats,
/// Raw value: sum of (price_cents * sats) for UTXOs in profit
unrealized_profit: u128,
/// Raw value: sum of (price_cents * sats) for UTXOs in loss
unrealized_loss: u128,
/// Raw value: sum of (price_cents * sats) for UTXOs in profit
}
/// Full cache state: core + invested capital + investor cap (128 bytes, 2 cache lines).
#[derive(Debug, Default, Clone)]
pub struct WithCapital {
core: WithoutCapital,
invested_capital_in_profit: u128,
/// Raw value: sum of (price_cents * sats) for UTXOs in loss
invested_capital_in_loss: u128,
/// Raw value: sum of (price_cents² * sats) for UTXOs in profit
investor_cap_in_profit: u128,
/// Raw value: sum of (price_cents² * sats) for UTXOs in loss
investor_cap_in_loss: u128,
}
impl CachedStateRaw {
/// Convert raw values to final output by dividing by ONE_BTC.
impl WithCapital {
fn to_output(&self) -> UnrealizedState {
#[inline(always)]
fn div_btc(raw: u128) -> Cents {
if raw == 0 {
Cents::ZERO
} else {
Cents::new((raw / Sats::ONE_BTC_U128) as u64)
}
}
let base = self.core.to_output();
UnrealizedState {
supply_in_profit: self.supply_in_profit,
supply_in_loss: self.supply_in_loss,
unrealized_profit: div_btc(self.unrealized_profit),
unrealized_loss: div_btc(self.unrealized_loss),
invested_capital_in_profit: div_btc(self.invested_capital_in_profit),
invested_capital_in_loss: div_btc(self.invested_capital_in_loss),
investor_cap_in_profit_raw: self.investor_cap_in_profit,
investor_cap_in_loss_raw: self.investor_cap_in_loss,
invested_capital_in_profit_raw: self.invested_capital_in_profit,
invested_capital_in_loss_raw: self.invested_capital_in_loss,
..base
}
}
}
#[inline(always)]
fn div_btc(raw: u128) -> Cents {
if raw == 0 {
Cents::ZERO
} else {
Cents::new((raw / Sats::ONE_BTC_U128) as u64)
}
}
impl WithoutCapital {
fn to_output(&self) -> UnrealizedState {
UnrealizedState {
supply_in_profit: self.supply_in_profit,
supply_in_loss: self.supply_in_loss,
unrealized_profit: div_btc(self.unrealized_profit),
unrealized_loss: div_btc(self.unrealized_loss),
..UnrealizedState::ZERO
}
}
}
/// Trait for accumulating profit/loss across BTreeMap entries.
/// `WithoutCapital` skips capital tracking; `WithCapital` tracks all fields.
pub trait Accumulate: Default + Clone + Send + Sync + 'static {
fn to_output(&self) -> UnrealizedState;
fn supply_in_profit(&self) -> &Sats;
fn supply_in_loss(&self) -> &Sats;
fn unrealized_profit(&mut self) -> &mut u128;
fn unrealized_loss(&mut self) -> &mut u128;
fn accumulate_profit(&mut self, price_u128: u128, invested_capital: u128, sats: Sats);
fn accumulate_loss(&mut self, price_u128: u128, invested_capital: u128, sats: Sats);
fn deaccumulate_profit(&mut self, price_u128: u128, invested_capital: u128, sats: Sats);
fn deaccumulate_loss(&mut self, price_u128: u128, invested_capital: u128, sats: Sats);
}
impl Accumulate for WithoutCapital {
fn to_output(&self) -> UnrealizedState { self.to_output() }
fn supply_in_profit(&self) -> &Sats { &self.supply_in_profit }
fn supply_in_loss(&self) -> &Sats { &self.supply_in_loss }
fn unrealized_profit(&mut self) -> &mut u128 { &mut self.unrealized_profit }
fn unrealized_loss(&mut self) -> &mut u128 { &mut self.unrealized_loss }
#[inline(always)]
fn accumulate_profit(&mut self, _price_u128: u128, _invested_capital: u128, sats: Sats) {
self.supply_in_profit += sats;
}
#[inline(always)]
fn accumulate_loss(&mut self, _price_u128: u128, _invested_capital: u128, sats: Sats) {
self.supply_in_loss += sats;
}
#[inline(always)]
fn deaccumulate_profit(&mut self, _price_u128: u128, _invested_capital: u128, sats: Sats) {
self.supply_in_profit -= sats;
}
#[inline(always)]
fn deaccumulate_loss(&mut self, _price_u128: u128, _invested_capital: u128, sats: Sats) {
self.supply_in_loss -= sats;
}
}
impl Accumulate for WithCapital {
fn to_output(&self) -> UnrealizedState { self.to_output() }
fn supply_in_profit(&self) -> &Sats { &self.core.supply_in_profit }
fn supply_in_loss(&self) -> &Sats { &self.core.supply_in_loss }
fn unrealized_profit(&mut self) -> &mut u128 { &mut self.core.unrealized_profit }
fn unrealized_loss(&mut self) -> &mut u128 { &mut self.core.unrealized_loss }
#[inline(always)]
fn accumulate_profit(&mut self, price_u128: u128, invested_capital: u128, sats: Sats) {
self.core.supply_in_profit += sats;
self.invested_capital_in_profit += invested_capital;
self.investor_cap_in_profit += price_u128 * invested_capital;
}
#[inline(always)]
fn accumulate_loss(&mut self, price_u128: u128, invested_capital: u128, sats: Sats) {
self.core.supply_in_loss += sats;
self.invested_capital_in_loss += invested_capital;
self.investor_cap_in_loss += price_u128 * invested_capital;
}
#[inline(always)]
fn deaccumulate_profit(&mut self, price_u128: u128, invested_capital: u128, sats: Sats) {
self.core.supply_in_profit -= sats;
self.invested_capital_in_profit -= invested_capital;
self.investor_cap_in_profit -= price_u128 * invested_capital;
}
#[inline(always)]
fn deaccumulate_loss(&mut self, price_u128: u128, invested_capital: u128, sats: Sats) {
self.core.supply_in_loss -= sats;
self.invested_capital_in_loss -= invested_capital;
self.investor_cap_in_loss -= price_u128 * invested_capital;
}
}
#[derive(Debug, Clone)]
pub struct CachedUnrealizedState {
state: CachedStateRaw,
pub(crate) struct CachedUnrealizedState<S: Accumulate> {
state: S,
at_price: CentsCompact,
/// Cached output to skip redundant u128 divisions when nothing changed.
cached_output: Option<UnrealizedState>,
}
impl CachedUnrealizedState {
impl<S: Accumulate> CachedUnrealizedState<S> {
pub(crate) fn compute_fresh(price: Cents, map: &CostBasisMap) -> Self {
let price: CentsCompact = price.into();
let state = Self::compute_raw(price, map);
@@ -104,7 +186,6 @@ impl CachedUnrealizedState {
}
}
/// Get the current cached state as output (without price update).
pub(crate) fn current_state(&self) -> UnrealizedState {
self.state.to_output()
}
@@ -129,22 +210,17 @@ impl CachedUnrealizedState {
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
let invested_capital = price_u128 * sats_u128;
let investor_cap = price_u128 * invested_capital;
if price <= self.at_price {
self.state.supply_in_profit += sats;
self.state.invested_capital_in_profit += invested_capital;
self.state.investor_cap_in_profit += investor_cap;
self.state.accumulate_profit(price_u128, invested_capital, sats);
if price < self.at_price {
let diff = (self.at_price - price).as_u128();
self.state.unrealized_profit += diff * sats_u128;
*self.state.unrealized_profit() += diff * sats_u128;
}
} else {
self.state.supply_in_loss += sats;
self.state.invested_capital_in_loss += invested_capital;
self.state.investor_cap_in_loss += investor_cap;
self.state.accumulate_loss(price_u128, invested_capital, sats);
let diff = (price - self.at_price).as_u128();
self.state.unrealized_loss += diff * sats_u128;
*self.state.unrealized_loss() += diff * sats_u128;
}
}
@@ -154,22 +230,17 @@ impl CachedUnrealizedState {
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
let invested_capital = price_u128 * sats_u128;
let investor_cap = price_u128 * invested_capital;
if price <= self.at_price {
self.state.supply_in_profit -= sats;
self.state.invested_capital_in_profit -= invested_capital;
self.state.investor_cap_in_profit -= investor_cap;
self.state.deaccumulate_profit(price_u128, invested_capital, sats);
if price < self.at_price {
let diff = (self.at_price - price).as_u128();
self.state.unrealized_profit -= diff * sats_u128;
*self.state.unrealized_profit() -= diff * sats_u128;
}
} else {
self.state.supply_in_loss -= sats;
self.state.invested_capital_in_loss -= invested_capital;
self.state.investor_cap_in_loss -= investor_cap;
self.state.deaccumulate_loss(price_u128, invested_capital, sats);
let diff = (price - self.at_price).as_u128();
self.state.unrealized_loss -= diff * sats_u128;
*self.state.unrealized_loss() -= diff * sats_u128;
}
}
@@ -178,120 +249,83 @@ impl CachedUnrealizedState {
if new_price > old_price {
let delta = (new_price - old_price).as_u128();
let original_supply_in_profit = self.state.supply_in_profit().as_u128();
// Save original supply for delta calculation (before crossing UTXOs move)
let original_supply_in_profit = self.state.supply_in_profit.as_u128();
// First, process UTXOs crossing from loss to profit
// Range (old_price, new_price] means: old_price < price <= new_price
for (&price, &sats) in
map.range((Bound::Excluded(old_price), Bound::Included(new_price)))
{
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
let invested_capital = price_u128 * sats_u128;
let investor_cap = price_u128 * invested_capital;
// Move between buckets
self.state.supply_in_loss -= sats;
self.state.supply_in_profit += sats;
self.state.invested_capital_in_loss -= invested_capital;
self.state.invested_capital_in_profit += invested_capital;
self.state.investor_cap_in_loss -= investor_cap;
self.state.investor_cap_in_profit += investor_cap;
self.state.deaccumulate_loss(price_u128, invested_capital, sats);
self.state.accumulate_profit(price_u128, invested_capital, sats);
// Remove their original contribution to unrealized_loss
// (price > old_price is always true due to Bound::Excluded)
let original_loss = (price - old_price).as_u128();
self.state.unrealized_loss -= original_loss * sats_u128;
*self.state.unrealized_loss() -= original_loss * sats_u128;
// Add their new contribution to unrealized_profit (if not at boundary)
if price < new_price {
let new_profit = (new_price - price).as_u128();
self.state.unrealized_profit += new_profit * sats_u128;
*self.state.unrealized_profit() += new_profit * sats_u128;
}
}
// Apply delta to non-crossing UTXOs only
// Non-crossing profit UTXOs: their profit increases by delta
self.state.unrealized_profit += delta * original_supply_in_profit;
// Non-crossing loss UTXOs: their loss decreases by delta
let non_crossing_loss_sats = self.state.supply_in_loss.as_u128(); // Already excludes crossing
self.state.unrealized_loss -= delta * non_crossing_loss_sats;
*self.state.unrealized_profit() += delta * original_supply_in_profit;
let non_crossing_loss_sats = self.state.supply_in_loss().as_u128();
*self.state.unrealized_loss() -= delta * non_crossing_loss_sats;
} else if new_price < old_price {
let delta = (old_price - new_price).as_u128();
let original_supply_in_loss = self.state.supply_in_loss().as_u128();
// Save original supply for delta calculation (before crossing UTXOs move)
let original_supply_in_loss = self.state.supply_in_loss.as_u128();
// First, process UTXOs crossing from profit to loss
// Range (new_price, old_price] means: new_price < price <= old_price
for (&price, &sats) in
map.range((Bound::Excluded(new_price), Bound::Included(old_price)))
{
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
let invested_capital = price_u128 * sats_u128;
let investor_cap = price_u128 * invested_capital;
// Move between buckets
self.state.supply_in_profit -= sats;
self.state.supply_in_loss += sats;
self.state.invested_capital_in_profit -= invested_capital;
self.state.invested_capital_in_loss += invested_capital;
self.state.investor_cap_in_profit -= investor_cap;
self.state.investor_cap_in_loss += investor_cap;
self.state.deaccumulate_profit(price_u128, invested_capital, sats);
self.state.accumulate_loss(price_u128, invested_capital, sats);
// Remove their original contribution to unrealized_profit (if not at boundary)
if price < old_price {
let original_profit = (old_price - price).as_u128();
self.state.unrealized_profit -= original_profit * sats_u128;
*self.state.unrealized_profit() -= original_profit * sats_u128;
}
// Add their new contribution to unrealized_loss
// (price > new_price is always true due to Bound::Excluded)
let new_loss = (price - new_price).as_u128();
self.state.unrealized_loss += new_loss * sats_u128;
*self.state.unrealized_loss() += new_loss * sats_u128;
}
// Apply delta to non-crossing UTXOs only
// Non-crossing loss UTXOs: their loss increases by delta
self.state.unrealized_loss += delta * original_supply_in_loss;
// Non-crossing profit UTXOs: their profit decreases by delta
let non_crossing_profit_sats = self.state.supply_in_profit.as_u128(); // Already excludes crossing
self.state.unrealized_profit -= delta * non_crossing_profit_sats;
*self.state.unrealized_loss() += delta * original_supply_in_loss;
let non_crossing_profit_sats = self.state.supply_in_profit().as_u128();
*self.state.unrealized_profit() -= delta * non_crossing_profit_sats;
}
self.at_price = new_price;
}
/// Compute raw cached state from the map.
fn compute_raw(current_price: CentsCompact, map: &CostBasisMap) -> CachedStateRaw {
let mut state = CachedStateRaw::default();
fn compute_raw(current_price: CentsCompact, map: &CostBasisMap) -> S {
let mut state = S::default();
for (&price, &sats) in map.iter() {
let sats_u128 = sats.as_u128();
let price_u128 = price.as_u128();
let invested_capital = price_u128 * sats_u128;
let investor_cap = price_u128 * invested_capital;
if price <= current_price {
state.supply_in_profit += sats;
state.invested_capital_in_profit += invested_capital;
state.investor_cap_in_profit += investor_cap;
state.accumulate_profit(price_u128, invested_capital, sats);
if price < current_price {
let diff = (current_price - price).as_u128();
state.unrealized_profit += diff * sats_u128;
*state.unrealized_profit() += diff * sats_u128;
}
} else {
state.supply_in_loss += sats;
state.invested_capital_in_loss += invested_capital;
state.investor_cap_in_loss += investor_cap;
state.accumulate_loss(price_u128, invested_capital, sats);
let diff = (price - current_price).as_u128();
state.unrealized_loss += diff * sats_u128;
*state.unrealized_loss() += diff * sats_u128;
}
}
state
}
}