mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
global: snapshot
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user