diff --git a/crates/brk_computer/src/distribution/cohorts/address/groups.rs b/crates/brk_computer/src/distribution/cohorts/address/groups.rs index 3e6d6ec27..b659561f4 100644 --- a/crates/brk_computer/src/distribution/cohorts/address/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/address/groups.rs @@ -5,10 +5,10 @@ use brk_cohort::{ }; use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Dollars, Height, Indexes, Sats, Version}; +use brk_types::{Height, Indexes, Version}; use derive_more::{Deref, DerefMut}; use rayon::prelude::*; -use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode}; +use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode}; use crate::{blocks, distribution::DynCohortVecs, indexes, prices}; @@ -111,29 +111,15 @@ impl AddressCohorts { } /// Second phase of post-processing: compute relative metrics. - pub(crate) fn compute_rest_part2( + pub(crate) fn compute_rest_part2( &mut self, - blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &Indexes, - height_to_market_cap: &HM, - all_supply_sats: &AS, exit: &Exit, - ) -> Result<()> - where - HM: ReadableVec + Sync, - AS: ReadableVec + Sync, - { - self.0.par_iter_mut().try_for_each(|v| { - v.compute_rest_part2( - blocks, - prices, - starting_indexes, - height_to_market_cap, - all_supply_sats, - exit, - ) - }) + ) -> Result<()> { + self.0 + .par_iter_mut() + .try_for_each(|v| v.compute_rest_part2(prices, starting_indexes, exit)) } /// Returns a parallel iterator over all vecs for parallel writing. diff --git a/crates/brk_computer/src/distribution/cohorts/address/vecs.rs b/crates/brk_computer/src/distribution/cohorts/address/vecs.rs index 511170381..e14fdb82b 100644 --- a/crates/brk_computer/src/distribution/cohorts/address/vecs.rs +++ b/crates/brk_computer/src/distribution/cohorts/address/vecs.rs @@ -3,19 +3,19 @@ use std::path::Path; use brk_cohort::{CohortContext, Filter, Filtered}; use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Cents, Dollars, Height, Indexes, Sats, StoredF64, StoredU64, Version}; +use brk_types::{Cents, Height, Indexes, StoredF64, StoredU64, Version}; use rayon::prelude::*; use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec}; use crate::{ blocks, - distribution::state::{AddressCohortState, CoreRealizedState}, + distribution::state::{AddressCohortState, MinimalRealizedState}, indexes, internal::ComputedFromHeight, prices, }; -use crate::distribution::metrics::{CoreCohortMetrics, ImportConfig}; +use crate::distribution::metrics::{ImportConfig, MinimalCohortMetrics}; use super::super::traits::{CohortVecs, DynCohortVecs}; #[derive(Traversable)] @@ -23,10 +23,10 @@ pub struct AddressCohortVecs { starting_height: Option, #[traversable(skip)] - pub state: Option>>, + pub state: Option>>, #[traversable(flatten)] - pub metrics: CoreCohortMetrics, + pub metrics: MinimalCohortMetrics, pub addr_count: ComputedFromHeight, pub addr_count_change_1m: ComputedFromHeight, @@ -56,7 +56,7 @@ impl AddressCohortVecs { state: states_path.map(|path| Box::new(AddressCohortState::new(path, &full_name))), - metrics: CoreCohortMetrics::forced_import(&cfg)?, + metrics: MinimalCohortMetrics::forced_import(&cfg)?, addr_count: ComputedFromHeight::forced_import( db, @@ -261,21 +261,11 @@ impl CohortVecs for AddressCohortVecs { fn compute_rest_part2( &mut self, - blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &Indexes, - height_to_market_cap: &impl ReadableVec, - all_supply_sats: &impl ReadableVec, exit: &Exit, ) -> Result<()> { - self.metrics.compute_rest_part2( - blocks, - prices, - starting_indexes, - height_to_market_cap, - all_supply_sats, - exit, - )?; - Ok(()) + self.metrics + .compute_rest_part2(prices, starting_indexes, exit) } } diff --git a/crates/brk_computer/src/distribution/cohorts/traits.rs b/crates/brk_computer/src/distribution/cohorts/traits.rs index b1f8c4eff..c4f8aaee0 100644 --- a/crates/brk_computer/src/distribution/cohorts/traits.rs +++ b/crates/brk_computer/src/distribution/cohorts/traits.rs @@ -1,6 +1,6 @@ use brk_error::Result; -use brk_types::{Cents, Dollars, Height, Indexes, Sats, Version}; -use vecdb::{Exit, ReadableVec}; +use brk_types::{Cents, Height, Indexes, Version}; +use vecdb::Exit; use crate::{blocks, prices}; @@ -65,11 +65,8 @@ pub trait CohortVecs: DynCohortVecs { /// Second phase of post-processing computations. fn compute_rest_part2( &mut self, - blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &Indexes, - height_to_market_cap: &impl ReadableVec, - all_supply_sats: &impl ReadableVec, exit: &Exit, ) -> Result<()>; } diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs index a1289bf4f..91ed1c563 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs @@ -13,14 +13,14 @@ use vecdb::{AnyStoredVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw, Storag use crate::{blocks, distribution::DynCohortVecs, indexes, prices}; use crate::distribution::metrics::{ - AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CompleteCohortMetrics, + AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics, ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig, MinimalCohortMetrics, SupplyMetrics, }; use super::{percentiles::PercentileCache, vecs::UTXOCohortVecs}; -use crate::distribution::state::{CoreRealizedState, RealizedState, UTXOCohortState}; +use crate::distribution::state::{CoreRealizedState, MinimalRealizedState, RealizedState, UTXOCohortState}; const VERSION: Version = Version::new(0); @@ -40,14 +40,14 @@ pub struct UTXOCohorts { pub sth: UTXOCohortVecs, RealizedState>, pub lth: UTXOCohortVecs, RealizedState>, pub age_range: ByAgeRange, RealizedState>>, - pub max_age: ByMaxAge, RealizedState>>, - pub min_age: ByMinAge, RealizedState>>, - pub ge_amount: ByGreatEqualAmount, CoreRealizedState>>, - pub amount_range: ByAmountRange, CoreRealizedState>>, - pub lt_amount: ByLowerThanAmount, CoreRealizedState>>, + pub max_age: ByMaxAge, CoreRealizedState>>, + pub min_age: ByMinAge, CoreRealizedState>>, + pub ge_amount: ByGreatEqualAmount, MinimalRealizedState>>, + pub amount_range: ByAmountRange, MinimalRealizedState>>, + pub lt_amount: ByLowerThanAmount, MinimalRealizedState>>, pub epoch: ByEpoch, CoreRealizedState>>, pub class: ByClass, CoreRealizedState>>, - pub type_: BySpendableType, CoreRealizedState>>, + pub type_: BySpendableType, MinimalRealizedState>>, #[traversable(skip)] pub(super) percentile_cache: PercentileCache, /// Cached partition_point positions for tick_tock boundary searches. @@ -121,12 +121,12 @@ impl UTXOCohorts { )) }; - let amount_range = ByAmountRange::try_new(&core_separate)?; let epoch = ByEpoch::try_new(&core_separate)?; let class = ByClass::try_new(&core_separate)?; - let type_ = BySpendableType::try_new( - &|f: Filter, name: &'static str| -> Result> { + // Helper for separate cohorts with MinimalCohortMetrics + MinimalRealizedState + let minimal_separate = + |f: Filter, name: &'static str| -> Result> { let full_name = CohortContext::Utxo.full_name(&f, name); let cfg = ImportConfig { db, @@ -140,8 +140,10 @@ impl UTXOCohorts { state, MinimalCohortMetrics::forced_import(&cfg)?, )) - }, - )?; + }; + + let amount_range = ByAmountRange::try_new(&minimal_separate)?; + let type_ = BySpendableType::try_new(&minimal_separate)?; // Phase 3: Import "all" cohort with pre-imported supply. let all = UTXOCohortVecs::new( @@ -179,43 +181,7 @@ impl UTXOCohorts { UTXOCohortVecs::new(None, ExtendedCohortMetrics::forced_import(&cfg)?) }; - // max_age: CompleteCohortMetrics (no state, aggregates from age_range) - let max_age = { - ByMaxAge::try_new(&|f: Filter, name: &'static str| -> Result> { - let full_name = CohortContext::Utxo.full_name(&f, name); - let cfg = ImportConfig { - db, - filter: &f, - full_name: &full_name, - version: v, - indexes, - }; - Ok(UTXOCohortVecs::new( - None, - CompleteCohortMetrics::forced_import(&cfg)?, - )) - })? - }; - - // min_age: CompleteCohortMetrics - let min_age = { - ByMinAge::try_new(&|f: Filter, name: &'static str| -> Result> { - let full_name = CohortContext::Utxo.full_name(&f, name); - let cfg = ImportConfig { - db, - filter: &f, - full_name: &full_name, - version: v, - indexes, - }; - Ok(UTXOCohortVecs::new( - None, - CompleteCohortMetrics::forced_import(&cfg)?, - )) - })? - }; - - // ge_amount, lt_amount: CoreCohortMetrics (no state) + // CoreCohortMetrics without state (no state, for aggregate cohorts) let core_no_state = |f: Filter, name: &'static str| -> Result> { let full_name = CohortContext::Utxo.full_name(&f, name); @@ -232,8 +198,31 @@ impl UTXOCohorts { )) }; - let lt_amount = ByLowerThanAmount::try_new(&core_no_state)?; - let ge_amount = ByGreatEqualAmount::try_new(&core_no_state)?; + // max_age: CoreCohortMetrics (no state, aggregates from age_range) + let max_age = ByMaxAge::try_new(&core_no_state)?; + + // min_age: CoreCohortMetrics (no state, aggregates from age_range) + let min_age = ByMinAge::try_new(&core_no_state)?; + + // MinimalCohortMetrics without state (for aggregate amount cohorts) + let minimal_no_state = + |f: Filter, name: &'static str| -> Result> { + let full_name = CohortContext::Utxo.full_name(&f, name); + let cfg = ImportConfig { + db, + filter: &f, + full_name: &full_name, + version: v, + indexes, + }; + Ok(UTXOCohortVecs::new( + None, + MinimalCohortMetrics::forced_import(&cfg)?, + )) + }; + + let lt_amount = ByLowerThanAmount::try_new(&minimal_no_state)?; + let ge_amount = ByGreatEqualAmount::try_new(&minimal_no_state)?; Ok(Self { all, @@ -314,18 +303,18 @@ impl UTXOCohorts { Box::new(|| { min_age.par_iter_mut().try_for_each(|vecs| { let sources = filter_sources_from(ar.iter(), Some(&vecs.metrics.filter)); - vecs.metrics.compute_from_sources(si, &sources, exit) + vecs.metrics.compute_from_base_sources(si, &sources, exit) }) }), Box::new(|| { max_age.par_iter_mut().try_for_each(|vecs| { let sources = filter_sources_from(ar.iter(), Some(&vecs.metrics.filter)); - vecs.metrics.compute_from_sources(si, &sources, exit) + vecs.metrics.compute_from_base_sources(si, &sources, exit) }) }), Box::new(|| { ge_amount.par_iter_mut().chain(lt_amount.par_iter_mut()).try_for_each(|vecs| { - let sources = filter_core_sources_from(amr.iter(), Some(&vecs.metrics.filter)); + let sources = filter_minimal_sources_from(amr.iter(), Some(&vecs.metrics.filter)); vecs.metrics.compute_from_sources(si, &sources, exit) }) }), @@ -384,8 +373,8 @@ impl UTXOCohorts { // 2. Compute net_sentiment.height for aggregate cohorts (weighted average). // Separate cohorts already computed net_sentiment in step 1 (inside compute_rest_part1). - // Note: min_age, max_age, epoch, class are Complete tier — no net_sentiment. - // Note: ge_amount, lt_amount, amount_range are Core tier — no net_sentiment. + // Note: min_age, max_age, epoch, class are Core tier — no net_sentiment. + // Note: ge_amount, lt_amount, amount_range are Minimal tier — no net_sentiment. { let Self { all, sth, lth, age_range, @@ -481,11 +470,11 @@ impl UTXOCohorts { Box::new(|| age_range.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))), Box::new(|| max_age.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))), Box::new(|| min_age.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))), - Box::new(|| ge_amount.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))), + Box::new(|| ge_amount.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))), Box::new(|| epoch.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))), Box::new(|| class.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))), - Box::new(|| amount_range.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))), - Box::new(|| lt_amount.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, ss, exit))), + Box::new(|| amount_range.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))), + Box::new(|| lt_amount.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))), Box::new(|| type_.par_iter_mut().try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, exit))), ]; @@ -612,11 +601,11 @@ fn filter_sources_from<'a, M: CohortMetricsBase + 'a>( } } -/// Filter CoreCohortMetrics source cohorts by an optional filter. -fn filter_core_sources_from<'a>( - sources: impl Iterator>, +/// Filter MinimalCohortMetrics source cohorts by an optional filter. +fn filter_minimal_sources_from<'a>( + sources: impl Iterator>, filter: Option<&Filter>, -) -> Vec<&'a CoreCohortMetrics> { +) -> Vec<&'a MinimalCohortMetrics> { match filter { Some(f) => sources .filter(|v| f.includes(&v.metrics.filter)) diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs b/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs index 2a1423109..91e822701 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs @@ -7,9 +7,9 @@ use vecdb::{Exit, ReadableVec}; use crate::{blocks, distribution::state::UTXOCohortState, prices}; use crate::distribution::metrics::{ - CohortMetricsBase, CompleteCohortMetrics, CoreCohortMetrics, MinimalCohortMetrics, + CohortMetricsBase, CoreCohortMetrics, MinimalCohortMetrics, }; -use crate::distribution::state::{CoreRealizedState, RealizedOps, RealizedState}; +use crate::distribution::state::{CoreRealizedState, MinimalRealizedState, RealizedOps, RealizedState}; use super::super::traits::DynCohortVecs; @@ -225,15 +225,15 @@ macro_rules! impl_import_state { }; } -// --- MinimalCohortMetrics: uses CoreRealizedState --- +// --- MinimalCohortMetrics: uses MinimalRealizedState --- -impl Filtered for UTXOCohortVecs { +impl Filtered for UTXOCohortVecs { fn filter(&self) -> &Filter { &self.metrics.filter } } -impl DynCohortVecs for UTXOCohortVecs { +impl DynCohortVecs for UTXOCohortVecs { fn min_stateful_height_len(&self) -> usize { self.metrics.min_stateful_height_len() } @@ -260,9 +260,10 @@ impl DynCohortVecs for UTXOCohortVecs { self.metrics .outputs .truncate_push(height, state.supply.utxo_count)?; + self.metrics.activity.truncate_push(height, state.sent)?; self.metrics .realized - .truncate_push(height, state.realized.cap())?; + .truncate_push(height, &state.realized)?; } Ok(()) @@ -392,94 +393,3 @@ impl DynCohortVecs for UTXOCohortVecs { } } -// --- CompleteCohortMetrics: uses full RealizedState --- - -impl Filtered for UTXOCohortVecs { - fn filter(&self) -> &Filter { - &self.metrics.filter - } -} - -impl DynCohortVecs for UTXOCohortVecs { - fn min_stateful_height_len(&self) -> usize { - self.metrics.min_stateful_height_len() - } - - fn reset_state_starting_height(&mut self) { - self.reset_state_impl(); - } - - impl_import_state!(); - - fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { - self.metrics.validate_computed_versions(base_version) - } - - fn truncate_push(&mut self, height: Height) -> Result<()> { - if self.state_starting_height.is_some_and(|h| h > height) { - return Ok(()); - } - - if let Some(state) = self.state.as_ref() { - self.metrics - .supply - .truncate_push(height, state.supply.value)?; - self.metrics - .outputs - .truncate_push(height, state.supply.utxo_count)?; - self.metrics.activity.truncate_push( - height, - state.sent, - state.satblocks_destroyed, - state.satdays_destroyed, - )?; - self.metrics - .realized - .truncate_push(height, &state.realized)?; - } - - Ok(()) - } - - fn compute_then_truncate_push_unrealized_states( - &mut self, - height: Height, - height_price: Cents, - _is_day_boundary: bool, - ) -> Result<()> { - if let Some(state) = self.state.as_mut() { - state.apply_pending(); - self.metrics - .cost_basis - .truncate_push_minmax(height, state)?; - let unrealized_state = state.compute_unrealized_state(height_price); - self.metrics - .unrealized - .truncate_push(height, &unrealized_state)?; - } - Ok(()) - } - - fn compute_rest_part1( - &mut self, - blocks: &blocks::Vecs, - prices: &prices::Vecs, - starting_indexes: &Indexes, - exit: &Exit, - ) -> Result<()> { - self.metrics - .compute_rest_part1(blocks, prices, starting_indexes, exit) - } - - fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> { - self.write_state_impl(height, cleanup) - } - - fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> { - self.reset_cost_basis_impl() - } - - fn reset_single_iteration_values(&mut self) { - self.reset_iteration_impl(); - } -} diff --git a/crates/brk_computer/src/distribution/compute/aggregates.rs b/crates/brk_computer/src/distribution/compute/aggregates.rs index f780492b3..d32aed210 100644 --- a/crates/brk_computer/src/distribution/compute/aggregates.rs +++ b/crates/brk_computer/src/distribution/compute/aggregates.rs @@ -70,14 +70,7 @@ where exit, )?; - address_cohorts.compute_rest_part2( - blocks, - prices, - starting_indexes, - height_to_market_cap, - &utxo_cohorts.all.metrics.supply.total.sats.height, - exit, - )?; + address_cohorts.compute_rest_part2(prices, starting_indexes, exit)?; Ok(()) } diff --git a/crates/brk_computer/src/distribution/metrics/cohort/all.rs b/crates/brk_computer/src/distribution/metrics/cohort/all.rs index 036c83118..29fc115d3 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/all.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/all.rs @@ -1,11 +1,13 @@ use brk_cohort::Filter; use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Cents, Dollars, Height, Indexes}; +use brk_types::{Bitcoin, Cents, Dollars, Height, Indexes, StoredF32, Version}; use vecdb::{Exit, ReadableVec, Rw, StorageMode}; use crate::{blocks, prices}; +use crate::internal::ComputedFromHeight; + use crate::distribution::metrics::{ ActivityMetrics, CostBasisWithExtended, ImportConfig, OutputsMetrics, RealizedAdjusted, RealizedWithExtended, RelativeForAll, SupplyMetrics, UnrealizedFull, @@ -26,6 +28,8 @@ pub struct AllCohortMetrics { pub unrealized: Box>, pub adjusted: Box>, pub relative: Box>, + pub dormancy: ComputedFromHeight, + pub velocity: ComputedFromHeight, } impl_cohort_metrics_base!(AllCohortMetrics, extended_cost_basis); @@ -55,6 +59,8 @@ impl AllCohortMetrics { unrealized: Box::new(unrealized), adjusted: Box::new(adjusted), relative: Box::new(relative), + dormancy: cfg.import_computed("dormancy", Version::ONE)?, + velocity: cfg.import_computed("velocity", Version::ONE)?, }) } @@ -97,6 +103,36 @@ impl AllCohortMetrics { exit, )?; + self.dormancy.height.compute_transform2( + starting_indexes.height, + &self.activity.coindays_destroyed.height, + &self.activity.sent.base.sats.height, + |(i, cdd, sent_sats, ..)| { + let sent_btc = f64::from(Bitcoin::from(sent_sats)); + if sent_btc == 0.0 { + (i, StoredF32::from(0.0f32)) + } else { + (i, StoredF32::from((f64::from(cdd) / sent_btc) as f32)) + } + }, + exit, + )?; + + self.velocity.height.compute_transform2( + starting_indexes.height, + &self.activity.sent.base.sats.height, + &self.supply.total.sats.height, + |(i, sent_sats, supply_sats, ..)| { + let supply = supply_sats.as_u128() as f64; + if supply == 0.0 { + (i, StoredF32::from(0.0f32)) + } else { + (i, StoredF32::from((sent_sats.as_u128() as f64 / supply) as f32)) + } + }, + exit, + )?; + Ok(()) } } diff --git a/crates/brk_computer/src/distribution/metrics/cohort/complete.rs b/crates/brk_computer/src/distribution/metrics/cohort/complete.rs deleted file mode 100644 index ed3bd8874..000000000 --- a/crates/brk_computer/src/distribution/metrics/cohort/complete.rs +++ /dev/null @@ -1,196 +0,0 @@ -use brk_cohort::Filter; -use brk_error::Result; -use brk_traversable::Traversable; -use brk_types::{Dollars, Height, Indexes, Sats, Version}; -use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; - -use crate::{blocks, prices}; - -use crate::distribution::metrics::{ - ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, - RealizedComplete, RelativeCompleteWithRelToAll, SupplyMetrics, UnrealizedComplete, -}; - -/// Complete cohort metrics (Tier C): ~216 stored vecs. -/// -/// Used for epoch, class, min_age, max_age cohorts. -/// Everything in Core, plus cost basis, CDD, value created/destroyed, -/// sent in profit/loss, net PnL change, etc. -/// -/// Does NOT include source-only fields (peak_regret, invested_capital, -/// raw BytesVecs) or extended-only fields (investor_price, sell_side_risk, -/// pain/greed/net_sentiment). -/// -/// Does NOT implement CohortMetricsBase — standalone, not usable as Source. -#[derive(Traversable)] -pub struct CompleteCohortMetrics { - #[traversable(skip)] - pub filter: Filter, - pub supply: Box>, - pub outputs: Box>, - pub activity: Box>, - pub realized: Box>, - pub cost_basis: Box>, - pub unrealized: Box>, - pub relative: Box>, -} - -impl CompleteCohortMetrics { - pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { - Ok(Self { - filter: cfg.filter.clone(), - supply: Box::new(SupplyMetrics::forced_import(cfg)?), - outputs: Box::new(OutputsMetrics::forced_import(cfg)?), - activity: Box::new(ActivityMetrics::forced_import(cfg)?), - realized: Box::new(RealizedComplete::forced_import(cfg)?), - cost_basis: Box::new(CostBasisBase::forced_import(cfg)?), - unrealized: Box::new(UnrealizedComplete::forced_import(cfg)?), - relative: Box::new(RelativeCompleteWithRelToAll::forced_import(cfg)?), - }) - } - - pub(crate) fn min_stateful_height_len(&self) -> usize { - self.supply - .min_len() - .min(self.outputs.min_len()) - .min(self.activity.min_len()) - .min(self.realized.min_stateful_height_len()) - .min(self.unrealized.min_stateful_height_len()) - .min(self.cost_basis.min_stateful_height_len()) - } - - pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { - self.supply.validate_computed_versions(base_version)?; - self.activity.validate_computed_versions(base_version)?; - Ok(()) - } - - pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { - let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); - vecs.extend(self.supply.collect_vecs_mut()); - vecs.extend(self.outputs.collect_vecs_mut()); - vecs.extend(self.activity.collect_vecs_mut()); - vecs.extend(self.realized.collect_vecs_mut()); - vecs.extend(self.cost_basis.collect_vecs_mut()); - vecs.extend(self.unrealized.collect_vecs_mut()); - vecs - } - - /// Aggregate Complete-tier metrics from Source cohort refs. - pub(crate) fn compute_from_sources( - &mut self, - starting_indexes: &Indexes, - others: &[&T], - exit: &Exit, - ) -> Result<()> { - // Supply, outputs, activity: use their existing compute_from_stateful - self.supply.compute_from_stateful( - starting_indexes, - &others.iter().map(|v| v.supply()).collect::>(), - exit, - )?; - self.outputs.compute_from_stateful( - starting_indexes, - &others.iter().map(|v| v.outputs()).collect::>(), - exit, - )?; - self.activity.compute_from_stateful( - starting_indexes, - &others.iter().map(|v| v.activity()).collect::>(), - exit, - )?; - - // Realized: aggregate only Complete-tier fields from Source's RealizedFull - let realized_complete_refs: Vec<&RealizedComplete> = others - .iter() - .map(|v| &v.realized_full().complete) - .collect(); - self.realized - .compute_from_stateful(starting_indexes, &realized_complete_refs, exit)?; - - // Unrealized: aggregate only Complete-tier fields - let unrealized_complete_refs: Vec<&UnrealizedComplete> = others - .iter() - .map(|v| &v.unrealized_full().complete) - .collect(); - self.unrealized - .compute_from_stateful(starting_indexes, &unrealized_complete_refs, exit)?; - - // Cost basis: use existing aggregation - self.cost_basis.compute_from_stateful( - starting_indexes, - &others - .iter() - .map(|v| v.cost_basis_base()) - .collect::>(), - exit, - )?; - - Ok(()) - } - - /// First phase: compute index transforms. - pub(crate) fn compute_rest_part1( - &mut self, - blocks: &blocks::Vecs, - prices: &prices::Vecs, - starting_indexes: &Indexes, - exit: &Exit, - ) -> Result<()> { - self.supply - .compute(prices, starting_indexes.height, exit)?; - self.supply - .compute_rest_part1(blocks, starting_indexes, exit)?; - self.outputs - .compute_rest(blocks, starting_indexes, exit)?; - self.activity - .sent - .compute(prices, starting_indexes.height, exit)?; - self.activity - .compute_rest_part1(blocks, prices, starting_indexes, exit)?; - - self.realized - .sent_in_profit - .compute(prices, starting_indexes.height, exit)?; - self.realized - .sent_in_loss - .compute(prices, starting_indexes.height, exit)?; - self.realized - .compute_rest_part1(starting_indexes, exit)?; - - self.unrealized.compute_rest(starting_indexes, exit)?; - - Ok(()) - } - - /// Second phase: compute relative metrics and remaining. - pub(crate) fn compute_rest_part2( - &mut self, - blocks: &blocks::Vecs, - prices: &prices::Vecs, - starting_indexes: &Indexes, - height_to_market_cap: &impl ReadableVec, - all_supply_sats: &impl ReadableVec, - exit: &Exit, - ) -> Result<()> { - self.realized.compute_rest_part2( - blocks, - prices, - starting_indexes, - &self.supply.total.btc.height, - height_to_market_cap, - exit, - )?; - - self.relative.compute( - starting_indexes.height, - &self.unrealized, - &self.supply.total.sats.height, - height_to_market_cap, - all_supply_sats, - exit, - )?; - - Ok(()) - } -} diff --git a/crates/brk_computer/src/distribution/metrics/cohort/core.rs b/crates/brk_computer/src/distribution/metrics/cohort/core.rs index c00f7b3b3..fd4bb395b 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/core.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/core.rs @@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; use crate::{blocks, prices}; use crate::distribution::metrics::{ - ActivityCore, RealizedCore, ImportConfig, OutputsMetrics, + ActivityCore, CohortMetricsBase, RealizedCore, ImportConfig, OutputsMetrics, RelativeCompleteWithRelToAll, SupplyMetrics, UnrealizedComplete, }; @@ -61,35 +61,36 @@ impl CoreCohortMetrics { vecs } - pub(crate) fn compute_from_sources( + /// Aggregate Core-tier fields from CohortMetricsBase sources (e.g. age_range → max_age/min_age). + pub(crate) fn compute_from_base_sources( &mut self, starting_indexes: &Indexes, - others: &[&CoreCohortMetrics], + others: &[&T], exit: &Exit, ) -> Result<()> { self.supply.compute_from_stateful( starting_indexes, - &others.iter().map(|v| v.supply.as_ref()).collect::>(), + &others.iter().map(|v| v.supply()).collect::>(), exit, )?; self.outputs.compute_from_stateful( starting_indexes, - &others.iter().map(|v| v.outputs.as_ref()).collect::>(), + &others.iter().map(|v| v.outputs()).collect::>(), exit, )?; self.activity.compute_from_stateful( starting_indexes, - &others.iter().map(|v| v.activity.as_ref()).collect::>(), + &others.iter().map(|v| &v.activity().core).collect::>(), exit, )?; self.realized.compute_from_stateful( starting_indexes, - &others.iter().map(|v| v.realized.as_ref()).collect::>(), + &others.iter().map(|v| &v.realized_full().core).collect::>(), exit, )?; self.unrealized.compute_from_stateful( starting_indexes, - &others.iter().map(|v| v.unrealized.as_ref()).collect::>(), + &others.iter().map(|v| &v.unrealized_full().complete).collect::>(), exit, )?; diff --git a/crates/brk_computer/src/distribution/metrics/cohort/extended.rs b/crates/brk_computer/src/distribution/metrics/cohort/extended.rs index e955e7867..f9b95544c 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/extended.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/extended.rs @@ -1,11 +1,13 @@ use brk_cohort::Filter; use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Dollars, Height, Indexes, Sats}; +use brk_types::{Bitcoin, Dollars, Height, Indexes, Sats, StoredF32, Version}; use vecdb::{Exit, ReadableVec, Rw, StorageMode}; use crate::{blocks, prices}; +use crate::internal::ComputedFromHeight; + use crate::distribution::metrics::{ ActivityMetrics, CostBasisWithExtended, ImportConfig, OutputsMetrics, RealizedWithExtended, RelativeWithExtended, SupplyMetrics, UnrealizedFull, @@ -24,6 +26,8 @@ pub struct ExtendedCohortMetrics { pub cost_basis: Box>, pub unrealized: Box>, pub relative: Box>, + pub dormancy: ComputedFromHeight, + pub velocity: ComputedFromHeight, } impl_cohort_metrics_base!(ExtendedCohortMetrics, extended_cost_basis); @@ -45,6 +49,8 @@ impl ExtendedCohortMetrics { cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?), unrealized: Box::new(unrealized), relative: Box::new(relative), + dormancy: cfg.import_computed("dormancy", Version::ONE)?, + velocity: cfg.import_computed("velocity", Version::ONE)?, }) } @@ -77,6 +83,36 @@ impl ExtendedCohortMetrics { exit, )?; + self.dormancy.height.compute_transform2( + starting_indexes.height, + &self.activity.coindays_destroyed.height, + &self.activity.sent.base.sats.height, + |(i, cdd, sent_sats, ..)| { + let sent_btc = f64::from(Bitcoin::from(sent_sats)); + if sent_btc == 0.0 { + (i, StoredF32::from(0.0f32)) + } else { + (i, StoredF32::from((f64::from(cdd) / sent_btc) as f32)) + } + }, + exit, + )?; + + self.velocity.height.compute_transform2( + starting_indexes.height, + &self.activity.sent.base.sats.height, + &self.supply.total.sats.height, + |(i, sent_sats, supply_sats, ..)| { + let supply = supply_sats.as_u128() as f64; + if supply == 0.0 { + (i, StoredF32::from(0.0f32)) + } else { + (i, StoredF32::from((sent_sats.as_u128() as f64 / supply) as f32)) + } + }, + exit, + )?; + Ok(()) } } diff --git a/crates/brk_computer/src/distribution/metrics/cohort/minimal.rs b/crates/brk_computer/src/distribution/metrics/cohort/minimal.rs index a365fde44..75c1d224f 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/minimal.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/minimal.rs @@ -10,19 +10,22 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableCloneableVec, ReadableVec, Rw, S use crate::{blocks, prices}; use crate::internal::{ - CentsUnsignedToDollars, ComputedFromHeight, ComputedFromHeightRatio, Identity, LazyFromHeight, - PercentFromHeight, Price, RatioSatsBp16, ValueFromHeight, + CentsUnsignedToDollars, ComputedFromHeight, ComputedFromHeightCumulative, + ComputedFromHeightRatio, Identity, LazyFromHeight, PercentFromHeight, Price, RatioSatsBp16, + ValueFromHeight, }; use crate::distribution::{ - metrics::{ImportConfig, OutputsMetrics, SupplyMetrics}, - state::UnrealizedState, + metrics::{ActivityCore, ImportConfig, OutputsMetrics, SupplyMetrics}, + state::{RealizedOps, UnrealizedState}, }; -/// Minimal realized metrics: realized cap, realized price, and MVRV ratio. +/// Minimal realized metrics: realized cap, realized price, MVRV, and realized P/L. #[derive(Traversable)] pub struct MinimalRealized { pub realized_cap_cents: ComputedFromHeight, + pub realized_profit: ComputedFromHeightCumulative, + pub realized_loss: ComputedFromHeightCumulative, pub realized_cap: LazyFromHeight, pub realized_price: Price>, pub realized_price_ratio: ComputedFromHeightRatio, @@ -43,16 +46,18 @@ pub struct MinimalRelative { pub supply_in_loss_rel_to_own_supply: PercentFromHeight, } -/// MinimalCohortMetrics (Tier A): ~15 stored vecs. +/// MinimalCohortMetrics: supply, outputs, sent+ema, realized cap/price/mvrv/profit/loss, +/// supply in profit/loss, relative to own supply. /// -/// Used for type_ cohorts where most metrics are irrelevant. -/// Does NOT implement CohortMetricsBase — standalone, not aggregatable. +/// Used for type_, amount, and address cohorts. +/// Does NOT implement CohortMetricsBase — standalone, not aggregatable via trait. #[derive(Traversable)] pub struct MinimalCohortMetrics { #[traversable(skip)] pub filter: Filter, pub supply: Box>, pub outputs: Box>, + pub activity: Box>, pub realized: Box>, pub unrealized: Box>, pub relative: Box>, @@ -68,6 +73,9 @@ impl MinimalRealized { &realized_cap_cents, ); + let realized_profit = cfg.import_cumulative("realized_profit", Version::ZERO)?; + let realized_loss = cfg.import_cumulative("realized_loss", Version::ZERO)?; + let realized_price = cfg.import_price("realized_price", Version::ONE)?; let realized_price_ratio = cfg.import_ratio("realized_price", Version::ONE)?; let mvrv = LazyFromHeight::from_lazy::, BasisPoints32>( @@ -78,6 +86,8 @@ impl MinimalRealized { Ok(Self { realized_cap_cents, + realized_profit, + realized_loss, realized_cap, realized_price, realized_price_ratio, @@ -86,18 +96,60 @@ impl MinimalRealized { } pub(crate) fn min_stateful_height_len(&self) -> usize { - self.realized_cap_cents.height.len() - } - - pub(crate) fn truncate_push(&mut self, height: Height, cap: Cents) -> Result<()> { self.realized_cap_cents .height - .truncate_push(height, cap)?; + .len() + .min(self.realized_profit.height.len()) + .min(self.realized_loss.height.len()) + } + + pub(crate) fn truncate_push( + &mut self, + height: Height, + state: &impl RealizedOps, + ) -> Result<()> { + self.realized_cap_cents + .height + .truncate_push(height, state.cap())?; + self.realized_profit + .height + .truncate_push(height, state.profit())?; + self.realized_loss + .height + .truncate_push(height, state.loss())?; Ok(()) } pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { - vec![&mut self.realized_cap_cents.height as &mut dyn AnyStoredVec] + vec![ + &mut self.realized_cap_cents.height as &mut dyn AnyStoredVec, + &mut self.realized_profit.height, + &mut self.realized_loss.height, + ] + } + + pub(crate) fn compute_from_sources( + &mut self, + starting_indexes: &Indexes, + others: &[&Self], + exit: &Exit, + ) -> Result<()> { + sum_others!(self, starting_indexes, others, exit; realized_cap_cents.height); + sum_others!(self, starting_indexes, others, exit; realized_profit.height); + sum_others!(self, starting_indexes, others, exit; realized_loss.height); + Ok(()) + } + + pub(crate) fn compute_rest_part1( + &mut self, + starting_indexes: &Indexes, + exit: &Exit, + ) -> Result<()> { + self.realized_profit + .compute_rest(starting_indexes.height, exit)?; + self.realized_loss + .compute_rest(starting_indexes.height, exit)?; + Ok(()) } pub(crate) fn compute_rest_part2( @@ -175,6 +227,17 @@ impl MinimalUnrealized { ] } + pub(crate) fn compute_from_sources( + &mut self, + starting_indexes: &Indexes, + others: &[&Self], + exit: &Exit, + ) -> Result<()> { + sum_others!(self, starting_indexes, others, exit; supply_in_profit.base.sats.height); + sum_others!(self, starting_indexes, others, exit; supply_in_loss.base.sats.height); + Ok(()) + } + pub(crate) fn compute_rest( &mut self, prices: &prices::Vecs, @@ -229,6 +292,7 @@ impl MinimalCohortMetrics { filter: cfg.filter.clone(), supply: Box::new(SupplyMetrics::forced_import(cfg)?), outputs: Box::new(OutputsMetrics::forced_import(cfg)?), + activity: Box::new(ActivityCore::forced_import(cfg)?), realized: Box::new(MinimalRealized::forced_import(cfg)?), unrealized: Box::new(MinimalUnrealized::forced_import(cfg)?), relative: Box::new(MinimalRelative::forced_import(cfg)?), @@ -239,6 +303,7 @@ impl MinimalCohortMetrics { self.supply .min_len() .min(self.outputs.min_len()) + .min(self.activity.min_len()) .min(self.realized.min_stateful_height_len()) .min(self.unrealized.min_stateful_height_len()) } @@ -252,11 +317,53 @@ impl MinimalCohortMetrics { let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); vecs.extend(self.supply.collect_vecs_mut()); vecs.extend(self.outputs.collect_vecs_mut()); + vecs.extend(self.activity.collect_vecs_mut()); vecs.extend(self.realized.collect_vecs_mut()); vecs.extend(self.unrealized.collect_vecs_mut()); vecs } + /// Aggregate Minimal-tier metrics from other MinimalCohortMetrics sources. + pub(crate) fn compute_from_sources( + &mut self, + starting_indexes: &Indexes, + others: &[&MinimalCohortMetrics], + exit: &Exit, + ) -> Result<()> { + self.supply.compute_from_stateful( + starting_indexes, + &others.iter().map(|v| v.supply.as_ref()).collect::>(), + exit, + )?; + self.outputs.compute_from_stateful( + starting_indexes, + &others.iter().map(|v| v.outputs.as_ref()).collect::>(), + exit, + )?; + self.activity.compute_from_stateful( + starting_indexes, + &others.iter().map(|v| v.activity.as_ref()).collect::>(), + exit, + )?; + self.realized.compute_from_sources( + starting_indexes, + &others + .iter() + .map(|v| v.realized.as_ref()) + .collect::>(), + exit, + )?; + self.unrealized.compute_from_sources( + starting_indexes, + &others + .iter() + .map(|v| v.unrealized.as_ref()) + .collect::>(), + exit, + )?; + Ok(()) + } + pub(crate) fn compute_rest_part1( &mut self, blocks: &blocks::Vecs, @@ -270,6 +377,10 @@ impl MinimalCohortMetrics { .compute_rest_part1(blocks, starting_indexes, exit)?; self.outputs .compute_rest(blocks, starting_indexes, exit)?; + self.activity + .compute_rest_part1(blocks, prices, starting_indexes, exit)?; + self.realized + .compute_rest_part1(starting_indexes, exit)?; self.unrealized .compute_rest(prices, starting_indexes.height, exit)?; Ok(()) diff --git a/crates/brk_computer/src/distribution/metrics/cohort/mod.rs b/crates/brk_computer/src/distribution/metrics/cohort/mod.rs index 44a3751a5..e2f6eaf67 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/mod.rs @@ -1,6 +1,5 @@ mod all; mod basic; -mod complete; mod core; mod extended; mod extended_adjusted; @@ -8,7 +7,6 @@ mod minimal; pub use all::*; pub use basic::*; -pub use complete::*; pub use core::*; pub use extended::*; pub use extended_adjusted::*; diff --git a/crates/brk_computer/src/distribution/metrics/mod.rs b/crates/brk_computer/src/distribution/metrics/mod.rs index 1e1dd1bf9..bc3e75c00 100644 --- a/crates/brk_computer/src/distribution/metrics/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/mod.rs @@ -76,6 +76,8 @@ macro_rules! impl_cohort_metrics_base { vecs.extend(self.realized.collect_vecs_mut()); vecs.extend(self.cost_basis.collect_vecs_mut()); vecs.extend(self.unrealized.collect_vecs_mut()); + vecs.push(&mut self.dormancy.height); + vecs.push(&mut self.velocity.height); vecs } } diff --git a/crates/brk_computer/src/distribution/metrics/realized/complete.rs b/crates/brk_computer/src/distribution/metrics/realized/complete.rs index 280eceac0..c4b8eceeb 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/complete.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/complete.rs @@ -1,7 +1,7 @@ use brk_error::Result; use brk_traversable::Traversable; use brk_types::{ - BasisPointsSigned32, Bitcoin, Cents, CentsSigned, Dollars, Height, Indexes, StoredF64, Version, + BasisPointsSigned32, Bitcoin, Cents, CentsSigned, Dollars, Height, Indexes, Version, }; use derive_more::{Deref, DerefMut}; use vecdb::{ @@ -12,9 +12,9 @@ use crate::{ blocks, distribution::state::RealizedState, internal::{ - CentsPlus, CentsUnsignedToDollars, ComputedFromHeight, LazyFromHeight, PercentFromHeight, - RatioCents64, RatioCentsSignedCentsBps32, RatioCentsSignedDollarsBps32, RollingEmas1w1m, - RollingEmas2w, RollingWindows, ValueFromHeightCumulative, + CentsUnsignedToDollars, ComputedFromHeight, LazyFromHeight, PercentFromHeight, + RatioCentsSignedCentsBps32, RatioCentsSignedDollarsBps32, RollingEmas2w, RollingWindows, + ValueFromHeightCumulative, }, prices, }; @@ -35,24 +35,15 @@ pub struct RealizedComplete { pub loss_value_created: ComputedFromHeight, pub loss_value_destroyed: ComputedFromHeight, - pub value_created: ComputedFromHeight, - pub value_destroyed: ComputedFromHeight, - pub capitulation_flow: LazyFromHeight, pub profit_flow: LazyFromHeight, - pub value_created_sum: RollingWindows, - pub value_destroyed_sum: RollingWindows, - pub gross_pnl_sum: RollingWindows, pub net_pnl_change_1m: ComputedFromHeight, pub net_pnl_change_1m_rel_to_realized_cap: PercentFromHeight, pub net_pnl_change_1m_rel_to_market_cap: PercentFromHeight, - pub sopr: RollingWindows, - pub sopr_24h_ema: RollingEmas1w1m, - pub sent_in_profit: ValueFromHeightCumulative, pub sent_in_profit_ema: RollingEmas2w, pub sent_in_loss: ValueFromHeightCumulative, @@ -69,8 +60,6 @@ impl RealizedComplete { let profit_value_destroyed = cfg.import_computed("profit_value_destroyed", v0)?; let loss_value_created = cfg.import_computed("loss_value_created", v0)?; let loss_value_destroyed = cfg.import_computed("loss_value_destroyed", v0)?; - let value_created = cfg.import_computed("value_created", v0)?; - let value_destroyed = cfg.import_computed("value_destroyed", v0)?; let capitulation_flow = LazyFromHeight::from_computed::( &cfg.name("capitulation_flow"), @@ -85,28 +74,17 @@ impl RealizedComplete { &profit_value_destroyed, ); - let value_created_sum = cfg.import_rolling("value_created", Version::ONE)?; - let value_destroyed_sum = cfg.import_rolling("value_destroyed", Version::ONE)?; let gross_pnl_sum = cfg.import_rolling("gross_pnl_sum", Version::ONE)?; - let sopr = cfg.import_rolling("sopr", Version::ONE)?; - let sopr_24h_ema = cfg.import_emas_1w_1m("sopr_24h", Version::ONE)?; - Ok(Self { core, profit_value_created, profit_value_destroyed, loss_value_created, loss_value_destroyed, - value_created, - value_destroyed, capitulation_flow, profit_flow, - value_created_sum, - value_destroyed_sum, gross_pnl_sum, - sopr, - sopr_24h_ema, net_pnl_change_1m: cfg.import_computed("net_pnl_change_1m", Version::new(3))?, net_pnl_change_1m_rel_to_realized_cap: cfg .import_percent_bps32("net_pnl_change_1m_rel_to_realized_cap", Version::new(4))?, @@ -216,34 +194,7 @@ impl RealizedComplete { exit, )?; - self.value_created - .compute_binary::( - starting_indexes.height, - &self.profit_value_created.height, - &self.loss_value_created.height, - exit, - )?; - self.value_destroyed - .compute_binary::( - starting_indexes.height, - &self.profit_value_destroyed.height, - &self.loss_value_destroyed.height, - exit, - )?; - let window_starts = blocks.count.window_starts(); - self.value_created_sum.compute_rolling_sum( - starting_indexes.height, - &window_starts, - &self.value_created.height, - exit, - )?; - self.value_destroyed_sum.compute_rolling_sum( - starting_indexes.height, - &window_starts, - &self.value_destroyed.height, - exit, - )?; self.gross_pnl_sum.compute_rolling_sum( starting_indexes.height, &window_starts, @@ -251,29 +202,6 @@ impl RealizedComplete { exit, )?; - for ((sopr, vc), vd) in self - .sopr - .as_mut_array() - .into_iter() - .zip(self.value_created_sum.as_array()) - .zip(self.value_destroyed_sum.as_array()) - { - sopr.compute_binary::( - starting_indexes.height, - &vc.height, - &vd.height, - exit, - )?; - } - - self.sopr_24h_ema.compute_from_24h( - starting_indexes.height, - &blocks.count.height_1w_ago, - &blocks.count.height_1m_ago, - &self.sopr._24h.height, - exit, - )?; - self.sent_in_profit_ema.compute( starting_indexes.height, &blocks.count.height_2w_ago, diff --git a/crates/brk_computer/src/distribution/metrics/realized/core.rs b/crates/brk_computer/src/distribution/metrics/realized/core.rs index dc0d7e7f7..2ee00f809 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/core.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/core.rs @@ -2,7 +2,7 @@ use brk_error::Result; use brk_traversable::Traversable; use brk_types::{ BasisPoints32, BasisPointsSigned32, Bitcoin, Cents, CentsSigned, Dollars, Height, Indexes, - Sats, StoredF32, Version, + Sats, StoredF32, StoredF64, Version, }; use vecdb::{ AnyStoredVec, AnyVec, Exit, ReadableCloneableVec, ReadableVec, Rw, StorageMode, WritableVec, @@ -14,8 +14,8 @@ use crate::{ internal::{ CentsUnsignedToDollars, ComputedFromHeight, ComputedFromHeightCumulative, ComputedFromHeightRatio, FiatFromHeight, Identity, LazyFromHeight, - NegCentsUnsignedToDollars, PercentFromHeight, Price, RatioCentsBp32, - RatioCentsSignedCentsBps32, + NegCentsUnsignedToDollars, PercentFromHeight, Price, RatioCents64, RatioCentsBp32, + RatioCentsSignedCentsBps32, RollingEmas1w1m, RollingWindows, }, prices, }; @@ -46,6 +46,13 @@ pub struct RealizedCore { pub realized_profit_rel_to_realized_cap: PercentFromHeight, pub realized_loss_rel_to_realized_cap: PercentFromHeight, pub net_realized_pnl_rel_to_realized_cap: PercentFromHeight, + + pub value_created: ComputedFromHeight, + pub value_destroyed: ComputedFromHeight, + pub value_created_sum: RollingWindows, + pub value_destroyed_sum: RollingWindows, + pub sopr: RollingWindows, + pub sopr_24h_ema: RollingEmas1w1m, } impl RealizedCore { @@ -93,6 +100,13 @@ impl RealizedCore { &realized_price_ratio.ratio, ); + let value_created = cfg.import_computed("value_created", v0)?; + let value_destroyed = cfg.import_computed("value_destroyed", v0)?; + let value_created_sum = cfg.import_rolling("value_created", v1)?; + let value_destroyed_sum = cfg.import_rolling("value_destroyed", v1)?; + let sopr = cfg.import_rolling("sopr", v1)?; + let sopr_24h_ema = cfg.import_emas_1w_1m("sopr_24h", v1)?; + Ok(Self { realized_cap_cents, realized_cap, @@ -111,6 +125,12 @@ impl RealizedCore { realized_profit_rel_to_realized_cap, realized_loss_rel_to_realized_cap, net_realized_pnl_rel_to_realized_cap, + value_created, + value_destroyed, + value_created_sum, + value_destroyed_sum, + sopr, + sopr_24h_ema, }) } @@ -120,6 +140,8 @@ impl RealizedCore { .len() .min(self.realized_profit.height.len()) .min(self.realized_loss.height.len()) + .min(self.value_created.height.len()) + .min(self.value_destroyed.height.len()) } pub(crate) fn truncate_push(&mut self, height: Height, state: &impl RealizedOps) -> Result<()> { @@ -132,6 +154,12 @@ impl RealizedCore { self.realized_loss .height .truncate_push(height, state.loss())?; + self.value_created + .height + .truncate_push(height, state.value_created())?; + self.value_destroyed + .height + .truncate_push(height, state.value_destroyed())?; Ok(()) } @@ -140,6 +168,8 @@ impl RealizedCore { &mut self.realized_cap_cents.height as &mut dyn AnyStoredVec, &mut self.realized_profit.height, &mut self.realized_loss.height, + &mut self.value_created.height, + &mut self.value_destroyed.height, ] } @@ -152,6 +182,8 @@ impl RealizedCore { sum_others!(self, starting_indexes, others, exit; realized_cap_cents.height); sum_others!(self, starting_indexes, others, exit; realized_profit.height); sum_others!(self, starting_indexes, others, exit; realized_loss.height); + sum_others!(self, starting_indexes, others, exit; value_created.height); + sum_others!(self, starting_indexes, others, exit; value_destroyed.height); Ok(()) } @@ -272,6 +304,44 @@ impl RealizedCore { exit, )?; + // SOPR: rolling sums of stateful value_created/destroyed, then ratio, then EMAs + let window_starts = blocks.count.window_starts(); + self.value_created_sum.compute_rolling_sum( + starting_indexes.height, + &window_starts, + &self.value_created.height, + exit, + )?; + self.value_destroyed_sum.compute_rolling_sum( + starting_indexes.height, + &window_starts, + &self.value_destroyed.height, + exit, + )?; + + for ((sopr, vc), vd) in self + .sopr + .as_mut_array() + .into_iter() + .zip(self.value_created_sum.as_array()) + .zip(self.value_destroyed_sum.as_array()) + { + sopr.compute_binary::( + starting_indexes.height, + &vc.height, + &vd.height, + exit, + )?; + } + + self.sopr_24h_ema.compute_from_24h( + starting_indexes.height, + &blocks.count.height_1w_ago, + &blocks.count.height_1m_ago, + &self.sopr._24h.height, + exit, + )?; + Ok(()) } } diff --git a/crates/brk_computer/src/distribution/state/cost_basis/realized.rs b/crates/brk_computer/src/distribution/state/cost_basis/realized.rs index 277fe7685..5452c9211 100644 --- a/crates/brk_computer/src/distribution/state/cost_basis/realized.rs +++ b/crates/brk_computer/src/distribution/state/cost_basis/realized.rs @@ -2,12 +2,17 @@ use std::cmp::Ordering; use brk_types::{Cents, CentsSats, CentsSquaredSats, Sats}; -/// Trait for realized state operations, implemented by both Core and Full variants. -/// Core skips extra fields (value_created/destroyed, peak_regret, sent_in_profit/loss, investor_cap). +/// Trait for realized state operations, implemented by Minimal, Core, and Full variants. pub trait RealizedOps: Default + Clone + Send + Sync + 'static { fn cap(&self) -> Cents; fn profit(&self) -> Cents; fn loss(&self) -> Cents; + fn value_created(&self) -> Cents { + Cents::ZERO + } + fn value_destroyed(&self) -> Cents { + Cents::ZERO + } fn set_cap_raw(&mut self, cap_raw: CentsSats); fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats); fn reset_single_iteration_values(&mut self); @@ -27,17 +32,16 @@ pub trait RealizedOps: Default + Clone + Send + Sync + 'static { ); } -/// Core realized state: only cap, profit, loss (48 bytes). -/// Used by CoreCohortMetrics and MinimalCohortMetrics cohorts -/// (epoch, class, amount_range, type_ — ~50 separate cohorts). +/// Minimal realized state: only cap, profit, loss. +/// Used by MinimalCohortMetrics cohorts (amount_range, type_, address — ~135 separate cohorts). #[derive(Debug, Default, Clone)] -pub struct CoreRealizedState { +pub struct MinimalRealizedState { cap_raw: u128, profit_raw: u128, loss_raw: u128, } -impl RealizedOps for CoreRealizedState { +impl RealizedOps for MinimalRealizedState { #[inline] fn cap(&self) -> Cents { if self.cap_raw == 0 { @@ -68,9 +72,7 @@ impl RealizedOps for CoreRealizedState { } #[inline] - fn set_investor_cap_raw(&mut self, _investor_cap_raw: CentsSquaredSats) { - // no-op for Core - } + fn set_investor_cap_raw(&mut self, _investor_cap_raw: CentsSquaredSats) {} #[inline] fn reset_single_iteration_values(&mut self) { @@ -119,9 +121,102 @@ impl RealizedOps for CoreRealizedState { } } +/// Core realized state: cap, profit, loss + value_created/destroyed for SOPR. +/// Used by CoreCohortMetrics cohorts (epoch, class, max_age, min_age — ~59 separate cohorts). +#[derive(Debug, Default, Clone)] +pub struct CoreRealizedState { + minimal: MinimalRealizedState, + value_created_raw: u128, + value_destroyed_raw: u128, +} + +impl RealizedOps for CoreRealizedState { + #[inline] + fn cap(&self) -> Cents { + self.minimal.cap() + } + + #[inline] + fn profit(&self) -> Cents { + self.minimal.profit() + } + + #[inline] + fn loss(&self) -> Cents { + self.minimal.loss() + } + + #[inline] + fn value_created(&self) -> Cents { + if self.value_created_raw == 0 { + return Cents::ZERO; + } + Cents::new((self.value_created_raw / Sats::ONE_BTC_U128) as u64) + } + + #[inline] + fn value_destroyed(&self) -> Cents { + if self.value_destroyed_raw == 0 { + return Cents::ZERO; + } + Cents::new((self.value_destroyed_raw / Sats::ONE_BTC_U128) as u64) + } + + #[inline] + fn set_cap_raw(&mut self, cap_raw: CentsSats) { + self.minimal.set_cap_raw(cap_raw); + } + + #[inline] + fn set_investor_cap_raw(&mut self, _investor_cap_raw: CentsSquaredSats) {} + + #[inline] + fn reset_single_iteration_values(&mut self) { + self.minimal.reset_single_iteration_values(); + self.value_created_raw = 0; + self.value_destroyed_raw = 0; + } + + #[inline] + fn increment(&mut self, price: Cents, sats: Sats) { + self.minimal.increment(price, sats); + } + + #[inline] + fn increment_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) { + self.minimal.increment_snapshot(price_sats, _investor_cap); + } + + #[inline] + fn decrement_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) { + self.minimal.decrement_snapshot(price_sats, _investor_cap); + } + + #[inline] + fn send( + &mut self, + sats: Sats, + current_ps: CentsSats, + prev_ps: CentsSats, + ath_ps: CentsSats, + prev_investor_cap: CentsSquaredSats, + ) { + self.minimal + .send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap); + self.value_created_raw += current_ps.as_u128(); + self.value_destroyed_raw += prev_ps.as_u128(); + } +} + +impl CoreRealizedState { + #[inline(always)] + pub(super) fn cap_raw_u128(&self) -> u128 { + self.minimal.cap_raw + } +} + /// Full realized state (~160 bytes). -/// Used by BasicCohortMetrics and CompleteCohortMetrics cohorts -/// (age_range — 21 separate cohorts). +/// Used by BasicCohortMetrics cohorts (age_range — 21 separate cohorts). #[derive(Debug, Default, Clone)] pub struct RealizedState { core: CoreRealizedState, @@ -159,6 +254,24 @@ impl RealizedOps for RealizedState { self.core.loss() } + #[inline] + fn value_created(&self) -> Cents { + let raw = self.profit_value_created_raw + self.loss_value_created_raw; + if raw == 0 { + return Cents::ZERO; + } + Cents::new((raw / Sats::ONE_BTC_U128) as u64) + } + + #[inline] + fn value_destroyed(&self) -> Cents { + let raw = self.profit_value_destroyed_raw + self.loss_value_destroyed_raw; + if raw == 0 { + return Cents::ZERO; + } + Cents::new((raw / Sats::ONE_BTC_U128) as u64) + } + #[inline] fn set_cap_raw(&mut self, cap_raw: CentsSats) { self.core.set_cap_raw(cap_raw); @@ -183,23 +296,21 @@ impl RealizedOps for RealizedState { #[inline] fn increment(&mut self, price: Cents, sats: Sats) { - if sats.is_zero() { - return; + self.core.increment(price, sats); + if sats.is_not_zero() { + self.investor_cap_raw += CentsSats::from_price_sats(price, sats).to_investor_cap(price); } - let price_sats = CentsSats::from_price_sats(price, sats); - self.core.cap_raw += price_sats.as_u128(); - self.investor_cap_raw += price_sats.to_investor_cap(price); } #[inline] fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) { - self.core.cap_raw += price_sats.as_u128(); + self.core.increment_snapshot(price_sats, investor_cap); self.investor_cap_raw += investor_cap; } #[inline] fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) { - self.core.cap_raw -= price_sats.as_u128(); + self.core.decrement_snapshot(price_sats, investor_cap); self.investor_cap_raw -= investor_cap; } @@ -212,32 +323,32 @@ impl RealizedOps for RealizedState { ath_ps: CentsSats, prev_investor_cap: CentsSquaredSats, ) { + // Delegate cap/profit/loss + value_created/destroyed to core + self.core + .send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap); + + // Per-component value flow tracking + let current = current_ps.as_u128(); + let prev = prev_ps.as_u128(); match current_ps.cmp(&prev_ps) { Ordering::Greater => { - self.core.profit_raw += (current_ps - prev_ps).as_u128(); - self.profit_value_created_raw += current_ps.as_u128(); - self.profit_value_destroyed_raw += prev_ps.as_u128(); + self.profit_value_created_raw += current; + self.profit_value_destroyed_raw += prev; self.sent_in_profit += sats; } Ordering::Less => { - self.core.loss_raw += (prev_ps - current_ps).as_u128(); - self.loss_value_created_raw += current_ps.as_u128(); - self.loss_value_destroyed_raw += prev_ps.as_u128(); + self.loss_value_created_raw += current; + self.loss_value_destroyed_raw += prev; self.sent_in_loss += sats; } Ordering::Equal => { - // Break-even: count as profit side (arbitrary but consistent) - self.profit_value_created_raw += current_ps.as_u128(); - self.profit_value_destroyed_raw += prev_ps.as_u128(); + self.profit_value_created_raw += current; + self.profit_value_destroyed_raw += prev; self.sent_in_profit += sats; } } - // Track peak regret: (peak - sell_price) × sats self.peak_regret_raw += (ath_ps - current_ps).as_u128(); - - // Inline decrement to avoid recomputation - self.core.cap_raw -= prev_ps.as_u128(); self.investor_cap_raw -= prev_investor_cap; } } @@ -247,16 +358,17 @@ impl RealizedState { /// investor_price = Σ(price² × sats) / Σ(price × sats) #[inline] pub(crate) fn investor_price(&self) -> Cents { - if self.core.cap_raw == 0 { + let cap_raw = self.core.cap_raw_u128(); + if cap_raw == 0 { return Cents::ZERO; } - Cents::new((self.investor_cap_raw / self.core.cap_raw) as u64) + Cents::new((self.investor_cap_raw / cap_raw) as u64) } /// Get raw realized cap for aggregation. #[inline] pub(crate) fn cap_raw(&self) -> CentsSats { - CentsSats::new(self.core.cap_raw) + CentsSats::new(self.core.cap_raw_u128()) } /// Get raw investor cap for aggregation. diff --git a/crates/brk_computer/src/internal/transform/currency.rs b/crates/brk_computer/src/internal/transform/currency.rs index 3c2540a1c..df3532cb3 100644 --- a/crates/brk_computer/src/internal/transform/currency.rs +++ b/crates/brk_computer/src/internal/transform/currency.rs @@ -69,15 +69,6 @@ impl UnaryTransform for CentsUnsignedToSats { } } -pub struct CentsPlus; - -impl BinaryTransform for CentsPlus { - #[inline(always)] - fn apply(lhs: Cents, rhs: Cents) -> Cents { - lhs + rhs - } -} - pub struct CentsSubtractToCentsSigned; impl BinaryTransform for CentsSubtractToCentsSigned { diff --git a/crates/brk_computer/src/internal/transform/mod.rs b/crates/brk_computer/src/internal/transform/mod.rs index 69a2f6dc2..fa60167b5 100644 --- a/crates/brk_computer/src/internal/transform/mod.rs +++ b/crates/brk_computer/src/internal/transform/mod.rs @@ -14,7 +14,7 @@ pub use bps::{ Bps32ToPercent, }; pub use currency::{ - CentsPlus, CentsSignedToDollars, CentsSubtractToCentsSigned, CentsTimesTenths, + CentsSignedToDollars, CentsSubtractToCentsSigned, CentsTimesTenths, CentsUnsignedToDollars, CentsUnsignedToSats, DollarsToSatsFract, NegCentsUnsignedToDollars, SatsSignedToBitcoin, SatsToBitcoin, SatsToCents, };