global: snapshot

This commit is contained in:
nym21
2026-03-10 12:25:49 +01:00
parent 8f93a5947e
commit b88f4762a5
21 changed files with 609 additions and 409 deletions

View File

@@ -1591,6 +1591,32 @@ impl _1m1w1y24hBpsPercentRatioPattern {
}
}
/// Pattern struct for repeated tree structure.
pub struct CoindaysCoinyearsDormancySentVelocityPattern {
pub coindays_destroyed: CumulativeRawSumPattern<StoredF64>,
pub coindays_destroyed_supply_adjusted: MetricPattern1<StoredF32>,
pub coinyears_destroyed: MetricPattern1<StoredF64>,
pub coinyears_destroyed_supply_adjusted: MetricPattern1<StoredF32>,
pub dormancy: MetricPattern1<StoredF32>,
pub sent: RawSumPattern3<Sats>,
pub velocity: MetricPattern1<StoredF32>,
}
impl CoindaysCoinyearsDormancySentVelocityPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
coindays_destroyed: CumulativeRawSumPattern::new(client.clone(), _m(&acc, "coindays_destroyed")),
coindays_destroyed_supply_adjusted: MetricPattern1::new(client.clone(), _m(&acc, "coindays_destroyed_supply_adjusted")),
coinyears_destroyed: MetricPattern1::new(client.clone(), _m(&acc, "coinyears_destroyed")),
coinyears_destroyed_supply_adjusted: MetricPattern1::new(client.clone(), _m(&acc, "coinyears_destroyed_supply_adjusted")),
dormancy: MetricPattern1::new(client.clone(), _m(&acc, "dormancy")),
sent: RawSumPattern3::new(client.clone(), _m(&acc, "sent")),
velocity: MetricPattern1::new(client.clone(), _m(&acc, "velocity")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct CumulativeDistributionRawRelSumValuePattern {
pub cumulative: MetricPattern1<Cents>,
@@ -2225,26 +2251,6 @@ impl CentsRelUsdPattern2 {
}
}
/// Pattern struct for repeated tree structure.
pub struct CoindaysDormancySentVelocityPattern {
pub coindays_destroyed: CumulativeRawSumPattern<StoredF64>,
pub dormancy: MetricPattern1<StoredF32>,
pub sent: RawSumPattern3<Sats>,
pub velocity: MetricPattern1<StoredF32>,
}
impl CoindaysDormancySentVelocityPattern {
/// Create a new pattern node with accumulated metric name.
pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {
Self {
coindays_destroyed: CumulativeRawSumPattern::new(client.clone(), _m(&acc, "coindays_destroyed")),
dormancy: MetricPattern1::new(client.clone(), _m(&acc, "dormancy")),
sent: RawSumPattern3::new(client.clone(), _m(&acc, "sent")),
velocity: MetricPattern1::new(client.clone(), _m(&acc, "velocity")),
}
}
}
/// Pattern struct for repeated tree structure.
pub struct CumulativeNegativeRawSumPattern {
pub cumulative: MetricPattern1<Cents>,
@@ -4245,6 +4251,7 @@ pub struct MetricsTree_Cointime_Cap {
pub vaulted_cap: CentsUsdPattern,
pub active_cap: CentsUsdPattern,
pub cointime_cap: CentsUsdPattern,
pub aviv: BpsRatioPattern,
}
impl MetricsTree_Cointime_Cap {
@@ -4255,6 +4262,7 @@ impl MetricsTree_Cointime_Cap {
vaulted_cap: CentsUsdPattern::new(client.clone(), "vaulted_cap".to_string()),
active_cap: CentsUsdPattern::new(client.clone(), "active_cap".to_string()),
cointime_cap: CentsUsdPattern::new(client.clone(), "cointime_cap".to_string()),
aviv: BpsRatioPattern::new(client.clone(), "aviv_ratio".to_string()),
}
}
}
@@ -6796,7 +6804,7 @@ impl MetricsTree_Distribution_UtxoCohorts {
pub struct MetricsTree_Distribution_UtxoCohorts_All {
pub supply: MetricsTree_Distribution_UtxoCohorts_All_Supply,
pub outputs: UtxoPattern3,
pub activity: CoindaysDormancySentVelocityPattern,
pub activity: CoindaysCoinyearsDormancySentVelocityPattern,
pub realized: CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern,
pub cost_basis: InvestedMaxMinPercentilesPattern,
pub unrealized: MetricsTree_Distribution_UtxoCohorts_All_Unrealized,
@@ -6807,7 +6815,7 @@ impl MetricsTree_Distribution_UtxoCohorts_All {
Self {
supply: MetricsTree_Distribution_UtxoCohorts_All_Supply::new(client.clone(), format!("{base_path}_supply")),
outputs: UtxoPattern3::new(client.clone(), "utxo_count".to_string()),
activity: CoindaysDormancySentVelocityPattern::new(client.clone(), "".to_string()),
activity: CoindaysCoinyearsDormancySentVelocityPattern::new(client.clone(), "".to_string()),
realized: CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern::new(client.clone(), "".to_string()),
cost_basis: InvestedMaxMinPercentilesPattern::new(client.clone(), "".to_string()),
unrealized: MetricsTree_Distribution_UtxoCohorts_All_Unrealized::new(client.clone(), format!("{base_path}_unrealized")),
@@ -6923,7 +6931,7 @@ pub struct MetricsTree_Distribution_UtxoCohorts_Sth {
pub realized: CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern,
pub supply: DeltaHalvedRelTotalPattern2,
pub outputs: UtxoPattern3,
pub activity: CoindaysDormancySentVelocityPattern,
pub activity: CoindaysCoinyearsDormancySentVelocityPattern,
pub cost_basis: InvestedMaxMinPercentilesPattern,
pub unrealized: GrossInvestedInvestorLossNetProfitSentimentPattern2,
}
@@ -6934,7 +6942,7 @@ impl MetricsTree_Distribution_UtxoCohorts_Sth {
realized: CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern::new(client.clone(), "sth".to_string()),
supply: DeltaHalvedRelTotalPattern2::new(client.clone(), "sth_supply".to_string()),
outputs: UtxoPattern3::new(client.clone(), "sth_utxo_count".to_string()),
activity: CoindaysDormancySentVelocityPattern::new(client.clone(), "sth".to_string()),
activity: CoindaysCoinyearsDormancySentVelocityPattern::new(client.clone(), "sth".to_string()),
cost_basis: InvestedMaxMinPercentilesPattern::new(client.clone(), "sth".to_string()),
unrealized: GrossInvestedInvestorLossNetProfitSentimentPattern2::new(client.clone(), "sth".to_string()),
}
@@ -6945,7 +6953,7 @@ impl MetricsTree_Distribution_UtxoCohorts_Sth {
pub struct MetricsTree_Distribution_UtxoCohorts_Lth {
pub supply: DeltaHalvedRelTotalPattern2,
pub outputs: UtxoPattern3,
pub activity: CoindaysDormancySentVelocityPattern,
pub activity: CoindaysCoinyearsDormancySentVelocityPattern,
pub realized: MetricsTree_Distribution_UtxoCohorts_Lth_Realized,
pub cost_basis: InvestedMaxMinPercentilesPattern,
pub unrealized: GrossInvestedInvestorLossNetProfitSentimentPattern2,
@@ -6956,7 +6964,7 @@ impl MetricsTree_Distribution_UtxoCohorts_Lth {
Self {
supply: DeltaHalvedRelTotalPattern2::new(client.clone(), "lth_supply".to_string()),
outputs: UtxoPattern3::new(client.clone(), "lth_utxo_count".to_string()),
activity: CoindaysDormancySentVelocityPattern::new(client.clone(), "lth".to_string()),
activity: CoindaysCoinyearsDormancySentVelocityPattern::new(client.clone(), "lth".to_string()),
realized: MetricsTree_Distribution_UtxoCohorts_Lth_Realized::new(client.clone(), format!("{base_path}_realized")),
cost_basis: InvestedMaxMinPercentilesPattern::new(client.clone(), "lth".to_string()),
unrealized: GrossInvestedInvestorLossNetProfitSentimentPattern2::new(client.clone(), "lth".to_string()),

View File

@@ -64,6 +64,14 @@ impl Vecs {
exit,
)?;
// AVIV = active_cap / investor_cap
self.aviv.compute_ratio(
starting_indexes,
&self.active_cap.cents.height,
&self.investor_cap.cents.height,
exit,
)?;
Ok(())
}
}

View File

@@ -3,7 +3,7 @@ use brk_types::Version;
use vecdb::Database;
use super::Vecs;
use crate::{indexes, internal::FiatPerBlock};
use crate::{indexes, internal::{FiatPerBlock, RatioPerBlock}};
impl Vecs {
pub(crate) fn forced_import(
@@ -17,6 +17,7 @@ impl Vecs {
vaulted_cap: FiatPerBlock::forced_import(db, "vaulted_cap", version, indexes)?,
active_cap: FiatPerBlock::forced_import(db, "active_cap", version, indexes)?,
cointime_cap: FiatPerBlock::forced_import(db, "cointime_cap", version, indexes)?,
aviv: RatioPerBlock::forced_import(db, "aviv", version, indexes)?,
})
}
}

View File

@@ -1,8 +1,8 @@
use brk_traversable::Traversable;
use brk_types::Cents;
use brk_types::{BasisPoints32, Cents};
use vecdb::{Rw, StorageMode};
use crate::internal::FiatPerBlock;
use crate::internal::{FiatPerBlock, RatioPerBlock};
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
@@ -11,4 +11,5 @@ pub struct Vecs<M: StorageMode = Rw> {
pub vaulted_cap: FiatPerBlock<Cents, M>,
pub active_cap: FiatPerBlock<Cents, M>,
pub cointime_cap: FiatPerBlock<Cents, M>,
pub aviv: RatioPerBlock<BasisPoints32, M>,
}

View File

@@ -64,14 +64,14 @@ pub struct UTXOCohortVecs<M: CohortMetricsState> {
state_starting_height: Option<Height>,
#[traversable(skip)]
pub state: Option<Box<UTXOCohortState<M::Realized>>>,
pub state: Option<Box<UTXOCohortState<M::Realized, M::CostBasis>>>,
#[traversable(flatten)]
pub metrics: M,
}
impl<M: CohortMetricsState> UTXOCohortVecs<M> {
pub(crate) fn new(state: Option<Box<UTXOCohortState<M::Realized>>>, metrics: M) -> Self {
pub(crate) fn new(state: Option<Box<UTXOCohortState<M::Realized, M::CostBasis>>>, metrics: M) -> Self {
Self {
state_starting_height: None,
state,

View File

@@ -5,7 +5,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{
blocks,
distribution::{metrics::ImportConfig, state::{CohortState, RealizedOps}},
distribution::{metrics::ImportConfig, state::{CohortState, CostBasisOps, RealizedOps}},
internal::PerBlockWithSum24h,
};
@@ -35,7 +35,7 @@ impl ActivityCore {
pub(crate) fn truncate_push(
&mut self,
height: Height,
state: &CohortState<impl RealizedOps>,
state: &CohortState<impl RealizedOps, impl CostBasisOps>,
) -> Result<()> {
self.sent.raw.height.truncate_push(height, state.sent)?;
self.coindays_destroyed.raw.height.truncate_push(

View File

@@ -2,11 +2,11 @@ use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Height, Indexes, Sats, StoredF32, StoredF64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use vecdb::{AnyStoredVec, Exit, ReadableCloneableVec, ReadableVec, Rw, StorageMode};
use crate::internal::{ComputedPerBlock, RollingWindowsFrom1w};
use crate::internal::{ComputedPerBlock, Identity, LazyPerBlock, RollingWindowsFrom1w};
use crate::{blocks, distribution::{metrics::ImportConfig, state::{CohortState, RealizedOps}}};
use crate::{blocks, distribution::{metrics::ImportConfig, state::{CohortState, CostBasisOps, RealizedOps}}};
use super::ActivityCore;
@@ -25,20 +25,37 @@ pub struct ActivityFull<M: StorageMode = Rw> {
#[traversable(wrap = "sent", rename = "sum")]
pub sent_sum_extended: RollingWindowsFrom1w<Sats, M>,
pub coinyears_destroyed: LazyPerBlock<StoredF64, StoredF64>,
pub dormancy: ComputedPerBlock<StoredF32, M>,
pub velocity: ComputedPerBlock<StoredF32, M>,
pub coindays_destroyed_supply_adjusted: ComputedPerBlock<StoredF32, M>,
pub coinyears_destroyed_supply_adjusted: ComputedPerBlock<StoredF32, M>,
}
impl ActivityFull {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let v1 = Version::ONE;
let coindays_destroyed_sum: RollingWindowsFrom1w<StoredF64> =
cfg.import("coindays_destroyed", v1)?;
let coinyears_destroyed = LazyPerBlock::from_computed::<Identity<StoredF64>>(
&cfg.name("coinyears_destroyed"),
v1,
coindays_destroyed_sum._1y.height.read_only_boxed_clone(),
&coindays_destroyed_sum._1y,
);
Ok(Self {
inner: ActivityCore::forced_import(cfg)?,
coindays_destroyed_cumulative: cfg.import("coindays_destroyed_cumulative", v1)?,
coindays_destroyed_sum: cfg.import("coindays_destroyed", v1)?,
coindays_destroyed_sum,
sent_sum_extended: cfg.import("sent", v1)?,
coinyears_destroyed,
dormancy: cfg.import("dormancy", v1)?,
velocity: cfg.import("velocity", v1)?,
coindays_destroyed_supply_adjusted: cfg.import("coindays_destroyed_supply_adjusted", v1)?,
coinyears_destroyed_supply_adjusted: cfg.import("coinyears_destroyed_supply_adjusted", v1)?,
})
}
@@ -49,7 +66,7 @@ impl ActivityFull {
pub(crate) fn full_truncate_push(
&mut self,
height: Height,
state: &CohortState<impl RealizedOps>,
state: &CohortState<impl RealizedOps, impl CostBasisOps>,
) -> Result<()> {
self.inner.truncate_push(height, state)
}
@@ -58,6 +75,8 @@ impl ActivityFull {
let mut vecs = self.inner.collect_vecs_mut();
vecs.push(&mut self.dormancy.height);
vecs.push(&mut self.velocity.height);
vecs.push(&mut self.coindays_destroyed_supply_adjusted.height);
vecs.push(&mut self.coinyears_destroyed_supply_adjusted.height);
vecs
}
@@ -142,6 +161,38 @@ impl ActivityFull {
exit,
)?;
// Supply-Adjusted CDD = sum_24h(CDD) / circulating_supply
self.coindays_destroyed_supply_adjusted.height.compute_transform2(
starting_indexes.height,
&self.inner.coindays_destroyed.sum._24h.height,
supply_total_sats,
|(i, cdd_24h, supply_sats, ..)| {
let supply = f64::from(Bitcoin::from(supply_sats));
if supply == 0.0 {
(i, StoredF32::from(0.0f32))
} else {
(i, StoredF32::from((f64::from(cdd_24h) / supply) as f32))
}
},
exit,
)?;
// Supply-Adjusted CYD = CYD / circulating_supply (CYD = 1y rolling sum of CDD)
self.coinyears_destroyed_supply_adjusted.height.compute_transform2(
starting_indexes.height,
&self.coinyears_destroyed.height,
supply_total_sats,
|(i, cdd_1y, supply_sats, ..)| {
let supply = f64::from(Bitcoin::from(supply_sats));
if supply == 0.0 {
(i, StoredF32::from(0.0f32))
} else {
(i, StoredF32::from((f64::from(cdd_1y) / supply) as f32))
}
},
exit,
)?;
Ok(())
}
}

View File

@@ -8,7 +8,7 @@ use brk_error::Result;
use brk_types::{Height, Indexes, Version};
use vecdb::Exit;
use crate::{blocks, distribution::state::{CohortState, RealizedOps}};
use crate::{blocks, distribution::state::{CohortState, CostBasisOps, RealizedOps}};
pub trait ActivityLike: Send + Sync {
fn as_core(&self) -> &ActivityCore;
@@ -17,7 +17,7 @@ pub trait ActivityLike: Send + Sync {
fn truncate_push<R: RealizedOps>(
&mut self,
height: Height,
state: &CohortState<R>,
state: &CohortState<R, impl CostBasisOps>,
) -> Result<()>;
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>;
fn compute_from_stateful(
@@ -38,7 +38,7 @@ impl ActivityLike for ActivityCore {
fn as_core(&self) -> &ActivityCore { self }
fn as_core_mut(&mut self) -> &mut ActivityCore { self }
fn min_len(&self) -> usize { self.min_len() }
fn truncate_push<R: RealizedOps>(&mut self, height: Height, state: &CohortState<R>) -> Result<()> {
fn truncate_push<R: RealizedOps>(&mut self, height: Height, state: &CohortState<R, impl CostBasisOps>) -> Result<()> {
self.truncate_push(height, state)
}
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
@@ -56,7 +56,7 @@ impl ActivityLike for ActivityFull {
fn as_core(&self) -> &ActivityCore { &self.inner }
fn as_core_mut(&mut self) -> &mut ActivityCore { &mut self.inner }
fn min_len(&self) -> usize { self.full_min_len() }
fn truncate_push<R: RealizedOps>(&mut self, height: Height, state: &CohortState<R>) -> Result<()> {
fn truncate_push<R: RealizedOps>(&mut self, height: Height, state: &CohortState<R, impl CostBasisOps>) -> Result<()> {
self.full_truncate_push(height, state)
}
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {

View File

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

View File

@@ -3,7 +3,7 @@ use brk_traversable::Traversable;
use brk_types::{Height, Indexes, StoredU64, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{distribution::state::{CohortState, RealizedOps}, internal::ComputedPerBlock};
use crate::{distribution::state::{CohortState, CostBasisOps, RealizedOps}, internal::ComputedPerBlock};
use crate::distribution::metrics::ImportConfig;
@@ -24,7 +24,7 @@ impl OutputsBase {
self.utxo_count.height.len()
}
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps>) -> Result<()> {
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps, impl CostBasisOps>) -> Result<()> {
self.utxo_count
.height
.truncate_push(height, StoredU64::from(state.supply.utxo_count))?;

View File

@@ -8,7 +8,7 @@ use vecdb::{
use crate::{
blocks,
distribution::state::{CohortState, RealizedOps},
distribution::state::{CohortState, CostBasisOps, RealizedOps},
internal::{
AmountPerBlockWithSum24h, ComputedPerBlock, FiatRollingDelta1m, LazyPerBlock,
NegCentsUnsignedToDollars, PerBlockWithSum24h, RatioCents64,
@@ -92,7 +92,7 @@ impl RealizedCore {
.min(self.sent.in_loss.raw.sats.height.len())
}
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps>) -> Result<()> {
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps, impl CostBasisOps>) -> Result<()> {
self.minimal.truncate_push(height, state)?;
self.sent
.in_profit

View File

@@ -12,7 +12,7 @@ use vecdb::{
use crate::{
blocks,
distribution::state::{CohortState, RealizedState},
distribution::state::{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>,
state: &CohortState<RealizedState, CostBasisData>,
) -> Result<()> {
self.core.truncate_push(height, state)?;
self.profit

View File

@@ -10,7 +10,7 @@ use vecdb::{
use crate::{
blocks,
distribution::state::{CohortState, RealizedOps},
distribution::state::{CohortState, CostBasisOps, RealizedOps},
internal::{
ComputedPerBlock, FiatPerBlock, FiatPerBlockWithSum24h, Identity, LazyPerBlock,
PerBlockWithSum24h, Price, RatioPerBlock,
@@ -83,7 +83,7 @@ impl RealizedMinimal {
.min(self.sopr.value_destroyed.raw.height.len())
}
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps>) -> Result<()> {
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps, impl CostBasisOps>) -> Result<()> {
self.cap.cents.height.truncate_push(height, state.realized.cap())?;
self.profit.raw.cents.height.truncate_push(height, state.realized.profit())?;
self.loss.raw.cents.height.truncate_push(height, state.realized.loss())?;

View File

@@ -12,7 +12,7 @@ use brk_error::Result;
use brk_types::{Height, Indexes};
use vecdb::Exit;
use crate::{blocks, distribution::state::{CohortState, RealizedState}};
use crate::{blocks, distribution::state::{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>) -> Result<()>;
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData>) -> 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>) -> Result<()> {
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData>) -> 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>) -> Result<()> {
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState, CostBasisData>) -> 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 brk_traversable::Traversable;
use brk_types::{Height, Indexes, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{distribution::state::{CohortState, RealizedOps}, prices};
use crate::{distribution::state::{CohortState, CostBasisOps, RealizedOps}, prices};
use crate::internal::{
AmountPerBlock, HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin,
@@ -40,7 +40,7 @@ impl SupplyBase {
self.total.sats.height.len()
}
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps>) -> Result<()> {
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState<impl RealizedOps, impl CostBasisOps>) -> Result<()> {
self.total.sats.height.truncate_push(height, state.supply.value)?;
Ok(())
}

View File

@@ -4,7 +4,7 @@ use brk_error::Result;
use brk_types::{Age, Cents, FundedAddressData, Sats, SupplyState};
use vecdb::unlikely;
use super::super::cost_basis::RealizedOps;
use super::super::cost_basis::{CostBasisRaw, RealizedOps};
use super::base::CohortState;
/// Significant digits for address cost basis prices (after rounding to dollars).
@@ -12,7 +12,7 @@ const COST_BASIS_PRICE_DIGITS: i32 = 4;
pub struct AddressCohortState<R: RealizedOps> {
pub addr_count: u64,
pub inner: CohortState<R>,
pub inner: CohortState<R, CostBasisRaw>,
}
impl<R: RealizedOps> AddressCohortState<R> {

View File

@@ -1,9 +1,9 @@
use std::{collections::BTreeMap, path::Path};
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, PendingDelta, RealizedOps, UnrealizedState};
use super::super::cost_basis::{CostBasisData, CostBasisOps, PendingDelta, RealizedOps, UnrealizedState};
pub struct SendPrecomputed {
pub sats: Sats,
@@ -49,54 +49,50 @@ impl SendPrecomputed {
}
}
pub struct CohortState<R: RealizedOps> {
pub struct CohortState<R: RealizedOps, C: CostBasisOps> {
pub supply: SupplyState,
pub realized: R,
pub sent: Sats,
pub satdays_destroyed: Sats,
cost_basis_data: CostBasisData,
cost_basis: C,
}
impl<R: RealizedOps> CohortState<R> {
impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
pub(crate) fn new(path: &Path, name: &str) -> Self {
Self {
supply: SupplyState::default(),
realized: R::default(),
sent: Sats::ZERO,
satdays_destroyed: Sats::ZERO,
cost_basis_data: CostBasisData::create(path, name),
cost_basis: C::create(path, name),
}
}
/// Enable price rounding for cost basis data.
pub(crate) fn with_price_rounding(mut self, digits: i32) -> Self {
self.cost_basis_data = self.cost_basis_data.with_price_rounding(digits);
self.cost_basis = self.cost_basis.with_price_rounding(digits);
self
}
pub(crate) fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
self.cost_basis_data.import_at_or_before(height)
self.cost_basis.import_at_or_before(height)
}
/// Restore realized cap from cost_basis_data after import.
/// Restore realized cap from cost_basis after import.
pub(crate) fn restore_realized_cap(&mut self) {
self.realized.set_cap_raw(self.cost_basis_data.cap_raw());
self.realized.set_cap_raw(self.cost_basis.cap_raw());
self.realized
.set_investor_cap_raw(self.cost_basis_data.investor_cap_raw());
.set_investor_cap_raw(self.cost_basis.investor_cap_raw());
}
pub(crate) fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
self.cost_basis_data.clean()?;
self.cost_basis_data.init();
self.cost_basis.clean()?;
self.cost_basis.init();
Ok(())
}
pub(crate) fn for_each_cost_basis_pending(&self, f: impl FnMut(&CentsCompact, &PendingDelta)) {
self.cost_basis_data.for_each_pending(f);
}
pub(crate) fn apply_pending(&mut self) {
self.cost_basis_data.apply_pending();
self.cost_basis.apply_pending();
}
pub(crate) fn reset_single_iteration_values(&mut self) {
@@ -113,7 +109,7 @@ impl<R: RealizedOps> CohortState<R> {
if s.supply_state.value > Sats::ZERO {
self.realized
.increment_snapshot(s.price_sats, s.investor_cap);
self.cost_basis_data.increment(
self.cost_basis.increment(
s.realized_price,
s.supply_state.value,
s.price_sats,
@@ -128,7 +124,7 @@ impl<R: RealizedOps> CohortState<R> {
if s.supply_state.value > Sats::ZERO {
self.realized
.decrement_snapshot(s.price_sats, s.investor_cap);
self.cost_basis_data.decrement(
self.cost_basis.decrement(
s.realized_price,
s.supply_state.value,
s.price_sats,
@@ -153,7 +149,7 @@ impl<R: RealizedOps> CohortState<R> {
if supply.value > Sats::ZERO {
self.realized.receive(snapshot.realized_price, supply.value);
self.cost_basis_data.increment(
self.cost_basis.increment(
snapshot.realized_price,
supply.value,
snapshot.price_sats,
@@ -175,7 +171,7 @@ impl<R: RealizedOps> CohortState<R> {
self.realized.receive(price, supply.value);
if current.supply_state.value.is_not_zero() {
self.cost_basis_data.increment(
self.cost_basis.increment(
current.realized_price,
current.supply_state.value,
current.price_sats,
@@ -184,7 +180,7 @@ impl<R: RealizedOps> CohortState<R> {
}
if prev.supply_state.value.is_not_zero() {
self.cost_basis_data.decrement(
self.cost_basis.decrement(
prev.realized_price,
prev.supply_state.value,
prev.price_sats,
@@ -208,7 +204,7 @@ impl<R: RealizedOps> CohortState<R> {
self.realized
.send(pre.sats, pre.current_ps, pre.prev_ps, pre.ath_ps, pre.prev_investor_cap);
self.cost_basis_data
self.cost_basis
.decrement(pre.prev_price, pre.sats, pre.prev_ps, pre.prev_investor_cap);
}
@@ -262,7 +258,7 @@ impl<R: RealizedOps> CohortState<R> {
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
if current.supply_state.value.is_not_zero() {
self.cost_basis_data.increment(
self.cost_basis.increment(
current.realized_price,
current.supply_state.value,
current.price_sats,
@@ -271,7 +267,7 @@ impl<R: RealizedOps> CohortState<R> {
}
if prev.supply_state.value.is_not_zero() {
self.cost_basis_data.decrement(
self.cost_basis.decrement(
prev.realized_price,
prev.supply_state.value,
prev.price_sats,
@@ -281,15 +277,22 @@ impl<R: RealizedOps> CohortState<R> {
}
}
pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState {
self.cost_basis_data.compute_unrealized_state(height_price)
}
pub(crate) fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
self.cost_basis_data.write(height, cleanup)
self.cost_basis.write(height, cleanup)
}
}
pub(crate) fn cost_basis_map(&self) -> &BTreeMap<CentsCompact, Sats> {
self.cost_basis_data.map()
/// Methods only available with full CostBasisData (map + unrealized).
impl<R: RealizedOps> CohortState<R, CostBasisData> {
pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState {
self.cost_basis.compute_unrealized_state(height_price)
}
pub(crate) fn for_each_cost_basis_pending(&self, f: impl FnMut(&CentsCompact, &PendingDelta)) {
self.cost_basis.for_each_pending(f);
}
pub(crate) fn cost_basis_map(&self) -> &std::collections::BTreeMap<CentsCompact, Sats> {
self.cost_basis.map()
}
}

View File

@@ -4,13 +4,13 @@ use brk_error::Result;
use brk_types::{Sats, SupplyState};
use derive_more::{Deref, DerefMut};
use super::super::cost_basis::RealizedOps;
use super::super::cost_basis::{CostBasisOps, RealizedOps};
use super::base::CohortState;
#[derive(Deref, DerefMut)]
pub struct UTXOCohortState<R: RealizedOps>(pub(crate) CohortState<R>);
pub struct UTXOCohortState<R: RealizedOps, C: CostBasisOps>(pub(crate) CohortState<R, C>);
impl<R: RealizedOps> UTXOCohortState<R> {
impl<R: RealizedOps, C: CostBasisOps> UTXOCohortState<R, C> {
pub(crate) fn new(path: &Path, name: &str) -> Self {
Self(CohortState::new(path, name))
}

View File

@@ -24,6 +24,15 @@ struct PendingRaw {
investor_cap_dec: CentsSquaredSats,
}
impl PendingRaw {
fn is_zero(&self) -> bool {
self.cap_inc == CentsSats::ZERO
&& self.cap_dec == CentsSats::ZERO
&& self.investor_cap_inc == CentsSquaredSats::ZERO
&& self.investor_cap_dec == CentsSquaredSats::ZERO
}
}
/// Pending increments and decrements for a single price bucket.
#[derive(Clone, Copy, Debug, Default)]
pub struct PendingDelta {
@@ -31,261 +40,79 @@ pub struct PendingDelta {
pub dec: Sats,
}
#[derive(Clone, Debug)]
pub struct CostBasisData {
pathbuf: PathBuf,
state: Option<State>,
pending: FxHashMap<CentsCompact, PendingDelta>,
pending_raw: PendingRaw,
cache: Option<CachedUnrealizedState>,
rounding_digits: Option<i32>,
/// Monotonically increasing counter, bumped on each apply_pending with actual changes.
generation: u64,
}
const STATE_TO_KEEP: usize = 10;
impl CostBasisData {
pub(crate) fn create(path: &Path, name: &str) -> Self {
Self {
pathbuf: path.join(format!("{name}_cost_basis")),
state: None,
pending: FxHashMap::default(),
pending_raw: PendingRaw::default(),
cache: None,
rounding_digits: None,
generation: 0,
}
}
pub(crate) fn with_price_rounding(mut self, digits: i32) -> Self {
self.rounding_digits = Some(digits);
self
}
#[inline]
fn round_price(&self, price: Cents) -> Cents {
match self.rounding_digits {
Some(digits) => price.round_to_dollar(digits),
None => price,
}
}
pub(crate) fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
let files = self.read_dir(None)?;
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
"No cost basis state found at or before height".into(),
))?;
self.state = Some(State::deserialize(&fs::read(path)?)?);
self.pending.clear();
self.pending_raw = PendingRaw::default();
self.cache = None;
Ok(height)
}
fn assert_pending_empty(&self) {
debug_assert!(
self.pending.is_empty() && self.pending_raw_is_zero(),
"CostBasisData: pending not empty, call apply_pending first"
);
}
fn pending_raw_is_zero(&self) -> bool {
self.pending_raw.cap_inc == CentsSats::ZERO
&& self.pending_raw.cap_dec == CentsSats::ZERO
&& self.pending_raw.investor_cap_inc == CentsSquaredSats::ZERO
&& self.pending_raw.investor_cap_dec == CentsSquaredSats::ZERO
}
pub(crate) fn map(&self) -> &CostBasisMap {
self.assert_pending_empty();
&self.state.as_ref().unwrap().base.map
}
pub(crate) fn is_empty(&self) -> bool {
self.pending.is_empty() && self.state.as_ref().unwrap().base.map.is_empty()
}
/// Get the exact cap_raw value (not recomputed from map).
pub(crate) fn cap_raw(&self) -> CentsSats {
self.assert_pending_empty();
self.state.as_ref().unwrap().cap_raw
}
/// Get the exact investor_cap_raw value (not recomputed from map).
pub(crate) fn investor_cap_raw(&self) -> CentsSquaredSats {
self.assert_pending_empty();
self.state.as_ref().unwrap().investor_cap_raw
}
/// Increment with pre-computed typed values.
/// Handles rounding and cache update.
pub(crate) fn increment(
/// Common interface for cost basis tracking.
///
/// Implemented by `CostBasisRaw` (scalars only) and `CostBasisData` (full map + scalars).
pub trait CostBasisOps: Send + Sync + 'static {
fn create(path: &Path, name: &str) -> Self;
fn with_price_rounding(self, digits: i32) -> Self;
fn import_at_or_before(&mut self, height: Height) -> Result<Height>;
fn cap_raw(&self) -> CentsSats;
fn investor_cap_raw(&self) -> CentsSquaredSats;
fn increment(
&mut self,
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
self.pending.entry(price.into()).or_default().inc += sats;
self.pending_raw.cap_inc += price_sats;
if investor_cap != CentsSquaredSats::ZERO {
self.pending_raw.investor_cap_inc += investor_cap;
}
if let Some(cache) = self.cache.as_mut() {
cache.on_receive(price, sats);
}
}
/// Decrement with pre-computed typed values.
/// Handles rounding and cache update.
pub(crate) fn decrement(
);
fn decrement(
&mut self,
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
self.pending.entry(price.into()).or_default().dec += sats;
self.pending_raw.cap_dec += price_sats;
if investor_cap != CentsSquaredSats::ZERO {
self.pending_raw.investor_cap_dec += investor_cap;
}
if let Some(cache) = self.cache.as_mut() {
cache.on_send(price, sats);
}
}
pub(crate) fn for_each_pending(&self, mut f: impl FnMut(&CentsCompact, &PendingDelta)) {
self.pending.iter().for_each(|(k, v)| f(k, v));
}
pub(crate) fn apply_pending(&mut self) {
if self.pending.is_empty() {
return;
}
self.generation = self.generation.wrapping_add(1);
let map = &mut self.state.as_mut().unwrap().base.map;
for (cents, PendingDelta { inc, dec }) in self.pending.drain() {
match map.entry(cents) {
Entry::Occupied(mut e) => {
*e.get_mut() += inc;
if unlikely(*e.get() < dec) {
panic!(
"CostBasisData::apply_pending underflow!\n\
Path: {:?}\n\
Price: {}\n\
Current + increments: {}\n\
Trying to decrement by: {}",
self.pathbuf,
cents.to_dollars(),
e.get(),
dec
);
fn apply_pending(&mut self);
fn init(&mut self);
fn clean(&mut self) -> Result<()>;
fn write(&mut self, height: Height, cleanup: bool) -> Result<()>;
}
*e.get_mut() -= dec;
if *e.get() == Sats::ZERO {
e.remove();
}
}
Entry::Vacant(e) => {
if unlikely(inc < dec) {
panic!(
"CostBasisData::apply_pending underflow (new entry)!\n\
Path: {:?}\n\
Price: {}\n\
Increment: {}\n\
Trying to decrement by: {}",
self.pathbuf,
cents.to_dollars(),
inc,
dec
);
}
let val = inc - dec;
if val != Sats::ZERO {
e.insert(val);
// ─── CostBasisRaw ───────────────────────────────────────────────────────────
#[derive(Clone, Default, Debug)]
struct RawState {
cap_raw: CentsSats,
}
impl RawState {
fn serialize(&self) -> Vec<u8> {
self.cap_raw.to_bytes().to_vec()
}
fn deserialize(data: &[u8]) -> Result<Self> {
Ok(Self {
cap_raw: CentsSats::from_bytes(&data[0..16])?,
})
}
}
// Apply raw values
let state = self.state.as_mut().unwrap();
state.cap_raw += self.pending_raw.cap_inc;
// Check for underflow before subtracting
if unlikely(state.cap_raw.inner() < self.pending_raw.cap_dec.inner()) {
panic!(
"CostBasisData::apply_pending cap_raw underflow!\n\
Path: {:?}\n\
Current cap_raw (after increments): {}\n\
Trying to decrement by: {}",
self.pathbuf, state.cap_raw, self.pending_raw.cap_dec
);
}
state.cap_raw -= self.pending_raw.cap_dec;
// Only process investor_cap if there are non-zero values
let has_investor_cap = self.pending_raw.investor_cap_inc != CentsSquaredSats::ZERO
|| self.pending_raw.investor_cap_dec != CentsSquaredSats::ZERO;
if has_investor_cap {
state.investor_cap_raw += self.pending_raw.investor_cap_inc;
if unlikely(state.investor_cap_raw.inner() < self.pending_raw.investor_cap_dec.inner()) {
panic!(
"CostBasisData::apply_pending investor_cap_raw underflow!\n\
Path: {:?}\n\
Current investor_cap_raw (after increments): {}\n\
Trying to decrement by: {}",
self.pathbuf, state.investor_cap_raw, self.pending_raw.investor_cap_dec
);
}
state.investor_cap_raw -= self.pending_raw.investor_cap_dec;
/// Lightweight cost basis tracking: only cap_raw and investor_cap_raw scalars.
/// No BTreeMap, no unrealized computation, no pending map.
/// Used by cohorts that only need realized cap on restart (amount_range, address).
#[derive(Clone, Debug)]
pub struct CostBasisRaw {
pathbuf: PathBuf,
state: Option<RawState>,
pending_raw: PendingRaw,
}
self.pending_raw = PendingRaw::default();
}
pub(crate) fn init(&mut self) {
self.state.replace(State::default());
self.pending.clear();
self.pending_raw = PendingRaw::default();
self.cache = None;
}
pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState {
if self.is_empty() {
return UnrealizedState::ZERO;
}
let map = &self.state.as_ref().unwrap().base.map;
if let Some(cache) = self.cache.as_mut() {
cache.get_at_price(height_price, map)
} else {
let cache = CachedUnrealizedState::compute_fresh(height_price, map);
let state = cache.current_state();
self.cache = Some(cache);
state
}
}
pub(crate) fn clean(&mut self) -> Result<()> {
let _ = fs::remove_dir_all(&self.pathbuf);
fs::create_dir_all(self.path_by_height())?;
self.cache = None;
Ok(())
}
fn path_by_height(&self) -> PathBuf {
impl CostBasisRaw {
pub(super) fn path_by_height(&self) -> PathBuf {
self.pathbuf.join("by_height")
}
fn read_dir(&self, keep_only_before: Option<Height>) -> Result<BTreeMap<Height, PathBuf>> {
pub(super) fn path_state(&self, height: Height) -> PathBuf {
self.path_by_height().join(height.to_string())
}
pub(super) fn read_dir(
&self,
keep_only_before: Option<Height>,
) -> Result<BTreeMap<Height, PathBuf>> {
let by_height = self.path_by_height();
if !by_height.exists() {
return Ok(BTreeMap::new());
@@ -305,15 +132,33 @@ impl CostBasisData {
None
}
})
.collect::<BTreeMap<Height, PathBuf>>())
.collect())
}
pub(crate) fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
self.apply_pending();
fn apply_pending_raw(&mut self) {
if self.pending_raw.is_zero() {
return;
}
let state = self.state.as_mut().unwrap();
state.cap_raw += self.pending_raw.cap_inc;
if unlikely(state.cap_raw.inner() < self.pending_raw.cap_dec.inner()) {
panic!(
"CostBasis cap_raw underflow!\n\
Path: {:?}\n\
Current cap_raw (after increments): {}\n\
Trying to decrement by: {}",
self.pathbuf, state.cap_raw, self.pending_raw.cap_dec
);
}
state.cap_raw -= self.pending_raw.cap_dec;
self.pending_raw = PendingRaw::default();
}
fn write_and_cleanup(&mut self, height: Height, cleanup: bool) -> Result<()> {
if cleanup {
let files = self.read_dir(Some(height))?;
for (_, path) in files
.iter()
.take(files.len().saturating_sub(STATE_TO_KEEP - 1))
@@ -321,46 +166,309 @@ impl CostBasisData {
fs::remove_file(path)?;
}
}
Ok(())
}
}
fs::write(
self.path_state(height),
self.state.as_ref().unwrap().serialize()?,
)?;
impl CostBasisOps for CostBasisRaw {
fn create(path: &Path, name: &str) -> Self {
Self {
pathbuf: path.join(format!("{name}_cost_basis")),
state: None,
pending_raw: PendingRaw::default(),
}
}
fn with_price_rounding(self, _digits: i32) -> Self {
self
}
fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
let files = self.read_dir(None)?;
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
"No cost basis state found at or before height".into(),
))?;
let data = fs::read(path)?;
// Handle both formats: full (map + raw at end) and raw-only (16 bytes).
self.state = Some(if data.len() == 16 {
RawState::deserialize(&data)?
} else {
let (_, rest) = CostBasisDistribution::deserialize_with_rest(&data)?;
RawState::deserialize(rest)?
});
self.pending_raw = PendingRaw::default();
Ok(height)
}
fn cap_raw(&self) -> CentsSats {
debug_assert!(self.pending_raw.is_zero());
self.state.as_ref().unwrap().cap_raw
}
fn investor_cap_raw(&self) -> CentsSquaredSats {
CentsSquaredSats::ZERO
}
#[inline]
fn increment(
&mut self,
_price: Cents,
_sats: Sats,
price_sats: CentsSats,
_investor_cap: CentsSquaredSats,
) {
self.pending_raw.cap_inc += price_sats;
}
#[inline]
fn decrement(
&mut self,
_price: Cents,
_sats: Sats,
price_sats: CentsSats,
_investor_cap: CentsSquaredSats,
) {
self.pending_raw.cap_dec += price_sats;
}
fn apply_pending(&mut self) {
self.apply_pending_raw();
}
fn init(&mut self) {
self.state.replace(RawState::default());
self.pending_raw = PendingRaw::default();
}
fn clean(&mut self) -> Result<()> {
let _ = fs::remove_dir_all(&self.pathbuf);
fs::create_dir_all(self.path_by_height())?;
Ok(())
}
fn path_state(&self, height: Height) -> PathBuf {
self.path_by_height().join(height.to_string())
fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
self.apply_pending_raw();
self.write_and_cleanup(height, cleanup)?;
fs::write(
self.path_state(height),
self.state.as_ref().unwrap().serialize(),
)?;
Ok(())
}
}
#[derive(Clone, Default, Debug)]
struct State {
base: CostBasisDistribution,
/// Exact realized cap: Σ(price × sats)
cap_raw: CentsSats,
/// Exact investor cap: Σ(price² × sats)
investor_cap_raw: CentsSquaredSats,
// ─── CostBasisData ──────────────────────────────────────────────────────────
/// 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.
#[derive(Clone, Debug)]
pub struct CostBasisData {
raw: CostBasisRaw,
map: Option<CostBasisDistribution>,
pending: FxHashMap<CentsCompact, PendingDelta>,
cache: Option<CachedUnrealizedState>,
rounding_digits: Option<i32>,
/// Monotonically increasing counter, bumped on each apply_pending with actual changes.
generation: u64,
}
impl State {
fn serialize(&self) -> Result<Vec<u8>> {
let mut buffer = self.base.serialize()?;
buffer.extend(self.cap_raw.to_bytes());
buffer.extend(self.investor_cap_raw.to_bytes());
Ok(buffer)
impl CostBasisData {
#[inline]
fn round_price(&self, price: Cents) -> Cents {
match self.rounding_digits {
Some(digits) => price.round_to_dollar(digits),
None => price,
}
}
fn deserialize(data: &[u8]) -> Result<Self> {
let (base, rest) = CostBasisDistribution::deserialize_with_rest(data)?;
let cap_raw = CentsSats::from_bytes(&rest[0..16])?;
let investor_cap_raw = CentsSquaredSats::from_bytes(&rest[16..32])?;
pub(crate) fn map(&self) -> &CostBasisMap {
debug_assert!(self.pending.is_empty() && self.raw.pending_raw.is_zero());
&self.map.as_ref().unwrap().map
}
Ok(Self {
base,
cap_raw,
investor_cap_raw,
})
pub(crate) fn is_empty(&self) -> bool {
self.pending.is_empty() && self.map.as_ref().unwrap().map.is_empty()
}
pub(crate) fn for_each_pending(&self, mut f: impl FnMut(&CentsCompact, &PendingDelta)) {
self.pending.iter().for_each(|(k, v)| f(k, v));
}
pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState {
if self.is_empty() {
return UnrealizedState::ZERO;
}
let map = &self.map.as_ref().unwrap().map;
if let Some(cache) = self.cache.as_mut() {
cache.get_at_price(height_price, map)
} else {
let cache = CachedUnrealizedState::compute_fresh(height_price, map);
let state = cache.current_state();
self.cache = Some(cache);
state
}
}
fn apply_map_pending(&mut self) {
if self.pending.is_empty() {
return;
}
self.generation = self.generation.wrapping_add(1);
let map = &mut self.map.as_mut().unwrap().map;
for (cents, PendingDelta { inc, dec }) in self.pending.drain() {
match map.entry(cents) {
Entry::Occupied(mut e) => {
*e.get_mut() += inc;
if unlikely(*e.get() < dec) {
panic!(
"CostBasisData::apply_pending underflow!\n\
Path: {:?}\n\
Price: {}\n\
Current + increments: {}\n\
Trying to decrement by: {}",
self.raw.pathbuf,
cents.to_dollars(),
e.get(),
dec
);
}
*e.get_mut() -= dec;
if *e.get() == Sats::ZERO {
e.remove();
}
}
Entry::Vacant(e) => {
if unlikely(inc < dec) {
panic!(
"CostBasisData::apply_pending underflow (new entry)!\n\
Path: {:?}\n\
Price: {}\n\
Increment: {}\n\
Trying to decrement by: {}",
self.raw.pathbuf,
cents.to_dollars(),
inc,
dec
);
}
let val = inc - dec;
if val != Sats::ZERO {
e.insert(val);
}
}
}
}
}
}
impl CostBasisOps for CostBasisData {
fn create(path: &Path, name: &str) -> Self {
Self {
raw: CostBasisRaw::create(path, name),
map: None,
pending: FxHashMap::default(),
cache: None,
rounding_digits: None,
generation: 0,
}
}
fn with_price_rounding(mut self, digits: i32) -> Self {
self.rounding_digits = Some(digits);
self
}
fn import_at_or_before(&mut self, height: Height) -> Result<Height> {
let files = self.raw.read_dir(None)?;
let (&height, path) = files.range(..=height).next_back().ok_or(Error::NotFound(
"No cost basis state found at or before height".into(),
))?;
let data = fs::read(path)?;
let (base, rest) = CostBasisDistribution::deserialize_with_rest(&data)?;
self.map = Some(base);
self.raw.state = Some(RawState::deserialize(rest)?);
self.pending.clear();
self.raw.pending_raw = PendingRaw::default();
self.cache = None;
Ok(height)
}
fn cap_raw(&self) -> CentsSats {
self.raw.cap_raw()
}
fn investor_cap_raw(&self) -> CentsSquaredSats {
self.raw.investor_cap_raw()
}
#[inline]
fn increment(
&mut self,
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
self.pending.entry(price.into()).or_default().inc += sats;
self.raw.pending_raw.cap_inc += price_sats;
if investor_cap != CentsSquaredSats::ZERO {
self.raw.pending_raw.investor_cap_inc += investor_cap;
}
if let Some(cache) = self.cache.as_mut() {
cache.on_receive(price, sats);
}
}
#[inline]
fn decrement(
&mut self,
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
self.pending.entry(price.into()).or_default().dec += sats;
self.raw.pending_raw.cap_dec += price_sats;
if investor_cap != CentsSquaredSats::ZERO {
self.raw.pending_raw.investor_cap_dec += investor_cap;
}
if let Some(cache) = self.cache.as_mut() {
cache.on_send(price, sats);
}
}
fn apply_pending(&mut self) {
self.apply_map_pending();
self.raw.apply_pending_raw();
}
fn init(&mut self) {
self.raw.init();
self.map.replace(CostBasisDistribution::default());
self.pending.clear();
self.cache = None;
}
fn clean(&mut self) -> Result<()> {
self.raw.clean()?;
self.cache = None;
Ok(())
}
fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {
self.apply_pending();
self.raw.write_and_cleanup(height, cleanup)?;
let raw_state = self.raw.state.as_ref().unwrap();
let mut buffer = self.map.as_ref().unwrap().serialize()?;
buffer.extend(raw_state.cap_raw.to_bytes());
fs::write(self.raw.path_state(height), buffer)?;
Ok(())
}
}

View File

@@ -2307,6 +2307,35 @@ function create_1m1w1y24hBpsPercentRatioPattern(client, acc) {
};
}
/**
* @typedef {Object} CoindaysCoinyearsDormancySentVelocityPattern
* @property {CumulativeRawSumPattern<StoredF64>} coindaysDestroyed
* @property {MetricPattern1<StoredF32>} coindaysDestroyedSupplyAdjusted
* @property {MetricPattern1<StoredF64>} coinyearsDestroyed
* @property {MetricPattern1<StoredF32>} coinyearsDestroyedSupplyAdjusted
* @property {MetricPattern1<StoredF32>} dormancy
* @property {RawSumPattern3<Sats>} sent
* @property {MetricPattern1<StoredF32>} velocity
*/
/**
* Create a CoindaysCoinyearsDormancySentVelocityPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {CoindaysCoinyearsDormancySentVelocityPattern}
*/
function createCoindaysCoinyearsDormancySentVelocityPattern(client, acc) {
return {
coindaysDestroyed: createCumulativeRawSumPattern(client, _m(acc, 'coindays_destroyed')),
coindaysDestroyedSupplyAdjusted: createMetricPattern1(client, _m(acc, 'coindays_destroyed_supply_adjusted')),
coinyearsDestroyed: createMetricPattern1(client, _m(acc, 'coinyears_destroyed')),
coinyearsDestroyedSupplyAdjusted: createMetricPattern1(client, _m(acc, 'coinyears_destroyed_supply_adjusted')),
dormancy: createMetricPattern1(client, _m(acc, 'dormancy')),
sent: createRawSumPattern3(client, _m(acc, 'sent')),
velocity: createMetricPattern1(client, _m(acc, 'velocity')),
};
}
/**
* @typedef {Object} CumulativeDistributionRawRelSumValuePattern
* @property {MetricPattern1<Cents>} cumulative
@@ -3030,29 +3059,6 @@ function createCentsRelUsdPattern2(client, acc) {
};
}
/**
* @typedef {Object} CoindaysDormancySentVelocityPattern
* @property {CumulativeRawSumPattern<StoredF64>} coindaysDestroyed
* @property {MetricPattern1<StoredF32>} dormancy
* @property {RawSumPattern3<Sats>} sent
* @property {MetricPattern1<StoredF32>} velocity
*/
/**
* Create a CoindaysDormancySentVelocityPattern pattern node
* @param {BrkClientBase} client
* @param {string} acc - Accumulated metric name
* @returns {CoindaysDormancySentVelocityPattern}
*/
function createCoindaysDormancySentVelocityPattern(client, acc) {
return {
coindaysDestroyed: createCumulativeRawSumPattern(client, _m(acc, 'coindays_destroyed')),
dormancy: createMetricPattern1(client, _m(acc, 'dormancy')),
sent: createRawSumPattern3(client, _m(acc, 'sent')),
velocity: createMetricPattern1(client, _m(acc, 'velocity')),
};
}
/**
* @typedef {Object} CumulativeNegativeRawSumPattern
* @property {MetricPattern1<Cents>} cumulative
@@ -4750,6 +4756,7 @@ function createRawPattern(client, acc) {
* @property {CentsUsdPattern} vaultedCap
* @property {CentsUsdPattern} activeCap
* @property {CentsUsdPattern} cointimeCap
* @property {BpsRatioPattern} aviv
*/
/**
@@ -5859,7 +5866,7 @@ function createRawPattern(client, acc) {
* @typedef {Object} MetricsTree_Distribution_UtxoCohorts_All
* @property {MetricsTree_Distribution_UtxoCohorts_All_Supply} supply
* @property {UtxoPattern3} outputs
* @property {CoindaysDormancySentVelocityPattern} activity
* @property {CoindaysCoinyearsDormancySentVelocityPattern} activity
* @property {CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern} realized
* @property {InvestedMaxMinPercentilesPattern} costBasis
* @property {MetricsTree_Distribution_UtxoCohorts_All_Unrealized} unrealized
@@ -5914,7 +5921,7 @@ function createRawPattern(client, acc) {
* @property {CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern} realized
* @property {DeltaHalvedRelTotalPattern2} supply
* @property {UtxoPattern3} outputs
* @property {CoindaysDormancySentVelocityPattern} activity
* @property {CoindaysCoinyearsDormancySentVelocityPattern} activity
* @property {InvestedMaxMinPercentilesPattern} costBasis
* @property {GrossInvestedInvestorLossNetProfitSentimentPattern2} unrealized
*/
@@ -5923,7 +5930,7 @@ function createRawPattern(client, acc) {
* @typedef {Object} MetricsTree_Distribution_UtxoCohorts_Lth
* @property {DeltaHalvedRelTotalPattern2} supply
* @property {UtxoPattern3} outputs
* @property {CoindaysDormancySentVelocityPattern} activity
* @property {CoindaysCoinyearsDormancySentVelocityPattern} activity
* @property {MetricsTree_Distribution_UtxoCohorts_Lth_Realized} realized
* @property {InvestedMaxMinPercentilesPattern} costBasis
* @property {GrossInvestedInvestorLossNetProfitSentimentPattern2} unrealized
@@ -7580,6 +7587,7 @@ class BrkClient extends BrkClientBase {
vaultedCap: createCentsUsdPattern(this, 'vaulted_cap'),
activeCap: createCentsUsdPattern(this, 'active_cap'),
cointimeCap: createCentsUsdPattern(this, 'cointime_cap'),
aviv: createBpsRatioPattern(this, 'aviv_ratio'),
},
pricing: {
vaultedPrice: createCentsSatsUsdPattern(this, 'vaulted_price'),
@@ -8349,7 +8357,7 @@ class BrkClient extends BrkClientBase {
halved: createBtcCentsSatsUsdPattern(this, 'supply_halved'),
},
outputs: createUtxoPattern3(this, 'utxo_count'),
activity: createCoindaysDormancySentVelocityPattern(this, ''),
activity: createCoindaysCoinyearsDormancySentVelocityPattern(this, ''),
realized: createCapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern(this, ''),
costBasis: createInvestedMaxMinPercentilesPattern(this, ''),
unrealized: {
@@ -8383,14 +8391,14 @@ class BrkClient extends BrkClientBase {
realized: createCapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern(this, 'sth'),
supply: createDeltaHalvedRelTotalPattern2(this, 'sth_supply'),
outputs: createUtxoPattern3(this, 'sth_utxo_count'),
activity: createCoindaysDormancySentVelocityPattern(this, 'sth'),
activity: createCoindaysCoinyearsDormancySentVelocityPattern(this, 'sth'),
costBasis: createInvestedMaxMinPercentilesPattern(this, 'sth'),
unrealized: createGrossInvestedInvestorLossNetProfitSentimentPattern2(this, 'sth'),
},
lth: {
supply: createDeltaHalvedRelTotalPattern2(this, 'lth_supply'),
outputs: createUtxoPattern3(this, 'lth_utxo_count'),
activity: createCoindaysDormancySentVelocityPattern(this, 'lth'),
activity: createCoindaysCoinyearsDormancySentVelocityPattern(this, 'lth'),
realized: {
profit: createCumulativeDistributionRawRelSumValuePattern(this, 'lth'),
loss: createCapitulationCumulativeNegativeRawRelSumValuePattern(this, 'lth'),

View File

@@ -2423,6 +2423,19 @@ class _1m1w1y24hBpsPercentRatioPattern:
self.percent: MetricPattern1[StoredF32] = MetricPattern1(client, acc)
self.ratio: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'ratio'))
class CoindaysCoinyearsDormancySentVelocityPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.coindays_destroyed: CumulativeRawSumPattern[StoredF64] = CumulativeRawSumPattern(client, _m(acc, 'coindays_destroyed'))
self.coindays_destroyed_supply_adjusted: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'coindays_destroyed_supply_adjusted'))
self.coinyears_destroyed: MetricPattern1[StoredF64] = MetricPattern1(client, _m(acc, 'coinyears_destroyed'))
self.coinyears_destroyed_supply_adjusted: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'coinyears_destroyed_supply_adjusted'))
self.dormancy: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'dormancy'))
self.sent: RawSumPattern3[Sats] = RawSumPattern3(client, _m(acc, 'sent'))
self.velocity: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'velocity'))
class CumulativeDistributionRawRelSumValuePattern:
"""Pattern struct for repeated tree structure."""
@@ -2740,16 +2753,6 @@ class CentsRelUsdPattern2:
self.rel_to_own_market_cap: BpsPercentRatioPattern = BpsPercentRatioPattern(client, _m(acc, 'rel_to_own_market_cap'))
self.usd: MetricPattern1[Dollars] = MetricPattern1(client, acc)
class CoindaysDormancySentVelocityPattern:
"""Pattern struct for repeated tree structure."""
def __init__(self, client: BrkClientBase, acc: str):
"""Create pattern node with accumulated metric name."""
self.coindays_destroyed: CumulativeRawSumPattern[StoredF64] = CumulativeRawSumPattern(client, _m(acc, 'coindays_destroyed'))
self.dormancy: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'dormancy'))
self.sent: RawSumPattern3[Sats] = RawSumPattern3(client, _m(acc, 'sent'))
self.velocity: MetricPattern1[StoredF32] = MetricPattern1(client, _m(acc, 'velocity'))
class CumulativeNegativeRawSumPattern:
"""Pattern struct for repeated tree structure."""
@@ -3704,6 +3707,7 @@ class MetricsTree_Cointime_Cap:
self.vaulted_cap: CentsUsdPattern = CentsUsdPattern(client, 'vaulted_cap')
self.active_cap: CentsUsdPattern = CentsUsdPattern(client, 'active_cap')
self.cointime_cap: CentsUsdPattern = CentsUsdPattern(client, 'cointime_cap')
self.aviv: BpsRatioPattern = BpsRatioPattern(client, 'aviv_ratio')
class MetricsTree_Cointime_Pricing:
"""Metrics tree node."""
@@ -4946,7 +4950,7 @@ class MetricsTree_Distribution_UtxoCohorts_All:
def __init__(self, client: BrkClientBase, base_path: str = ''):
self.supply: MetricsTree_Distribution_UtxoCohorts_All_Supply = MetricsTree_Distribution_UtxoCohorts_All_Supply(client)
self.outputs: UtxoPattern3 = UtxoPattern3(client, 'utxo_count')
self.activity: CoindaysDormancySentVelocityPattern = CoindaysDormancySentVelocityPattern(client, '')
self.activity: CoindaysCoinyearsDormancySentVelocityPattern = CoindaysCoinyearsDormancySentVelocityPattern(client, '')
self.realized: CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern = CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern(client, '')
self.cost_basis: InvestedMaxMinPercentilesPattern = InvestedMaxMinPercentilesPattern(client, '')
self.unrealized: MetricsTree_Distribution_UtxoCohorts_All_Unrealized = MetricsTree_Distribution_UtxoCohorts_All_Unrealized(client)
@@ -4958,7 +4962,7 @@ class MetricsTree_Distribution_UtxoCohorts_Sth:
self.realized: CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern = CapGrossInvestorLossMvrvNetNuplPeakPriceProfitSentSoprPattern(client, 'sth')
self.supply: DeltaHalvedRelTotalPattern2 = DeltaHalvedRelTotalPattern2(client, 'sth_supply')
self.outputs: UtxoPattern3 = UtxoPattern3(client, 'sth_utxo_count')
self.activity: CoindaysDormancySentVelocityPattern = CoindaysDormancySentVelocityPattern(client, 'sth')
self.activity: CoindaysCoinyearsDormancySentVelocityPattern = CoindaysCoinyearsDormancySentVelocityPattern(client, 'sth')
self.cost_basis: InvestedMaxMinPercentilesPattern = InvestedMaxMinPercentilesPattern(client, 'sth')
self.unrealized: GrossInvestedInvestorLossNetProfitSentimentPattern2 = GrossInvestedInvestorLossNetProfitSentimentPattern2(client, 'sth')
@@ -5009,7 +5013,7 @@ class MetricsTree_Distribution_UtxoCohorts_Lth:
def __init__(self, client: BrkClientBase, base_path: str = ''):
self.supply: DeltaHalvedRelTotalPattern2 = DeltaHalvedRelTotalPattern2(client, 'lth_supply')
self.outputs: UtxoPattern3 = UtxoPattern3(client, 'lth_utxo_count')
self.activity: CoindaysDormancySentVelocityPattern = CoindaysDormancySentVelocityPattern(client, 'lth')
self.activity: CoindaysCoinyearsDormancySentVelocityPattern = CoindaysCoinyearsDormancySentVelocityPattern(client, 'lth')
self.realized: MetricsTree_Distribution_UtxoCohorts_Lth_Realized = MetricsTree_Distribution_UtxoCohorts_Lth_Realized(client)
self.cost_basis: InvestedMaxMinPercentilesPattern = InvestedMaxMinPercentilesPattern(client, 'lth')
self.unrealized: GrossInvestedInvestorLossNetProfitSentimentPattern2 = GrossInvestedInvestorLossNetProfitSentimentPattern2(client, 'lth')