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