diff --git a/crates/brk_computer/src/blocks/count/vecs.rs b/crates/brk_computer/src/blocks/count/vecs.rs index 4f75cf2d6..87bdb0bfc 100644 --- a/crates/brk_computer/src/blocks/count/vecs.rs +++ b/crates/brk_computer/src/blocks/count/vecs.rs @@ -8,7 +8,8 @@ use crate::internal::{ComputedFromHeightSumCum, ConstantVecs, RollingWindows, Wi pub struct Vecs { pub block_count_target: ConstantVecs, pub block_count: ComputedFromHeightSumCum, - // Rolling window starts (height-indexed only, no date aggregation needed) + pub block_count_sum: RollingWindows, + pub height_24h_ago: M::Stored>>, pub height_3d_ago: M::Stored>>, pub height_1w_ago: M::Stored>>, @@ -39,8 +40,6 @@ pub struct Vecs { pub height_6y_ago: M::Stored>>, pub height_8y_ago: M::Stored>>, pub height_10y_ago: M::Stored>>, - // Rolling window block counts - pub block_count_sum: RollingWindows, } impl Vecs { diff --git a/crates/brk_computer/src/blocks/difficulty/vecs.rs b/crates/brk_computer/src/blocks/difficulty/vecs.rs index ef269eeca..c7a74b8c8 100644 --- a/crates/brk_computer/src/blocks/difficulty/vecs.rs +++ b/crates/brk_computer/src/blocks/difficulty/vecs.rs @@ -7,7 +7,6 @@ use crate::internal::{ComputedFromHeightLast, ComputedHeightDerivedLast}; /// Difficulty metrics: raw difficulty, derived stats, adjustment, and countdown #[derive(Traversable)] pub struct Vecs { - /// Raw difficulty with day1/period stats - merges with indexer's raw pub raw: ComputedHeightDerivedLast, pub as_hash: ComputedFromHeightLast, pub adjustment: ComputedFromHeightLast, diff --git a/crates/brk_computer/src/distribution/cohorts/address/groups.rs b/crates/brk_computer/src/distribution/cohorts/address/groups.rs index ad20c125f..4f0b926a9 100644 --- a/crates/brk_computer/src/distribution/cohorts/address/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/address/groups.rs @@ -12,7 +12,7 @@ use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode}; use crate::{ComputeIndexes, blocks, distribution::DynCohortVecs, indexes, prices}; -use crate::distribution::metrics::SupplyMetrics; +use crate::distribution::metrics::{CohortMetricsBase, SupplyMetrics}; use super::{super::traits::CohortVecs, vecs::AddressCohortVecs}; @@ -33,7 +33,7 @@ impl AddressCohorts { indexes: &indexes::Vecs, prices: &prices::Vecs, states_path: &Path, - all_supply: Option<&SupplyMetrics>, + all_supply: &SupplyMetrics, ) -> Result { let v = version + VERSION; @@ -140,7 +140,7 @@ impl AddressCohorts { blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &ComputeIndexes, - height_to_market_cap: Option<&HM>, + height_to_market_cap: &HM, exit: &Exit, ) -> Result<()> where @@ -198,12 +198,8 @@ impl AddressCohorts { /// Reset cost_basis_data for all separate cohorts (called during fresh start). pub(crate) fn reset_separate_cost_basis_data(&mut self) -> Result<()> { - self.par_iter_separate_mut().try_for_each(|v| { - if let Some(state) = v.state.as_mut() { - state.reset_cost_basis_data_if_needed()?; - } - Ok(()) - }) + self.par_iter_separate_mut() + .try_for_each(|v| v.reset_cost_basis_data_if_needed()) } /// Validate computed versions for all separate cohorts. diff --git a/crates/brk_computer/src/distribution/cohorts/address/vecs.rs b/crates/brk_computer/src/distribution/cohorts/address/vecs.rs index 67573e2b9..f2ddfbdac 100644 --- a/crates/brk_computer/src/distribution/cohorts/address/vecs.rs +++ b/crates/brk_computer/src/distribution/cohorts/address/vecs.rs @@ -15,7 +15,7 @@ use crate::{ prices, }; -use crate::distribution::metrics::{CohortMetrics, ImportConfig, SupplyMetrics}; +use crate::distribution::metrics::{BasicCohortMetrics, CohortMetricsBase, ImportConfig, SupplyMetrics}; use super::super::traits::{CohortVecs, DynCohortVecs}; @@ -33,7 +33,7 @@ pub struct AddressCohortVecs { /// Metric vectors #[traversable(flatten)] - pub metrics: CohortMetrics, + pub metrics: BasicCohortMetrics, pub addr_count: ComputedFromHeightLast, pub addr_count_30d_change: ComputedFromHeightLast, @@ -43,7 +43,7 @@ impl AddressCohortVecs { /// Import address cohort from database. /// /// `all_supply` is the supply metrics from the "all" cohort, used as global - /// sources for `*_rel_to_market_cap` ratios. Pass `None` if not available. + /// sources for `*_rel_to_market_cap` ratios. #[allow(clippy::too_many_arguments)] pub(crate) fn forced_import( db: &Database, @@ -53,7 +53,7 @@ impl AddressCohortVecs { indexes: &indexes::Vecs, prices: &prices::Vecs, states_path: Option<&Path>, - all_supply: Option<&SupplyMetrics>, + all_supply: &SupplyMetrics, ) -> Result { let full_name = CohortContext::Address.full_name(&filter, name); @@ -65,7 +65,6 @@ impl AddressCohortVecs { version, indexes, prices, - up_to_1h_realized: None, }; Ok(Self { @@ -74,7 +73,7 @@ impl AddressCohortVecs { state: states_path .map(|path| Box::new(AddressCohortState::new(path, &full_name))), - metrics: CohortMetrics::forced_import(&cfg, all_supply)?, + metrics: BasicCohortMetrics::forced_import(&cfg, all_supply)?, addr_count: ComputedFromHeightLast::forced_import( db, @@ -227,6 +226,35 @@ impl DynCohortVecs for AddressCohortVecs { .compute_rest_part1(blocks, prices, starting_indexes, exit)?; Ok(()) } + + fn compute_net_sentiment_height( + &mut self, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + self.metrics + .compute_net_sentiment_height(starting_indexes, exit) + } + + fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> { + if let Some(state) = self.state.as_mut() { + state.inner.write(height, cleanup)?; + } + Ok(()) + } + + fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> { + if let Some(state) = self.state.as_mut() { + state.inner.reset_cost_basis_data_if_needed()?; + } + Ok(()) + } + + fn reset_single_iteration_values(&mut self) { + if let Some(state) = self.state.as_mut() { + state.inner.reset_single_iteration_values(); + } + } } impl CohortVecs for AddressCohortVecs { @@ -258,7 +286,7 @@ impl CohortVecs for AddressCohortVecs { blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &ComputeIndexes, - height_to_market_cap: Option<&impl ReadableVec>, + height_to_market_cap: &impl ReadableVec, exit: &Exit, ) -> Result<()> { self.metrics.compute_rest_part2( diff --git a/crates/brk_computer/src/distribution/cohorts/traits.rs b/crates/brk_computer/src/distribution/cohorts/traits.rs index 09f058a6b..f975d39d3 100644 --- a/crates/brk_computer/src/distribution/cohorts/traits.rs +++ b/crates/brk_computer/src/distribution/cohorts/traits.rs @@ -38,9 +38,27 @@ pub trait DynCohortVecs: Send + Sync { starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()>; + + /// Compute net_sentiment.height for separate cohorts (greed - pain). + fn compute_net_sentiment_height( + &mut self, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()>; + + /// Write state checkpoint to disk. + fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()>; + + /// Reset cost basis data (called during fresh start). + fn reset_cost_basis_data_if_needed(&mut self) -> Result<()>; + + /// Reset per-block iteration values. + fn reset_single_iteration_values(&mut self); } /// Static dispatch trait for cohort vectors with additional methods. +/// +/// Used by address cohorts where all cohorts share the same concrete type. pub trait CohortVecs: DynCohortVecs { /// Compute aggregate cohort from component cohorts. fn compute_from_stateful( @@ -56,7 +74,7 @@ pub trait CohortVecs: DynCohortVecs { blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &ComputeIndexes, - height_to_market_cap: Option<&impl ReadableVec>, + height_to_market_cap: &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 acbc8570e..61c29f196 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs @@ -2,8 +2,7 @@ use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path}; use brk_cohort::{ AGE_BOUNDARIES, ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount, - ByMaxAge, ByMinAge, BySpendableType, ByTerm, ByYear, Filter, Filtered, StateLevel, TERM_NAMES, - Term, UTXOGroups, + ByMaxAge, ByMinAge, BySpendableType, ByYear, CohortContext, Filter, Filtered, TERM_NAMES, Term, }; use brk_error::Result; use brk_traversable::Traversable; @@ -11,7 +10,6 @@ use brk_types::{ Cents, CentsCompact, CostBasisDistribution, Date, Day1, Dollars, Height, ONE_HOUR_IN_SEC, Sats, StoredF32, Timestamp, Version, }; -use derive_more::{Deref, DerefMut}; use rayon::prelude::*; use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode, VecIndex, WritableVec}; @@ -23,7 +21,15 @@ use crate::{ prices, }; -use super::{super::traits::CohortVecs, vecs::UTXOCohortVecs}; +use crate::distribution::metrics::{ + AdjustedCohortMetrics, AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, + ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig, PeakRegretCohortMetrics, + RealizedBase, SupplyMetrics, +}; + +use super::vecs::UTXOCohortVecs; + +use crate::distribution::state::UTXOCohortState; const VERSION: Version = Version::new(0); @@ -31,10 +37,32 @@ const VERSION: Version = Version::new(0); const COST_BASIS_PRICE_DIGITS: i32 = 5; /// All UTXO cohorts organized by filter type. -#[derive(Deref, DerefMut, Traversable)] -pub struct UTXOCohorts(pub(crate) UTXOGroups>); +/// +/// Each group uses a concrete metrics type matching its required features: +/// - age_range: extended realized + extended cost basis + peak regret +/// - epoch/year/amount/type: basic metrics with relative +/// - all: extended + adjusted + peak regret (no rel_to_all) +/// - sth: extended + adjusted + peak regret +/// - lth: extended + peak regret +/// - max_age: adjusted + peak regret +/// - min_age: peak regret +#[derive(Traversable)] +pub struct UTXOCohorts { + pub all: UTXOCohortVecs>, + pub sth: UTXOCohortVecs>, + pub lth: UTXOCohortVecs>, + pub age_range: ByAgeRange>>, + pub max_age: ByMaxAge>>, + pub min_age: ByMinAge>>, + pub ge_amount: ByGreatEqualAmount>>, + pub amount_range: ByAmountRange>>, + pub lt_amount: ByLowerThanAmount>>, + pub epoch: ByEpoch>>, + pub year: ByYear>>, + pub type_: BySpendableType>>, +} -impl UTXOCohorts { +impl UTXOCohorts { /// Import all UTXO cohorts from database. pub(crate) fn forced_import( db: &Database, @@ -45,106 +73,191 @@ impl UTXOCohorts { ) -> Result { let v = version + VERSION; - // Phase 1: Import base cohorts that don't need adjusted (age_range, amount_range, etc.) - // These are the source cohorts for overlapping computations. - let base = |f: Filter, name: &'static str| { - UTXOCohortVecs::forced_import( - db, - f, - name, - v, - indexes, - prices, - states_path, - StateLevel::Full, - None, - None, - ) - }; - - let age_range = ByAgeRange::try_new(&base)?; - let amount_range = ByAmountRange::try_new(&base)?; - let epoch = ByEpoch::try_new(&base)?; - let year = ByYear::try_new(&base)?; - let type_ = BySpendableType::try_new(&base)?; - - // Get up_to_1h realized for adjusted computation (cohort - up_to_1h) - let up_to_1h_realized = &age_range.up_to_1h.metrics.realized; - - // Phase 2: Import "all" cohort (needs up_to_1h for adjusted, is global supply source) - let all = UTXOCohortVecs::forced_import( + // Phase 1: Import "all" supply first so it can be referenced by all cohorts' relative metrics. + let all_full_name = CohortContext::Utxo.full_name(&Filter::All, ""); + let all_cfg = ImportConfig { db, - Filter::All, - "", - version + VERSION + Version::ONE, + filter: Filter::All, + full_name: &all_full_name, + context: CohortContext::Utxo, + version: v + Version::ONE, indexes, prices, - states_path, - StateLevel::PriceOnly, + }; + let all_supply = SupplyMetrics::forced_import(&all_cfg)?; + + // Phase 2: Import separate (stateful) cohorts. + + // age_range: ExtendedCohortMetrics with full state + let age_range = { + let s = &all_supply; + ByAgeRange::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, + context: CohortContext::Utxo, + version: v, + indexes, + prices, + }; + let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name))); + Ok(UTXOCohortVecs::new( + state, + ExtendedCohortMetrics::forced_import(&cfg, s)?, + )) + })? + }; + + // Helper for separate cohorts with BasicCohortMetrics + full state + let basic_separate = + |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, + context: CohortContext::Utxo, + version: v, + indexes, + prices, + }; + let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name))); + Ok(UTXOCohortVecs::new( + state, + BasicCohortMetrics::forced_import(&cfg, &all_supply)?, + )) + }; + + let amount_range = ByAmountRange::try_new(&basic_separate)?; + let epoch = ByEpoch::try_new(&basic_separate)?; + let year = ByYear::try_new(&basic_separate)?; + let type_ = BySpendableType::try_new(&basic_separate)?; + + // Phase 3: Get up_to_1h realized for adjusted computation. + let up_to_1h_realized: &RealizedBase = &age_range.up_to_1h.metrics.realized; + + // Phase 4: Import "all" cohort with pre-imported supply. + let all = UTXOCohortVecs::new( None, - Some(up_to_1h_realized), - )?; + AllCohortMetrics::forced_import_with_supply(&all_cfg, all_supply, up_to_1h_realized)?, + ); - let all_supply = Some(all.metrics.supply.as_ref()); + let all_supply_ref = &all.metrics.supply; - // Phase 3: Import cohorts that need adjusted and/or all_supply - let price_only_adjusted = |f: Filter, name: &'static str| { - UTXOCohortVecs::forced_import( + // Phase 5: Import aggregate cohorts. + + // sth: ExtendedAdjustedCohortMetrics + let sth = { + let f = Filter::Term(Term::Sth); + let full_name = CohortContext::Utxo.full_name(&f, "sth"); + let cfg = ImportConfig { db, - f, - name, - v, + filter: f, + full_name: &full_name, + context: CohortContext::Utxo, + version: v, indexes, prices, - states_path, - StateLevel::PriceOnly, - all_supply, - Some(up_to_1h_realized), - ) - }; - - let term = ByTerm::try_new(&price_only_adjusted)?; - - let none_adjusted = |f: Filter, name: &'static str| { - UTXOCohortVecs::forced_import( - db, - f, - name, - v, - indexes, - prices, - states_path, - StateLevel::None, - all_supply, - Some(up_to_1h_realized), - ) - }; - - let max_age = ByMaxAge::try_new(&none_adjusted)?; - - // Phase 4: Import remaining cohorts (no adjusted needed) - let none = |f: Filter, name: &'static str| { - UTXOCohortVecs::forced_import( - db, - f, - name, - v, - indexes, - prices, - states_path, - StateLevel::None, - all_supply, + }; + UTXOCohortVecs::new( None, + ExtendedAdjustedCohortMetrics::forced_import( + &cfg, + all_supply_ref, + up_to_1h_realized, + )?, ) }; - let min_age = ByMinAge::try_new(&none)?; - let lt_amount = ByLowerThanAmount::try_new(&none)?; - let ge_amount = ByGreatEqualAmount::try_new(&none)?; + // lth: ExtendedCohortMetrics + let lth = { + let f = Filter::Term(Term::Lth); + let full_name = CohortContext::Utxo.full_name(&f, "lth"); + let cfg = ImportConfig { + db, + filter: f, + full_name: &full_name, + context: CohortContext::Utxo, + version: v, + indexes, + prices, + }; + UTXOCohortVecs::new( + None, + ExtendedCohortMetrics::forced_import(&cfg, all_supply_ref)?, + ) + }; - Ok(Self(UTXOGroups { + // max_age: AdjustedCohortMetrics (adjusted + peak_regret) + let max_age = { + let s = all_supply_ref; + 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, + context: CohortContext::Utxo, + version: v, + indexes, + prices, + }; + Ok(UTXOCohortVecs::new( + None, + AdjustedCohortMetrics::forced_import(&cfg, s, up_to_1h_realized)?, + )) + })? + }; + + // min_age: PeakRegretCohortMetrics + let min_age = { + let s = all_supply_ref; + 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, + context: CohortContext::Utxo, + version: v, + indexes, + prices, + }; + Ok(UTXOCohortVecs::new( + None, + PeakRegretCohortMetrics::forced_import(&cfg, s)?, + )) + })? + }; + + // ge_amount, lt_amount: BasicCohortMetrics (no state) + let basic_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, + context: CohortContext::Utxo, + version: v, + indexes, + prices, + }; + Ok(UTXOCohortVecs::new( + None, + BasicCohortMetrics::forced_import(&cfg, all_supply_ref)?, + )) + }; + + let lt_amount = ByLowerThanAmount::try_new(&basic_no_state)?; + let ge_amount = ByGreatEqualAmount::try_new(&basic_no_state)?; + + Ok(Self { all, - term, + sth, + lth, epoch, year, type_, @@ -154,86 +267,230 @@ impl UTXOCohorts { amount_range, lt_amount, ge_amount, - })) + }) } - /// Apply a function to each aggregate cohort with its source cohorts (in parallel). - fn for_each_aggregate(&mut self, f: F) -> Result<()> - where - F: Fn(&mut UTXOCohortVecs, Vec<&UTXOCohortVecs>) -> Result<()> + Sync, - { - let by_age_range = &self.0.age_range; - let by_amount_range = &self.0.amount_range; + // === Iteration helpers === - // Build (aggregate, sources) pairs - let pairs: Vec<_> = [(&mut self.0.all, by_age_range.iter().collect::>())] - .into_iter() - .chain(self.0.min_age.iter_mut().map(|vecs| { - let filter = vecs.filter().clone(); - ( - vecs, - by_age_range - .iter() - .filter(|other| filter.includes(other.filter())) - .collect(), - ) - })) - .chain(self.0.max_age.iter_mut().map(|vecs| { - let filter = vecs.filter().clone(); - ( - vecs, - by_age_range - .iter() - .filter(|other| filter.includes(other.filter())) - .collect(), - ) - })) - .chain(self.0.term.iter_mut().map(|vecs| { - let filter = vecs.filter().clone(); - ( - vecs, - by_age_range - .iter() - .filter(|other| filter.includes(other.filter())) - .collect(), - ) - })) - .chain(self.0.ge_amount.iter_mut().map(|vecs| { - let filter = vecs.filter().clone(); - ( - vecs, - by_amount_range - .iter() - .filter(|other| filter.includes(other.filter())) - .collect(), - ) - })) - .chain(self.0.lt_amount.iter_mut().map(|vecs| { - let filter = vecs.filter().clone(); - ( - vecs, - by_amount_range - .iter() - .filter(|other| filter.includes(other.filter())) - .collect(), - ) - })) - .collect(); - - pairs - .into_par_iter() - .try_for_each(|(vecs, sources)| f(vecs, sources)) + /// Parallel iterator over all separate (stateful) cohorts. + pub(crate) fn par_iter_separate_mut( + &mut self, + ) -> impl ParallelIterator { + let mut v: Vec<&mut dyn DynCohortVecs> = Vec::new(); + v.extend( + self.age_range + .iter_mut() + .map(|x| x as &mut dyn DynCohortVecs), + ); + v.extend(self.epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + v.extend(self.year.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + v.extend( + self.amount_range + .iter_mut() + .map(|x| x as &mut dyn DynCohortVecs), + ); + v.extend(self.type_.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + v.into_par_iter() } + /// Immutable iterator over all separate (stateful) cohorts. + pub(crate) fn iter_separate(&self) -> impl Iterator { + let mut v: Vec<&dyn DynCohortVecs> = Vec::new(); + v.extend(self.age_range.iter().map(|x| x as &dyn DynCohortVecs)); + v.extend(self.epoch.iter().map(|x| x as &dyn DynCohortVecs)); + v.extend(self.year.iter().map(|x| x as &dyn DynCohortVecs)); + v.extend(self.amount_range.iter().map(|x| x as &dyn DynCohortVecs)); + v.extend(self.type_.iter().map(|x| x as &dyn DynCohortVecs)); + v.into_iter() + } + + /// Mutable iterator over all separate cohorts (non-parallel). + pub(crate) fn iter_separate_mut(&mut self) -> impl Iterator { + let mut v: Vec<&mut dyn DynCohortVecs> = Vec::new(); + v.extend( + self.age_range + .iter_mut() + .map(|x| x as &mut dyn DynCohortVecs), + ); + v.extend(self.epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + v.extend(self.year.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + v.extend( + self.amount_range + .iter_mut() + .map(|x| x as &mut dyn DynCohortVecs), + ); + v.extend(self.type_.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + v.into_iter() + } + + // === Computation methods === + /// Compute overlapping cohorts from component age/amount range cohorts. pub(crate) fn compute_overlapping_vecs( &mut self, starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - self.for_each_aggregate(|vecs, sources| { - vecs.compute_from_stateful(starting_indexes, &sources, exit) - }) + let age_range = &self.age_range; + let amount_range = &self.amount_range; + + // all: aggregate of all age_range (base + peak_regret) + // Note: realized.extended rolling sums are computed from base in compute_rest_part2. + // Note: cost_basis.extended percentiles are computed in truncate_push_aggregate_percentiles. + { + let sources_dyn: Vec<&dyn CohortMetricsBase> = age_range + .iter() + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + self.all + .metrics + .compute_base_from_others(starting_indexes, &sources_dyn, exit)?; + + let pr_sources: Vec<_> = age_range + .iter() + .map(|v| &v.metrics.unrealized.peak_regret_ext) + .collect(); + self.all + .metrics + .unrealized + .peak_regret_ext + .compute_from_stateful(starting_indexes, &pr_sources, exit)?; + } + + // sth: aggregate of matching age_range (base + peak_regret) + { + let sth_filter = self.sth.metrics.filter().clone(); + let matching: Vec<_> = age_range + .iter() + .filter(|v| sth_filter.includes(v.metrics.filter())) + .collect(); + + let sources_dyn: Vec<&dyn CohortMetricsBase> = matching + .iter() + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + self.sth + .metrics + .compute_base_from_others(starting_indexes, &sources_dyn, exit)?; + + let pr_sources: Vec<_> = matching + .iter() + .map(|v| &v.metrics.unrealized.peak_regret_ext) + .collect(); + self.sth + .metrics + .unrealized + .peak_regret_ext + .compute_from_stateful(starting_indexes, &pr_sources, exit)?; + } + + // lth: aggregate of matching age_range (base + peak_regret) + { + let lth_filter = self.lth.metrics.filter().clone(); + let matching: Vec<_> = age_range + .iter() + .filter(|v| lth_filter.includes(v.metrics.filter())) + .collect(); + + let sources_dyn: Vec<&dyn CohortMetricsBase> = matching + .iter() + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + self.lth + .metrics + .compute_base_from_others(starting_indexes, &sources_dyn, exit)?; + + let pr_sources: Vec<_> = matching + .iter() + .map(|v| &v.metrics.unrealized.peak_regret_ext) + .collect(); + self.lth + .metrics + .unrealized + .peak_regret_ext + .compute_from_stateful(starting_indexes, &pr_sources, exit)?; + } + + // min_age: base + peak_regret from matching age_range + self.min_age + .iter_mut() + .collect::>() + .into_par_iter() + .try_for_each(|vecs| -> Result<()> { + let filter = vecs.metrics.filter().clone(); + let matching: Vec<_> = age_range + .iter() + .filter(|v| filter.includes(v.metrics.filter())) + .collect(); + + let sources_dyn: Vec<&dyn CohortMetricsBase> = matching + .iter() + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + vecs.metrics + .compute_base_from_others(starting_indexes, &sources_dyn, exit)?; + + let pr_sources: Vec<_> = matching + .iter() + .map(|v| &v.metrics.unrealized.peak_regret_ext) + .collect(); + vecs.metrics + .unrealized + .peak_regret_ext + .compute_from_stateful(starting_indexes, &pr_sources, exit)?; + + Ok(()) + })?; + + // max_age: base + peak_regret from matching age_range + self.max_age + .iter_mut() + .collect::>() + .into_par_iter() + .try_for_each(|vecs| -> Result<()> { + let filter = vecs.metrics.filter().clone(); + let matching: Vec<_> = age_range + .iter() + .filter(|v| filter.includes(v.metrics.filter())) + .collect(); + + let sources_dyn: Vec<&dyn CohortMetricsBase> = matching + .iter() + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + vecs.metrics + .compute_base_from_others(starting_indexes, &sources_dyn, exit)?; + + let pr_sources: Vec<_> = matching + .iter() + .map(|v| &v.metrics.unrealized.peak_regret_ext) + .collect(); + vecs.metrics + .unrealized + .peak_regret_ext + .compute_from_stateful(starting_indexes, &pr_sources, exit)?; + + Ok(()) + })?; + + // ge_amount, lt_amount: base only from matching amount_range + self.ge_amount + .iter_mut() + .chain(self.lt_amount.iter_mut()) + .collect::>() + .into_par_iter() + .try_for_each(|vecs| { + let filter = vecs.metrics.filter().clone(); + let sources_dyn: Vec<&dyn CohortMetricsBase> = amount_range + .iter() + .filter(|v| filter.includes(v.metrics.filter())) + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + vecs.metrics + .compute_base_from_others(starting_indexes, &sources_dyn, exit) + })?; + + Ok(()) } /// First phase of post-processing: compute index transforms. @@ -244,22 +501,136 @@ impl UTXOCohorts { starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - // 1. Compute all metrics except net_sentiment - self.par_iter_mut() - .try_for_each(|v| v.compute_rest_part1(blocks, prices, starting_indexes, exit))?; + // 1. Compute all metrics except net_sentiment (all cohorts via DynCohortVecs) + { + let mut all: Vec<&mut dyn DynCohortVecs> = Vec::new(); + all.push(&mut self.all); + all.push(&mut self.sth); + all.push(&mut self.lth); + all.extend(self.max_age.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + all.extend(self.min_age.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + all.extend( + self.ge_amount + .iter_mut() + .map(|x| x as &mut dyn DynCohortVecs), + ); + all.extend( + self.age_range + .iter_mut() + .map(|x| x as &mut dyn DynCohortVecs), + ); + all.extend(self.epoch.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + all.extend(self.year.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + all.extend( + self.amount_range + .iter_mut() + .map(|x| x as &mut dyn DynCohortVecs), + ); + all.extend( + self.lt_amount + .iter_mut() + .map(|x| x as &mut dyn DynCohortVecs), + ); + all.extend(self.type_.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); + all.into_par_iter() + .try_for_each(|v| v.compute_rest_part1(blocks, prices, starting_indexes, exit))?; + } // 2. Compute net_sentiment.height for separate cohorts (greed - pain) - self.par_iter_separate_mut().try_for_each(|v| { - v.metrics - .compute_net_sentiment_height(starting_indexes, exit) - })?; + self.par_iter_separate_mut() + .try_for_each(|v| v.compute_net_sentiment_height(starting_indexes, exit))?; // 3. Compute net_sentiment.height for aggregate cohorts (weighted average) - self.for_each_aggregate(|vecs, sources| { - let metrics: Vec<_> = sources.iter().map(|v| &v.metrics).collect(); - vecs.metrics - .compute_net_sentiment_from_others(starting_indexes, &metrics, exit) - })?; + { + let age_range = &self.age_range; + let amount_range = &self.amount_range; + + // all + { + let sources: Vec<_> = age_range + .iter() + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + self.all.metrics.compute_net_sentiment_from_others_dyn( + starting_indexes, + &sources, + exit, + )?; + } + + // sth + { + let filter = self.sth.metrics.filter().clone(); + let sources: Vec<_> = age_range + .iter() + .filter(|v| filter.includes(v.metrics.filter())) + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + self.sth.metrics.compute_net_sentiment_from_others_dyn( + starting_indexes, + &sources, + exit, + )?; + } + + // lth + { + let filter = self.lth.metrics.filter().clone(); + let sources: Vec<_> = age_range + .iter() + .filter(|v| filter.includes(v.metrics.filter())) + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + self.lth.metrics.compute_net_sentiment_from_others_dyn( + starting_indexes, + &sources, + exit, + )?; + } + + // min_age, max_age from age_range + for vecs in self.min_age.iter_mut() { + let filter = vecs.metrics.filter().clone(); + let sources: Vec<_> = age_range + .iter() + .filter(|v| filter.includes(v.metrics.filter())) + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + vecs.metrics.compute_net_sentiment_from_others_dyn( + starting_indexes, + &sources, + exit, + )?; + } + for vecs in self.max_age.iter_mut() { + let filter = vecs.metrics.filter().clone(); + let sources: Vec<_> = age_range + .iter() + .filter(|v| filter.includes(v.metrics.filter())) + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + vecs.metrics.compute_net_sentiment_from_others_dyn( + starting_indexes, + &sources, + exit, + )?; + } + + // ge_amount, lt_amount from amount_range + for vecs in self.ge_amount.iter_mut().chain(self.lt_amount.iter_mut()) { + let filter = vecs.metrics.filter().clone(); + let sources: Vec<_> = amount_range + .iter() + .filter(|v| filter.includes(v.metrics.filter())) + .map(|v| &v.metrics as &dyn CohortMetricsBase) + .collect(); + vecs.metrics.compute_net_sentiment_from_others_dyn( + starting_indexes, + &sources, + exit, + )?; + } + } Ok(()) } @@ -270,27 +641,153 @@ impl UTXOCohorts { blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &ComputeIndexes, - height_to_market_cap: Option<&HM>, + height_to_market_cap: &HM, exit: &Exit, ) -> Result<()> where HM: ReadableVec + Sync, { - self.par_iter_mut().try_for_each(|v| { - v.compute_rest_part2(blocks, prices, starting_indexes, height_to_market_cap, exit) - }) + self.all.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + )?; + self.sth.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + )?; + self.lth.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + )?; + self.age_range.par_iter_mut().try_for_each(|v| { + v.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + ) + })?; + self.max_age.par_iter_mut().try_for_each(|v| { + v.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + ) + })?; + self.min_age.par_iter_mut().try_for_each(|v| { + v.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + ) + })?; + self.ge_amount.par_iter_mut().try_for_each(|v| { + v.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + ) + })?; + self.epoch.par_iter_mut().try_for_each(|v| { + v.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + ) + })?; + self.year.par_iter_mut().try_for_each(|v| { + v.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + ) + })?; + self.amount_range.par_iter_mut().try_for_each(|v| { + v.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + ) + })?; + self.lt_amount.par_iter_mut().try_for_each(|v| { + v.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + ) + })?; + self.type_.par_iter_mut().try_for_each(|v| { + v.metrics.compute_rest_part2( + blocks, + prices, + starting_indexes, + height_to_market_cap, + exit, + ) + })?; + Ok(()) } /// Returns a parallel iterator over all vecs for parallel writing. pub(crate) fn par_iter_vecs_mut( &mut self, ) -> impl ParallelIterator { - // Collect all vecs from all cohorts (separate + aggregate) - self.0 - .iter_mut() - .flat_map(|v| v.par_iter_vecs_mut().collect::>()) - .collect::>() - .into_par_iter() + let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); + vecs.extend(self.all.metrics.collect_all_vecs_mut()); + vecs.extend(self.sth.metrics.collect_all_vecs_mut()); + vecs.extend(self.lth.metrics.collect_all_vecs_mut()); + for v in self.age_range.iter_mut() { + vecs.extend(v.metrics.collect_all_vecs_mut()); + } + for v in self.max_age.iter_mut() { + vecs.extend(v.metrics.collect_all_vecs_mut()); + } + for v in self.min_age.iter_mut() { + vecs.extend(v.metrics.collect_all_vecs_mut()); + } + for v in self.ge_amount.iter_mut() { + vecs.extend(v.metrics.collect_all_vecs_mut()); + } + for v in self.epoch.iter_mut() { + vecs.extend(v.metrics.collect_all_vecs_mut()); + } + for v in self.year.iter_mut() { + vecs.extend(v.metrics.collect_all_vecs_mut()); + } + for v in self.amount_range.iter_mut() { + vecs.extend(v.metrics.collect_all_vecs_mut()); + } + for v in self.lt_amount.iter_mut() { + vecs.extend(v.metrics.collect_all_vecs_mut()); + } + for v in self.type_.iter_mut() { + vecs.extend(v.metrics.collect_all_vecs_mut()); + } + vecs.into_par_iter() } /// Commit all states to disk (separate from vec writes for parallelization). @@ -324,19 +821,11 @@ impl UTXOCohorts { /// Reset cost_basis_data for all separate cohorts (called during fresh start). pub(crate) fn reset_separate_cost_basis_data(&mut self) -> Result<()> { - self.par_iter_separate_mut().try_for_each(|v| { - if let Some(state) = v.state.as_mut() { - state.reset_cost_basis_data_if_needed()?; - } - Ok(()) - }) + self.par_iter_separate_mut() + .try_for_each(|v| v.reset_cost_basis_data_if_needed()) } /// Compute and push percentiles for aggregate cohorts (all, sth, lth). - /// Computes on-demand by merging age_range cohorts' cost_basis_data data. - /// This avoids maintaining redundant aggregate cost_basis_data maps. - /// Computes both sat-weighted (percentiles) and USD-weighted (invested_capital) percentiles. - /// Also writes daily cost basis snapshots to states_path when day1 is provided. pub(crate) fn truncate_push_aggregate_percentiles( &mut self, height: Height, @@ -345,10 +834,7 @@ impl UTXOCohorts { states_path: &Path, ) -> Result<()> { // Collect (filter, entries, total_sats, total_usd) from age_range cohorts. - // Keep data in CentsUnsigned to avoid float conversions until output. - // Compute totals during collection to avoid a second pass. let age_range_data: Vec<_> = self - .0 .age_range .iter() .filter_map(|sub| { @@ -369,13 +855,34 @@ impl UTXOCohorts { }) .collect(); - // Compute percentiles for each aggregate filter in parallel - self.0.par_iter_aggregate_mut().try_for_each(|aggregate| { - let filter = aggregate.filter().clone(); + // Build list of (filter, cost_basis_extended, cohort_name) for aggregate cohorts + struct AggregateTarget<'a> { + filter: Filter, + extended: &'a mut crate::distribution::metrics::CostBasisExtended, + cohort_name: Option<&'static str>, + } - let cost_basis = &mut aggregate.metrics.cost_basis; + let mut targets = [ + AggregateTarget { + filter: self.all.metrics.filter().clone(), + extended: &mut self.all.metrics.cost_basis.extended, + cohort_name: Some("all"), + }, + AggregateTarget { + filter: self.sth.metrics.filter().clone(), + extended: &mut self.sth.metrics.cost_basis.extended, + cohort_name: Some(TERM_NAMES.short.id), + }, + AggregateTarget { + filter: self.lth.metrics.filter().clone(), + extended: &mut self.lth.metrics.cost_basis.extended, + cohort_name: Some(TERM_NAMES.long.id), + }, + ]; + + for target in targets.iter_mut() { + let filter = &target.filter; - // Collect relevant cohort data for this aggregate and sum totals let mut total_sats: u64 = 0; let mut total_usd: u128 = 0; let relevant: Vec<_> = age_range_data @@ -390,33 +897,35 @@ impl UTXOCohorts { if total_sats == 0 { let nan_prices = [Dollars::NAN; PERCENTILES_LEN]; - if let Some(percentiles) = cost_basis.percentiles.as_mut() { - percentiles.truncate_push(height, &nan_prices)?; - } - if let Some(invested_capital) = cost_basis.invested_capital.as_mut() { - invested_capital.truncate_push(height, &nan_prices)?; - } - if let Some(spot_pct) = cost_basis.spot_cost_basis_percentile.as_mut() { - spot_pct.height.truncate_push(height, StoredF32::NAN)?; - } - if let Some(spot_pct) = cost_basis.spot_invested_capital_percentile.as_mut() { - spot_pct.height.truncate_push(height, StoredF32::NAN)?; - } - return Ok(()); + target + .extended + .percentiles + .truncate_push(height, &nan_prices)?; + target + .extended + .invested_capital + .truncate_push(height, &nan_prices)?; + target + .extended + .spot_cost_basis_percentile + .height + .truncate_push(height, StoredF32::NAN)?; + target + .extended + .spot_invested_capital_percentile + .height + .truncate_push(height, StoredF32::NAN)?; + continue; } - // K-way merge using min-heap: O(n log k) where k = number of cohorts - // Collects merged price->sats map while computing percentiles + // K-way merge using min-heap let mut heap: BinaryHeap> = BinaryHeap::new(); - - // Initialize heap with first entry from each cohort for (cohort_idx, entries) in relevant.iter().enumerate() { if !entries.is_empty() { heap.push(Reverse((entries[0].0, cohort_idx, 0))); } } - // Compute both sat-weighted and USD-weighted percentiles in one pass let sat_targets = PERCENTILES.map(|p| total_sats * u64::from(p) / 100); let usd_targets = PERCENTILES.map(|p| total_usd * u128::from(p) / 100); @@ -432,7 +941,6 @@ impl UTXOCohorts { let mut sats_at_price: u64 = 0; let mut usd_at_price: u128 = 0; - // Only collect merged entries when writing snapshots (date boundary) let collect_merged = day1_opt.is_some(); let max_unique_prices = if collect_merged { relevant.iter().map(|e| e.len()).max().unwrap_or(0) @@ -441,7 +949,6 @@ impl UTXOCohorts { }; let mut merged: Vec<(CentsCompact, Sats)> = Vec::with_capacity(max_unique_prices); - // Finalize a price point: compute percentiles and optionally accumulate for merged vec let mut finalize_price = |price: Cents, sats: u64, usd: u128| { cumsum_sats += sats; cumsum_usd += usd; @@ -477,7 +984,6 @@ impl UTXOCohorts { let amount_u64 = u64::from(amount); let price_u128 = price.as_u128(); - // If price changed, finalize previous price if let Some(prev_price) = current_price && prev_price != price { @@ -490,45 +996,42 @@ impl UTXOCohorts { sats_at_price += amount_u64; usd_at_price += price_u128 * amount_u64 as u128; - // Push next entry from this cohort let next_idx = entry_idx + 1; if next_idx < entries.len() { heap.push(Reverse((entries[next_idx].0, cohort_idx, next_idx))); } } - // Finalize last price if let Some(price) = current_price { finalize_price(price, sats_at_price, usd_at_price); } - // Push both sat-weighted and USD-weighted results - if let Some(percentiles) = cost_basis.percentiles.as_mut() { - percentiles.truncate_push(height, &sat_result)?; - } - if let Some(invested_capital) = cost_basis.invested_capital.as_mut() { - invested_capital.truncate_push(height, &usd_result)?; - } + target + .extended + .percentiles + .truncate_push(height, &sat_result)?; + target + .extended + .invested_capital + .truncate_push(height, &usd_result)?; - // Compute and push spot percentile ranks - if let Some(spot_pct) = cost_basis.spot_cost_basis_percentile.as_mut() { - let rank = compute_spot_percentile_rank(&sat_result, spot); - spot_pct.height.truncate_push(height, rank)?; - } - if let Some(spot_pct) = cost_basis.spot_invested_capital_percentile.as_mut() { - let rank = compute_spot_percentile_rank(&usd_result, spot); - spot_pct.height.truncate_push(height, rank)?; - } - - // Write daily cost basis snapshot (only at date boundaries) - if let Some(day1) = day1_opt { - let cohort_name = match &filter { - Filter::All => "all", - Filter::Term(Term::Sth) => TERM_NAMES.short.id, - Filter::Term(Term::Lth) => TERM_NAMES.long.id, - _ => return Ok(()), - }; + let rank = compute_spot_percentile_rank(&sat_result, spot); + target + .extended + .spot_cost_basis_percentile + .height + .truncate_push(height, rank)?; + let rank = compute_spot_percentile_rank(&usd_result, spot); + target + .extended + .spot_invested_capital_percentile + .height + .truncate_push(height, rank)?; + // Write daily cost basis snapshot + if let Some(day1) = day1_opt + && let Some(cohort_name) = target.cohort_name + { let date = Date::from(day1); let dir = states_path.join(format!("utxo_{cohort_name}_cost_basis/by_date")); fs::create_dir_all(&dir)?; @@ -538,30 +1041,38 @@ impl UTXOCohorts { CostBasisDistribution::serialize_iter(merged.into_iter())?, )?; } + } - Ok(()) - }) + Ok(()) } - /// Validate computed versions for all cohorts (separate and aggregate). + /// Validate computed versions for all cohorts. pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { // Validate separate cohorts self.par_iter_separate_mut() .try_for_each(|v| v.validate_computed_versions(base_version))?; - // Validate aggregate cohorts' cost_basis percentiles - for v in self.0.iter_aggregate_mut() { - v.validate_computed_versions(base_version)?; + // Validate aggregate cohorts + self.all.metrics.validate_computed_versions(base_version)?; + self.sth.metrics.validate_computed_versions(base_version)?; + self.lth.metrics.validate_computed_versions(base_version)?; + for v in self.min_age.iter_mut() { + v.metrics.validate_computed_versions(base_version)?; + } + for v in self.max_age.iter_mut() { + v.metrics.validate_computed_versions(base_version)?; + } + for v in self.ge_amount.iter_mut() { + v.metrics.validate_computed_versions(base_version)?; + } + for v in self.lt_amount.iter_mut() { + v.metrics.validate_computed_versions(base_version)?; } Ok(()) } /// Compute and push peak regret for all age_range cohorts. - /// - /// Uses split points to efficiently compute regret per cohort. - /// All 21 cohorts are computed in parallel, then pushed sequentially. - /// Called once per day when day1 changes. pub(crate) fn compute_and_push_peak_regret( &mut self, chain_state: &[BlockState], @@ -575,14 +1086,15 @@ impl UTXOCohorts { let start_height = FIRST_PRICE_HEIGHT; let end_height = current_height.to_usize() + 1; - // Early return: push zeros if no price data yet if end_height <= start_height { - for cohort in self.0.age_range.iter_mut() { - if let Some(peak_regret) = cohort.metrics.unrealized.peak_regret.as_mut() { - peak_regret - .height - .truncate_push(current_height, Dollars::ZERO)?; - } + for cohort in self.age_range.iter_mut() { + cohort + .metrics + .unrealized + .peak_regret_ext + .peak_regret + .height + .truncate_push(current_height, Dollars::ZERO)?; } return Ok(()); } @@ -590,14 +1102,12 @@ impl UTXOCohorts { let spot_u128 = spot.as_u128(); let current_ts = *current_timestamp; - // Compute split points: splits[k] = first index where age < AGE_BOUNDARIES[k] let splits: [usize; 20] = std::array::from_fn(|k| { let boundary_seconds = (AGE_BOUNDARIES[k] as u32) * ONE_HOUR_IN_SEC; let threshold_ts = current_ts.saturating_sub(boundary_seconds); chain_state[..end_height].partition_point(|b| *b.timestamp <= threshold_ts) }); - // Build ranges for all 21 cohorts let ranges: [(usize, usize); 21] = std::array::from_fn(|i| { if i == 0 { (splits[0], end_height) @@ -608,7 +1118,6 @@ impl UTXOCohorts { } }); - // Compute regret for all cohorts in parallel let regrets: [Dollars; 21] = ranges .into_par_iter() .map(|(range_start, range_end)| { @@ -626,7 +1135,6 @@ impl UTXOCohorts { } let cost_basis = block.price; - let receive_height = Height::from(effective_start + i); let peak = price_range_max.max_between(receive_height, current_height); let peak_u128 = peak.as_u128(); @@ -646,11 +1154,14 @@ impl UTXOCohorts { .try_into() .unwrap(); - // Push results to cohorts - for (cohort, regret) in self.0.age_range.iter_mut().zip(regrets) { - if let Some(peak_regret) = cohort.metrics.unrealized.peak_regret.as_mut() { - peak_regret.height.truncate_push(current_height, regret)?; - } + for (cohort, regret) in self.age_range.iter_mut().zip(regrets) { + cohort + .metrics + .unrealized + .peak_regret_ext + .peak_regret + .height + .truncate_push(current_height, regret)?; } Ok(()) diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/receive.rs b/crates/brk_computer/src/distribution/cohorts/utxo/receive.rs index 83cfea0a7..3c6c66828 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/receive.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/receive.rs @@ -1,10 +1,11 @@ use brk_types::{Cents, Height, Timestamp}; +use vecdb::Rw; use crate::distribution::state::Transacted; use super::groups::UTXOCohorts; -impl UTXOCohorts { +impl UTXOCohorts { /// Process received outputs for this block. /// /// New UTXOs are added to: @@ -23,15 +24,9 @@ impl UTXOCohorts { let supply_state = received.spendable_supply; // New UTXOs go into up_to_1h, current epoch, and current year - [ - &mut self.0.age_range.up_to_1h, - self.0.epoch.mut_vec_from_height(height), - self.0.year.mut_vec_from_timestamp(timestamp), - ] - .into_iter() - .for_each(|v| { - v.state.as_mut().unwrap().receive_utxo(&supply_state, price); - }); + self.age_range.up_to_1h.state.as_mut().unwrap().receive_utxo(&supply_state, price); + self.epoch.mut_vec_from_height(height).state.as_mut().unwrap().receive_utxo(&supply_state, price); + self.year.mut_vec_from_timestamp(timestamp).state.as_mut().unwrap().receive_utxo(&supply_state, price); // Update output type cohorts self.type_ diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/send.rs b/crates/brk_computer/src/distribution/cohorts/utxo/send.rs index 7d2a82cce..4d5eaf7bf 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/send.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/send.rs @@ -1,6 +1,6 @@ use brk_types::{Age, Height}; use rustc_hash::FxHashMap; -use vecdb::VecIndex; +use vecdb::{Rw, VecIndex}; use crate::distribution::{ compute::PriceRangeMax, @@ -9,7 +9,7 @@ use crate::distribution::{ use super::groups::UTXOCohorts; -impl UTXOCohorts { +impl UTXOCohorts { /// Process spent inputs for this block. /// /// Each input references a UTXO created at some previous height. @@ -47,7 +47,7 @@ impl UTXOCohorts { let peak_price = price_range_max.max_between(receive_height, send_height); // Update age range cohort (direct index lookup) - self.0.age_range.get_mut(age).state.as_mut().unwrap().send_utxo( + self.age_range.get_mut(age).state.as_mut().unwrap().send_utxo( &sent.spendable_supply, current_price, prev_price, @@ -56,8 +56,7 @@ impl UTXOCohorts { ); // Update epoch cohort (direct lookup by height) - self.0 - .epoch + self.epoch .mut_vec_from_height(receive_height) .state .as_mut().unwrap() @@ -70,8 +69,7 @@ impl UTXOCohorts { ); // Update year cohort (direct lookup by timestamp) - self.0 - .year + self.year .mut_vec_from_timestamp(block_state.timestamp) .state .as_mut().unwrap() @@ -88,7 +86,7 @@ impl UTXOCohorts { .spendable .iter_typed() .for_each(|(output_type, supply_state)| { - self.0.type_.get_mut(output_type).state.as_mut().unwrap().send_utxo( + self.type_.get_mut(output_type).state.as_mut().unwrap().send_utxo( supply_state, current_price, prev_price, @@ -101,7 +99,7 @@ impl UTXOCohorts { sent.by_size_group .iter_typed() .for_each(|(group, supply_state)| { - self.0.amount_range.get_mut(group).state.as_mut().unwrap().send_utxo( + self.amount_range.get_mut(group).state.as_mut().unwrap().send_utxo( supply_state, current_price, prev_price, diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs b/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs index 0e00ccbde..4ce52d753 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs @@ -1,11 +1,12 @@ use brk_cohort::AGE_BOUNDARIES; use brk_types::{ONE_HOUR_IN_SEC, Timestamp}; +use vecdb::Rw; use crate::distribution::state::BlockState; use super::groups::UTXOCohorts; -impl UTXOCohorts { +impl UTXOCohorts { /// Handle age transitions when processing a new block. /// /// UTXOs age with each block. When they cross hour boundaries, @@ -32,7 +33,7 @@ impl UTXOCohorts { // Cohort i covers hours [BOUNDARIES[i-1], BOUNDARIES[i]) // Cohort 0 covers [0, 1) hours // Cohort 20 covers [15*365*24, infinity) hours - let mut age_cohorts: Vec<_> = self.0.age_range.iter_mut().map(|v| &mut v.state).collect(); + let mut age_cohorts: Vec<_> = self.age_range.iter_mut().map(|v| &mut v.state).collect(); // For each boundary (in hours), find blocks that just crossed it for (boundary_idx, &boundary_hours) in AGE_BOUNDARIES.iter().enumerate() { diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs b/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs index 7fac37cd4..01a2024b6 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs @@ -1,22 +1,23 @@ -use std::path::Path; - -use brk_cohort::{CohortContext, Filter, Filtered, StateLevel}; +use brk_cohort::{Filter, Filtered}; use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Cents, Dollars, Height, Version}; -use rayon::prelude::*; -use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode}; +use brk_types::{Cents, Height, Version}; +use vecdb::{Exit, ReadableVec}; -use crate::{ComputeIndexes, blocks, distribution::state::UTXOCohortState, indexes, prices}; +use crate::{ComputeIndexes, blocks, distribution::state::UTXOCohortState, prices}; -use crate::distribution::metrics::{CohortMetrics, ImportConfig, RealizedMetrics, SupplyMetrics}; +use crate::distribution::metrics::CohortMetricsBase; -use super::super::traits::{CohortVecs, DynCohortVecs}; +use super::super::traits::DynCohortVecs; /// UTXO cohort with metrics and optional runtime state. +/// +/// Generic over the metrics type to support different cohort configurations +/// (e.g. AllCohortMetrics, ExtendedCohortMetrics, BasicCohortMetrics, etc.) #[derive(Traversable)] -pub struct UTXOCohortVecs { +pub struct UTXOCohortVecs { /// Starting height when state was imported + #[traversable(skip)] state_starting_height: Option, /// Runtime state for block-by-block processing (separate cohorts only) @@ -25,85 +26,28 @@ pub struct UTXOCohortVecs { /// Metric vectors #[traversable(flatten)] - pub metrics: CohortMetrics, + pub metrics: Metrics, } -impl UTXOCohortVecs { - /// Import UTXO cohort from database. - /// - /// `all_supply` is the supply metrics from the "all" cohort, used as global - /// sources for `*_rel_to_market_cap` ratios. Pass `None` for the "all" cohort itself. - /// - /// `up_to_1h_realized` is used for cohorts where `compute_adjusted()` is true, - /// to create lazy adjusted vecs: adjusted = cohort - up_to_1h. - #[allow(clippy::too_many_arguments)] - pub(crate) fn forced_import( - db: &Database, - filter: Filter, - name: &str, - version: Version, - indexes: &indexes::Vecs, - prices: &prices::Vecs, - states_path: &Path, - state_level: StateLevel, - all_supply: Option<&SupplyMetrics>, - up_to_1h_realized: Option<&RealizedMetrics>, - ) -> Result { - let full_name = CohortContext::Utxo.full_name(&filter, name); - - let cfg = ImportConfig { - db, - filter, - full_name: &full_name, - context: CohortContext::Utxo, - version, - indexes, - prices, - up_to_1h_realized, - }; - - Ok(Self { +impl UTXOCohortVecs { + /// Create a new UTXOCohortVecs with state and metrics. + pub(crate) fn new(state: Option>, metrics: Metrics) -> Self { + Self { state_starting_height: None, - - state: if state_level.is_full() { - Some(Box::new(UTXOCohortState::new(states_path, &full_name))) - } else { - None - }, - - metrics: CohortMetrics::forced_import(&cfg, all_supply)?, - }) - } - - /// Reset state starting height to zero and reset state values. - pub(crate) fn reset_state_starting_height(&mut self) { - self.state_starting_height = Some(Height::ZERO); - if let Some(state) = self.state.as_mut() { - state.reset(); + state, + metrics, } } - /// Returns a parallel iterator over all vecs for parallel writing. - pub(crate) fn par_iter_vecs_mut(&mut self) -> impl ParallelIterator { - self.metrics.par_iter_mut() - } - - /// Commit state to disk (separate from vec writes for parallelization). - pub(crate) fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> { - if let Some(state) = self.state.as_mut() { - state.write(height, cleanup)?; - } - Ok(()) - } } -impl Filtered for UTXOCohortVecs { +impl Filtered for UTXOCohortVecs { fn filter(&self) -> &Filter { - &self.metrics.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() } @@ -116,18 +60,13 @@ impl DynCohortVecs for UTXOCohortVecs { } fn import_state(&mut self, starting_height: Height) -> Result { - // Import state from runtime state if present if let Some(state) = self.state.as_mut() { - // State files are saved AT height H, so to resume at H+1 we need to import at H - // Decrement first, then increment result to match expected starting_height if let Some(mut prev_height) = starting_height.decremented() { - // Import cost_basis_data state file (may adjust prev_height to actual file found) prev_height = state.import_at_or_before(prev_height)?; - // Restore supply state from height-indexed vectors state.supply.value = self .metrics - .supply + .supply() .total .sats .height @@ -135,20 +74,18 @@ impl DynCohortVecs for UTXOCohortVecs { .unwrap(); state.supply.utxo_count = *self .metrics - .outputs + .outputs() .utxo_count .height .collect_one(prev_height) .unwrap(); - // Restore realized cap from persisted exact values state.restore_realized_cap(); let result = prev_height.incremented(); self.state_starting_height = Some(result); Ok(result) } else { - // starting_height is 0, nothing to import self.state_starting_height = Some(Height::ZERO); Ok(Height::ZERO) } @@ -167,7 +104,6 @@ impl DynCohortVecs for UTXOCohortVecs { return Ok(()); } - // Push from state to metrics if let Some(state) = self.state.as_ref() { self.metrics.truncate_push(height, state)?; } @@ -181,11 +117,8 @@ impl DynCohortVecs for UTXOCohortVecs { height_price: Cents, ) -> Result<()> { if let Some(state) = self.state.as_mut() { - self.metrics.compute_then_truncate_push_unrealized_states( - height, - height_price, - state, - )?; + self.metrics + .compute_then_truncate_push_unrealized_states(height, height_price, state)?; } Ok(()) } @@ -200,36 +133,33 @@ impl DynCohortVecs for UTXOCohortVecs { self.metrics .compute_rest_part1(blocks, prices, starting_indexes, exit) } -} -impl CohortVecs for UTXOCohortVecs { - fn compute_from_stateful( + fn compute_net_sentiment_height( &mut self, starting_indexes: &ComputeIndexes, - others: &[&Self], exit: &Exit, ) -> Result<()> { - self.metrics.compute_from_stateful( - starting_indexes, - &others.iter().map(|v| &v.metrics).collect::>(), - exit, - ) + self.metrics + .compute_net_sentiment_height(starting_indexes, exit) } - fn compute_rest_part2( - &mut self, - blocks: &blocks::Vecs, - prices: &prices::Vecs, - starting_indexes: &ComputeIndexes, - height_to_market_cap: Option<&impl ReadableVec>, - exit: &Exit, - ) -> Result<()> { - self.metrics.compute_rest_part2( - blocks, - prices, - starting_indexes, - height_to_market_cap, - exit, - ) + fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> { + if let Some(state) = self.state.as_mut() { + state.write(height, cleanup)?; + } + Ok(()) + } + + fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> { + if let Some(state) = self.state.as_mut() { + state.reset_cost_basis_data_if_needed()?; + } + Ok(()) + } + + fn reset_single_iteration_values(&mut self) { + if let Some(state) = self.state.as_mut() { + state.reset_single_iteration_values(); + } } } diff --git a/crates/brk_computer/src/distribution/compute/aggregates.rs b/crates/brk_computer/src/distribution/compute/aggregates.rs index 6262d657b..83237ee19 100644 --- a/crates/brk_computer/src/distribution/compute/aggregates.rs +++ b/crates/brk_computer/src/distribution/compute/aggregates.rs @@ -54,7 +54,7 @@ pub(crate) fn compute_rest_part2( blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &ComputeIndexes, - height_to_market_cap: Option<&HM>, + height_to_market_cap: &HM, exit: &Exit, ) -> Result<()> where diff --git a/crates/brk_computer/src/distribution/compute/block_loop.rs b/crates/brk_computer/src/distribution/compute/block_loop.rs index da8354787..26fea5912 100644 --- a/crates/brk_computer/src/distribution/compute/block_loop.rs +++ b/crates/brk_computer/src/distribution/compute/block_loop.rs @@ -520,17 +520,13 @@ pub(crate) fn process_blocks( /// Reset per-block values for all separate cohorts. fn reset_block_values(utxo_cohorts: &mut UTXOCohorts, address_cohorts: &mut AddressCohorts) { - utxo_cohorts.iter_separate_mut().for_each(|v| { - if let Some(state) = v.state.as_mut() { - state.reset_single_iteration_values(); - } - }); + utxo_cohorts + .iter_separate_mut() + .for_each(|v| v.reset_single_iteration_values()); - address_cohorts.iter_separate_mut().for_each(|v| { - if let Some(state) = v.state.as_mut() { - state.inner.reset_single_iteration_values(); - } - }); + address_cohorts + .iter_separate_mut() + .for_each(|v| v.reset_single_iteration_values()); } /// Push cohort states to height-indexed vectors. diff --git a/crates/brk_computer/src/distribution/metrics/cohort/adjusted.rs b/crates/brk_computer/src/distribution/metrics/cohort/adjusted.rs new file mode 100644 index 000000000..626b95a6d --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cohort/adjusted.rs @@ -0,0 +1,122 @@ +use brk_cohort::Filter; +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Cents, Dollars, Height, Version}; +use rayon::prelude::*; +use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices}; + +use crate::distribution::metrics::{ + ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase, + RealizedWithAdjusted, RelativeWithPeakRegret, SupplyMetrics, UnrealizedBase, + UnrealizedWithPeakRegret, +}; + +/// Cohort metrics with adjusted realized + peak regret (no extended). +/// Used by: max_age cohorts. +#[derive(Traversable)] +pub struct AdjustedCohortMetrics { + #[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 CohortMetricsBase for AdjustedCohortMetrics { + fn filter(&self) -> &Filter { &self.filter } + fn supply(&self) -> &SupplyMetrics { &self.supply } + fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply } + fn outputs(&self) -> &OutputsMetrics { &self.outputs } + fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs } + fn activity(&self) -> &ActivityMetrics { &self.activity } + fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity } + fn realized_base(&self) -> &RealizedBase { &self.realized } + fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized } + fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized } + fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized } + fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis } + fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis } + + 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(()) + } + fn compute_then_truncate_push_unrealized_states( + &mut self, height: Height, height_price: Cents, state: &mut CohortState, + ) -> Result<()> { + state.apply_pending(); + self.cost_basis.truncate_push_minmax(height, state)?; + let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None); + self.unrealized.base.truncate_push(height, &height_unrealized_state)?; + Ok(()) + } + fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); + vecs.extend(self.supply.par_iter_mut().collect::>()); + vecs.extend(self.outputs.par_iter_mut().collect::>()); + vecs.extend(self.activity.par_iter_mut().collect::>()); + vecs.extend(self.realized.collect_vecs_mut()); + vecs.extend(self.cost_basis.collect_vecs_mut()); + vecs.extend(self.unrealized.base.collect_vecs_mut()); + vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut()); + vecs + } +} + +impl AdjustedCohortMetrics { + pub(crate) fn forced_import( + cfg: &ImportConfig, + all_supply: &SupplyMetrics, + up_to_1h: &RealizedBase, + ) -> Result { + let supply = SupplyMetrics::forced_import(cfg)?; + let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?; + let realized = RealizedWithAdjusted::forced_import(cfg, up_to_1h)?; + + let relative = RelativeWithPeakRegret::forced_import( + cfg, + &unrealized.base, + &supply, + all_supply, + &realized.base, + &unrealized.peak_regret_ext.peak_regret, + ); + + Ok(Self { + filter: cfg.filter.clone(), + supply: Box::new(supply), + outputs: Box::new(OutputsMetrics::forced_import(cfg)?), + activity: Box::new(ActivityMetrics::forced_import(cfg)?), + realized: Box::new(realized), + cost_basis: Box::new(CostBasisBase::forced_import(cfg)?), + unrealized: Box::new(unrealized), + relative: Box::new(relative), + }) + } + + pub(crate) fn compute_rest_part2( + &mut self, + blocks: &blocks::Vecs, + prices: &prices::Vecs, + starting_indexes: &ComputeIndexes, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + self.realized.compute_rest_part2( + blocks, + prices, + starting_indexes, + &self.supply.total.btc.height, + height_to_market_cap, + exit, + ) + } + +} diff --git a/crates/brk_computer/src/distribution/metrics/cohort/all.rs b/crates/brk_computer/src/distribution/metrics/cohort/all.rs new file mode 100644 index 000000000..7fef6464d --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cohort/all.rs @@ -0,0 +1,128 @@ +use brk_cohort::Filter; +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Cents, Dollars, Height, Version}; +use rayon::prelude::*; +use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices}; + +use crate::distribution::metrics::{ + ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig, + OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeForAll, SupplyMetrics, + UnrealizedBase, UnrealizedWithPeakRegret, +}; + +/// All-cohort metrics: extended + adjusted realized, extended cost basis, +/// peak regret, relative for-all (no rel_to_all). +/// Used by: the "all" cohort. +#[derive(Traversable)] +pub struct AllCohortMetrics { + #[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 CohortMetricsBase for AllCohortMetrics { + fn filter(&self) -> &Filter { &self.filter } + fn supply(&self) -> &SupplyMetrics { &self.supply } + fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply } + fn outputs(&self) -> &OutputsMetrics { &self.outputs } + fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs } + fn activity(&self) -> &ActivityMetrics { &self.activity } + fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity } + fn realized_base(&self) -> &RealizedBase { &self.realized } + fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized } + fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized } + fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized } + fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis } + fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis } + fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { + self.supply.validate_computed_versions(base_version)?; + self.activity.validate_computed_versions(base_version)?; + self.cost_basis.validate_computed_versions(base_version)?; + Ok(()) + } + fn compute_then_truncate_push_unrealized_states( + &mut self, height: Height, height_price: Cents, state: &mut CohortState, + ) -> Result<()> { + state.apply_pending(); + self.cost_basis.truncate_push_minmax(height, state)?; + let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None); + self.unrealized.base.truncate_push(height, &height_unrealized_state)?; + let spot = height_price.to_dollars(); + self.cost_basis.extended.truncate_push_percentiles(height, state, spot)?; + Ok(()) + } + fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); + vecs.extend(self.supply.par_iter_mut().collect::>()); + vecs.extend(self.outputs.par_iter_mut().collect::>()); + vecs.extend(self.activity.par_iter_mut().collect::>()); + vecs.extend(self.realized.collect_vecs_mut()); + vecs.extend(self.cost_basis.base.collect_vecs_mut()); + vecs.extend(self.cost_basis.extended.collect_vecs_mut()); + vecs.extend(self.unrealized.base.collect_vecs_mut()); + vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut()); + vecs + } +} + +impl AllCohortMetrics { + /// Import the "all" cohort metrics with a pre-imported supply. + /// + /// Supply is imported first (before other cohorts) so it can be used as `all_supply` + /// reference for relative metric lazy vecs in other cohorts. + pub(crate) fn forced_import_with_supply( + cfg: &ImportConfig, + supply: SupplyMetrics, + up_to_1h: &RealizedBase, + ) -> Result { + let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?; + let realized = RealizedWithExtendedAdjusted::forced_import(cfg, up_to_1h)?; + + let relative = RelativeForAll::forced_import( + cfg, + &unrealized.base, + &supply, + &realized.base, + &unrealized.peak_regret_ext.peak_regret, + ); + + Ok(Self { + filter: cfg.filter.clone(), + supply: Box::new(supply), + outputs: Box::new(OutputsMetrics::forced_import(cfg)?), + activity: Box::new(ActivityMetrics::forced_import(cfg)?), + realized: Box::new(realized), + cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?), + unrealized: Box::new(unrealized), + relative: Box::new(relative), + }) + } + + pub(crate) fn compute_rest_part2( + &mut self, + blocks: &blocks::Vecs, + prices: &prices::Vecs, + starting_indexes: &ComputeIndexes, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + self.realized.compute_rest_part2( + blocks, + prices, + starting_indexes, + &self.supply.total.btc.height, + height_to_market_cap, + exit, + ) + } + +} diff --git a/crates/brk_computer/src/distribution/metrics/cohort/basic.rs b/crates/brk_computer/src/distribution/metrics/cohort/basic.rs new file mode 100644 index 000000000..0afca9dac --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cohort/basic.rs @@ -0,0 +1,155 @@ +use brk_cohort::Filter; +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Cents, Dollars, Height, Version}; +use rayon::prelude::*; +use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices}; + +use crate::distribution::metrics::{ + ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase, + RelativeWithRelToAll, SupplyMetrics, UnrealizedBase, +}; + +/// Basic cohort metrics: no extensions, with relative (rel_to_all). +/// Used by: epoch, year, type (spendable), amount, address cohorts. +#[derive(Traversable)] +pub struct BasicCohortMetrics { + #[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 CohortMetricsBase for BasicCohortMetrics { + fn filter(&self) -> &Filter { &self.filter } + fn supply(&self) -> &SupplyMetrics { &self.supply } + fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply } + fn outputs(&self) -> &OutputsMetrics { &self.outputs } + fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs } + fn activity(&self) -> &ActivityMetrics { &self.activity } + fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity } + fn realized_base(&self) -> &RealizedBase { &self.realized } + fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized } + fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized } + fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized } + fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis } + fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis } + 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(()) + } + fn compute_then_truncate_push_unrealized_states( + &mut self, height: Height, height_price: Cents, state: &mut CohortState, + ) -> Result<()> { + state.apply_pending(); + self.cost_basis.truncate_push_minmax(height, state)?; + let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None); + self.unrealized.truncate_push(height, &height_unrealized_state)?; + Ok(()) + } + fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); + vecs.extend(self.supply.par_iter_mut().collect::>()); + vecs.extend(self.outputs.par_iter_mut().collect::>()); + vecs.extend(self.activity.par_iter_mut().collect::>()); + vecs.extend(self.realized.collect_vecs_mut()); + vecs.extend(self.cost_basis.collect_vecs_mut()); + vecs.extend(self.unrealized.collect_vecs_mut()); + vecs + } +} + +impl BasicCohortMetrics { + pub(crate) fn forced_import( + cfg: &ImportConfig, + all_supply: &SupplyMetrics, + ) -> Result { + let supply = SupplyMetrics::forced_import(cfg)?; + let unrealized = UnrealizedBase::forced_import(cfg)?; + let realized = RealizedBase::forced_import(cfg)?; + + let relative = RelativeWithRelToAll::forced_import( + cfg, &unrealized, &supply, all_supply, &realized, + ); + + Ok(Self { + filter: cfg.filter.clone(), + supply: Box::new(supply), + outputs: Box::new(OutputsMetrics::forced_import(cfg)?), + activity: Box::new(ActivityMetrics::forced_import(cfg)?), + realized: Box::new(realized), + cost_basis: Box::new(CostBasisBase::forced_import(cfg)?), + unrealized: Box::new(unrealized), + relative: Box::new(relative), + }) + } + + pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator { + self.collect_all_vecs_mut().into_par_iter() + } + + pub(crate) fn compute_rest_part2( + &mut self, + blocks: &blocks::Vecs, + prices: &prices::Vecs, + starting_indexes: &ComputeIndexes, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + self.realized.compute_rest_part2_base( + blocks, + prices, + starting_indexes, + &self.supply.total.btc.height, + height_to_market_cap, + exit, + ) + } + + pub(crate) fn compute_from_stateful( + &mut self, + starting_indexes: &ComputeIndexes, + others: &[&Self], + exit: &Exit, + ) -> Result<()> { + 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, + )?; + self.realized.compute_from_stateful( + starting_indexes, + &others.iter().map(|v| &*v.realized).collect::>(), + exit, + )?; + self.unrealized.compute_from_stateful( + starting_indexes, + &others.iter().map(|v| &*v.unrealized).collect::>(), + exit, + )?; + self.cost_basis.compute_from_stateful( + starting_indexes, + &others.iter().map(|v| &*v.cost_basis).collect::>(), + exit, + )?; + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/metrics/cohort/extended.rs b/crates/brk_computer/src/distribution/metrics/cohort/extended.rs new file mode 100644 index 000000000..ee50e0bb3 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cohort/extended.rs @@ -0,0 +1,125 @@ +use brk_cohort::Filter; +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Cents, Dollars, Height, Version}; +use rayon::prelude::*; +use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices}; + +use crate::distribution::metrics::{ + ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig, + OutputsMetrics, RealizedBase, RealizedWithExtended, RelativeWithExtended, SupplyMetrics, + UnrealizedBase, UnrealizedWithPeakRegret, +}; + +/// Cohort metrics with extended realized + extended cost basis + peak regret (no adjusted). +/// Used by: lth, age_range cohorts. +#[derive(Traversable)] +pub struct ExtendedCohortMetrics { + #[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 CohortMetricsBase for ExtendedCohortMetrics { + fn filter(&self) -> &Filter { &self.filter } + fn supply(&self) -> &SupplyMetrics { &self.supply } + fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply } + fn outputs(&self) -> &OutputsMetrics { &self.outputs } + fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs } + fn activity(&self) -> &ActivityMetrics { &self.activity } + fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity } + fn realized_base(&self) -> &RealizedBase { &self.realized } + fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized } + fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized } + fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized } + fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis } + fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis } + + fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { + self.supply.validate_computed_versions(base_version)?; + self.activity.validate_computed_versions(base_version)?; + self.cost_basis.validate_computed_versions(base_version)?; + Ok(()) + } + fn compute_then_truncate_push_unrealized_states( + &mut self, height: Height, height_price: Cents, state: &mut CohortState, + ) -> Result<()> { + state.apply_pending(); + self.cost_basis.truncate_push_minmax(height, state)?; + let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None); + self.unrealized.base.truncate_push(height, &height_unrealized_state)?; + let spot = height_price.to_dollars(); + self.cost_basis.extended.truncate_push_percentiles(height, state, spot)?; + Ok(()) + } + fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); + vecs.extend(self.supply.par_iter_mut().collect::>()); + vecs.extend(self.outputs.par_iter_mut().collect::>()); + vecs.extend(self.activity.par_iter_mut().collect::>()); + vecs.extend(self.realized.collect_vecs_mut()); + vecs.extend(self.cost_basis.base.collect_vecs_mut()); + vecs.extend(self.cost_basis.extended.collect_vecs_mut()); + vecs.extend(self.unrealized.base.collect_vecs_mut()); + vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut()); + vecs + } +} + +impl ExtendedCohortMetrics { + pub(crate) fn forced_import( + cfg: &ImportConfig, + all_supply: &SupplyMetrics, + ) -> Result { + let supply = SupplyMetrics::forced_import(cfg)?; + let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?; + let realized = RealizedWithExtended::forced_import(cfg)?; + + let relative = RelativeWithExtended::forced_import( + cfg, + &unrealized.base, + &supply, + all_supply, + &realized.base, + &unrealized.peak_regret_ext.peak_regret, + ); + + Ok(Self { + filter: cfg.filter.clone(), + supply: Box::new(supply), + outputs: Box::new(OutputsMetrics::forced_import(cfg)?), + activity: Box::new(ActivityMetrics::forced_import(cfg)?), + realized: Box::new(realized), + cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?), + unrealized: Box::new(unrealized), + relative: Box::new(relative), + }) + } + + pub(crate) fn compute_rest_part2( + &mut self, + blocks: &blocks::Vecs, + prices: &prices::Vecs, + starting_indexes: &ComputeIndexes, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + self.realized.compute_rest_part2( + blocks, + prices, + starting_indexes, + &self.supply.total.btc.height, + height_to_market_cap, + exit, + ) + } + +} diff --git a/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs b/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs new file mode 100644 index 000000000..3bce565a4 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs @@ -0,0 +1,125 @@ +use brk_cohort::Filter; +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Cents, Dollars, Height, Version}; +use rayon::prelude::*; +use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices}; + +use crate::distribution::metrics::{ + ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig, + OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeWithExtended, + SupplyMetrics, UnrealizedBase, UnrealizedWithPeakRegret, +}; + +/// Cohort metrics with extended + adjusted realized, extended cost basis, peak regret. +/// Used by: sth cohort. +#[derive(Traversable)] +pub struct ExtendedAdjustedCohortMetrics { + #[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 CohortMetricsBase for ExtendedAdjustedCohortMetrics { + fn filter(&self) -> &Filter { &self.filter } + fn supply(&self) -> &SupplyMetrics { &self.supply } + fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply } + fn outputs(&self) -> &OutputsMetrics { &self.outputs } + fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs } + fn activity(&self) -> &ActivityMetrics { &self.activity } + fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity } + fn realized_base(&self) -> &RealizedBase { &self.realized } + fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized } + fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized } + fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized } + fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis } + fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis } + fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { + self.supply.validate_computed_versions(base_version)?; + self.activity.validate_computed_versions(base_version)?; + self.cost_basis.validate_computed_versions(base_version)?; + Ok(()) + } + fn compute_then_truncate_push_unrealized_states( + &mut self, height: Height, height_price: Cents, state: &mut CohortState, + ) -> Result<()> { + state.apply_pending(); + self.cost_basis.truncate_push_minmax(height, state)?; + let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None); + self.unrealized.base.truncate_push(height, &height_unrealized_state)?; + let spot = height_price.to_dollars(); + self.cost_basis.extended.truncate_push_percentiles(height, state, spot)?; + Ok(()) + } + fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); + vecs.extend(self.supply.par_iter_mut().collect::>()); + vecs.extend(self.outputs.par_iter_mut().collect::>()); + vecs.extend(self.activity.par_iter_mut().collect::>()); + vecs.extend(self.realized.collect_vecs_mut()); + vecs.extend(self.cost_basis.base.collect_vecs_mut()); + vecs.extend(self.cost_basis.extended.collect_vecs_mut()); + vecs.extend(self.unrealized.base.collect_vecs_mut()); + vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut()); + vecs + } +} + +impl ExtendedAdjustedCohortMetrics { + pub(crate) fn forced_import( + cfg: &ImportConfig, + all_supply: &SupplyMetrics, + up_to_1h: &RealizedBase, + ) -> Result { + let supply = SupplyMetrics::forced_import(cfg)?; + let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?; + let realized = RealizedWithExtendedAdjusted::forced_import(cfg, up_to_1h)?; + + let relative = RelativeWithExtended::forced_import( + cfg, + &unrealized.base, + &supply, + all_supply, + &realized.base, + &unrealized.peak_regret_ext.peak_regret, + ); + + Ok(Self { + filter: cfg.filter.clone(), + supply: Box::new(supply), + outputs: Box::new(OutputsMetrics::forced_import(cfg)?), + activity: Box::new(ActivityMetrics::forced_import(cfg)?), + realized: Box::new(realized), + cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?), + unrealized: Box::new(unrealized), + relative: Box::new(relative), + }) + } + + pub(crate) fn compute_rest_part2( + &mut self, + blocks: &blocks::Vecs, + prices: &prices::Vecs, + starting_indexes: &ComputeIndexes, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + self.realized.compute_rest_part2( + blocks, + prices, + starting_indexes, + &self.supply.total.btc.height, + height_to_market_cap, + exit, + ) + } + +} diff --git a/crates/brk_computer/src/distribution/metrics/cohort/mod.rs b/crates/brk_computer/src/distribution/metrics/cohort/mod.rs new file mode 100644 index 000000000..a00e068cc --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cohort/mod.rs @@ -0,0 +1,15 @@ +mod adjusted; +mod all; +mod basic; +mod extended; +mod extended_adjusted; + +mod peak_regret; + +pub use adjusted::*; +pub use all::*; +pub use basic::*; +pub use extended::*; +pub use extended_adjusted::*; + +pub use peak_regret::*; diff --git a/crates/brk_computer/src/distribution/metrics/cohort/peak_regret.rs b/crates/brk_computer/src/distribution/metrics/cohort/peak_regret.rs new file mode 100644 index 000000000..34eea86e2 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cohort/peak_regret.rs @@ -0,0 +1,120 @@ +use brk_cohort::Filter; +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Cents, Dollars, Height, Version}; +use rayon::prelude::*; +use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices}; + +use crate::distribution::metrics::{ + ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase, + RelativeWithPeakRegret, SupplyMetrics, UnrealizedBase, UnrealizedWithPeakRegret, +}; + +/// Cohort metrics with peak regret unrealized + relative (no extended, no adjusted). +/// Used by: min_age cohorts. +#[derive(Traversable)] +pub struct PeakRegretCohortMetrics { + #[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 CohortMetricsBase for PeakRegretCohortMetrics { + fn filter(&self) -> &Filter { &self.filter } + fn supply(&self) -> &SupplyMetrics { &self.supply } + fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply } + fn outputs(&self) -> &OutputsMetrics { &self.outputs } + fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs } + fn activity(&self) -> &ActivityMetrics { &self.activity } + fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity } + fn realized_base(&self) -> &RealizedBase { &self.realized } + fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized } + fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized } + fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized } + fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis } + fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis } + + 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(()) + } + fn compute_then_truncate_push_unrealized_states( + &mut self, height: Height, height_price: Cents, state: &mut CohortState, + ) -> Result<()> { + state.apply_pending(); + self.cost_basis.truncate_push_minmax(height, state)?; + let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None); + self.unrealized.base.truncate_push(height, &height_unrealized_state)?; + Ok(()) + } + fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); + vecs.extend(self.supply.par_iter_mut().collect::>()); + vecs.extend(self.outputs.par_iter_mut().collect::>()); + vecs.extend(self.activity.par_iter_mut().collect::>()); + vecs.extend(self.realized.collect_vecs_mut()); + vecs.extend(self.cost_basis.collect_vecs_mut()); + vecs.extend(self.unrealized.base.collect_vecs_mut()); + vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut()); + vecs + } +} + +impl PeakRegretCohortMetrics { + pub(crate) fn forced_import( + cfg: &ImportConfig, + all_supply: &SupplyMetrics, + ) -> Result { + let supply = SupplyMetrics::forced_import(cfg)?; + let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?; + let realized = RealizedBase::forced_import(cfg)?; + + let relative = RelativeWithPeakRegret::forced_import( + cfg, + &unrealized.base, + &supply, + all_supply, + &realized, + &unrealized.peak_regret_ext.peak_regret, + ); + + Ok(Self { + filter: cfg.filter.clone(), + supply: Box::new(supply), + outputs: Box::new(OutputsMetrics::forced_import(cfg)?), + activity: Box::new(ActivityMetrics::forced_import(cfg)?), + realized: Box::new(realized), + cost_basis: Box::new(CostBasisBase::forced_import(cfg)?), + unrealized: Box::new(unrealized), + relative: Box::new(relative), + }) + } + + pub(crate) fn compute_rest_part2( + &mut self, + blocks: &blocks::Vecs, + prices: &prices::Vecs, + starting_indexes: &ComputeIndexes, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + self.realized.compute_rest_part2_base( + blocks, + prices, + starting_indexes, + &self.supply.total.btc.height, + height_to_market_cap, + exit, + ) + } + +} diff --git a/crates/brk_computer/src/distribution/metrics/config.rs b/crates/brk_computer/src/distribution/metrics/config.rs index f2cb4aeee..77101868a 100644 --- a/crates/brk_computer/src/distribution/metrics/config.rs +++ b/crates/brk_computer/src/distribution/metrics/config.rs @@ -4,8 +4,6 @@ use vecdb::Database; use crate::{indexes, prices}; -use super::RealizedMetrics; - /// Configuration for importing metrics. pub struct ImportConfig<'a> { pub db: &'a Database, @@ -15,9 +13,6 @@ pub struct ImportConfig<'a> { pub version: Version, pub indexes: &'a indexes::Vecs, pub prices: &'a prices::Vecs, - /// Source for lazy adjusted computation: adjusted = cohort - up_to_1h. - /// Required for cohorts where `compute_adjusted()` is true. - pub up_to_1h_realized: Option<&'a RealizedMetrics>, } impl<'a> ImportConfig<'a> { @@ -26,21 +21,6 @@ impl<'a> ImportConfig<'a> { self.filter.is_extended(self.context) } - /// Whether to compute relative-to-all metrics. - pub(crate) fn compute_rel_to_all(&self) -> bool { - self.filter.compute_rel_to_all() - } - - /// Whether to compute adjusted metrics (SOPR, etc.). - pub(crate) fn compute_adjusted(&self) -> bool { - self.filter.compute_adjusted(self.context) - } - - /// Whether to compute relative metrics (invested capital %, NUPL ratios, etc.). - pub(crate) fn compute_relative(&self) -> bool { - self.filter.compute_relative() - } - /// Get full metric name with filter prefix. pub(crate) fn name(&self, suffix: &str) -> String { if self.full_name.is_empty() { @@ -52,12 +32,4 @@ impl<'a> ImportConfig<'a> { } } - /// Whether this cohort needs peak_regret metric. - /// True for UTXO cohorts with age-based filters (all, term, time). - /// age_range cohorts compute directly, others aggregate from age_range. - pub(crate) fn compute_peak_regret(&self) -> bool { - matches!(self.context, CohortContext::Utxo) - && matches!(self.filter, Filter::All | Filter::Term(_) | Filter::Time(_)) - } - } diff --git a/crates/brk_computer/src/distribution/metrics/cost_basis.rs b/crates/brk_computer/src/distribution/metrics/cost_basis.rs deleted file mode 100644 index c9249be1e..000000000 --- a/crates/brk_computer/src/distribution/metrics/cost_basis.rs +++ /dev/null @@ -1,248 +0,0 @@ -use brk_error::Result; -use brk_traversable::Traversable; -use brk_types::{Dollars, Height, StoredF32, Version}; -use rayon::prelude::*; -use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec}; - -use crate::{ - ComputeIndexes, - distribution::state::CohortState, - internal::{ - ComputedFromHeightLast, PERCENTILES_LEN, Price, PriceFromHeight, PercentilesVecs, - compute_spot_percentile_rank, - }, -}; - -use super::ImportConfig; - -/// Cost basis metrics. -#[derive(Traversable)] -pub struct CostBasisMetrics { - /// Minimum cost basis for any UTXO at this height - pub min: Price>, - - /// Maximum cost basis for any UTXO at this height - pub max: Price>, - - /// Cost basis percentiles (sat-weighted) - pub percentiles: Option>, - - /// Invested capital percentiles (USD-weighted) - pub invested_capital: Option>, - - /// What percentile of cost basis is below spot (sat-weighted) - pub spot_cost_basis_percentile: Option>, - - /// What percentile of invested capital is below spot (USD-weighted) - pub spot_invested_capital_percentile: Option>, -} - -impl CostBasisMetrics { - /// Import cost basis metrics from database. - pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { - let extended = cfg.extended(); - - Ok(Self { - min: PriceFromHeight::forced_import( - cfg.db, - &cfg.name("min_cost_basis"), - cfg.version, - cfg.indexes, - )?, - max: PriceFromHeight::forced_import( - cfg.db, - &cfg.name("max_cost_basis"), - cfg.version, - cfg.indexes, - )?, - percentiles: extended - .then(|| { - PercentilesVecs::forced_import( - cfg.db, - &cfg.name("cost_basis"), - cfg.version, - cfg.indexes, - true, - ) - }) - .transpose()?, - invested_capital: extended - .then(|| { - PercentilesVecs::forced_import( - cfg.db, - &cfg.name("invested_capital"), - cfg.version, - cfg.indexes, - true, - ) - }) - .transpose()?, - spot_cost_basis_percentile: extended - .then(|| { - ComputedFromHeightLast::forced_import( - cfg.db, - &cfg.name("spot_cost_basis_percentile"), - cfg.version, - cfg.indexes, - ) - }) - .transpose()?, - spot_invested_capital_percentile: extended - .then(|| { - ComputedFromHeightLast::forced_import( - cfg.db, - &cfg.name("spot_invested_capital_percentile"), - cfg.version, - cfg.indexes, - ) - }) - .transpose()?, - }) - } - - /// Get minimum length across height-indexed vectors written in block loop. - pub(crate) fn min_stateful_height_len(&self) -> usize { - let mut min = self.min.height.len().min(self.max.height.len()); - if let Some(v) = &self.spot_cost_basis_percentile { - min = min.min(v.height.len()); - } - if let Some(v) = &self.spot_invested_capital_percentile { - min = min.min(v.height.len()); - } - if let Some(p) = &self.percentiles { - min = min.min(p.min_stateful_height_len()); - } - if let Some(p) = &self.invested_capital { - min = min.min(p.min_stateful_height_len()); - } - min - } - - /// Push min/max cost basis from state. - pub(crate) fn truncate_push_minmax(&mut self, height: Height, state: &CohortState) -> Result<()> { - self.min.height.truncate_push( - height, - state - .cost_basis_data_first_key_value() - .map(|(cents, _)| cents.into()) - .unwrap_or(Dollars::NAN), - )?; - self.max.height.truncate_push( - height, - state - .cost_basis_data_last_key_value() - .map(|(cents, _)| cents.into()) - .unwrap_or(Dollars::NAN), - )?; - Ok(()) - } - - /// Push cost basis percentiles and spot ranks at every height. - pub(crate) fn truncate_push_percentiles( - &mut self, - height: Height, - state: &mut CohortState, - spot: Dollars, - ) -> Result<()> { - let computed = state.compute_percentiles(); - - // Sat-weighted percentiles and spot rank - let sat_prices = computed - .as_ref() - .map(|p| p.sat_weighted.map(|c| c.to_dollars())) - .unwrap_or([Dollars::NAN; PERCENTILES_LEN]); - - if let Some(percentiles) = self.percentiles.as_mut() { - percentiles.truncate_push(height, &sat_prices)?; - } - if let Some(spot_pct) = self.spot_cost_basis_percentile.as_mut() { - let rank = compute_spot_percentile_rank(&sat_prices, spot); - spot_pct.height.truncate_push(height, rank)?; - } - - // USD-weighted percentiles and spot rank - let usd_prices = computed - .as_ref() - .map(|p| p.usd_weighted.map(|c| c.to_dollars())) - .unwrap_or([Dollars::NAN; PERCENTILES_LEN]); - - if let Some(invested_capital) = self.invested_capital.as_mut() { - invested_capital.truncate_push(height, &usd_prices)?; - } - if let Some(spot_pct) = self.spot_invested_capital_percentile.as_mut() { - let rank = compute_spot_percentile_rank(&usd_prices, spot); - spot_pct.height.truncate_push(height, rank)?; - } - - Ok(()) - } - - /// Returns a parallel iterator over all vecs for parallel writing. - pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator { - let mut vecs: Vec<&mut dyn AnyStoredVec> = vec![&mut self.min.height, &mut self.max.height]; - if let Some(percentiles) = self.percentiles.as_mut() { - vecs.extend( - percentiles - .vecs - .iter_mut() - .flatten() - .map(|v| &mut v.height as &mut dyn AnyStoredVec), - ); - } - if let Some(invested_capital) = self.invested_capital.as_mut() { - vecs.extend( - invested_capital - .vecs - .iter_mut() - .flatten() - .map(|v| &mut v.height as &mut dyn AnyStoredVec), - ); - } - if let Some(v) = self.spot_cost_basis_percentile.as_mut() { - vecs.push(&mut v.height); - } - if let Some(v) = self.spot_invested_capital_percentile.as_mut() { - vecs.push(&mut v.height); - } - vecs.into_par_iter() - } - - /// Validate computed versions or reset if mismatched. - pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { - if let Some(percentiles) = self.percentiles.as_mut() { - percentiles.validate_computed_version_or_reset(base_version)?; - } - if let Some(invested_capital) = self.invested_capital.as_mut() { - invested_capital.validate_computed_version_or_reset(base_version)?; - } - if let Some(v) = self.spot_cost_basis_percentile.as_mut() { - v.height - .validate_computed_version_or_reset(base_version)?; - } - if let Some(v) = self.spot_invested_capital_percentile.as_mut() { - v.height - .validate_computed_version_or_reset(base_version)?; - } - Ok(()) - } - - /// Compute aggregate values from separate cohorts. - pub(crate) fn compute_from_stateful( - &mut self, - starting_indexes: &ComputeIndexes, - others: &[&Self], - exit: &Exit, - ) -> Result<()> { - self.min.height.compute_min_of_others( - starting_indexes.height, - &others.iter().map(|v| &v.min.height).collect::>(), - exit, - )?; - self.max.height.compute_max_of_others( - starting_indexes.height, - &others.iter().map(|v| &v.max.height).collect::>(), - exit, - )?; - Ok(()) - } -} diff --git a/crates/brk_computer/src/distribution/metrics/cost_basis/base.rs b/crates/brk_computer/src/distribution/metrics/cost_basis/base.rs new file mode 100644 index 000000000..7e3230e15 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cost_basis/base.rs @@ -0,0 +1,93 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Dollars, Height}; +use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec}; + +use crate::{ + ComputeIndexes, + distribution::state::CohortState, + internal::{ComputedFromHeightLast, Price, PriceFromHeight}, +}; + +use crate::distribution::metrics::ImportConfig; + +/// Base cost basis metrics (always computed). +#[derive(Traversable)] +pub struct CostBasisBase { + /// Minimum cost basis for any UTXO at this height + pub min: Price>, + + /// Maximum cost basis for any UTXO at this height + pub max: Price>, +} + +impl CostBasisBase { + pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + Ok(Self { + min: PriceFromHeight::forced_import( + cfg.db, + &cfg.name("min_cost_basis"), + cfg.version, + cfg.indexes, + )?, + max: PriceFromHeight::forced_import( + cfg.db, + &cfg.name("max_cost_basis"), + cfg.version, + cfg.indexes, + )?, + }) + } + + pub(crate) fn min_stateful_height_len(&self) -> usize { + self.min.height.len().min(self.max.height.len()) + } + + pub(crate) fn truncate_push_minmax( + &mut self, + height: Height, + state: &CohortState, + ) -> Result<()> { + self.min.height.truncate_push( + height, + state + .cost_basis_data_first_key_value() + .map(|(cents, _)| cents.into()) + .unwrap_or(Dollars::NAN), + )?; + self.max.height.truncate_push( + height, + state + .cost_basis_data_last_key_value() + .map(|(cents, _)| cents.into()) + .unwrap_or(Dollars::NAN), + )?; + Ok(()) + } + + pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + vec![ + &mut self.min.height as &mut dyn AnyStoredVec, + &mut self.max.height, + ] + } + + pub(crate) fn compute_from_stateful( + &mut self, + starting_indexes: &ComputeIndexes, + others: &[&Self], + exit: &Exit, + ) -> Result<()> { + self.min.height.compute_min_of_others( + starting_indexes.height, + &others.iter().map(|v| &v.min.height).collect::>(), + exit, + )?; + self.max.height.compute_max_of_others( + starting_indexes.height, + &others.iter().map(|v| &v.max.height).collect::>(), + exit, + )?; + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/metrics/cost_basis/extended.rs b/crates/brk_computer/src/distribution/metrics/cost_basis/extended.rs new file mode 100644 index 000000000..7cc64931b --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cost_basis/extended.rs @@ -0,0 +1,130 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Dollars, Height, StoredF32, Version}; +use vecdb::{AnyStoredVec, Rw, StorageMode, WritableVec}; + +use crate::{ + distribution::state::CohortState, + internal::{ + ComputedFromHeightLast, PERCENTILES_LEN, PercentilesVecs, compute_spot_percentile_rank, + }, +}; + +use crate::distribution::metrics::ImportConfig; + +/// Extended cost basis metrics (only for extended cohorts). +#[derive(Traversable)] +pub struct CostBasisExtended { + /// Cost basis percentiles (sat-weighted) + pub percentiles: PercentilesVecs, + + /// Invested capital percentiles (USD-weighted) + pub invested_capital: PercentilesVecs, + + /// What percentile of cost basis is below spot (sat-weighted) + pub spot_cost_basis_percentile: ComputedFromHeightLast, + + /// What percentile of invested capital is below spot (USD-weighted) + pub spot_invested_capital_percentile: ComputedFromHeightLast, +} + +impl CostBasisExtended { + pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + Ok(Self { + percentiles: PercentilesVecs::forced_import( + cfg.db, + &cfg.name("cost_basis"), + cfg.version, + cfg.indexes, + true, + )?, + invested_capital: PercentilesVecs::forced_import( + cfg.db, + &cfg.name("invested_capital"), + cfg.version, + cfg.indexes, + true, + )?, + spot_cost_basis_percentile: ComputedFromHeightLast::forced_import( + cfg.db, + &cfg.name("spot_cost_basis_percentile"), + cfg.version, + cfg.indexes, + )?, + spot_invested_capital_percentile: ComputedFromHeightLast::forced_import( + cfg.db, + &cfg.name("spot_invested_capital_percentile"), + cfg.version, + cfg.indexes, + )?, + }) + } + + pub(crate) fn truncate_push_percentiles( + &mut self, + height: Height, + state: &mut CohortState, + spot: Dollars, + ) -> Result<()> { + let computed = state.compute_percentiles(); + + let sat_prices = computed + .as_ref() + .map(|p| p.sat_weighted.map(|c| c.to_dollars())) + .unwrap_or([Dollars::NAN; PERCENTILES_LEN]); + + self.percentiles.truncate_push(height, &sat_prices)?; + let rank = compute_spot_percentile_rank(&sat_prices, spot); + self.spot_cost_basis_percentile + .height + .truncate_push(height, rank)?; + + let usd_prices = computed + .as_ref() + .map(|p| p.usd_weighted.map(|c| c.to_dollars())) + .unwrap_or([Dollars::NAN; PERCENTILES_LEN]); + + self.invested_capital.truncate_push(height, &usd_prices)?; + let rank = compute_spot_percentile_rank(&usd_prices, spot); + self.spot_invested_capital_percentile + .height + .truncate_push(height, rank)?; + + Ok(()) + } + + pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); + vecs.extend( + self.percentiles + .vecs + .iter_mut() + .flatten() + .map(|v| &mut v.height as &mut dyn AnyStoredVec), + ); + vecs.extend( + self.invested_capital + .vecs + .iter_mut() + .flatten() + .map(|v| &mut v.height as &mut dyn AnyStoredVec), + ); + vecs.push(&mut self.spot_cost_basis_percentile.height); + vecs.push(&mut self.spot_invested_capital_percentile.height); + vecs + } + + pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { + self.percentiles + .validate_computed_version_or_reset(base_version)?; + self.invested_capital + .validate_computed_version_or_reset(base_version)?; + self.spot_cost_basis_percentile + .height + .validate_computed_version_or_reset(base_version)?; + self.spot_invested_capital_percentile + .height + .validate_computed_version_or_reset(base_version)?; + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/metrics/cost_basis/mod.rs b/crates/brk_computer/src/distribution/metrics/cost_basis/mod.rs new file mode 100644 index 000000000..79ae3654e --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cost_basis/mod.rs @@ -0,0 +1,9 @@ +mod base; +mod extended; + +mod with_extended; + +pub use base::*; +pub use extended::*; + +pub use with_extended::*; diff --git a/crates/brk_computer/src/distribution/metrics/cost_basis/with_extended.rs b/crates/brk_computer/src/distribution/metrics/cost_basis/with_extended.rs new file mode 100644 index 000000000..fa65c6b75 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/cost_basis/with_extended.rs @@ -0,0 +1,35 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::Version; +use derive_more::{Deref, DerefMut}; +use vecdb::{Rw, StorageMode}; + +use crate::distribution::metrics::ImportConfig; + +use super::{CostBasisBase, CostBasisExtended}; + +/// Cost basis metrics with guaranteed extended (no Option). +#[derive(Deref, DerefMut, Traversable)] +#[traversable(merge)] +pub struct CostBasisWithExtended { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub base: CostBasisBase, + #[traversable(flatten)] + pub extended: CostBasisExtended, +} + +impl CostBasisWithExtended { + pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + Ok(Self { + base: CostBasisBase::forced_import(cfg)?, + extended: CostBasisExtended::forced_import(cfg)?, + }) + } + + pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { + self.extended.validate_computed_versions(base_version) + } + +} diff --git a/crates/brk_computer/src/distribution/metrics/mod.rs b/crates/brk_computer/src/distribution/metrics/mod.rs index 36cd8c5a3..aad927ecc 100644 --- a/crates/brk_computer/src/distribution/metrics/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/mod.rs @@ -1,4 +1,5 @@ mod activity; +mod cohort; mod config; mod cost_basis; mod outputs; @@ -8,6 +9,7 @@ mod supply; mod unrealized; pub use activity::*; +pub use cohort::*; pub use config::*; pub use cost_basis::*; pub use outputs::*; @@ -18,220 +20,123 @@ pub use unrealized::*; use brk_cohort::Filter; use brk_error::Result; -use brk_traversable::Traversable; -use brk_types::{Cents, Dollars, Height, Version}; -use rayon::prelude::*; -use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; +use brk_types::{Cents, Height, Version}; +use vecdb::{AnyStoredVec, Exit}; use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices}; -/// All metrics for a cohort, organized by category. -#[derive(Traversable)] -pub struct CohortMetrics { - #[traversable(skip)] - pub filter: Filter, +/// Trait defining the interface for cohort metrics containers. +/// +/// Provides typed accessor methods for base sub-metric components, default +/// implementations for shared operations that only use base fields, and +/// required methods for operations that vary by extension level. +pub trait CohortMetricsBase: Send + Sync { + fn filter(&self) -> &Filter; + fn supply(&self) -> &SupplyMetrics; + fn supply_mut(&mut self) -> &mut SupplyMetrics; + fn outputs(&self) -> &OutputsMetrics; + fn outputs_mut(&mut self) -> &mut OutputsMetrics; + fn activity(&self) -> &ActivityMetrics; + fn activity_mut(&mut self) -> &mut ActivityMetrics; + fn realized_base(&self) -> &RealizedBase; + fn realized_base_mut(&mut self) -> &mut RealizedBase; + fn unrealized_base(&self) -> &UnrealizedBase; + fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase; + fn cost_basis_base(&self) -> &CostBasisBase; + fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase; - /// Supply metrics (always computed) - pub supply: Box>, + // === Required methods (vary by extension level) === - /// Output metrics - UTXO count (always computed) - pub outputs: Box>, + /// Validate computed versions against base version. + /// Extended types also validate cost_basis extended versions. + fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>; - /// Transaction activity (always computed) - pub activity: Box>, + /// Compute and push unrealized states. + /// Extended types also push cost_basis percentiles. + fn compute_then_truncate_push_unrealized_states( + &mut self, + height: Height, + height_price: Cents, + state: &mut CohortState, + ) -> Result<()>; - /// Realized cap and profit/loss - pub realized: Box>, + /// Collect all stored vecs for parallel writing. + fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec>; - /// Unrealized profit/loss - pub unrealized: Box>, - - /// Cost basis metrics - pub cost_basis: Box>, - - /// Relative metrics (not all cohorts compute this) - pub relative: Option>, -} - -impl CohortMetrics { - /// Import all metrics from database. - /// - /// `all_supply` is the supply metrics from the "all" cohort, used as global - /// sources for `*_rel_to_market_cap` and `*_rel_to_circulating_supply` ratios. - /// Pass `None` for the "all" cohort itself. - pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: Option<&SupplyMetrics>) -> Result { - let supply = SupplyMetrics::forced_import(cfg)?; - let outputs = OutputsMetrics::forced_import(cfg)?; - - let unrealized = UnrealizedMetrics::forced_import(cfg)?; - let realized = RealizedMetrics::forced_import(cfg)?; - - let relative = cfg - .compute_relative() - .then(|| { - RelativeMetrics::forced_import( - cfg, - &unrealized, - &supply, - all_supply, - Some(&realized), - ) - }) - .transpose()?; - - Ok(Self { - filter: cfg.filter.clone(), - supply: Box::new(supply), - outputs: Box::new(outputs), - activity: Box::new(ActivityMetrics::forced_import(cfg)?), - realized: Box::new(realized), - cost_basis: Box::new(CostBasisMetrics::forced_import(cfg)?), - relative: relative.map(Box::new), - unrealized: Box::new(unrealized), - }) - } + // === Default methods (shared across all cohort metric types, use base fields only) === /// Get minimum length across height-indexed vectors written in block loop. - pub(crate) fn min_stateful_height_len(&self) -> usize { - self.supply + 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()) + .min(self.outputs().min_len()) + .min(self.activity().min_len()) + .min(self.realized_base().min_stateful_height_len()) + .min(self.unrealized_base().min_stateful_height_len()) + .min(self.cost_basis_base().min_stateful_height_len()) } /// Push state values to height-indexed vectors. - pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { - self.supply.truncate_push(height, state.supply.value)?; - self.outputs + fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { + self.supply_mut() + .truncate_push(height, state.supply.value)?; + self.outputs_mut() .truncate_push(height, state.supply.utxo_count)?; - self.activity.truncate_push( + self.activity_mut().truncate_push( height, state.sent, state.satblocks_destroyed, state.satdays_destroyed, )?; - - self.realized.truncate_push(height, &state.realized)?; - + self.realized_base_mut() + .truncate_push(height, &state.realized)?; Ok(()) } - /// Returns a parallel iterator over all vecs for parallel writing. - pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator { - let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new(); - - vecs.extend(self.supply.par_iter_mut().collect::>()); - vecs.extend(self.outputs.par_iter_mut().collect::>()); - vecs.extend(self.activity.par_iter_mut().collect::>()); - vecs.extend(self.realized.par_iter_mut().collect::>()); - vecs.extend(self.unrealized.par_iter_mut().collect::>()); - vecs.extend(self.cost_basis.par_iter_mut().collect::>()); - - vecs.into_par_iter() - } - - /// Validate computed versions against base version. - 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)?; - self.realized.validate_computed_versions(base_version)?; - self.cost_basis.validate_computed_versions(base_version)?; - - Ok(()) - } - - /// Compute and push unrealized states and percentiles. - pub(crate) fn compute_then_truncate_push_unrealized_states( - &mut self, - height: Height, - height_price: Cents, - state: &mut CohortState, - ) -> Result<()> { - // Apply pending updates before reading - state.apply_pending(); - - self.cost_basis.truncate_push_minmax(height, state)?; - - let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None); - - self.unrealized - .truncate_push(height, &height_unrealized_state)?; - - let spot = height_price.to_dollars(); - self.cost_basis - .truncate_push_percentiles(height, state, spot)?; - - Ok(()) - } - - /// Compute aggregate cohort values from separate cohorts. - pub(crate) fn compute_from_stateful( + /// Compute net_sentiment.height as capital-weighted average of component cohorts (same type). + fn compute_net_sentiment_from_others( &mut self, starting_indexes: &ComputeIndexes, others: &[&Self], exit: &Exit, - ) -> Result<()> { - 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, - )?; + ) -> Result<()> + where + Self: Sized, + { + let weights: Vec<_> = others + .iter() + .map(|o| &o.realized_base().realized_cap.height) + .collect(); + let values: Vec<_> = others + .iter() + .map(|o| &o.unrealized_base().net_sentiment.height) + .collect(); - self.realized.compute_from_stateful( - starting_indexes, - &others.iter().map(|v| &*v.realized).collect::>(), - exit, - )?; - self.unrealized.compute_from_stateful( - starting_indexes, - &others.iter().map(|v| &*v.unrealized).collect::>(), - exit, - )?; - self.cost_basis.compute_from_stateful( - starting_indexes, - &others.iter().map(|v| &*v.cost_basis).collect::>(), - exit, - )?; + self.unrealized_base_mut() + .net_sentiment + .height + .compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?; Ok(()) } - /// Compute net_sentiment.height as capital-weighted average of component cohorts. - /// - /// For aggregate cohorts, the simple greed-pain formula produces values outside - /// the range of components due to asymmetric weighting. This computes net_sentiment - /// as a proper weighted average using realized_cap as weight. - /// - /// Only computes height; day1 derivation is done separately via compute_net_sentiment_rest. - pub(crate) fn compute_net_sentiment_from_others( + /// Compute net_sentiment.height as capital-weighted average from heterogeneous sources. + fn compute_net_sentiment_from_others_dyn( &mut self, starting_indexes: &ComputeIndexes, - others: &[&Self], + others: &[&dyn CohortMetricsBase], exit: &Exit, ) -> Result<()> { let weights: Vec<_> = others .iter() - .map(|o| &o.realized.realized_cap.height) + .map(|o| &o.realized_base().realized_cap.height) .collect(); let values: Vec<_> = others .iter() - .map(|o| &o.unrealized.net_sentiment.height) + .map(|o| &o.unrealized_base().net_sentiment.height) .collect(); - self.unrealized + self.unrealized_base_mut() .net_sentiment .height .compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?; @@ -240,55 +145,81 @@ impl CohortMetrics { } /// First phase of computed metrics (indexes from height). - pub(crate) fn compute_rest_part1( + fn compute_rest_part1( &mut self, blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - self.supply.compute_rest_part1(blocks, starting_indexes, exit)?; - self.outputs.compute_rest(blocks, starting_indexes, exit)?; - self.activity.compute_rest_part1(blocks, starting_indexes, exit)?; + self.supply_mut() + .compute_rest_part1(blocks, starting_indexes, exit)?; + self.outputs_mut() + .compute_rest(blocks, starting_indexes, exit)?; + self.activity_mut() + .compute_rest_part1(blocks, starting_indexes, exit)?; - self.realized.compute_rest_part1(starting_indexes, exit)?; + self.realized_base_mut() + .compute_rest_part1(starting_indexes, exit)?; - self.unrealized + self.unrealized_base_mut() .compute_rest(prices, starting_indexes, exit)?; Ok(()) } - /// Second phase of computed metrics (ratios, relative values). - pub(crate) fn compute_rest_part2( - &mut self, - blocks: &blocks::Vecs, - prices: &prices::Vecs, - starting_indexes: &ComputeIndexes, - height_to_market_cap: Option<&impl ReadableVec>, - exit: &Exit, - ) -> Result<()> { - self.realized.compute_rest_part2( - blocks, - prices, - starting_indexes, - &self.supply.total.btc.height, - height_to_market_cap, - exit, - )?; - - Ok(()) - } - /// Compute net_sentiment.height for separate cohorts (greed - pain). - /// Called only for separate cohorts; aggregates compute via weighted average in compute_from_stateful. - pub(crate) fn compute_net_sentiment_height( + fn compute_net_sentiment_height( &mut self, starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - self.unrealized + self.unrealized_base_mut() .compute_net_sentiment_height(starting_indexes, exit)?; Ok(()) } + + /// Compute aggregate base metrics from heterogeneous source cohorts. + /// Uses only base fields (supply, outputs, activity, realized_base, unrealized_base, cost_basis_base). + fn compute_base_from_others( + &mut self, + starting_indexes: &ComputeIndexes, + others: &[&dyn CohortMetricsBase], + exit: &Exit, + ) -> Result<()> + where + Self: Sized, + { + self.supply_mut().compute_from_stateful( + starting_indexes, + &others.iter().map(|v| v.supply()).collect::>(), + exit, + )?; + self.outputs_mut().compute_from_stateful( + starting_indexes, + &others.iter().map(|v| v.outputs()).collect::>(), + exit, + )?; + self.activity_mut().compute_from_stateful( + starting_indexes, + &others.iter().map(|v| v.activity()).collect::>(), + exit, + )?; + self.realized_base_mut().compute_from_stateful( + starting_indexes, + &others.iter().map(|v| v.realized_base()).collect::>(), + exit, + )?; + self.unrealized_base_mut().compute_from_stateful( + starting_indexes, + &others.iter().map(|v| v.unrealized_base()).collect::>(), + exit, + )?; + self.cost_basis_base_mut().compute_from_stateful( + starting_indexes, + &others.iter().map(|v| v.cost_basis_base()).collect::>(), + exit, + )?; + Ok(()) + } } diff --git a/crates/brk_computer/src/distribution/metrics/realized/adjusted.rs b/crates/brk_computer/src/distribution/metrics/realized/adjusted.rs new file mode 100644 index 000000000..b51df1802 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/realized/adjusted.rs @@ -0,0 +1,170 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Dollars, StoredF64, Version}; +use vecdb::{Exit, Ident, ReadableCloneableVec, Rw, StorageMode}; + +use crate::{ + ComputeIndexes, blocks, + internal::{ + ComputedFromHeightLast, DollarsMinus, LazyBinaryFromHeightLast, + LazyFromHeightLast, Ratio64, + }, +}; + +use crate::distribution::metrics::ImportConfig; + +use super::RealizedBase; + +/// Adjusted realized metrics (only for adjusted cohorts: all, sth, max_age). +#[derive(Traversable)] +pub struct RealizedAdjusted { + // === Adjusted Value (lazy: cohort - up_to_1h) === + pub adjusted_value_created: LazyBinaryFromHeightLast, + pub adjusted_value_destroyed: LazyBinaryFromHeightLast, + + // === Adjusted Value Created/Destroyed Rolling Sums === + pub adjusted_value_created_24h: ComputedFromHeightLast, + pub adjusted_value_created_7d: ComputedFromHeightLast, + pub adjusted_value_created_30d: ComputedFromHeightLast, + pub adjusted_value_created_1y: ComputedFromHeightLast, + pub adjusted_value_destroyed_24h: ComputedFromHeightLast, + pub adjusted_value_destroyed_7d: ComputedFromHeightLast, + pub adjusted_value_destroyed_30d: ComputedFromHeightLast, + pub adjusted_value_destroyed_1y: ComputedFromHeightLast, + + // === Adjusted SOPR (rolling window ratios) === + pub adjusted_sopr: LazyFromHeightLast, + pub adjusted_sopr_24h: LazyBinaryFromHeightLast, + pub adjusted_sopr_7d: LazyBinaryFromHeightLast, + pub adjusted_sopr_30d: LazyBinaryFromHeightLast, + pub adjusted_sopr_1y: LazyBinaryFromHeightLast, + pub adjusted_sopr_24h_7d_ema: ComputedFromHeightLast, + pub adjusted_sopr_7d_ema: LazyFromHeightLast, + pub adjusted_sopr_24h_30d_ema: ComputedFromHeightLast, + pub adjusted_sopr_30d_ema: LazyFromHeightLast, +} + +impl RealizedAdjusted { + pub(crate) fn forced_import( + cfg: &ImportConfig, + base: &RealizedBase, + up_to_1h: &RealizedBase, + ) -> Result { + let v1 = Version::ONE; + + macro_rules! import_rolling { + ($name:expr) => { + ComputedFromHeightLast::forced_import(cfg.db, &cfg.name($name), cfg.version + v1, cfg.indexes)? + }; + } + + let adjusted_value_created = LazyBinaryFromHeightLast::from_both_binary_block::< + DollarsMinus, Dollars, Dollars, Dollars, Dollars, + >( + &cfg.name("adjusted_value_created"), + cfg.version, + &base.value_created, + &up_to_1h.value_created, + ); + let adjusted_value_destroyed = LazyBinaryFromHeightLast::from_both_binary_block::< + DollarsMinus, Dollars, Dollars, Dollars, Dollars, + >( + &cfg.name("adjusted_value_destroyed"), + cfg.version, + &base.value_destroyed, + &up_to_1h.value_destroyed, + ); + + let adjusted_value_created_24h = import_rolling!("adjusted_value_created_24h"); + let adjusted_value_created_7d = import_rolling!("adjusted_value_created_7d"); + let adjusted_value_created_30d = import_rolling!("adjusted_value_created_30d"); + let adjusted_value_created_1y = import_rolling!("adjusted_value_created_1y"); + let adjusted_value_destroyed_24h = import_rolling!("adjusted_value_destroyed_24h"); + let adjusted_value_destroyed_7d = import_rolling!("adjusted_value_destroyed_7d"); + let adjusted_value_destroyed_30d = import_rolling!("adjusted_value_destroyed_30d"); + let adjusted_value_destroyed_1y = import_rolling!("adjusted_value_destroyed_1y"); + + let adjusted_sopr_24h = LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("adjusted_sopr_24h"), cfg.version + v1, &adjusted_value_created_24h, &adjusted_value_destroyed_24h, + ); + let adjusted_sopr_7d = LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("adjusted_sopr_7d"), cfg.version + v1, &adjusted_value_created_7d, &adjusted_value_destroyed_7d, + ); + let adjusted_sopr_30d = LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("adjusted_sopr_30d"), cfg.version + v1, &adjusted_value_created_30d, &adjusted_value_destroyed_30d, + ); + let adjusted_sopr_1y = LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("adjusted_sopr_1y"), cfg.version + v1, &adjusted_value_created_1y, &adjusted_value_destroyed_1y, + ); + let adjusted_sopr = LazyFromHeightLast::from_binary::( + &cfg.name("adjusted_sopr"), cfg.version + v1, &adjusted_sopr_24h, + ); + + let adjusted_sopr_24h_7d_ema = import_rolling!("adjusted_sopr_24h_7d_ema"); + let adjusted_sopr_7d_ema = LazyFromHeightLast::from_computed::( + &cfg.name("adjusted_sopr_7d_ema"), cfg.version + v1, + adjusted_sopr_24h_7d_ema.height.read_only_boxed_clone(), &adjusted_sopr_24h_7d_ema, + ); + let adjusted_sopr_24h_30d_ema = import_rolling!("adjusted_sopr_24h_30d_ema"); + let adjusted_sopr_30d_ema = LazyFromHeightLast::from_computed::( + &cfg.name("adjusted_sopr_30d_ema"), cfg.version + v1, + adjusted_sopr_24h_30d_ema.height.read_only_boxed_clone(), &adjusted_sopr_24h_30d_ema, + ); + + Ok(RealizedAdjusted { + adjusted_value_created, + adjusted_value_destroyed, + adjusted_value_created_24h, + adjusted_value_created_7d, + adjusted_value_created_30d, + adjusted_value_created_1y, + adjusted_value_destroyed_24h, + adjusted_value_destroyed_7d, + adjusted_value_destroyed_30d, + adjusted_value_destroyed_1y, + adjusted_sopr, + adjusted_sopr_24h, + adjusted_sopr_7d, + adjusted_sopr_30d, + adjusted_sopr_1y, + adjusted_sopr_24h_7d_ema, + adjusted_sopr_7d_ema, + adjusted_sopr_24h_30d_ema, + adjusted_sopr_30d_ema, + }) + } + + pub(crate) fn compute_rest_part2_adj( + &mut self, + blocks: &blocks::Vecs, + starting_indexes: &ComputeIndexes, + exit: &Exit, + ) -> Result<()> { + // Adjusted value created/destroyed rolling sums + self.adjusted_value_created_24h.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &self.adjusted_value_created.height, exit)?; + self.adjusted_value_created_7d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &self.adjusted_value_created.height, exit)?; + self.adjusted_value_created_30d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &self.adjusted_value_created.height, exit)?; + self.adjusted_value_created_1y.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &self.adjusted_value_created.height, exit)?; + + self.adjusted_value_destroyed_24h.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &self.adjusted_value_destroyed.height, exit)?; + self.adjusted_value_destroyed_7d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &self.adjusted_value_destroyed.height, exit)?; + self.adjusted_value_destroyed_30d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &self.adjusted_value_destroyed.height, exit)?; + self.adjusted_value_destroyed_1y.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &self.adjusted_value_destroyed.height, exit)?; + + // Adjusted SOPR EMAs + self.adjusted_sopr_24h_7d_ema.height.compute_rolling_average( + starting_indexes.height, + &blocks.count.height_1w_ago, + &self.adjusted_sopr.height, + exit, + )?; + self.adjusted_sopr_24h_30d_ema.height.compute_rolling_average( + starting_indexes.height, + &blocks.count.height_1m_ago, + &self.adjusted_sopr.height, + exit, + )?; + + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/metrics/realized.rs b/crates/brk_computer/src/distribution/metrics/realized/base.rs similarity index 62% rename from crates/brk_computer/src/distribution/metrics/realized.rs rename to crates/brk_computer/src/distribution/metrics/realized/base.rs index 45fe2dc02..92d03a78a 100644 --- a/crates/brk_computer/src/distribution/metrics/realized.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/base.rs @@ -4,7 +4,6 @@ use brk_types::{ Bitcoin, Cents, CentsSats, CentsSquaredSats, Dollars, Height, StoredF32, StoredF64, Version, }; -use rayon::prelude::*; use vecdb::{ AnyStoredVec, AnyVec, BytesVec, Exit, WritableVec, Ident, ImportableVec, ReadableCloneableVec, ReadableVec, Negate, Rw, StorageMode, @@ -15,7 +14,7 @@ use crate::{ distribution::state::RealizedState, internal::{ CentsUnsignedToDollars, ComputedFromHeightCum, ComputedFromHeightLast, - ComputedFromHeightRatio, DollarsMinus, DollarsPlus, + ComputedFromHeightRatio, DollarsPlus, DollarsSquaredDivide, LazyBinaryFromHeightLast, LazyBinaryPriceFromHeight, LazyComputedValueFromHeightCum, LazyFromHeightLast, LazyPriceFromCents, PercentageDollarsF32, Price, PriceFromHeight, @@ -24,36 +23,32 @@ use crate::{ prices, }; -use super::ImportConfig; +use crate::distribution::metrics::ImportConfig; -/// Realized cap and related metrics. +/// Base realized metrics (always computed). #[derive(Traversable)] -pub struct RealizedMetrics { +pub struct RealizedBase { // === Realized Cap === pub realized_cap_cents: ComputedFromHeightLast, pub realized_cap: LazyFromHeightLast, pub realized_price: Price>, pub realized_price_extra: ComputedFromHeightRatio, - pub realized_cap_rel_to_own_market_cap: Option>, pub realized_cap_30d_delta: ComputedFromHeightLast, - // === Investor Price (dollar-weighted average acquisition price) === + // === Investor Price === pub investor_price_cents: ComputedFromHeightLast, pub investor_price: LazyPriceFromCents, pub investor_price_extra: ComputedFromHeightRatio, - // === Floor/Ceiling Price Bands (lazy: realized²/investor, investor²/realized) === + // === Floor/Ceiling Price Bands === pub lower_price_band: LazyBinaryPriceFromHeight, pub upper_price_band: LazyBinaryPriceFromHeight, - // === Raw values for aggregation (needed to compute investor_price for aggregated cohorts) === - /// Raw Σ(price × sats) for realized cap aggregation + // === Raw values for aggregation === pub cap_raw: M::Stored>, - /// Raw Σ(price² × sats) for investor_price aggregation pub investor_cap_raw: M::Stored>, - // === MVRV (Market Value to Realized Value) === - // Proxy for realized_price_extra.ratio (close / realized_price = market_cap / realized_cap) + // === MVRV === pub mvrv: LazyFromHeightLast, // === Realized Profit/Loss === @@ -76,29 +71,13 @@ pub struct RealizedMetrics { // === Total Realized PnL === pub total_realized_pnl: LazyFromHeightLast, - // === Realized Profit/Loss Rolling Sums === - pub realized_profit_24h: Option>, - pub realized_profit_7d: Option>, - pub realized_profit_30d: Option>, - pub realized_profit_1y: Option>, - pub realized_loss_24h: Option>, - pub realized_loss_7d: Option>, - pub realized_loss_30d: Option>, - pub realized_loss_1y: Option>, - - // === Realized Profit to Loss Ratio (lazy from rolling sums) === - pub realized_profit_to_loss_ratio_24h: Option>, - pub realized_profit_to_loss_ratio_7d: Option>, - pub realized_profit_to_loss_ratio_30d: Option>, - pub realized_profit_to_loss_ratio_1y: Option>, - // === Value Created/Destroyed Splits (stored) === pub profit_value_created: ComputedFromHeightLast, pub profit_value_destroyed: ComputedFromHeightLast, pub loss_value_created: ComputedFromHeightLast, pub loss_value_destroyed: ComputedFromHeightLast, - // === Value Created/Destroyed Totals (lazy: profit + loss) === + // === Value Created/Destroyed Totals (lazy) === pub value_created: LazyBinaryFromHeightLast, pub value_destroyed: LazyBinaryFromHeightLast, @@ -106,10 +85,6 @@ pub struct RealizedMetrics { pub capitulation_flow: LazyFromHeightLast, pub profit_flow: LazyFromHeightLast, - // === Adjusted Value (lazy: cohort - up_to_1h) === - pub adjusted_value_created: Option>, - pub adjusted_value_destroyed: Option>, - // === Value Created/Destroyed Rolling Sums === pub value_created_24h: ComputedFromHeightLast, pub value_created_7d: ComputedFromHeightLast, @@ -131,27 +106,6 @@ pub struct RealizedMetrics { pub sopr_24h_30d_ema: ComputedFromHeightLast, pub sopr_30d_ema: LazyFromHeightLast, - // === Adjusted Value Created/Destroyed Rolling Sums === - pub adjusted_value_created_24h: Option>, - pub adjusted_value_created_7d: Option>, - pub adjusted_value_created_30d: Option>, - pub adjusted_value_created_1y: Option>, - pub adjusted_value_destroyed_24h: Option>, - pub adjusted_value_destroyed_7d: Option>, - pub adjusted_value_destroyed_30d: Option>, - pub adjusted_value_destroyed_1y: Option>, - - // === Adjusted SOPR (rolling window ratios) === - pub adjusted_sopr: Option>, - pub adjusted_sopr_24h: Option>, - pub adjusted_sopr_7d: Option>, - pub adjusted_sopr_30d: Option>, - pub adjusted_sopr_1y: Option>, - pub adjusted_sopr_24h_7d_ema: Option>, - pub adjusted_sopr_7d_ema: Option>, - pub adjusted_sopr_24h_30d_ema: Option>, - pub adjusted_sopr_30d_ema: Option>, - // === Sell Side Risk Rolling Sum Intermediates === pub realized_value_24h: ComputedFromHeightLast, pub realized_value_7d: ComputedFromHeightLast, @@ -175,32 +129,23 @@ pub struct RealizedMetrics { pub net_realized_pnl_cumulative_30d_delta_rel_to_market_cap: ComputedFromHeightLast, // === Peak Regret === - /// Realized peak regret: Σ((peak - sell_price) × sats) - /// where peak = max price during holding period. - /// "How much more could have been made by selling at peak instead" pub peak_regret: ComputedFromHeightCum, - /// Peak regret as % of realized cap pub peak_regret_rel_to_realized_cap: LazyBinaryFromHeightLast, // === Sent in Profit/Loss === - /// Sats sent in profit (sats/btc/usd) pub sent_in_profit: LazyComputedValueFromHeightCum, - /// 14-day EMA of sent in profit (sats, btc, usd) pub sent_in_profit_14d_ema: ValueEmaFromHeight, - /// Sats sent in loss (sats/btc/usd) pub sent_in_loss: LazyComputedValueFromHeightCum, - /// 14-day EMA of sent in loss (sats, btc, usd) pub sent_in_loss_14d_ema: ValueEmaFromHeight, } -impl RealizedMetrics { - /// Import realized metrics from database. +impl RealizedBase { + /// Import realized base metrics from database. pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { let v1 = Version::ONE; let v2 = Version::new(2); let v3 = Version::new(3); let extended = cfg.extended(); - let compute_adjusted = cfg.compute_adjusted(); // Import combined types using forced_import which handles height + derived let realized_cap_cents = ComputedFromHeightLast::forced_import( @@ -273,7 +218,6 @@ impl RealizedMetrics { cfg.indexes, )?; - // realized_value is the source for total_realized_pnl (they're identical) let realized_value = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("realized_value"), @@ -281,7 +225,6 @@ impl RealizedMetrics { cfg.indexes, )?; - // total_realized_pnl is a lazy alias to realized_value let total_realized_pnl = LazyFromHeightLast::from_computed::( &cfg.name("total_realized_pnl"), cfg.version + v1, @@ -289,7 +232,6 @@ impl RealizedMetrics { &realized_value, ); - // Construct lazy ratio vecs let realized_profit_rel_to_realized_cap = LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::( &cfg.name("realized_profit_rel_to_realized_cap"), @@ -321,7 +263,6 @@ impl RealizedMetrics { cfg.indexes, )?; - // Investor price (dollar-weighted average acquisition price) let investor_price_cents = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("investor_price_cents"), @@ -344,7 +285,6 @@ impl RealizedMetrics { extended, )?; - // Floor price = realized² / investor (lower band) let lower_price_band = LazyBinaryPriceFromHeight::from_price_and_lazy_price::( &cfg.name("lower_price_band"), @@ -353,7 +293,6 @@ impl RealizedMetrics { &investor_price, ); - // Ceiling price = investor² / realized (upper band) let upper_price_band = LazyBinaryPriceFromHeight::from_lazy_price_and_price::( &cfg.name("upper_price_band"), @@ -362,33 +301,28 @@ impl RealizedMetrics { &realized_price, ); - // Raw values for aggregation let cap_raw = BytesVec::forced_import(cfg.db, &cfg.name("cap_raw"), cfg.version)?; let investor_cap_raw = BytesVec::forced_import(cfg.db, &cfg.name("investor_cap_raw"), cfg.version)?; - // Import the 4 splits (stored) let profit_value_created = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("profit_value_created"), cfg.version, cfg.indexes, )?; - let profit_value_destroyed = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("profit_value_destroyed"), cfg.version, cfg.indexes, )?; - let loss_value_created = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("loss_value_created"), cfg.version, cfg.indexes, )?; - let loss_value_destroyed = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("loss_value_destroyed"), @@ -396,14 +330,12 @@ impl RealizedMetrics { cfg.indexes, )?; - // Create lazy totals (profit + loss) let value_created = LazyBinaryFromHeightLast::from_computed_last::( &cfg.name("value_created"), cfg.version, &profit_value_created, &loss_value_created, ); - let value_destroyed = LazyBinaryFromHeightLast::from_computed_last::( &cfg.name("value_destroyed"), cfg.version, @@ -411,14 +343,12 @@ impl RealizedMetrics { &loss_value_destroyed, ); - // Create lazy aliases let capitulation_flow = LazyFromHeightLast::from_computed::( &cfg.name("capitulation_flow"), cfg.version, loss_value_destroyed.height.read_only_boxed_clone(), &loss_value_destroyed, ); - let profit_flow = LazyFromHeightLast::from_computed::( &cfg.name("profit_flow"), cfg.version, @@ -426,41 +356,6 @@ impl RealizedMetrics { &profit_value_destroyed, ); - // Create lazy adjusted vecs if compute_adjusted and up_to_1h is available - let adjusted_value_created = - (compute_adjusted && cfg.up_to_1h_realized.is_some()).then(|| { - let up_to_1h = cfg.up_to_1h_realized.unwrap(); - LazyBinaryFromHeightLast::from_both_binary_block::< - DollarsMinus, - Dollars, - Dollars, - Dollars, - Dollars, - >( - &cfg.name("adjusted_value_created"), - cfg.version, - &value_created, - &up_to_1h.value_created, - ) - }); - let adjusted_value_destroyed = - (compute_adjusted && cfg.up_to_1h_realized.is_some()).then(|| { - let up_to_1h = cfg.up_to_1h_realized.unwrap(); - LazyBinaryFromHeightLast::from_both_binary_block::< - DollarsMinus, - Dollars, - Dollars, - Dollars, - Dollars, - >( - &cfg.name("adjusted_value_destroyed"), - cfg.version, - &value_destroyed, - &up_to_1h.value_destroyed, - ) - }); - - // Create realized_price_extra first so we can reference its ratio for MVRV proxy let realized_price_extra = ComputedFromHeightRatio::forced_import( cfg.db, &cfg.name("realized_price"), @@ -470,8 +365,6 @@ impl RealizedMetrics { extended, )?; - // MVRV is a lazy proxy for realized_price_extra.ratio - // ratio = close / realized_price = market_cap / realized_cap = MVRV let mvrv = LazyFromHeightLast::from_computed::( &cfg.name("mvrv"), cfg.version, @@ -479,17 +372,12 @@ impl RealizedMetrics { &realized_price_extra.ratio, ); - // === Rolling sum intermediates (must be imported before lazy ratios reference them) === + // === Rolling sum intermediates === macro_rules! import_rolling { ($name:expr) => { ComputedFromHeightLast::forced_import(cfg.db, &cfg.name($name), cfg.version + v1, cfg.indexes)? }; } - macro_rules! import_rolling_opt { - ($cond:expr, $name:expr) => { - $cond.then(|| ComputedFromHeightLast::forced_import(cfg.db, &cfg.name($name), cfg.version + v1, cfg.indexes)).transpose()? - }; - } let value_created_24h = import_rolling!("value_created_24h"); let value_created_7d = import_rolling!("value_created_7d"); @@ -500,30 +388,12 @@ impl RealizedMetrics { let value_destroyed_30d = import_rolling!("value_destroyed_30d"); let value_destroyed_1y = import_rolling!("value_destroyed_1y"); - let adjusted_value_created_24h = import_rolling_opt!(compute_adjusted, "adjusted_value_created_24h"); - let adjusted_value_created_7d = import_rolling_opt!(compute_adjusted, "adjusted_value_created_7d"); - let adjusted_value_created_30d = import_rolling_opt!(compute_adjusted, "adjusted_value_created_30d"); - let adjusted_value_created_1y = import_rolling_opt!(compute_adjusted, "adjusted_value_created_1y"); - let adjusted_value_destroyed_24h = import_rolling_opt!(compute_adjusted, "adjusted_value_destroyed_24h"); - let adjusted_value_destroyed_7d = import_rolling_opt!(compute_adjusted, "adjusted_value_destroyed_7d"); - let adjusted_value_destroyed_30d = import_rolling_opt!(compute_adjusted, "adjusted_value_destroyed_30d"); - let adjusted_value_destroyed_1y = import_rolling_opt!(compute_adjusted, "adjusted_value_destroyed_1y"); - let realized_value_24h = import_rolling!("realized_value_24h"); let realized_value_7d = import_rolling!("realized_value_7d"); let realized_value_30d = import_rolling!("realized_value_30d"); let realized_value_1y = import_rolling!("realized_value_1y"); - let realized_profit_24h = import_rolling_opt!(extended, "realized_profit_24h"); - let realized_profit_7d = import_rolling_opt!(extended, "realized_profit_7d"); - let realized_profit_30d = import_rolling_opt!(extended, "realized_profit_30d"); - let realized_profit_1y = import_rolling_opt!(extended, "realized_profit_1y"); - let realized_loss_24h = import_rolling_opt!(extended, "realized_loss_24h"); - let realized_loss_7d = import_rolling_opt!(extended, "realized_loss_7d"); - let realized_loss_30d = import_rolling_opt!(extended, "realized_loss_30d"); - let realized_loss_1y = import_rolling_opt!(extended, "realized_loss_1y"); - - // === Rolling window lazy ratios (from rolling sum intermediates) === + // === Rolling window lazy ratios === let sopr_24h = LazyBinaryFromHeightLast::from_computed_last::( &cfg.name("sopr_24h"), cfg.version + v1, &value_created_24h, &value_destroyed_24h, ); @@ -540,26 +410,6 @@ impl RealizedMetrics { &cfg.name("sopr"), cfg.version + v1, &sopr_24h, ); - macro_rules! lazy_binary_from_opt_last { - ($transform:ty, $name:expr, $s1:expr, $s2:expr) => { - ($s1.is_some() && $s2.is_some()).then(|| { - LazyBinaryFromHeightLast::from_computed_last::<$transform>( - &cfg.name($name), cfg.version + v1, - $s1.as_ref().unwrap(), $s2.as_ref().unwrap(), - ) - }) - }; - } - let adjusted_sopr_24h = lazy_binary_from_opt_last!(Ratio64, "adjusted_sopr_24h", adjusted_value_created_24h, adjusted_value_destroyed_24h); - let adjusted_sopr_7d = lazy_binary_from_opt_last!(Ratio64, "adjusted_sopr_7d", adjusted_value_created_7d, adjusted_value_destroyed_7d); - let adjusted_sopr_30d = lazy_binary_from_opt_last!(Ratio64, "adjusted_sopr_30d", adjusted_value_created_30d, adjusted_value_destroyed_30d); - let adjusted_sopr_1y = lazy_binary_from_opt_last!(Ratio64, "adjusted_sopr_1y", adjusted_value_created_1y, adjusted_value_destroyed_1y); - let adjusted_sopr = adjusted_sopr_24h.as_ref().map(|sopr_24h| { - LazyFromHeightLast::from_binary::( - &cfg.name("adjusted_sopr"), cfg.version + v1, sopr_24h, - ) - }); - let sell_side_risk_ratio_24h = LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::( &cfg.name("sell_side_risk_ratio_24h"), cfg.version + v1, &realized_value_24h, &realized_cap, ); @@ -576,11 +426,6 @@ impl RealizedMetrics { &cfg.name("sell_side_risk_ratio"), cfg.version + v1, &sell_side_risk_ratio_24h, ); - let realized_profit_to_loss_ratio_24h = lazy_binary_from_opt_last!(Ratio64, "realized_profit_to_loss_ratio_24h", realized_profit_24h, realized_loss_24h); - let realized_profit_to_loss_ratio_7d = lazy_binary_from_opt_last!(Ratio64, "realized_profit_to_loss_ratio_7d", realized_profit_7d, realized_loss_7d); - let realized_profit_to_loss_ratio_30d = lazy_binary_from_opt_last!(Ratio64, "realized_profit_to_loss_ratio_30d", realized_profit_30d, realized_loss_30d); - let realized_profit_to_loss_ratio_1y = lazy_binary_from_opt_last!(Ratio64, "realized_profit_to_loss_ratio_1y", realized_profit_1y, realized_loss_1y); - // === EMA imports + identity aliases === let sopr_24h_7d_ema = import_rolling!("sopr_24h_7d_ema"); let sopr_7d_ema = LazyFromHeightLast::from_computed::( @@ -593,21 +438,6 @@ impl RealizedMetrics { sopr_24h_30d_ema.height.read_only_boxed_clone(), &sopr_24h_30d_ema, ); - let adjusted_sopr_24h_7d_ema = import_rolling_opt!(compute_adjusted, "adjusted_sopr_24h_7d_ema"); - let adjusted_sopr_7d_ema = adjusted_sopr_24h_7d_ema.as_ref().map(|ema| { - LazyFromHeightLast::from_computed::( - &cfg.name("adjusted_sopr_7d_ema"), cfg.version + v1, - ema.height.read_only_boxed_clone(), ema, - ) - }); - let adjusted_sopr_24h_30d_ema = import_rolling_opt!(compute_adjusted, "adjusted_sopr_24h_30d_ema"); - let adjusted_sopr_30d_ema = adjusted_sopr_24h_30d_ema.as_ref().map(|ema| { - LazyFromHeightLast::from_computed::( - &cfg.name("adjusted_sopr_30d_ema"), cfg.version + v1, - ema.height.read_only_boxed_clone(), ema, - ) - }); - let sell_side_risk_ratio_24h_7d_ema = import_rolling!("sell_side_risk_ratio_24h_7d_ema"); let sell_side_risk_ratio_7d_ema = LazyFromHeightLast::from_computed::( &cfg.name("sell_side_risk_ratio_7d_ema"), cfg.version + v1, @@ -628,44 +458,24 @@ impl RealizedMetrics { ); Ok(Self { - // === Realized Cap === realized_cap_cents, realized_cap, realized_price, realized_price_extra, - realized_cap_rel_to_own_market_cap: extended - .then(|| { - ComputedFromHeightLast::forced_import( - cfg.db, - &cfg.name("realized_cap_rel_to_own_market_cap"), - cfg.version, - cfg.indexes, - ) - }) - .transpose()?, realized_cap_30d_delta: ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("realized_cap_30d_delta"), cfg.version, cfg.indexes, )?, - - // === Investor Price === investor_price_cents, investor_price, investor_price_extra, - - // === Floor/Ceiling Price Bands === lower_price_band, upper_price_band, - cap_raw, investor_cap_raw, - - // === MVRV === mvrv, - - // === Realized Profit/Loss === realized_profit, realized_profit_7d_ema, realized_loss, @@ -674,50 +484,18 @@ impl RealizedMetrics { net_realized_pnl, net_realized_pnl_7d_ema, realized_value, - - // === Realized vs Realized Cap Ratios (lazy) === realized_profit_rel_to_realized_cap, realized_loss_rel_to_realized_cap, net_realized_pnl_rel_to_realized_cap, - - // === Total Realized PnL === total_realized_pnl, - - // === Realized Profit/Loss Rolling Sums === - realized_profit_24h, - realized_profit_7d, - realized_profit_30d, - realized_profit_1y, - realized_loss_24h, - realized_loss_7d, - realized_loss_30d, - realized_loss_1y, - - // === Realized Profit to Loss Ratio (lazy from rolling sums) === - realized_profit_to_loss_ratio_24h, - realized_profit_to_loss_ratio_7d, - realized_profit_to_loss_ratio_30d, - realized_profit_to_loss_ratio_1y, - - // === Value Created/Destroyed Splits (stored) === profit_value_created, profit_value_destroyed, loss_value_created, loss_value_destroyed, - - // === Value Created/Destroyed Totals (lazy: profit + loss) === value_created, value_destroyed, - - // === Capitulation/Profit Flow (lazy aliases) === capitulation_flow, profit_flow, - - // === Adjusted Value (lazy: cohort - up_to_1h) === - adjusted_value_created, - adjusted_value_destroyed, - - // === Value Created/Destroyed Rolling Sums === value_created_24h, value_created_7d, value_created_30d, @@ -726,8 +504,6 @@ impl RealizedMetrics { value_destroyed_7d, value_destroyed_30d, value_destroyed_1y, - - // === SOPR (rolling window ratios) === sopr, sopr_24h, sopr_7d, @@ -737,35 +513,10 @@ impl RealizedMetrics { sopr_7d_ema, sopr_24h_30d_ema, sopr_30d_ema, - - // === Adjusted Value Created/Destroyed Rolling Sums === - adjusted_value_created_24h, - adjusted_value_created_7d, - adjusted_value_created_30d, - adjusted_value_created_1y, - adjusted_value_destroyed_24h, - adjusted_value_destroyed_7d, - adjusted_value_destroyed_30d, - adjusted_value_destroyed_1y, - - // === Adjusted SOPR (rolling window ratios) === - adjusted_sopr, - adjusted_sopr_24h, - adjusted_sopr_7d, - adjusted_sopr_30d, - adjusted_sopr_1y, - adjusted_sopr_24h_7d_ema, - adjusted_sopr_7d_ema, - adjusted_sopr_24h_30d_ema, - adjusted_sopr_30d_ema, - - // === Sell Side Risk Rolling Sum Intermediates === realized_value_24h, realized_value_7d, realized_value_30d, realized_value_1y, - - // === Sell Side Risk (rolling window ratios) === sell_side_risk_ratio, sell_side_risk_ratio_24h, sell_side_risk_ratio_7d, @@ -775,8 +526,6 @@ impl RealizedMetrics { sell_side_risk_ratio_7d_ema, sell_side_risk_ratio_24h_30d_ema, sell_side_risk_ratio_30d_ema, - - // === Net Realized PnL Deltas === net_realized_pnl_cumulative_30d_delta: ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("net_realized_pnl_cumulative_30d_delta"), @@ -797,12 +546,8 @@ impl RealizedMetrics { cfg.version + v3, cfg.indexes, )?, - - // === ATH Regret === peak_regret, peak_regret_rel_to_realized_cap, - - // === Sent in Profit/Loss === sent_in_profit: LazyComputedValueFromHeightCum::forced_import( cfg.db, &cfg.name("sent_in_profit"), @@ -852,7 +597,6 @@ impl RealizedMetrics { } /// Push realized state values to height-indexed vectors. - /// State values are CentsUnsigned (deterministic), converted to Dollars for storage. pub(crate) fn truncate_push(&mut self, height: Height, state: &RealizedState) -> Result<()> { self.realized_cap_cents .height @@ -866,11 +610,9 @@ impl RealizedMetrics { self.investor_price_cents .height .truncate_push(height, state.investor_price())?; - // Push raw values for aggregation self.cap_raw.truncate_push(height, state.cap_raw())?; self.investor_cap_raw .truncate_push(height, state.investor_cap_raw())?; - // Push the 4 splits (totals are derived lazily) self.profit_value_created .height .truncate_push(height, state.profit_value_created().to_dollars())?; @@ -883,12 +625,9 @@ impl RealizedMetrics { self.loss_value_destroyed .height .truncate_push(height, state.loss_value_destroyed().to_dollars())?; - // ATH regret self.peak_regret .height .truncate_push(height, state.peak_regret().to_dollars())?; - - // Volume at profit/loss self.sent_in_profit .sats .height @@ -901,34 +640,23 @@ impl RealizedMetrics { Ok(()) } - /// Returns a parallel iterator over all vecs for parallel writing. - pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator { + /// Returns a Vec of mutable references to all stored vecs for parallel writing. + pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { vec![ &mut self.realized_cap_cents.height as &mut dyn AnyStoredVec, &mut self.realized_profit.height, &mut self.realized_loss.height, &mut self.investor_price_cents.height, - // Raw values for aggregation &mut self.cap_raw as &mut dyn AnyStoredVec, &mut self.investor_cap_raw as &mut dyn AnyStoredVec, - // The 4 splits (totals are derived lazily) &mut self.profit_value_created.height, &mut self.profit_value_destroyed.height, &mut self.loss_value_created.height, &mut self.loss_value_destroyed.height, - // ATH regret &mut self.peak_regret.height, - // Sent in profit/loss &mut self.sent_in_profit.sats.height, &mut self.sent_in_loss.sats.height, ] - .into_par_iter() - } - - /// Validate computed versions against base version. - pub(crate) fn validate_computed_versions(&mut self, _base_version: Version) -> Result<()> { - // Validation logic for computed vecs - Ok(()) } /// Compute aggregate values from separate cohorts. @@ -964,8 +692,6 @@ impl RealizedMetrics { )?; // Aggregate raw values for investor_price computation - // (BytesVec doesn't have compute_sum_of_others, so we manually iterate) - // Validate version for investor_price_cents (same pattern as compute_sum_of_others) let investor_price_dep_version = others .iter() .map(|o| o.investor_price_cents.height.version()) @@ -974,13 +700,11 @@ impl RealizedMetrics { .height .validate_computed_version_or_reset(investor_price_dep_version)?; - // Start from where the target vecs left off (handles fresh/reset vecs) let start = self .cap_raw .len() .min(self.investor_cap_raw.len()) .min(self.investor_price_cents.height.len()); - // End at the minimum length across all source vecs let end = others.iter().map(|o| o.cap_raw.len()).min().unwrap_or(0); for i in start..end { @@ -998,7 +722,6 @@ impl RealizedMetrics { self.investor_cap_raw .truncate_push(height, sum_investor_cap)?; - // Compute investor_price from aggregated raw values let investor_price = if sum_cap.inner() == 0 { Cents::ZERO } else { @@ -1009,13 +732,11 @@ impl RealizedMetrics { .truncate_push(height, investor_price)?; } - // Write to persist computed_version (same pattern as compute_sum_of_others) { let _lock = exit.lock(); self.investor_price_cents.height.write()?; } - // Aggregate the 4 splits (totals are derived lazily) self.profit_value_created.height.compute_sum_of_others( starting_indexes.height, &others @@ -1048,7 +769,6 @@ impl RealizedMetrics { .collect::>(), exit, )?; - // ATH regret self.peak_regret.height.compute_sum_of_others( starting_indexes.height, &others @@ -1057,8 +777,6 @@ impl RealizedMetrics { .collect::>(), exit, )?; - - // Volume at profit/loss self.sent_in_profit.sats.height.compute_sum_of_others( starting_indexes.height, &others @@ -1085,16 +803,11 @@ impl RealizedMetrics { starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - // realized_cap_cents: ComputedFromHeightLast - day1 is lazy, nothing to compute - // investor_price_cents: ComputedFromHeightLast - day1 is lazy, nothing to compute - - // realized_profit/loss: ComputedFromHeightCum - compute cumulative from height self.realized_profit .compute_cumulative(starting_indexes.height, exit)?; self.realized_loss .compute_cumulative(starting_indexes.height, exit)?; - // net_realized_pnl = profit - loss self.net_realized_pnl .compute(starting_indexes.height, exit, |vec| { vec.compute_subtract( @@ -1106,10 +819,6 @@ impl RealizedMetrics { Ok(()) })?; - // realized_value = profit + loss - // Note: total_realized_pnl is a lazy alias to realized_value since both - // compute profit + loss with sum aggregation, making them identical. - // ComputedFromHeightLast: day1 is lazy, just compute the height vec directly self.realized_value.height.compute_add( starting_indexes.height, &self.realized_profit.height, @@ -1117,15 +826,9 @@ impl RealizedMetrics { exit, )?; - // Compute derived aggregations for the 4 splits - // (value_created, value_destroyed, capitulation_flow, profit_flow are derived lazily) - // ComputedFromHeightLast: day1 is lazy, nothing to compute - - // ATH regret: ComputedFromHeightCum - compute cumulative from height self.peak_regret .compute_cumulative(starting_indexes.height, exit)?; - // Volume at profit/loss: LazyComputedValueFromHeightCum - compute cumulative self.sent_in_profit .compute_cumulative(starting_indexes.height, exit)?; self.sent_in_loss @@ -1134,18 +837,17 @@ impl RealizedMetrics { Ok(()) } - /// Second phase of computed metrics (realized price from realized cap / supply). + /// Second phase of computed metrics (base-only parts: realized price, rolling sums, EMAs). #[allow(clippy::too_many_arguments)] - pub(crate) fn compute_rest_part2( + pub(crate) fn compute_rest_part2_base( &mut self, blocks: &blocks::Vecs, prices: &prices::Vecs, starting_indexes: &ComputeIndexes, height_to_supply: &impl ReadableVec, - height_to_market_cap: Option<&impl ReadableVec>, + height_to_market_cap: &impl ReadableVec, exit: &Exit, ) -> Result<()> { - // realized_price = realized_cap / supply self.realized_price.height.compute_divide( starting_indexes.height, &self.realized_cap.height, @@ -1169,7 +871,6 @@ impl RealizedMetrics { Some(&self.investor_price.height), )?; - // realized_cap_30d_delta: height-level rolling change self.realized_cap_30d_delta.height.compute_rolling_change( starting_indexes.height, &blocks.count.height_1m_ago, @@ -1177,7 +878,7 @@ impl RealizedMetrics { exit, )?; - // === Rolling sum intermediates (must be computed before lazy ratios/EMAs that read them) === + // === Rolling sum intermediates === macro_rules! rolling_sum { ($target:expr, $window:expr, $source:expr) => { $target.height.compute_rolling_sum( @@ -1186,7 +887,6 @@ impl RealizedMetrics { }; } - // Value created/destroyed rolling sums (from lazy binary totals) rolling_sum!(self.value_created_24h, &blocks.count.height_24h_ago, &self.value_created.height); rolling_sum!(self.value_created_7d, &blocks.count.height_1w_ago, &self.value_created.height); rolling_sum!(self.value_created_30d, &blocks.count.height_1m_ago, &self.value_created.height); @@ -1196,71 +896,13 @@ impl RealizedMetrics { rolling_sum!(self.value_destroyed_30d, &blocks.count.height_1m_ago, &self.value_destroyed.height); rolling_sum!(self.value_destroyed_1y, &blocks.count.height_1y_ago, &self.value_destroyed.height); - // Adjusted value created/destroyed rolling sums (from lazy adjusted totals) - if let Some(source) = self.adjusted_value_created.as_ref() { - macro_rules! rolling_sum_opt { - ($target:expr, $window:expr) => { - if let Some(f) = $target.as_mut() { - f.height.compute_rolling_sum( - starting_indexes.height, $window, &source.height, exit, - )?; - } - }; - } - rolling_sum_opt!(self.adjusted_value_created_24h, &blocks.count.height_24h_ago); - rolling_sum_opt!(self.adjusted_value_created_7d, &blocks.count.height_1w_ago); - rolling_sum_opt!(self.adjusted_value_created_30d, &blocks.count.height_1m_ago); - rolling_sum_opt!(self.adjusted_value_created_1y, &blocks.count.height_1y_ago); - } - if let Some(source) = self.adjusted_value_destroyed.as_ref() { - macro_rules! rolling_sum_opt { - ($target:expr, $window:expr) => { - if let Some(f) = $target.as_mut() { - f.height.compute_rolling_sum( - starting_indexes.height, $window, &source.height, exit, - )?; - } - }; - } - rolling_sum_opt!(self.adjusted_value_destroyed_24h, &blocks.count.height_24h_ago); - rolling_sum_opt!(self.adjusted_value_destroyed_7d, &blocks.count.height_1w_ago); - rolling_sum_opt!(self.adjusted_value_destroyed_30d, &blocks.count.height_1m_ago); - rolling_sum_opt!(self.adjusted_value_destroyed_1y, &blocks.count.height_1y_ago); - } - - // Realized value rolling sums (for sell_side_risk_ratio) + // Realized value rolling sums rolling_sum!(self.realized_value_24h, &blocks.count.height_24h_ago, &self.realized_value.height); rolling_sum!(self.realized_value_7d, &blocks.count.height_1w_ago, &self.realized_value.height); rolling_sum!(self.realized_value_30d, &blocks.count.height_1m_ago, &self.realized_value.height); rolling_sum!(self.realized_value_1y, &blocks.count.height_1y_ago, &self.realized_value.height); - // Realized profit/loss rolling sums (for realized_profit_to_loss_ratio) - if let Some(f) = self.realized_profit_24h.as_mut() { - f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &self.realized_profit.height, exit)?; - } - if let Some(f) = self.realized_profit_7d.as_mut() { - f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &self.realized_profit.height, exit)?; - } - if let Some(f) = self.realized_profit_30d.as_mut() { - f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &self.realized_profit.height, exit)?; - } - if let Some(f) = self.realized_profit_1y.as_mut() { - f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &self.realized_profit.height, exit)?; - } - if let Some(f) = self.realized_loss_24h.as_mut() { - f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &self.realized_loss.height, exit)?; - } - if let Some(f) = self.realized_loss_7d.as_mut() { - f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &self.realized_loss.height, exit)?; - } - if let Some(f) = self.realized_loss_30d.as_mut() { - f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &self.realized_loss.height, exit)?; - } - if let Some(f) = self.realized_loss_1y.as_mut() { - f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &self.realized_loss.height, exit)?; - } - - // 7d rolling average of realized profit (height-level) + // 7d rolling averages self.realized_profit_7d_ema .height .compute_rolling_average( @@ -1269,16 +911,12 @@ impl RealizedMetrics { &self.realized_profit.height, exit, )?; - - // 7d rolling average of realized loss (height-level) self.realized_loss_7d_ema.height.compute_rolling_average( starting_indexes.height, &blocks.count.height_1w_ago, &self.realized_loss.height, exit, )?; - - // 7d rolling average of net realized PnL (height-level) self.net_realized_pnl_7d_ema .height .compute_rolling_average( @@ -1288,7 +926,7 @@ impl RealizedMetrics { exit, )?; - // 14-day rolling average of sent in profit (sats and dollars) + // 14-day rolling average of sent in profit/loss self.sent_in_profit_14d_ema.compute_rolling_average( starting_indexes.height, &blocks.count.height_2w_ago, @@ -1296,8 +934,6 @@ impl RealizedMetrics { &self.sent_in_profit.usd.height, exit, )?; - - // 14-day rolling average of sent in loss (sats and dollars) self.sent_in_loss_14d_ema.compute_rolling_average( starting_indexes.height, &blocks.count.height_2w_ago, @@ -1306,14 +942,13 @@ impl RealizedMetrics { exit, )?; - // 7d/30d rolling average of SOPR (from 24h rolling ratio) + // SOPR EMAs self.sopr_24h_7d_ema.height.compute_rolling_average( starting_indexes.height, &blocks.count.height_1w_ago, &self.sopr.height, exit, )?; - self.sopr_24h_30d_ema.height.compute_rolling_average( starting_indexes.height, &blocks.count.height_1m_ago, @@ -1321,28 +956,7 @@ impl RealizedMetrics { exit, )?; - // Optional: adjusted SOPR rolling averages (from 24h rolling ratio) - if let Some(adjusted_sopr) = self.adjusted_sopr.as_ref() { - if let Some(ema_7d) = self.adjusted_sopr_24h_7d_ema.as_mut() { - ema_7d.height.compute_rolling_average( - starting_indexes.height, - &blocks.count.height_1w_ago, - &adjusted_sopr.height, - exit, - )?; - } - - if let Some(ema_30d) = self.adjusted_sopr_24h_30d_ema.as_mut() { - ema_30d.height.compute_rolling_average( - starting_indexes.height, - &blocks.count.height_1m_ago, - &adjusted_sopr.height, - exit, - )?; - } - } - - // 7d/30d rolling average of sell_side_risk_ratio (from 24h rolling ratio) + // Sell side risk EMAs self.sell_side_risk_ratio_24h_7d_ema .height .compute_rolling_average( @@ -1351,7 +965,6 @@ impl RealizedMetrics { &self.sell_side_risk_ratio.height, exit, )?; - self.sell_side_risk_ratio_24h_30d_ema .height .compute_rolling_average( @@ -1361,7 +974,7 @@ impl RealizedMetrics { exit, )?; - // Net realized PnL cumulative 30d delta (height-level rolling change) + // Net realized PnL cumulative 30d delta self.net_realized_pnl_cumulative_30d_delta .height .compute_rolling_change( @@ -1371,7 +984,6 @@ impl RealizedMetrics { exit, )?; - // Relative to realized cap (height-level) self.net_realized_pnl_cumulative_30d_delta_rel_to_realized_cap .height .compute_percentage( @@ -1381,27 +993,14 @@ impl RealizedMetrics { exit, )?; - // Relative to market cap (height-level) - if let Some(height_to_market_cap) = height_to_market_cap { - self.net_realized_pnl_cumulative_30d_delta_rel_to_market_cap - .height - .compute_percentage( - starting_indexes.height, - &self.net_realized_pnl_cumulative_30d_delta.height, - height_to_market_cap, - exit, - )?; - - // Optional: realized_cap_rel_to_own_market_cap - if let Some(rel_vec) = self.realized_cap_rel_to_own_market_cap.as_mut() { - rel_vec.height.compute_percentage( - starting_indexes.height, - &self.realized_cap.height, - height_to_market_cap, - exit, - )?; - } - } + self.net_realized_pnl_cumulative_30d_delta_rel_to_market_cap + .height + .compute_percentage( + starting_indexes.height, + &self.net_realized_pnl_cumulative_30d_delta.height, + height_to_market_cap, + exit, + )?; Ok(()) } diff --git a/crates/brk_computer/src/distribution/metrics/realized/extended.rs b/crates/brk_computer/src/distribution/metrics/realized/extended.rs new file mode 100644 index 000000000..c18500114 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/realized/extended.rs @@ -0,0 +1,121 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Dollars, Height, StoredF32, StoredF64, Version}; +use vecdb::{Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ + ComputeIndexes, blocks, + internal::{ + ComputedFromHeightLast, LazyBinaryFromHeightLast, Ratio64, + }, +}; + +use crate::distribution::metrics::ImportConfig; + +use super::RealizedBase; + +/// Extended realized metrics (only for extended cohorts: all, sth, lth, age_range). +#[derive(Traversable)] +pub struct RealizedExtended { + pub realized_cap_rel_to_own_market_cap: ComputedFromHeightLast, + + // === Realized Profit/Loss Rolling Sums === + pub realized_profit_24h: ComputedFromHeightLast, + pub realized_profit_7d: ComputedFromHeightLast, + pub realized_profit_30d: ComputedFromHeightLast, + pub realized_profit_1y: ComputedFromHeightLast, + pub realized_loss_24h: ComputedFromHeightLast, + pub realized_loss_7d: ComputedFromHeightLast, + pub realized_loss_30d: ComputedFromHeightLast, + pub realized_loss_1y: ComputedFromHeightLast, + + // === Realized Profit to Loss Ratio (lazy from rolling sums) === + pub realized_profit_to_loss_ratio_24h: LazyBinaryFromHeightLast, + pub realized_profit_to_loss_ratio_7d: LazyBinaryFromHeightLast, + pub realized_profit_to_loss_ratio_30d: LazyBinaryFromHeightLast, + pub realized_profit_to_loss_ratio_1y: LazyBinaryFromHeightLast, +} + +impl RealizedExtended { + pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + let v1 = Version::ONE; + + macro_rules! import_rolling { + ($name:expr) => { + ComputedFromHeightLast::forced_import(cfg.db, &cfg.name($name), cfg.version + v1, cfg.indexes)? + }; + } + + let realized_profit_24h = import_rolling!("realized_profit_24h"); + let realized_profit_7d = import_rolling!("realized_profit_7d"); + let realized_profit_30d = import_rolling!("realized_profit_30d"); + let realized_profit_1y = import_rolling!("realized_profit_1y"); + let realized_loss_24h = import_rolling!("realized_loss_24h"); + let realized_loss_7d = import_rolling!("realized_loss_7d"); + let realized_loss_30d = import_rolling!("realized_loss_30d"); + let realized_loss_1y = import_rolling!("realized_loss_1y"); + + let realized_profit_to_loss_ratio_24h = LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("realized_profit_to_loss_ratio_24h"), cfg.version + v1, &realized_profit_24h, &realized_loss_24h, + ); + let realized_profit_to_loss_ratio_7d = LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("realized_profit_to_loss_ratio_7d"), cfg.version + v1, &realized_profit_7d, &realized_loss_7d, + ); + let realized_profit_to_loss_ratio_30d = LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("realized_profit_to_loss_ratio_30d"), cfg.version + v1, &realized_profit_30d, &realized_loss_30d, + ); + let realized_profit_to_loss_ratio_1y = LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("realized_profit_to_loss_ratio_1y"), cfg.version + v1, &realized_profit_1y, &realized_loss_1y, + ); + + Ok(RealizedExtended { + realized_cap_rel_to_own_market_cap: ComputedFromHeightLast::forced_import( + cfg.db, + &cfg.name("realized_cap_rel_to_own_market_cap"), + cfg.version, + cfg.indexes, + )?, + realized_profit_24h, + realized_profit_7d, + realized_profit_30d, + realized_profit_1y, + realized_loss_24h, + realized_loss_7d, + realized_loss_30d, + realized_loss_1y, + realized_profit_to_loss_ratio_24h, + realized_profit_to_loss_ratio_7d, + realized_profit_to_loss_ratio_30d, + realized_profit_to_loss_ratio_1y, + }) + } + + pub(crate) fn compute_rest_part2_ext( + &mut self, + base: &RealizedBase, + blocks: &blocks::Vecs, + starting_indexes: &ComputeIndexes, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + // Realized profit/loss rolling sums + self.realized_profit_24h.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &base.realized_profit.height, exit)?; + self.realized_profit_7d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &base.realized_profit.height, exit)?; + self.realized_profit_30d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &base.realized_profit.height, exit)?; + self.realized_profit_1y.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &base.realized_profit.height, exit)?; + self.realized_loss_24h.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &base.realized_loss.height, exit)?; + self.realized_loss_7d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &base.realized_loss.height, exit)?; + self.realized_loss_30d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &base.realized_loss.height, exit)?; + self.realized_loss_1y.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &base.realized_loss.height, exit)?; + + // Realized cap relative to own market cap + self.realized_cap_rel_to_own_market_cap.height.compute_percentage( + starting_indexes.height, + &base.realized_cap.height, + height_to_market_cap, + exit, + )?; + + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/metrics/realized/mod.rs b/crates/brk_computer/src/distribution/metrics/realized/mod.rs new file mode 100644 index 000000000..497a804a6 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/realized/mod.rs @@ -0,0 +1,15 @@ +mod adjusted; +mod base; +mod extended; + +mod with_adjusted; +mod with_extended; +mod with_extended_adjusted; + +pub use adjusted::*; +pub use base::*; +pub use extended::*; + +pub use with_adjusted::*; +pub use with_extended::*; +pub use with_extended_adjusted::*; diff --git a/crates/brk_computer/src/distribution/metrics/realized/with_adjusted.rs b/crates/brk_computer/src/distribution/metrics/realized/with_adjusted.rs new file mode 100644 index 000000000..651946149 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/realized/with_adjusted.rs @@ -0,0 +1,59 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Bitcoin, Dollars, Height}; +use derive_more::{Deref, DerefMut}; +use vecdb::{Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ComputeIndexes, blocks, prices}; + +use crate::distribution::metrics::ImportConfig; + +use super::{RealizedAdjusted, RealizedBase}; + +/// Realized metrics with guaranteed adjusted (no Option). +#[derive(Deref, DerefMut, Traversable)] +#[traversable(merge)] +pub struct RealizedWithAdjusted { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub base: RealizedBase, + #[traversable(flatten)] + pub adjusted: RealizedAdjusted, +} + +impl RealizedWithAdjusted { + pub(crate) fn forced_import(cfg: &ImportConfig, up_to_1h: &RealizedBase) -> Result { + let base = RealizedBase::forced_import(cfg)?; + let adjusted = RealizedAdjusted::forced_import(cfg, &base, up_to_1h)?; + Ok(Self { base, adjusted }) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn compute_rest_part2( + &mut self, + blocks: &blocks::Vecs, + prices: &prices::Vecs, + starting_indexes: &ComputeIndexes, + height_to_supply: &impl ReadableVec, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + self.base.compute_rest_part2_base( + blocks, + prices, + starting_indexes, + height_to_supply, + height_to_market_cap, + exit, + )?; + + self.adjusted.compute_rest_part2_adj( + blocks, + starting_indexes, + exit, + )?; + + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/metrics/realized/with_extended.rs b/crates/brk_computer/src/distribution/metrics/realized/with_extended.rs new file mode 100644 index 000000000..e512b85d4 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/realized/with_extended.rs @@ -0,0 +1,61 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Bitcoin, Dollars, Height}; +use derive_more::{Deref, DerefMut}; +use vecdb::{Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ComputeIndexes, blocks, prices}; + +use crate::distribution::metrics::ImportConfig; + +use super::{RealizedBase, RealizedExtended}; + +/// Realized metrics with guaranteed extended (no Option). +#[derive(Deref, DerefMut, Traversable)] +#[traversable(merge)] +pub struct RealizedWithExtended { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub base: RealizedBase, + #[traversable(flatten)] + pub extended: RealizedExtended, +} + +impl RealizedWithExtended { + pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + let base = RealizedBase::forced_import(cfg)?; + let extended = RealizedExtended::forced_import(cfg)?; + Ok(Self { base, extended }) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn compute_rest_part2( + &mut self, + blocks: &blocks::Vecs, + prices: &prices::Vecs, + starting_indexes: &ComputeIndexes, + height_to_supply: &impl ReadableVec, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + self.base.compute_rest_part2_base( + blocks, + prices, + starting_indexes, + height_to_supply, + height_to_market_cap, + exit, + )?; + + self.extended.compute_rest_part2_ext( + &self.base, + blocks, + starting_indexes, + height_to_market_cap, + exit, + )?; + + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/metrics/realized/with_extended_adjusted.rs b/crates/brk_computer/src/distribution/metrics/realized/with_extended_adjusted.rs new file mode 100644 index 000000000..b823c005d --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/realized/with_extended_adjusted.rs @@ -0,0 +1,74 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::{Bitcoin, Dollars, Height}; +use derive_more::{Deref, DerefMut}; +use vecdb::{Exit, ReadableVec, Rw, StorageMode}; + +use crate::{ComputeIndexes, blocks, prices}; + +use crate::distribution::metrics::ImportConfig; + +use super::{RealizedAdjusted, RealizedBase, RealizedExtended}; + +/// Realized metrics with guaranteed extended AND adjusted (no Options). +#[derive(Deref, DerefMut, Traversable)] +#[traversable(merge)] +pub struct RealizedWithExtendedAdjusted { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub base: RealizedBase, + #[traversable(flatten)] + pub extended: RealizedExtended, + #[traversable(flatten)] + pub adjusted: RealizedAdjusted, +} + +impl RealizedWithExtendedAdjusted { + pub(crate) fn forced_import(cfg: &ImportConfig, up_to_1h: &RealizedBase) -> Result { + let base = RealizedBase::forced_import(cfg)?; + let extended = RealizedExtended::forced_import(cfg)?; + let adjusted = RealizedAdjusted::forced_import(cfg, &base, up_to_1h)?; + Ok(Self { + base, + extended, + adjusted, + }) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn compute_rest_part2( + &mut self, + blocks: &blocks::Vecs, + prices: &prices::Vecs, + starting_indexes: &ComputeIndexes, + height_to_supply: &impl ReadableVec, + height_to_market_cap: &impl ReadableVec, + exit: &Exit, + ) -> Result<()> { + self.base.compute_rest_part2_base( + blocks, + prices, + starting_indexes, + height_to_supply, + height_to_market_cap, + exit, + )?; + + self.extended.compute_rest_part2_ext( + &self.base, + blocks, + starting_indexes, + height_to_market_cap, + exit, + )?; + + self.adjusted.compute_rest_part2_adj( + blocks, + starting_indexes, + exit, + )?; + + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative.rs b/crates/brk_computer/src/distribution/metrics/relative.rs deleted file mode 100644 index d996f1d01..000000000 --- a/crates/brk_computer/src/distribution/metrics/relative.rs +++ /dev/null @@ -1,328 +0,0 @@ -use brk_cohort::Filter; -use brk_error::Result; -use brk_traversable::Traversable; -use brk_types::{Dollars, Sats, StoredF32, StoredF64, Version}; - -use crate::internal::{ - LazyBinaryFromHeightLast, NegPercentageDollarsF32, PercentageDollarsF32, PercentageSatsF64, -}; - -use super::{ImportConfig, RealizedMetrics, SupplyMetrics, UnrealizedMetrics}; - -/// Relative metrics comparing cohort values to global values. -/// All `rel_to_` vecs are lazy - computed on-demand from their sources. -#[derive(Clone, Traversable)] -pub struct RelativeMetrics { - // === Supply Relative to Circulating Supply (lazy from global supply) === - pub supply_rel_to_circulating_supply: - Option>, - - // === Supply in Profit/Loss Relative to Own Supply (lazy) === - pub supply_in_profit_rel_to_own_supply: LazyBinaryFromHeightLast, - pub supply_in_loss_rel_to_own_supply: LazyBinaryFromHeightLast, - - // === Supply in Profit/Loss Relative to Circulating Supply (lazy from global supply) === - pub supply_in_profit_rel_to_circulating_supply: - Option>, - pub supply_in_loss_rel_to_circulating_supply: - Option>, - - // === Unrealized vs Market Cap (lazy from global market cap) === - pub unrealized_profit_rel_to_market_cap: - Option>, - pub unrealized_loss_rel_to_market_cap: - Option>, - pub neg_unrealized_loss_rel_to_market_cap: - Option>, - pub net_unrealized_pnl_rel_to_market_cap: - Option>, - - // === NUPL (Net Unrealized Profit/Loss) === - pub nupl: Option>, - - // === Unrealized vs Own Market Cap (lazy) === - pub unrealized_profit_rel_to_own_market_cap: - Option>, - pub unrealized_loss_rel_to_own_market_cap: - Option>, - pub neg_unrealized_loss_rel_to_own_market_cap: - Option>, - pub net_unrealized_pnl_rel_to_own_market_cap: - Option>, - - // === Unrealized vs Own Total Unrealized PnL (lazy) === - pub unrealized_profit_rel_to_own_total_unrealized_pnl: - Option>, - pub unrealized_loss_rel_to_own_total_unrealized_pnl: - Option>, - pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl: - Option>, - pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl: - Option>, - - // === Invested Capital in Profit/Loss as % of Realized Cap === - pub invested_capital_in_profit_pct: - Option>, - pub invested_capital_in_loss_pct: - Option>, - - // === Unrealized Peak Regret Relative to Market Cap (lazy) === - pub unrealized_peak_regret_rel_to_market_cap: - Option>, -} - -impl RelativeMetrics { - /// Import relative metrics from database. - /// - /// All `rel_to_` metrics are lazy - computed on-demand from their sources. - /// `all_supply` provides global sources for `*_rel_to_market_cap` and `*_rel_to_circulating_supply`. - /// `realized` provides realized_cap for invested capital percentage metrics. - pub(crate) fn forced_import( - cfg: &ImportConfig, - unrealized: &UnrealizedMetrics, - supply: &SupplyMetrics, - all_supply: Option<&SupplyMetrics>, - realized: Option<&RealizedMetrics>, - ) -> Result { - let v1 = Version::ONE; - let v2 = Version::new(2); - let extended = cfg.extended(); - let compute_rel_to_all = cfg.compute_rel_to_all(); - - // Global sources from "all" cohort - let global_supply_sats = all_supply.map(|s| &s.total.sats); - let global_market_cap = all_supply.map(|s| &s.total.usd); - - // Own market cap source - let own_market_cap = &supply.total.usd; - - // For "all" cohort, own_market_cap IS the global market cap - let market_cap = global_market_cap.or_else(|| { - matches!(cfg.filter, Filter::All).then_some(own_market_cap) - }); - - Ok(Self { - // === Supply Relative to Circulating Supply === - supply_rel_to_circulating_supply: (compute_rel_to_all - && global_supply_sats.is_some()) - .then(|| { - LazyBinaryFromHeightLast::from_computed_last::( - &cfg.name("supply_rel_to_circulating_supply"), - cfg.version + v1, - &supply.total.sats, - global_supply_sats.unwrap(), - ) - }), - - // === Supply in Profit/Loss Relative to Own Supply === - supply_in_profit_rel_to_own_supply: - LazyBinaryFromHeightLast::from_computed_last::( - &cfg.name("supply_in_profit_rel_to_own_supply"), - cfg.version + v1, - &unrealized.supply_in_profit.sats, - &supply.total.sats, - ), - supply_in_loss_rel_to_own_supply: - LazyBinaryFromHeightLast::from_computed_last::( - &cfg.name("supply_in_loss_rel_to_own_supply"), - cfg.version + v1, - &unrealized.supply_in_loss.sats, - &supply.total.sats, - ), - - // === Supply in Profit/Loss Relative to Circulating Supply === - supply_in_profit_rel_to_circulating_supply: (compute_rel_to_all - && global_supply_sats.is_some()) - .then(|| { - LazyBinaryFromHeightLast::from_computed_last::( - &cfg.name("supply_in_profit_rel_to_circulating_supply"), - cfg.version + v1, - &unrealized.supply_in_profit.sats, - global_supply_sats.unwrap(), - ) - }), - supply_in_loss_rel_to_circulating_supply: (compute_rel_to_all - && global_supply_sats.is_some()) - .then(|| { - LazyBinaryFromHeightLast::from_computed_last::( - &cfg.name("supply_in_loss_rel_to_circulating_supply"), - cfg.version + v1, - &unrealized.supply_in_loss.sats, - global_supply_sats.unwrap(), - ) - }), - - // === Unrealized vs Market Cap === - unrealized_profit_rel_to_market_cap: market_cap.map(|mc| { - LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< - PercentageDollarsF32, _, _, - >( - &cfg.name("unrealized_profit_rel_to_market_cap"), - cfg.version + v2, - &unrealized.unrealized_profit, - mc, - ) - }), - unrealized_loss_rel_to_market_cap: market_cap.map(|mc| { - LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< - PercentageDollarsF32, _, _, - >( - &cfg.name("unrealized_loss_rel_to_market_cap"), - cfg.version + v2, - &unrealized.unrealized_loss, - mc, - ) - }), - neg_unrealized_loss_rel_to_market_cap: market_cap.map(|mc| { - LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< - NegPercentageDollarsF32, _, _, - >( - &cfg.name("neg_unrealized_loss_rel_to_market_cap"), - cfg.version + v2, - &unrealized.unrealized_loss, - mc, - ) - }), - net_unrealized_pnl_rel_to_market_cap: market_cap.map(|mc| { - LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::< - PercentageDollarsF32, _, _, _, _, - >( - &cfg.name("net_unrealized_pnl_rel_to_market_cap"), - cfg.version + v2, - &unrealized.net_unrealized_pnl, - mc, - ) - }), - - // NUPL is a proxy for net_unrealized_pnl_rel_to_market_cap - nupl: market_cap.map(|mc| { - LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::< - PercentageDollarsF32, _, _, _, _, - >( - &cfg.name("nupl"), - cfg.version + v2, - &unrealized.net_unrealized_pnl, - mc, - ) - }), - - // === Unrealized vs Own Market Cap (lazy, optional) === - unrealized_profit_rel_to_own_market_cap: (extended && compute_rel_to_all) - .then(|| { - LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< - PercentageDollarsF32, _, _, - >( - &cfg.name("unrealized_profit_rel_to_own_market_cap"), - cfg.version + v2, - &unrealized.unrealized_profit, - own_market_cap, - ) - }), - unrealized_loss_rel_to_own_market_cap: (extended && compute_rel_to_all) - .then(|| { - LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< - PercentageDollarsF32, _, _, - >( - &cfg.name("unrealized_loss_rel_to_own_market_cap"), - cfg.version + v2, - &unrealized.unrealized_loss, - own_market_cap, - ) - }), - neg_unrealized_loss_rel_to_own_market_cap: (extended && compute_rel_to_all) - .then(|| { - LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< - NegPercentageDollarsF32, _, _, - >( - &cfg.name("neg_unrealized_loss_rel_to_own_market_cap"), - cfg.version + v2, - &unrealized.unrealized_loss, - own_market_cap, - ) - }), - net_unrealized_pnl_rel_to_own_market_cap: (extended && compute_rel_to_all) - .then(|| { - LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::< - PercentageDollarsF32, _, _, _, _, - >( - &cfg.name("net_unrealized_pnl_rel_to_own_market_cap"), - cfg.version + v2, - &unrealized.net_unrealized_pnl, - own_market_cap, - ) - }), - - // === Unrealized vs Own Total Unrealized PnL (lazy, optional) === - unrealized_profit_rel_to_own_total_unrealized_pnl: extended.then(|| { - LazyBinaryFromHeightLast::from_block_last_and_binary_block::( - &cfg.name("unrealized_profit_rel_to_own_total_unrealized_pnl"), - cfg.version + v1, - &unrealized.unrealized_profit, - &unrealized.total_unrealized_pnl, - ) - }), - unrealized_loss_rel_to_own_total_unrealized_pnl: extended.then(|| { - LazyBinaryFromHeightLast::from_block_last_and_binary_block::( - &cfg.name("unrealized_loss_rel_to_own_total_unrealized_pnl"), - cfg.version + v1, - &unrealized.unrealized_loss, - &unrealized.total_unrealized_pnl, - ) - }), - neg_unrealized_loss_rel_to_own_total_unrealized_pnl: extended.then(|| { - LazyBinaryFromHeightLast::from_block_last_and_binary_block::( - &cfg.name("neg_unrealized_loss_rel_to_own_total_unrealized_pnl"), - cfg.version + v1, - &unrealized.unrealized_loss, - &unrealized.total_unrealized_pnl, - ) - }), - net_unrealized_pnl_rel_to_own_total_unrealized_pnl: extended.then(|| { - LazyBinaryFromHeightLast::from_both_binary_block::( - &cfg.name("net_unrealized_pnl_rel_to_own_total_unrealized_pnl"), - cfg.version + v2, - &unrealized.net_unrealized_pnl, - &unrealized.total_unrealized_pnl, - ) - }), - - // === Invested Capital in Profit/Loss as % of Realized Cap === - invested_capital_in_profit_pct: realized.map(|r| { - LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::< - PercentageDollarsF32, _, - >( - &cfg.name("invested_capital_in_profit_pct"), - cfg.version, - &unrealized.invested_capital_in_profit, - &r.realized_cap, - ) - }), - invested_capital_in_loss_pct: realized.map(|r| { - LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::< - PercentageDollarsF32, _, - >( - &cfg.name("invested_capital_in_loss_pct"), - cfg.version, - &unrealized.invested_capital_in_loss, - &r.realized_cap, - ) - }), - - // === Peak Regret Relative to Market Cap === - unrealized_peak_regret_rel_to_market_cap: unrealized - .peak_regret - .as_ref() - .zip(market_cap) - .map(|(pr, mc)| { - LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< - PercentageDollarsF32, _, _, - >( - &cfg.name("unrealized_peak_regret_rel_to_market_cap"), - cfg.version, - pr, - mc, - ) - }), - }) - } -} diff --git a/crates/brk_computer/src/distribution/metrics/relative/base.rs b/crates/brk_computer/src/distribution/metrics/relative/base.rs new file mode 100644 index 000000000..32866d3ac --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/base.rs @@ -0,0 +1,131 @@ +use brk_traversable::Traversable; +use brk_types::{Cents, Dollars, Sats, StoredF32, StoredF64, Version}; + +use crate::internal::{ + LazyBinaryComputedFromHeightLast, LazyBinaryFromHeightLast, LazyFromHeightLast, + NegPercentageDollarsF32, PercentageDollarsF32, PercentageSatsF64, +}; + +use crate::distribution::metrics::{ImportConfig, SupplyMetrics, UnrealizedBase}; + +/// Base relative metrics (always computed when relative is enabled). +/// All fields are non-Optional - market_cap and realized_cap are always +/// available when relative metrics are enabled. +#[derive(Clone, Traversable)] +pub struct RelativeBase { + // === Supply in Profit/Loss Relative to Own Supply === + pub supply_in_profit_rel_to_own_supply: LazyBinaryFromHeightLast, + pub supply_in_loss_rel_to_own_supply: LazyBinaryFromHeightLast, + + // === Unrealized vs Market Cap === + pub unrealized_profit_rel_to_market_cap: LazyBinaryFromHeightLast, + pub unrealized_loss_rel_to_market_cap: LazyBinaryFromHeightLast, + pub neg_unrealized_loss_rel_to_market_cap: + LazyBinaryFromHeightLast, + pub net_unrealized_pnl_rel_to_market_cap: + LazyBinaryFromHeightLast, + pub nupl: LazyBinaryFromHeightLast, + + // === Invested Capital in Profit/Loss as % of Realized Cap === + pub invested_capital_in_profit_pct: LazyBinaryFromHeightLast, + pub invested_capital_in_loss_pct: LazyBinaryFromHeightLast, +} + +impl RelativeBase { + /// Import base relative metrics. + /// + /// `market_cap` is either `all_supply.total.usd` (for non-"all" cohorts) + /// or `supply.total.usd` (for the "all" cohort itself). + pub(crate) fn forced_import( + cfg: &ImportConfig, + unrealized: &UnrealizedBase, + supply: &SupplyMetrics, + market_cap: &LazyBinaryComputedFromHeightLast, + realized_cap: &LazyFromHeightLast, + ) -> Self { + let v1 = Version::ONE; + let v2 = Version::new(2); + + Self { + supply_in_profit_rel_to_own_supply: + LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("supply_in_profit_rel_to_own_supply"), + cfg.version + v1, + &unrealized.supply_in_profit.sats, + &supply.total.sats, + ), + supply_in_loss_rel_to_own_supply: + LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("supply_in_loss_rel_to_own_supply"), + cfg.version + v1, + &unrealized.supply_in_loss.sats, + &supply.total.sats, + ), + + unrealized_profit_rel_to_market_cap: + LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< + PercentageDollarsF32, _, _, + >( + &cfg.name("unrealized_profit_rel_to_market_cap"), + cfg.version + v2, + &unrealized.unrealized_profit, + market_cap, + ), + unrealized_loss_rel_to_market_cap: + LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< + PercentageDollarsF32, _, _, + >( + &cfg.name("unrealized_loss_rel_to_market_cap"), + cfg.version + v2, + &unrealized.unrealized_loss, + market_cap, + ), + neg_unrealized_loss_rel_to_market_cap: + LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< + NegPercentageDollarsF32, _, _, + >( + &cfg.name("neg_unrealized_loss_rel_to_market_cap"), + cfg.version + v2, + &unrealized.unrealized_loss, + market_cap, + ), + net_unrealized_pnl_rel_to_market_cap: + LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::< + PercentageDollarsF32, _, _, _, _, + >( + &cfg.name("net_unrealized_pnl_rel_to_market_cap"), + cfg.version + v2, + &unrealized.net_unrealized_pnl, + market_cap, + ), + nupl: + LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::< + PercentageDollarsF32, _, _, _, _, + >( + &cfg.name("nupl"), + cfg.version + v2, + &unrealized.net_unrealized_pnl, + market_cap, + ), + + invested_capital_in_profit_pct: + LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::< + PercentageDollarsF32, _, + >( + &cfg.name("invested_capital_in_profit_pct"), + cfg.version, + &unrealized.invested_capital_in_profit, + realized_cap, + ), + invested_capital_in_loss_pct: + LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::< + PercentageDollarsF32, _, + >( + &cfg.name("invested_capital_in_loss_pct"), + cfg.version, + &unrealized.invested_capital_in_loss, + realized_cap, + ), + } + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative/extended_own_market_cap.rs b/crates/brk_computer/src/distribution/metrics/relative/extended_own_market_cap.rs new file mode 100644 index 000000000..721212289 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/extended_own_market_cap.rs @@ -0,0 +1,71 @@ +use brk_traversable::Traversable; +use brk_types::{Dollars, Sats, StoredF32, Version}; + +use crate::internal::{ + LazyBinaryComputedFromHeightLast, LazyBinaryFromHeightLast, + NegPercentageDollarsF32, PercentageDollarsF32, +}; + +use crate::distribution::metrics::{ImportConfig, UnrealizedBase}; + +/// Extended relative metrics for own market cap (extended && rel_to_all). +#[derive(Clone, Traversable)] +pub struct RelativeExtendedOwnMarketCap { + pub unrealized_profit_rel_to_own_market_cap: + LazyBinaryFromHeightLast, + pub unrealized_loss_rel_to_own_market_cap: + LazyBinaryFromHeightLast, + pub neg_unrealized_loss_rel_to_own_market_cap: + LazyBinaryFromHeightLast, + pub net_unrealized_pnl_rel_to_own_market_cap: + LazyBinaryFromHeightLast, +} + +impl RelativeExtendedOwnMarketCap { + pub(crate) fn forced_import( + cfg: &ImportConfig, + unrealized: &UnrealizedBase, + own_market_cap: &LazyBinaryComputedFromHeightLast, + ) -> Self { + let v2 = Version::new(2); + + Self { + unrealized_profit_rel_to_own_market_cap: + LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< + PercentageDollarsF32, _, _, + >( + &cfg.name("unrealized_profit_rel_to_own_market_cap"), + cfg.version + v2, + &unrealized.unrealized_profit, + own_market_cap, + ), + unrealized_loss_rel_to_own_market_cap: + LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< + PercentageDollarsF32, _, _, + >( + &cfg.name("unrealized_loss_rel_to_own_market_cap"), + cfg.version + v2, + &unrealized.unrealized_loss, + own_market_cap, + ), + neg_unrealized_loss_rel_to_own_market_cap: + LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< + NegPercentageDollarsF32, _, _, + >( + &cfg.name("neg_unrealized_loss_rel_to_own_market_cap"), + cfg.version + v2, + &unrealized.unrealized_loss, + own_market_cap, + ), + net_unrealized_pnl_rel_to_own_market_cap: + LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::< + PercentageDollarsF32, _, _, _, _, + >( + &cfg.name("net_unrealized_pnl_rel_to_own_market_cap"), + cfg.version + v2, + &unrealized.net_unrealized_pnl, + own_market_cap, + ), + } + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative/extended_own_pnl.rs b/crates/brk_computer/src/distribution/metrics/relative/extended_own_pnl.rs new file mode 100644 index 000000000..67caa2e3c --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/extended_own_pnl.rs @@ -0,0 +1,62 @@ +use brk_traversable::Traversable; +use brk_types::{Dollars, StoredF32, Version}; + +use crate::internal::{ + LazyBinaryFromHeightLast, NegPercentageDollarsF32, PercentageDollarsF32, +}; + +use crate::distribution::metrics::{ImportConfig, UnrealizedBase}; + +/// Extended relative metrics for own total unrealized PnL (extended only). +#[derive(Clone, Traversable)] +pub struct RelativeExtendedOwnPnl { + pub unrealized_profit_rel_to_own_total_unrealized_pnl: + LazyBinaryFromHeightLast, + pub unrealized_loss_rel_to_own_total_unrealized_pnl: + LazyBinaryFromHeightLast, + pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl: + LazyBinaryFromHeightLast, + pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl: + LazyBinaryFromHeightLast, +} + +impl RelativeExtendedOwnPnl { + pub(crate) fn forced_import( + cfg: &ImportConfig, + unrealized: &UnrealizedBase, + ) -> Self { + let v1 = Version::ONE; + let v2 = Version::new(2); + + Self { + unrealized_profit_rel_to_own_total_unrealized_pnl: + LazyBinaryFromHeightLast::from_block_last_and_binary_block::( + &cfg.name("unrealized_profit_rel_to_own_total_unrealized_pnl"), + cfg.version + v1, + &unrealized.unrealized_profit, + &unrealized.total_unrealized_pnl, + ), + unrealized_loss_rel_to_own_total_unrealized_pnl: + LazyBinaryFromHeightLast::from_block_last_and_binary_block::( + &cfg.name("unrealized_loss_rel_to_own_total_unrealized_pnl"), + cfg.version + v1, + &unrealized.unrealized_loss, + &unrealized.total_unrealized_pnl, + ), + neg_unrealized_loss_rel_to_own_total_unrealized_pnl: + LazyBinaryFromHeightLast::from_block_last_and_binary_block::( + &cfg.name("neg_unrealized_loss_rel_to_own_total_unrealized_pnl"), + cfg.version + v1, + &unrealized.unrealized_loss, + &unrealized.total_unrealized_pnl, + ), + net_unrealized_pnl_rel_to_own_total_unrealized_pnl: + LazyBinaryFromHeightLast::from_both_binary_block::( + &cfg.name("net_unrealized_pnl_rel_to_own_total_unrealized_pnl"), + cfg.version + v2, + &unrealized.net_unrealized_pnl, + &unrealized.total_unrealized_pnl, + ), + } + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative/for_all.rs b/crates/brk_computer/src/distribution/metrics/relative/for_all.rs new file mode 100644 index 000000000..afb66d3af --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/for_all.rs @@ -0,0 +1,43 @@ +use brk_types::Dollars; +use brk_traversable::Traversable; +use derive_more::{Deref, DerefMut}; + +use crate::internal::ComputedFromHeightLast; + +use crate::distribution::metrics::{ImportConfig, RealizedBase, SupplyMetrics, UnrealizedBase}; + +use super::{RelativeBase, RelativeExtendedOwnPnl, RelativePeakRegret}; + +/// Relative metrics for the "all" cohort (base + own_pnl + peak_regret, NO rel_to_all). +#[derive(Clone, Deref, DerefMut, Traversable)] +#[traversable(merge)] +pub struct RelativeForAll { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub base: RelativeBase, + #[traversable(flatten)] + pub extended_own_pnl: RelativeExtendedOwnPnl, + #[traversable(flatten)] + pub peak_regret: RelativePeakRegret, +} + +impl RelativeForAll { + pub(crate) fn forced_import( + cfg: &ImportConfig, + unrealized: &UnrealizedBase, + supply: &SupplyMetrics, + realized_base: &RealizedBase, + peak_regret: &ComputedFromHeightLast, + ) -> Self { + // For the "all" cohort, market_cap = own market cap + let market_cap = &supply.total.usd; + Self { + base: RelativeBase::forced_import( + cfg, unrealized, supply, market_cap, &realized_base.realized_cap, + ), + extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg, unrealized), + peak_regret: RelativePeakRegret::forced_import(cfg, peak_regret, market_cap), + } + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative/mod.rs b/crates/brk_computer/src/distribution/metrics/relative/mod.rs new file mode 100644 index 000000000..dfd7046a6 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/mod.rs @@ -0,0 +1,21 @@ +mod base; +mod extended_own_market_cap; +mod extended_own_pnl; +mod for_all; + +mod peak_regret; +mod to_all; +mod with_extended; +mod with_peak_regret; +mod with_rel_to_all; + +pub use base::*; +pub use extended_own_market_cap::*; +pub use extended_own_pnl::*; +pub use for_all::*; + +pub use peak_regret::*; +pub use to_all::*; +pub use with_extended::*; +pub use with_peak_regret::*; +pub use with_rel_to_all::*; diff --git a/crates/brk_computer/src/distribution/metrics/relative/peak_regret.rs b/crates/brk_computer/src/distribution/metrics/relative/peak_regret.rs new file mode 100644 index 000000000..3acb04391 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/peak_regret.rs @@ -0,0 +1,36 @@ +use brk_traversable::Traversable; +use brk_types::{Dollars, Sats, StoredF32}; + +use crate::internal::{ + ComputedFromHeightLast, LazyBinaryComputedFromHeightLast, LazyBinaryFromHeightLast, + PercentageDollarsF32, +}; + +use crate::distribution::metrics::ImportConfig; + +/// Peak regret relative metric. +#[derive(Clone, Traversable)] +pub struct RelativePeakRegret { + pub unrealized_peak_regret_rel_to_market_cap: + LazyBinaryFromHeightLast, +} + +impl RelativePeakRegret { + pub(crate) fn forced_import( + cfg: &ImportConfig, + peak_regret: &ComputedFromHeightLast, + market_cap: &LazyBinaryComputedFromHeightLast, + ) -> Self { + Self { + unrealized_peak_regret_rel_to_market_cap: + LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::< + PercentageDollarsF32, _, _, + >( + &cfg.name("unrealized_peak_regret_rel_to_market_cap"), + cfg.version, + peak_regret, + market_cap, + ), + } + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative/to_all.rs b/crates/brk_computer/src/distribution/metrics/relative/to_all.rs new file mode 100644 index 000000000..4842f46c7 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/to_all.rs @@ -0,0 +1,53 @@ +use brk_traversable::Traversable; +use brk_types::{Sats, StoredF64, Version}; + +use crate::internal::{LazyBinaryFromHeightLast, PercentageSatsF64}; + +use crate::distribution::metrics::{ImportConfig, SupplyMetrics, UnrealizedBase}; + +/// Relative-to-all metrics (not present for the "all" cohort itself). +#[derive(Clone, Traversable)] +pub struct RelativeToAll { + pub supply_rel_to_circulating_supply: + LazyBinaryFromHeightLast, + pub supply_in_profit_rel_to_circulating_supply: + LazyBinaryFromHeightLast, + pub supply_in_loss_rel_to_circulating_supply: + LazyBinaryFromHeightLast, +} + +impl RelativeToAll { + pub(crate) fn forced_import( + cfg: &ImportConfig, + unrealized: &UnrealizedBase, + supply: &SupplyMetrics, + all_supply: &SupplyMetrics, + ) -> Self { + let v1 = Version::ONE; + let gs = &all_supply.total.sats; + + Self { + supply_rel_to_circulating_supply: + LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("supply_rel_to_circulating_supply"), + cfg.version + v1, + &supply.total.sats, + gs, + ), + supply_in_profit_rel_to_circulating_supply: + LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("supply_in_profit_rel_to_circulating_supply"), + cfg.version + v1, + &unrealized.supply_in_profit.sats, + gs, + ), + supply_in_loss_rel_to_circulating_supply: + LazyBinaryFromHeightLast::from_computed_last::( + &cfg.name("supply_in_loss_rel_to_circulating_supply"), + cfg.version + v1, + &unrealized.supply_in_loss.sats, + gs, + ), + } + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative/with_extended.rs b/crates/brk_computer/src/distribution/metrics/relative/with_extended.rs new file mode 100644 index 000000000..c0f516421 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/with_extended.rs @@ -0,0 +1,56 @@ +use brk_types::Dollars; +use brk_traversable::Traversable; +use derive_more::{Deref, DerefMut}; + +use crate::internal::ComputedFromHeightLast; + +use crate::distribution::metrics::{ImportConfig, RealizedBase, SupplyMetrics, UnrealizedBase}; + +use super::{ + RelativeBase, RelativeExtendedOwnMarketCap, RelativeExtendedOwnPnl, + RelativePeakRegret, RelativeToAll, +}; + +/// Full extended relative metrics (base + rel_to_all + own_market_cap + own_pnl + peak_regret). +/// Used by: sth, lth, age_range cohorts. +#[derive(Clone, Deref, DerefMut, Traversable)] +#[traversable(merge)] +pub struct RelativeWithExtended { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub base: RelativeBase, + #[traversable(flatten)] + pub rel_to_all: RelativeToAll, + #[traversable(flatten)] + pub extended_own_market_cap: RelativeExtendedOwnMarketCap, + #[traversable(flatten)] + pub extended_own_pnl: RelativeExtendedOwnPnl, + #[traversable(flatten)] + pub peak_regret: RelativePeakRegret, +} + +impl RelativeWithExtended { + pub(crate) fn forced_import( + cfg: &ImportConfig, + unrealized: &UnrealizedBase, + supply: &SupplyMetrics, + all_supply: &SupplyMetrics, + realized_base: &RealizedBase, + peak_regret: &ComputedFromHeightLast, + ) -> Self { + let market_cap = &all_supply.total.usd; + let own_market_cap = &supply.total.usd; + Self { + base: RelativeBase::forced_import( + cfg, unrealized, supply, market_cap, &realized_base.realized_cap, + ), + rel_to_all: RelativeToAll::forced_import(cfg, unrealized, supply, all_supply), + extended_own_market_cap: RelativeExtendedOwnMarketCap::forced_import( + cfg, unrealized, own_market_cap, + ), + extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg, unrealized), + peak_regret: RelativePeakRegret::forced_import(cfg, peak_regret, market_cap), + } + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative/with_peak_regret.rs b/crates/brk_computer/src/distribution/metrics/relative/with_peak_regret.rs new file mode 100644 index 000000000..9dfae9e5d --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/with_peak_regret.rs @@ -0,0 +1,44 @@ +use brk_types::Dollars; +use brk_traversable::Traversable; +use derive_more::{Deref, DerefMut}; + +use crate::internal::ComputedFromHeightLast; + +use crate::distribution::metrics::{ImportConfig, RealizedBase, SupplyMetrics, UnrealizedBase}; + +use super::{RelativeBase, RelativePeakRegret, RelativeToAll}; + +/// Relative metrics with rel_to_all + peak_regret (no extended). +/// Used by: max_age, min_age cohorts. +#[derive(Clone, Deref, DerefMut, Traversable)] +#[traversable(merge)] +pub struct RelativeWithPeakRegret { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub base: RelativeBase, + #[traversable(flatten)] + pub rel_to_all: RelativeToAll, + #[traversable(flatten)] + pub peak_regret: RelativePeakRegret, +} + +impl RelativeWithPeakRegret { + pub(crate) fn forced_import( + cfg: &ImportConfig, + unrealized: &UnrealizedBase, + supply: &SupplyMetrics, + all_supply: &SupplyMetrics, + realized_base: &RealizedBase, + peak_regret: &ComputedFromHeightLast, + ) -> Self { + let market_cap = &all_supply.total.usd; + Self { + base: RelativeBase::forced_import( + cfg, unrealized, supply, market_cap, &realized_base.realized_cap, + ), + rel_to_all: RelativeToAll::forced_import(cfg, unrealized, supply, all_supply), + peak_regret: RelativePeakRegret::forced_import(cfg, peak_regret, market_cap), + } + } +} diff --git a/crates/brk_computer/src/distribution/metrics/relative/with_rel_to_all.rs b/crates/brk_computer/src/distribution/metrics/relative/with_rel_to_all.rs new file mode 100644 index 000000000..c1170c3d8 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/relative/with_rel_to_all.rs @@ -0,0 +1,37 @@ +use brk_traversable::Traversable; +use derive_more::{Deref, DerefMut}; + +use crate::distribution::metrics::{ImportConfig, RealizedBase, SupplyMetrics, UnrealizedBase}; + +use super::{RelativeBase, RelativeToAll}; + +/// Relative metrics with rel_to_all (no extended, no peak_regret). +/// Used by: epoch, year, type, amount, address cohorts. +#[derive(Clone, Deref, DerefMut, Traversable)] +#[traversable(merge)] +pub struct RelativeWithRelToAll { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub base: RelativeBase, + #[traversable(flatten)] + pub rel_to_all: RelativeToAll, +} + +impl RelativeWithRelToAll { + pub(crate) fn forced_import( + cfg: &ImportConfig, + unrealized: &UnrealizedBase, + supply: &SupplyMetrics, + all_supply: &SupplyMetrics, + realized_base: &RealizedBase, + ) -> Self { + let market_cap = &all_supply.total.usd; + Self { + base: RelativeBase::forced_import( + cfg, unrealized, supply, market_cap, &realized_base.realized_cap, + ), + rel_to_all: RelativeToAll::forced_import(cfg, unrealized, supply, all_supply), + } + } +} diff --git a/crates/brk_computer/src/distribution/metrics/unrealized.rs b/crates/brk_computer/src/distribution/metrics/unrealized/base.rs similarity index 65% rename from crates/brk_computer/src/distribution/metrics/unrealized.rs rename to crates/brk_computer/src/distribution/metrics/unrealized/base.rs index ad95728b8..dae624805 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/base.rs @@ -1,27 +1,26 @@ use brk_error::Result; use brk_traversable::Traversable; use brk_types::{Cents, CentsSats, CentsSquaredSats, Dollars, Height, Version}; -use rayon::prelude::*; use vecdb::{ - AnyStoredVec, AnyVec, BytesVec, Exit, WritableVec, ImportableVec, ReadableCloneableVec, - ReadableVec, Negate, Rw, StorageMode, + AnyStoredVec, AnyVec, BytesVec, Exit, ImportableVec, Negate, ReadableCloneableVec, ReadableVec, + Rw, StorageMode, WritableVec, }; use crate::{ ComputeIndexes, distribution::state::UnrealizedState, internal::{ - ComputedFromHeightLast, DollarsMinus, DollarsPlus, - LazyBinaryFromHeightLast, LazyFromHeightLast, ValueFromHeightLast, + ComputedFromHeightLast, DollarsMinus, DollarsPlus, LazyBinaryFromHeightLast, + LazyFromHeightLast, ValueFromHeightLast, }, prices, }; -use super::ImportConfig; +use crate::distribution::metrics::ImportConfig; -/// Unrealized profit/loss metrics. +/// Base unrealized profit/loss metrics (always computed). #[derive(Traversable)] -pub struct UnrealizedMetrics { +pub struct UnrealizedBase { // === Supply in Profit/Loss === pub supply_in_profit: ValueFromHeightLast, pub supply_in_loss: ValueFromHeightLast, @@ -35,21 +34,14 @@ pub struct UnrealizedMetrics { pub invested_capital_in_loss: ComputedFromHeightLast, // === Raw values for precise aggregation (used to compute pain/greed indices) === - /// Σ(price × sats) for UTXOs in profit (raw u128, no indexes) pub invested_capital_in_profit_raw: M::Stored>, - /// Σ(price × sats) for UTXOs in loss (raw u128, no indexes) pub invested_capital_in_loss_raw: M::Stored>, - /// Σ(price² × sats) for UTXOs in profit (raw u128, no indexes) pub investor_cap_in_profit_raw: M::Stored>, - /// Σ(price² × sats) for UTXOs in loss (raw u128, no indexes) pub investor_cap_in_loss_raw: M::Stored>, - // === Pain/Greed Indices (computed in compute_rest from raw values + spot price) === - /// investor_price_of_losers - spot (average distance underwater, weighted by $) + // === Pain/Greed Indices === pub pain_index: ComputedFromHeightLast, - /// spot - investor_price_of_winners (average distance in profit, weighted by $) pub greed_index: ComputedFromHeightLast, - /// greed_index - pain_index (positive = greedy market, negative = painful market) pub net_sentiment: ComputedFromHeightLast, // === Negated === @@ -58,18 +50,10 @@ pub struct UnrealizedMetrics { // === Net and Total === pub net_unrealized_pnl: LazyBinaryFromHeightLast, pub total_unrealized_pnl: LazyBinaryFromHeightLast, - - // === Peak Regret (age_range cohorts only) === - /// Unrealized peak regret: sum of (peak_price - reference_price) × supply - /// where reference_price = max(spot, cost_basis) and peak = max price during holding period. - /// Only computed for age_range cohorts, then aggregated for overlapping cohorts. - pub peak_regret: Option>, } -impl UnrealizedMetrics { - /// Import unrealized metrics from database. +impl UnrealizedBase { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { - // === Supply in Profit/Loss === let supply_in_profit = ValueFromHeightLast::forced_import( cfg.db, &cfg.name("supply_in_profit"), @@ -85,7 +69,6 @@ impl UnrealizedMetrics { cfg.prices, )?; - // === Unrealized Profit/Loss === let unrealized_profit = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("unrealized_profit"), @@ -99,7 +82,6 @@ impl UnrealizedMetrics { cfg.indexes, )?; - // === Invested Capital in Profit/Loss === let invested_capital_in_profit = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("invested_capital_in_profit"), @@ -113,7 +95,6 @@ impl UnrealizedMetrics { cfg.indexes, )?; - // === Raw values for precise aggregation === let invested_capital_in_profit_raw = BytesVec::forced_import( cfg.db, &cfg.name("invested_capital_in_profit_raw"), @@ -124,12 +105,17 @@ impl UnrealizedMetrics { &cfg.name("invested_capital_in_loss_raw"), cfg.version, )?; - let investor_cap_in_profit_raw = - BytesVec::forced_import(cfg.db, &cfg.name("investor_cap_in_profit_raw"), cfg.version)?; - let investor_cap_in_loss_raw = - BytesVec::forced_import(cfg.db, &cfg.name("investor_cap_in_loss_raw"), cfg.version)?; + let investor_cap_in_profit_raw = BytesVec::forced_import( + cfg.db, + &cfg.name("investor_cap_in_profit_raw"), + cfg.version, + )?; + let investor_cap_in_loss_raw = BytesVec::forced_import( + cfg.db, + &cfg.name("investor_cap_in_loss_raw"), + cfg.version, + )?; - // === Pain/Greed Indices === let pain_index = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("pain_index"), @@ -145,11 +131,10 @@ impl UnrealizedMetrics { let net_sentiment = ComputedFromHeightLast::forced_import( cfg.db, &cfg.name("net_sentiment"), - cfg.version + Version::ONE, // v1: weighted average for aggregate cohorts + cfg.version + Version::ONE, cfg.indexes, )?; - // === Negated === let neg_unrealized_loss = LazyFromHeightLast::from_computed::( &cfg.name("neg_unrealized_loss"), cfg.version, @@ -157,7 +142,6 @@ impl UnrealizedMetrics { &unrealized_loss, ); - // === Net and Total === let net_unrealized_pnl = LazyBinaryFromHeightLast::from_computed_last::( &cfg.name("net_unrealized_pnl"), cfg.version, @@ -171,19 +155,6 @@ impl UnrealizedMetrics { &unrealized_loss, ); - // Peak regret: only for age-based UTXO cohorts - let peak_regret = cfg - .compute_peak_regret() - .then(|| { - ComputedFromHeightLast::forced_import( - cfg.db, - &cfg.name("unrealized_peak_regret"), - cfg.version, - cfg.indexes, - ) - }) - .transpose()?; - Ok(Self { supply_in_profit, supply_in_loss, @@ -201,11 +172,9 @@ impl UnrealizedMetrics { neg_unrealized_loss, net_unrealized_pnl, total_unrealized_pnl, - peak_regret, }) } - /// Get minimum length across height-indexed vectors written in block loop. pub(crate) fn min_stateful_height_len(&self) -> usize { self.supply_in_profit .sats @@ -222,8 +191,11 @@ impl UnrealizedMetrics { .min(self.investor_cap_in_loss_raw.len()) } - /// Push unrealized state values to height-indexed vectors. - pub(crate) fn truncate_push(&mut self, height: Height, height_state: &UnrealizedState) -> Result<()> { + pub(crate) fn truncate_push( + &mut self, + height: Height, + height_state: &UnrealizedState, + ) -> Result<()> { self.supply_in_profit .sats .height @@ -245,7 +217,6 @@ impl UnrealizedMetrics { .height .truncate_push(height, height_state.invested_capital_in_loss.to_dollars())?; - // Raw values for aggregation self.invested_capital_in_profit_raw.truncate_push( height, CentsSats::new(height_state.invested_capital_in_profit_raw), @@ -266,57 +237,59 @@ impl UnrealizedMetrics { Ok(()) } - /// Returns a parallel iterator over all vecs for parallel writing. - pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator { - let mut vecs: Vec<&mut dyn AnyStoredVec> = vec![ - &mut self.supply_in_profit.sats.height, + pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + vec![ + &mut self.supply_in_profit.sats.height as &mut dyn AnyStoredVec, &mut self.supply_in_loss.sats.height, &mut self.unrealized_profit.height, &mut self.unrealized_loss.height, &mut self.invested_capital_in_profit.height, &mut self.invested_capital_in_loss.height, - &mut self.invested_capital_in_profit_raw, - &mut self.invested_capital_in_loss_raw, - &mut self.investor_cap_in_profit_raw, - &mut self.investor_cap_in_loss_raw, - ]; - if let Some(pr) = &mut self.peak_regret { - vecs.push(&mut pr.height); - } - vecs.into_par_iter() + &mut self.invested_capital_in_profit_raw as &mut dyn AnyStoredVec, + &mut self.invested_capital_in_loss_raw as &mut dyn AnyStoredVec, + &mut self.investor_cap_in_profit_raw as &mut dyn AnyStoredVec, + &mut self.investor_cap_in_loss_raw as &mut dyn AnyStoredVec, + ] } - /// Compute aggregate values from separate cohorts. pub(crate) fn compute_from_stateful( &mut self, starting_indexes: &ComputeIndexes, others: &[&Self], exit: &Exit, ) -> Result<()> { - self.supply_in_profit.sats.height.compute_sum_of_others( - starting_indexes.height, - &others - .iter() - .map(|v| &v.supply_in_profit.sats.height) - .collect::>(), - exit, - )?; - self.supply_in_loss.sats.height.compute_sum_of_others( - starting_indexes.height, - &others - .iter() - .map(|v| &v.supply_in_loss.sats.height) - .collect::>(), - exit, - )?; - self.unrealized_profit.height.compute_sum_of_others( - starting_indexes.height, - &others - .iter() - .map(|v| &v.unrealized_profit.height) - .collect::>(), - exit, - )?; + self.supply_in_profit + .sats + .height + .compute_sum_of_others( + starting_indexes.height, + &others + .iter() + .map(|v| &v.supply_in_profit.sats.height) + .collect::>(), + exit, + )?; + self.supply_in_loss + .sats + .height + .compute_sum_of_others( + starting_indexes.height, + &others + .iter() + .map(|v| &v.supply_in_loss.sats.height) + .collect::>(), + exit, + )?; + self.unrealized_profit + .height + .compute_sum_of_others( + starting_indexes.height, + &others + .iter() + .map(|v| &v.unrealized_profit.height) + .collect::>(), + exit, + )?; self.unrealized_loss.height.compute_sum_of_others( starting_indexes.height, &others @@ -335,24 +308,24 @@ impl UnrealizedMetrics { .collect::>(), exit, )?; - self.invested_capital_in_loss.height.compute_sum_of_others( - starting_indexes.height, - &others - .iter() - .map(|v| &v.invested_capital_in_loss.height) - .collect::>(), - exit, - )?; + self.invested_capital_in_loss + .height + .compute_sum_of_others( + starting_indexes.height, + &others + .iter() + .map(|v| &v.invested_capital_in_loss.height) + .collect::>(), + exit, + )?; - // Raw values for aggregation - manually sum since BytesVec doesn't have compute_sum_of_others - // Start from where the target vecs left off (handles fresh/reset vecs) + // Raw values for aggregation let start = self .invested_capital_in_profit_raw .len() .min(self.invested_capital_in_loss_raw.len()) .min(self.investor_cap_in_profit_raw.len()) .min(self.investor_cap_in_loss_raw.len()); - // End at the minimum length across all source vecs let end = others .iter() .map(|o| o.invested_capital_in_profit_raw.len()) @@ -368,10 +341,22 @@ impl UnrealizedMetrics { let mut sum_investor_loss = CentsSquaredSats::ZERO; for o in others.iter() { - sum_invested_profit += o.invested_capital_in_profit_raw.collect_one_at(i).unwrap(); - sum_invested_loss += o.invested_capital_in_loss_raw.collect_one_at(i).unwrap(); - sum_investor_profit += o.investor_cap_in_profit_raw.collect_one_at(i).unwrap(); - sum_investor_loss += o.investor_cap_in_loss_raw.collect_one_at(i).unwrap(); + sum_invested_profit += o + .invested_capital_in_profit_raw + .collect_one_at(i) + .unwrap(); + sum_invested_loss += o + .invested_capital_in_loss_raw + .collect_one_at(i) + .unwrap(); + sum_investor_profit += o + .investor_cap_in_profit_raw + .collect_one_at(i) + .unwrap(); + sum_investor_loss += o + .investor_cap_in_loss_raw + .collect_one_at(i) + .unwrap(); } self.invested_capital_in_profit_raw @@ -384,21 +369,6 @@ impl UnrealizedMetrics { .truncate_push(height, sum_investor_loss)?; } - // Peak regret aggregation (only if this cohort has peak_regret) - if let Some(pr) = &mut self.peak_regret { - let other_prs: Vec<_> = others - .iter() - .filter_map(|v| v.peak_regret.as_ref()) - .collect(); - if !other_prs.is_empty() { - pr.height.compute_sum_of_others( - starting_indexes.height, - &other_prs.iter().map(|v| &v.height).collect::>(), - exit, - )?; - } - } - Ok(()) } @@ -409,8 +379,6 @@ impl UnrealizedMetrics { starting_indexes: &ComputeIndexes, exit: &Exit, ) -> Result<()> { - // Height-based types now have lazy day1, no compute_rest needed. - // Pain index: investor_price_of_losers - spot self.pain_index.height.compute_transform3( starting_indexes.height, @@ -451,15 +419,10 @@ impl UnrealizedMetrics { exit, )?; - // Net sentiment height (greed - pain) computed separately for separate cohorts only - // Aggregate cohorts compute it via weighted average in compute_from_stateful - // Dateindex derivation for ALL cohorts happens in compute_net_sentiment_rest - Ok(()) } /// Compute net_sentiment.height for separate cohorts (greed - pain). - /// Aggregate cohorts skip this - their height is computed via weighted average in compute_from_stateful. pub(crate) fn compute_net_sentiment_height( &mut self, starting_indexes: &ComputeIndexes, diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/mod.rs b/crates/brk_computer/src/distribution/metrics/unrealized/mod.rs new file mode 100644 index 000000000..2d5c32382 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/unrealized/mod.rs @@ -0,0 +1,9 @@ +mod base; + +mod peak_regret; +mod with_peak_regret; + +pub use base::*; + +pub use peak_regret::*; +pub use with_peak_regret::*; diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/peak_regret.rs b/crates/brk_computer/src/distribution/metrics/unrealized/peak_regret.rs new file mode 100644 index 000000000..c16e3cc57 --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/unrealized/peak_regret.rs @@ -0,0 +1,49 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use brk_types::Dollars; +use vecdb::{AnyStoredVec, Exit, Rw, StorageMode}; + +use crate::{ComputeIndexes, internal::ComputedFromHeightLast}; + +use crate::distribution::metrics::ImportConfig; + +/// Unrealized peak regret extension (only for age-based UTXO cohorts). +#[derive(Traversable)] +pub struct UnrealizedPeakRegret { + /// Unrealized peak regret: sum of (peak_price - reference_price) x supply + pub peak_regret: ComputedFromHeightLast, +} + +impl UnrealizedPeakRegret { + pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + Ok(Self { + peak_regret: ComputedFromHeightLast::forced_import( + cfg.db, + &cfg.name("unrealized_peak_regret"), + cfg.version, + cfg.indexes, + )?, + }) + } + + pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + vec![&mut self.peak_regret.height] + } + + pub(crate) fn compute_from_stateful( + &mut self, + starting_indexes: &ComputeIndexes, + others: &[&Self], + exit: &Exit, + ) -> Result<()> { + self.peak_regret.height.compute_sum_of_others( + starting_indexes.height, + &others + .iter() + .map(|v| &v.peak_regret.height) + .collect::>(), + exit, + )?; + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/with_peak_regret.rs b/crates/brk_computer/src/distribution/metrics/unrealized/with_peak_regret.rs new file mode 100644 index 000000000..a06f10fde --- /dev/null +++ b/crates/brk_computer/src/distribution/metrics/unrealized/with_peak_regret.rs @@ -0,0 +1,30 @@ +use brk_error::Result; +use brk_traversable::Traversable; +use derive_more::{Deref, DerefMut}; +use vecdb::{Rw, StorageMode}; + +use crate::distribution::metrics::ImportConfig; + +use super::{UnrealizedBase, UnrealizedPeakRegret}; + +/// Unrealized metrics with guaranteed peak regret (no Option). +#[derive(Deref, DerefMut, Traversable)] +#[traversable(merge)] +pub struct UnrealizedWithPeakRegret { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub base: UnrealizedBase, + #[traversable(flatten)] + pub peak_regret_ext: UnrealizedPeakRegret, +} + +impl UnrealizedWithPeakRegret { + pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { + Ok(Self { + base: UnrealizedBase::forced_import(cfg)?, + peak_regret_ext: UnrealizedPeakRegret::forced_import(cfg)?, + }) + } + +} diff --git a/crates/brk_computer/src/distribution/state/cohort/address.rs b/crates/brk_computer/src/distribution/state/cohort/address.rs index 323e15ff5..9409f8a33 100644 --- a/crates/brk_computer/src/distribution/state/cohort/address.rs +++ b/crates/brk_computer/src/distribution/state/cohort/address.rs @@ -33,10 +33,6 @@ impl AddressCohortState { self.inner.realized = RealizedState::default(); } - pub(crate) fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> { - self.inner.reset_cost_basis_data_if_needed() - } - pub(crate) fn send( &mut self, addressdata: &mut FundedAddressData, diff --git a/crates/brk_computer/src/distribution/vecs.rs b/crates/brk_computer/src/distribution/vecs.rs index c4b237c47..ce1e94b5e 100644 --- a/crates/brk_computer/src/distribution/vecs.rs +++ b/crates/brk_computer/src/distribution/vecs.rs @@ -90,7 +90,7 @@ impl Vecs { indexes, prices, &states_path, - Some(&utxo_cohorts.all.metrics.supply), + &utxo_cohorts.all.metrics.supply, )?; // Create address data BytesVecs first so we can also use them for identity mappings @@ -374,7 +374,7 @@ impl Vecs { blocks, prices, starting_indexes, - Some(&height_to_market_cap), + &height_to_market_cap, exit, )?; diff --git a/crates/brk_computer/src/internal/multi/from_height/percentiles.rs b/crates/brk_computer/src/internal/multi/from_height/percentiles.rs index f5d496cc1..a45b89f51 100644 --- a/crates/brk_computer/src/internal/multi/from_height/percentiles.rs +++ b/crates/brk_computer/src/internal/multi/from_height/percentiles.rs @@ -2,7 +2,7 @@ use brk_error::Result; use brk_traversable::{Traversable, TreeNode}; use brk_types::{Dollars, Height, StoredF32, Version}; use vecdb::{ - AnyExportableVec, AnyVec, Database, ReadOnlyClone, Ro, Rw, StorageMode, WritableVec, + AnyExportableVec, Database, ReadOnlyClone, Ro, Rw, StorageMode, WritableVec, }; use crate::indexes; @@ -90,16 +90,6 @@ impl PercentilesVecs { Ok(Self { vecs }) } - /// Get minimum length across height-indexed vectors written in block loop. - pub(crate) fn min_stateful_height_len(&self) -> usize { - self.vecs - .iter() - .filter_map(|v| v.as_ref()) - .map(|v| v.height.len()) - .min() - .unwrap_or(usize::MAX) - } - /// Push percentile prices at this height. pub(crate) fn truncate_push( &mut self, diff --git a/crates/brk_computer/src/internal/multi/height_derived/lazy_last.rs b/crates/brk_computer/src/internal/multi/height_derived/lazy_last.rs index aed940493..1a386761e 100644 --- a/crates/brk_computer/src/internal/multi/height_derived/lazy_last.rs +++ b/crates/brk_computer/src/internal/multi/height_derived/lazy_last.rs @@ -4,8 +4,8 @@ use brk_traversable::Traversable; use brk_types::{ - Day1, Day3, DifficultyEpoch, HalvingEpoch, Hour1, Hour12, Hour4, Minute1, Minute10, Minute30, - Minute5, Month1, Month3, Month6, Version, Week1, Year1, Year10, + Day1, Day3, DifficultyEpoch, HalvingEpoch, Hour1, Hour4, Hour12, Minute1, Minute5, Minute10, + Minute30, Month1, Month3, Month6, Version, Week1, Year1, Year10, }; use derive_more::{Deref, DerefMut}; use schemars::JsonSchema; @@ -19,29 +19,30 @@ use crate::{ }, }; -pub type LazyHeightDerivedLastInner = Indexes< - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, - LazyTransformLast, ->; - #[derive(Clone, Deref, DerefMut, Traversable)] #[traversable(transparent)] -pub struct LazyHeightDerivedLast(pub LazyHeightDerivedLastInner) +pub struct LazyHeightDerivedLast( + #[allow(clippy::type_complexity)] + pub Indexes< + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + LazyTransformLast, + >, +) where T: ComputedVecValue + PartialOrd + JsonSchema, S1T: ComputedVecValue; diff --git a/crates/brk_computer/src/internal/single/lazy/mod.rs b/crates/brk_computer/src/internal/single/lazy/mod.rs index 75aa2cb6f..81db81e6a 100644 --- a/crates/brk_computer/src/internal/single/lazy/mod.rs +++ b/crates/brk_computer/src/internal/single/lazy/mod.rs @@ -200,6 +200,5 @@ pub use max::*; pub use min::*; pub use percentile::*; pub use percentiles::*; -pub use sparse_last::*; pub use sum::*; pub use sum_cum::*;