diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index c1cc12089..38424755b 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -4277,6 +4277,15 @@ pub struct MetricsTree_Cointime_Pricing { pub true_market_mean_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern, pub cointime_price: CentsSatsUsdPattern, pub cointime_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern, + pub transfer_price: CentsSatsUsdPattern, + pub transfer_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern, + pub balanced_price: CentsSatsUsdPattern, + pub balanced_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern, + pub terminal_price: CentsSatsUsdPattern, + pub terminal_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern, + pub delta_price: CentsSatsUsdPattern, + pub delta_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern, + pub cumulative_market_cap: MetricPattern1, } impl MetricsTree_Cointime_Pricing { @@ -4290,6 +4299,15 @@ impl MetricsTree_Cointime_Pricing { true_market_mean_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern::new(client.clone(), "true_market_mean_ratio".to_string()), cointime_price: CentsSatsUsdPattern::new(client.clone(), "cointime_price".to_string()), cointime_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern::new(client.clone(), "cointime_price_ratio".to_string()), + transfer_price: CentsSatsUsdPattern::new(client.clone(), "transfer_price".to_string()), + transfer_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern::new(client.clone(), "transfer_price_ratio".to_string()), + balanced_price: CentsSatsUsdPattern::new(client.clone(), "balanced_price".to_string()), + balanced_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern::new(client.clone(), "balanced_price_ratio".to_string()), + terminal_price: CentsSatsUsdPattern::new(client.clone(), "terminal_price".to_string()), + terminal_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern::new(client.clone(), "terminal_price_ratio".to_string()), + delta_price: CentsSatsUsdPattern::new(client.clone(), "delta_price".to_string()), + delta_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern::new(client.clone(), "delta_price_ratio".to_string()), + cumulative_market_cap: MetricPattern1::new(client.clone(), "cumulative_market_cap".to_string()), } } } diff --git a/crates/brk_computer/src/cointime/pricing/compute.rs b/crates/brk_computer/src/cointime/pricing/compute.rs index 856166575..e28a09b08 100644 --- a/crates/brk_computer/src/cointime/pricing/compute.rs +++ b/crates/brk_computer/src/cointime/pricing/compute.rs @@ -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(()) } } diff --git a/crates/brk_computer/src/cointime/pricing/import.rs b/crates/brk_computer/src/cointime/pricing/import.rs index a8d41a46e..d387dac2c 100644 --- a/crates/brk_computer/src/cointime/pricing/import.rs +++ b/crates/brk_computer/src/cointime/pricing/import.rs @@ -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, }) } } diff --git a/crates/brk_computer/src/cointime/pricing/vecs.rs b/crates/brk_computer/src/cointime/pricing/vecs.rs index 2f5f8e910..f205341de 100644 --- a/crates/brk_computer/src/cointime/pricing/vecs.rs +++ b/crates/brk_computer/src/cointime/pricing/vecs.rs @@ -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 { pub true_market_mean_ratio: RatioPerBlockExtended, pub cointime_price: Price>, pub cointime_price_ratio: RatioPerBlockExtended, + pub transfer_price: Price>, + pub transfer_price_ratio: RatioPerBlockExtended, + pub balanced_price: Price>, + pub balanced_price_ratio: RatioPerBlockExtended, + pub terminal_price: Price>, + pub terminal_price_ratio: RatioPerBlockExtended, + pub delta_price: Price>, + pub delta_price_ratio: RatioPerBlockExtended, + + pub cumulative_market_cap: ComputedPerBlock, } diff --git a/crates/brk_computer/src/distribution/metrics/mod.rs b/crates/brk_computer/src/distribution/metrics/mod.rs index dc3bc8b74..bd7365519 100644 --- a/crates/brk_computer/src/distribution/metrics/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/mod.rs @@ -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 CohortMetricsState for TypeCohortMetrics { type Realized = MinimalRealizedState; - type CostBasis = CostBasisData; + type CostBasis = CostBasisData; } impl CohortMetricsState for MinimalCohortMetrics { type Realized = MinimalRealizedState; @@ -83,26 +83,26 @@ impl CohortMetricsState for MinimalCohortMetrics { } impl CohortMetricsState for CoreCohortMetrics { type Realized = CoreRealizedState; - type CostBasis = CostBasisData; + type CostBasis = CostBasisData; } impl CohortMetricsState for BasicCohortMetrics { type Realized = RealizedState; - type CostBasis = CostBasisData; + type CostBasis = CostBasisData; } impl CohortMetricsState for ExtendedCohortMetrics { type Realized = RealizedState; - type CostBasis = CostBasisData; + type CostBasis = CostBasisData; } impl CohortMetricsState for ExtendedAdjustedCohortMetrics { type Realized = RealizedState; - type CostBasis = CostBasisData; + type CostBasis = CostBasisData; } impl CohortMetricsState for AllCohortMetrics { type Realized = RealizedState; - type CostBasis = CostBasisData; + type CostBasis = CostBasisData; } -pub trait CohortMetricsBase: CohortMetricsState + Send + Sync { +pub trait CohortMetricsBase: CohortMetricsState> + Send + Sync { type ActivityVecs: ActivityLike; type RealizedVecs: RealizedLike; type UnrealizedVecs: UnrealizedLike; @@ -142,7 +142,7 @@ pub trait CohortMetricsBase: CohortMetricsState, + state: &mut CohortState>, ) -> Result<()> { state.apply_pending(); let unrealized_state = state.compute_unrealized_state(height_price); @@ -162,7 +162,7 @@ pub trait CohortMetricsBase: CohortMetricsState) -> Result<()> { + fn truncate_push(&mut self, height: Height, state: &CohortState>) -> Result<()> { self.supply_mut().truncate_push(height, state)?; self.outputs_mut().truncate_push(height, state)?; self.activity_mut().truncate_push(height, state)?; diff --git a/crates/brk_computer/src/distribution/metrics/realized/full.rs b/crates/brk_computer/src/distribution/metrics/realized/full.rs index c079eeb72..510912647 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/full.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/full.rs @@ -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, + state: &CohortState>, ) -> Result<()> { self.core.truncate_push(height, state)?; self.profit diff --git a/crates/brk_computer/src/distribution/metrics/realized/mod.rs b/crates/brk_computer/src/distribution/metrics/realized/mod.rs index 67eb484c1..3f5719ccd 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/mod.rs @@ -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) -> Result<()>; + fn truncate_push(&mut self, height: Height, state: &CohortState>) -> 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) -> Result<()> { + fn truncate_push(&mut self, height: Height, state: &CohortState>) -> 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) -> Result<()> { + fn truncate_push(&mut self, height: Height, state: &CohortState>) -> Result<()> { self.truncate_push(height, state) } fn compute_rest_part1(&mut self, blocks: &blocks::Vecs, starting_indexes: &Indexes, exit: &Exit) -> Result<()> { diff --git a/crates/brk_computer/src/distribution/state/cohort/base.rs b/crates/brk_computer/src/distribution/state/cohort/base.rs index ce948eca6..f1cbf0e7e 100644 --- a/crates/brk_computer/src/distribution/state/cohort/base.rs +++ b/crates/brk_computer/src/distribution/state/cohort/base.rs @@ -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 CohortState { } } -/// Methods only available with full CostBasisData (map + unrealized). -impl CohortState { +/// Methods only available with CostBasisData (map + unrealized). +impl CohortState> { pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState { self.cost_basis.compute_unrealized_state(height_price) } diff --git a/crates/brk_computer/src/distribution/state/cost_basis/data.rs b/crates/brk_computer/src/distribution/state/cost_basis/data.rs index 2444a463c..e738b6200 100644 --- a/crates/brk_computer/src/distribution/state/cost_basis/data.rs +++ b/crates/brk_computer/src/distribution/state/cost_basis/data.rs @@ -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; @@ -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 { raw: CostBasisRaw, map: Option, pending: FxHashMap, - cache: Option, + cache: Option>, rounding_digits: Option, - /// Monotonically increasing counter, bumped on each apply_pending with actual changes. generation: u64, } -impl CostBasisData { +impl CostBasisData { #[inline] fn round_price(&self, price: Cents) -> Cents { match self.rounding_digits { @@ -364,7 +366,7 @@ impl CostBasisData { } } -impl CostBasisOps for CostBasisData { +impl CostBasisOps for CostBasisData { fn create(path: &Path, name: &str) -> Self { Self { raw: CostBasisRaw::create(path, name), diff --git a/crates/brk_computer/src/distribution/state/cost_basis/mod.rs b/crates/brk_computer/src/distribution/state/cost_basis/mod.rs index 51a02a6c4..f1f5a40aa 100644 --- a/crates/brk_computer/src/distribution/state/cost_basis/mod.rs +++ b/crates/brk_computer/src/distribution/state/cost_basis/mod.rs @@ -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; diff --git a/crates/brk_computer/src/distribution/state/cost_basis/unrealized.rs b/crates/brk_computer/src/distribution/state/cost_basis/unrealized.rs index 4b03194de..60b878640 100644 --- a/crates/brk_computer/src/distribution/state/cost_basis/unrealized.rs +++ b/crates/brk_computer/src/distribution/state/cost_basis/unrealized.rs @@ -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 { + state: S, at_price: CentsCompact, - /// Cached output to skip redundant u128 divisions when nothing changed. cached_output: Option, } -impl CachedUnrealizedState { +impl CachedUnrealizedState { 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 } } + diff --git a/crates/brk_types/src/stored_f64.rs b/crates/brk_types/src/stored_f64.rs index 2c66347bc..cbf736391 100644 --- a/crates/brk_types/src/stored_f64.rs +++ b/crates/brk_types/src/stored_f64.rs @@ -10,7 +10,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; -use crate::{Bitcoin, Dollars, StoredU64}; +use crate::{Bitcoin, Cents, Dollars, StoredU64}; /// Fixed-size 64-bit floating point value optimized for on-disk storage #[derive(Debug, Deref, Default, Clone, Copy, Serialize, Deserialize, Pco, JsonSchema)] diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index a70f832bf..9022fb440 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -4769,6 +4769,15 @@ function createRawPattern(client, acc) { * @property {BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern} trueMarketMeanRatio * @property {CentsSatsUsdPattern} cointimePrice * @property {BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern} cointimePriceRatio + * @property {CentsSatsUsdPattern} transferPrice + * @property {BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern} transferPriceRatio + * @property {CentsSatsUsdPattern} balancedPrice + * @property {BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern} balancedPriceRatio + * @property {CentsSatsUsdPattern} terminalPrice + * @property {BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern} terminalPriceRatio + * @property {CentsSatsUsdPattern} deltaPrice + * @property {BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern} deltaPriceRatio + * @property {MetricPattern1} cumulativeMarketCap */ /** @@ -7598,6 +7607,15 @@ class BrkClient extends BrkClientBase { trueMarketMeanRatio: createBpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(this, 'true_market_mean_ratio'), cointimePrice: createCentsSatsUsdPattern(this, 'cointime_price'), cointimePriceRatio: createBpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(this, 'cointime_price_ratio'), + transferPrice: createCentsSatsUsdPattern(this, 'transfer_price'), + transferPriceRatio: createBpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(this, 'transfer_price_ratio'), + balancedPrice: createCentsSatsUsdPattern(this, 'balanced_price'), + balancedPriceRatio: createBpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(this, 'balanced_price_ratio'), + terminalPrice: createCentsSatsUsdPattern(this, 'terminal_price'), + terminalPriceRatio: createBpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(this, 'terminal_price_ratio'), + deltaPrice: createCentsSatsUsdPattern(this, 'delta_price'), + deltaPriceRatio: createBpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(this, 'delta_price_ratio'), + cumulativeMarketCap: createMetricPattern1(this, 'cumulative_market_cap'), }, adjusted: { inflationRate: createBpsPercentRatioPattern(this, 'cointime_adj_inflation_rate'), diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 688eafcaa..81cf020ec 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -3721,6 +3721,15 @@ class MetricsTree_Cointime_Pricing: self.true_market_mean_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern = BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(client, 'true_market_mean_ratio') self.cointime_price: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'cointime_price') self.cointime_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern = BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(client, 'cointime_price_ratio') + self.transfer_price: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'transfer_price') + self.transfer_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern = BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(client, 'transfer_price_ratio') + self.balanced_price: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'balanced_price') + self.balanced_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern = BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(client, 'balanced_price_ratio') + self.terminal_price: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'terminal_price') + self.terminal_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern = BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(client, 'terminal_price_ratio') + self.delta_price: CentsSatsUsdPattern = CentsSatsUsdPattern(client, 'delta_price') + self.delta_price_ratio: BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern = BpsPct1Pct2Pct5Pct95Pct98Pct99RatioSmaPattern(client, 'delta_price_ratio') + self.cumulative_market_cap: MetricPattern1[Dollars] = MetricPattern1(client, 'cumulative_market_cap') class MetricsTree_Cointime_Adjusted: """Metrics tree node."""