From 92cb184a5ce1771ff96249a98c0ee734b5f29042 Mon Sep 17 00:00:00 2001 From: nym21 Date: Fri, 6 Mar 2026 14:40:52 +0100 Subject: [PATCH] global: snapshot --- .../src/distribution/cohorts/address/vecs.rs | 8 +- .../src/distribution/cohorts/utxo/groups.rs | 58 ++- .../src/distribution/cohorts/utxo/send.rs | 4 +- .../src/distribution/cohorts/utxo/vecs.rs | 302 ++++++--------- .../src/distribution/metrics/activity/base.rs | 14 +- .../distribution/metrics/cohort/adjusted.rs | 143 ------- .../src/distribution/metrics/cohort/all.rs | 97 +---- .../src/distribution/metrics/cohort/basic.rs | 85 +---- .../distribution/metrics/cohort/complete.rs | 4 +- .../distribution/metrics/cohort/extended.rs | 75 +--- .../metrics/cohort/extended_adjusted.rs | 140 ++----- .../src/distribution/metrics/cohort/mod.rs | 2 - .../distribution/metrics/cost_basis/base.rs | 4 +- .../metrics/cost_basis/extended.rs | 4 +- .../src/distribution/metrics/mod.rs | 196 +++++++--- .../distribution/metrics/realized/adjusted.rs | 2 +- .../distribution/metrics/realized/complete.rs | 22 +- .../src/distribution/metrics/realized/core.rs | 20 +- .../distribution/metrics/realized/extended.rs | 58 +-- .../src/distribution/metrics/realized/mod.rs | 4 - .../metrics/realized/with_adjusted.rs | 64 ---- .../metrics/realized/with_extended.rs | 3 +- .../realized/with_extended_adjusted.rs | 79 ---- .../distribution/metrics/unrealized/base.rs | 14 +- .../metrics/unrealized/complete.rs | 18 +- .../src/distribution/state/cohort/address.rs | 11 +- .../src/distribution/state/cohort/base.rs | 79 ++-- .../src/distribution/state/cohort/utxo.rs | 9 +- .../distribution/state/cost_basis/realized.rs | 350 +++++++++++------- .../brk_computer/src/outputs/spent/compute.rs | 5 +- crates/brk_traversable_derive/src/lib.rs | 124 ++++--- 31 files changed, 741 insertions(+), 1257 deletions(-) delete mode 100644 crates/brk_computer/src/distribution/metrics/cohort/adjusted.rs delete mode 100644 crates/brk_computer/src/distribution/metrics/realized/with_adjusted.rs delete mode 100644 crates/brk_computer/src/distribution/metrics/realized/with_extended_adjusted.rs diff --git a/crates/brk_computer/src/distribution/cohorts/address/vecs.rs b/crates/brk_computer/src/distribution/cohorts/address/vecs.rs index ae4fafb1b..511170381 100644 --- a/crates/brk_computer/src/distribution/cohorts/address/vecs.rs +++ b/crates/brk_computer/src/distribution/cohorts/address/vecs.rs @@ -8,7 +8,11 @@ use rayon::prelude::*; use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec}; use crate::{ - blocks, distribution::state::AddressCohortState, indexes, internal::ComputedFromHeight, prices, + blocks, + distribution::state::{AddressCohortState, CoreRealizedState}, + indexes, + internal::ComputedFromHeight, + prices, }; use crate::distribution::metrics::{CoreCohortMetrics, ImportConfig}; @@ -19,7 +23,7 @@ pub struct AddressCohortVecs { starting_height: Option, #[traversable(skip)] - pub state: Option>, + pub state: Option>>, #[traversable(flatten)] pub metrics: CoreCohortMetrics, diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs index 6b3d9ad17..a1289bf4f 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs @@ -20,7 +20,7 @@ use crate::distribution::metrics::{ use super::{percentiles::PercentileCache, vecs::UTXOCohortVecs}; -use crate::distribution::state::UTXOCohortState; +use crate::distribution::state::{CoreRealizedState, RealizedState, UTXOCohortState}; const VERSION: Version = Version::new(0); @@ -36,18 +36,18 @@ const VERSION: Version = Version::new(0); /// - min_age: basic #[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 class: ByClass>>, - pub type_: BySpendableType>>, + pub all: UTXOCohortVecs, RealizedState>, + pub sth: UTXOCohortVecs, RealizedState>, + pub lth: UTXOCohortVecs, RealizedState>, + pub age_range: ByAgeRange, RealizedState>>, + pub max_age: ByMaxAge, RealizedState>>, + pub min_age: ByMinAge, RealizedState>>, + pub ge_amount: ByGreatEqualAmount, CoreRealizedState>>, + pub amount_range: ByAmountRange, CoreRealizedState>>, + pub lt_amount: ByLowerThanAmount, CoreRealizedState>>, + pub epoch: ByEpoch, CoreRealizedState>>, + pub class: ByClass, CoreRealizedState>>, + pub type_: BySpendableType, CoreRealizedState>>, #[traversable(skip)] pub(super) percentile_cache: PercentileCache, /// Cached partition_point positions for tick_tock boundary searches. @@ -86,7 +86,7 @@ impl UTXOCohorts { // Helper for separate cohorts with BasicCohortMetrics + full state let basic_separate = - |f: Filter, name: &'static str| -> Result> { + |f: Filter, name: &'static str| -> Result> { let full_name = CohortContext::Utxo.full_name(&f, name); let cfg = ImportConfig { db, @@ -105,7 +105,7 @@ impl UTXOCohorts { let age_range = ByAgeRange::try_new(&basic_separate)?; let core_separate = - |f: Filter, name: &'static str| -> Result> { + |f: Filter, name: &'static str| -> Result> { let full_name = CohortContext::Utxo.full_name(&f, name); let cfg = ImportConfig { db, @@ -126,7 +126,7 @@ impl UTXOCohorts { let class = ByClass::try_new(&core_separate)?; let type_ = BySpendableType::try_new( - &|f: Filter, name: &'static str| -> Result> { + &|f: Filter, name: &'static str| -> Result> { let full_name = CohortContext::Utxo.full_name(&f, name); let cfg = ImportConfig { db, @@ -181,7 +181,7 @@ impl UTXOCohorts { // max_age: CompleteCohortMetrics (no state, aggregates from age_range) let max_age = { - ByMaxAge::try_new(&|f: Filter, name: &'static str| -> Result<_> { + ByMaxAge::try_new(&|f: Filter, name: &'static str| -> Result> { let full_name = CohortContext::Utxo.full_name(&f, name); let cfg = ImportConfig { db, @@ -199,7 +199,7 @@ impl UTXOCohorts { // min_age: CompleteCohortMetrics let min_age = { - ByMinAge::try_new(&|f: Filter, name: &'static str| -> Result<_> { + ByMinAge::try_new(&|f: Filter, name: &'static str| -> Result> { let full_name = CohortContext::Utxo.full_name(&f, name); let cfg = ImportConfig { db, @@ -217,7 +217,7 @@ impl UTXOCohorts { // ge_amount, lt_amount: CoreCohortMetrics (no state) let core_no_state = - |f: Filter, name: &'static str| -> Result> { + |f: Filter, name: &'static str| -> Result> { let full_name = CohortContext::Utxo.full_name(&f, name); let cfg = ImportConfig { db, @@ -398,15 +398,15 @@ impl UTXOCohorts { let tasks: Vec Result<()> + Send + '_>> = vec![ Box::new(|| { let sources = filter_sources_from(ar.iter(), None); - all.metrics.compute_net_sentiment_from_others_dyn(si, &sources, exit) + all.metrics.compute_net_sentiment_from_others(si, &sources, exit) }), Box::new(|| { let sources = filter_sources_from(ar.iter(), Some(sth.metrics.filter())); - sth.metrics.compute_net_sentiment_from_others_dyn(si, &sources, exit) + sth.metrics.compute_net_sentiment_from_others(si, &sources, exit) }), Box::new(|| { let sources = filter_sources_from(ar.iter(), Some(lth.metrics.filter())); - lth.metrics.compute_net_sentiment_from_others_dyn(si, &sources, exit) + lth.metrics.compute_net_sentiment_from_others(si, &sources, exit) }), ]; @@ -597,26 +597,24 @@ impl UTXOCohorts { } } -/// Filter source cohorts by an optional filter, returning dyn CohortMetricsBase refs. +/// Filter source cohorts by an optional filter. /// If filter is None, returns all sources (used for "all" aggregate). fn filter_sources_from<'a, M: CohortMetricsBase + 'a>( - sources: impl Iterator>, + sources: impl Iterator>, filter: Option<&Filter>, -) -> Vec<&'a dyn CohortMetricsBase> { +) -> Vec<&'a M> { match filter { Some(f) => sources .filter(|v| f.includes(v.metrics.filter())) - .map(|v| &v.metrics as &dyn CohortMetricsBase) - .collect(), - None => sources - .map(|v| &v.metrics as &dyn CohortMetricsBase) + .map(|v| &v.metrics) .collect(), + None => sources.map(|v| &v.metrics).collect(), } } /// Filter CoreCohortMetrics source cohorts by an optional filter. fn filter_core_sources_from<'a>( - sources: impl Iterator>, + sources: impl Iterator>, filter: Option<&Filter>, ) -> Vec<&'a CoreCohortMetrics> { match filter { diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/send.rs b/crates/brk_computer/src/distribution/cohorts/utxo/send.rs index 260e19c4b..6c0394ebd 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/send.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/send.rs @@ -4,7 +4,7 @@ use vecdb::{Rw, VecIndex}; use crate::distribution::{ compute::PriceRangeMax, - state::{BlockState, CohortState, Transacted}, + state::{BlockState, SendPrecomputed, Transacted}, }; use super::groups::UTXOCohorts; @@ -51,7 +51,7 @@ impl UTXOCohorts { let peak_price = price_range_max.max_between(receive_height, send_height); // Pre-compute once for age_range, epoch, year (all share sent.spendable_supply) - if let Some(pre) = CohortState::precompute_send( + if let Some(pre) = SendPrecomputed::new( &sent.spendable_supply, current_price, prev_price, diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs b/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs index 202b38900..2a1423109 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/vecs.rs @@ -6,52 +6,83 @@ use vecdb::{Exit, ReadableVec}; use crate::{blocks, distribution::state::UTXOCohortState, prices}; -use crate::distribution::metrics::{CohortMetricsBase, CompleteCohortMetrics, CoreCohortMetrics, MinimalCohortMetrics}; +use crate::distribution::metrics::{ + CohortMetricsBase, CompleteCohortMetrics, CoreCohortMetrics, MinimalCohortMetrics, +}; +use crate::distribution::state::{CoreRealizedState, RealizedOps, RealizedState}; use super::super::traits::DynCohortVecs; #[derive(Traversable)] -pub struct UTXOCohortVecs { - /// Starting height when state was imported +pub struct UTXOCohortVecs { #[traversable(skip)] state_starting_height: Option, - /// Runtime state for block-by-block processing (separate cohorts only) #[traversable(skip)] - pub state: Option>, + pub state: Option>>, - /// Metric vectors #[traversable(flatten)] pub metrics: Metrics, } -impl UTXOCohortVecs { - /// Create a new UTXOCohortVecs with state and metrics. - pub(crate) fn new(state: Option>, metrics: Metrics) -> Self { +// --- Shared state helpers (identical across all DynCohortVecs impls) --- + +impl UTXOCohortVecs { + pub(crate) fn new(state: Option>>, metrics: Metrics) -> Self { Self { state_starting_height: None, state, metrics, } } + + fn reset_state_impl(&mut self) { + self.state_starting_height = Some(Height::ZERO); + if let Some(state) = self.state.as_mut() { + state.reset(); + } + } + + fn write_state_impl(&mut self, height: Height, cleanup: bool) -> Result<()> { + if let Some(state) = self.state.as_mut() { + state.write(height, cleanup)?; + } + Ok(()) + } + + fn reset_cost_basis_impl(&mut self) -> Result<()> { + if let Some(state) = self.state.as_mut() { + state.reset_cost_basis_data_if_needed()?; + } + Ok(()) + } + + fn reset_iteration_impl(&mut self) { + if let Some(state) = self.state.as_mut() { + state.reset_single_iteration_values(); + } + } } -impl Filtered for UTXOCohortVecs { +// --- Blanket impl for CohortMetricsBase types (always use full RealizedState) --- + +impl Filtered + for UTXOCohortVecs +{ fn filter(&self) -> &Filter { self.metrics.filter() } } -impl DynCohortVecs for UTXOCohortVecs { +impl DynCohortVecs + for UTXOCohortVecs +{ fn min_stateful_height_len(&self) -> usize { self.metrics.min_stateful_height_len() } fn reset_state_starting_height(&mut self) { - self.state_starting_height = Some(Height::ZERO); - if let Some(state) = self.state.as_mut() { - state.reset(); - } + self.reset_state_impl(); } fn import_state(&mut self, starting_height: Height) -> Result { @@ -132,8 +163,6 @@ impl DynCohortVecs for UTXOCohortVecs< ) -> Result<()> { self.metrics .compute_rest_part1(blocks, prices, starting_indexes, exit)?; - // Separate cohorts (with state) compute net_sentiment = greed - pain directly. - // Aggregate cohorts get it via weighted average in groups.rs. if self.state.is_some() { self.metrics .compute_net_sentiment_height(starting_indexes, exit)?; @@ -142,79 +171,78 @@ impl DynCohortVecs for UTXOCohortVecs< } fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> { - if let Some(state) = self.state.as_mut() { - state.write(height, cleanup)?; - } - Ok(()) + self.write_state_impl(height, cleanup) } 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(()) + self.reset_cost_basis_impl() } fn reset_single_iteration_values(&mut self) { - if let Some(state) = self.state.as_mut() { - state.reset_single_iteration_values(); - } + self.reset_iteration_impl(); } } -impl Filtered for UTXOCohortVecs { +// --- Shared import_state for non-blanket impls (direct field access) --- + +macro_rules! impl_import_state { + () => { + fn import_state(&mut self, starting_height: Height) -> Result { + if let Some(state) = self.state.as_mut() { + if let Some(mut prev_height) = starting_height.decremented() { + prev_height = state.import_at_or_before(prev_height)?; + + state.supply.value = self + .metrics + .supply + .total + .sats + .height + .collect_one(prev_height) + .unwrap(); + state.supply.utxo_count = *self + .metrics + .outputs + .utxo_count + .height + .collect_one(prev_height) + .unwrap(); + + state.restore_realized_cap(); + + let result = prev_height.incremented(); + self.state_starting_height = Some(result); + Ok(result) + } else { + self.state_starting_height = Some(Height::ZERO); + Ok(Height::ZERO) + } + } else { + self.state_starting_height = Some(starting_height); + Ok(starting_height) + } + } + }; +} + +// --- MinimalCohortMetrics: uses CoreRealizedState --- + +impl Filtered for UTXOCohortVecs { fn filter(&self) -> &Filter { &self.metrics.filter } } -impl DynCohortVecs for UTXOCohortVecs { +impl DynCohortVecs for UTXOCohortVecs { fn min_stateful_height_len(&self) -> usize { self.metrics.min_stateful_height_len() } fn reset_state_starting_height(&mut self) { - self.state_starting_height = Some(Height::ZERO); - if let Some(state) = self.state.as_mut() { - state.reset(); - } + self.reset_state_impl(); } - fn import_state(&mut self, starting_height: Height) -> Result { - if let Some(state) = self.state.as_mut() { - if let Some(mut prev_height) = starting_height.decremented() { - prev_height = state.import_at_or_before(prev_height)?; - - state.supply.value = self - .metrics - .supply - .total - .sats - .height - .collect_one(prev_height) - .unwrap(); - state.supply.utxo_count = *self - .metrics - .outputs - .utxo_count - .height - .collect_one(prev_height) - .unwrap(); - - state.restore_realized_cap(); - - let result = prev_height.incremented(); - self.state_starting_height = Some(result); - Ok(result) - } else { - self.state_starting_height = Some(Height::ZERO); - Ok(Height::ZERO) - } - } else { - self.state_starting_height = Some(starting_height); - Ok(starting_height) - } - } + impl_import_state!(); fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { self.metrics.validate_computed_versions(base_version) @@ -268,79 +296,36 @@ impl DynCohortVecs for UTXOCohortVecs { } fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> { - if let Some(state) = self.state.as_mut() { - state.write(height, cleanup)?; - } - Ok(()) + self.write_state_impl(height, cleanup) } 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(()) + self.reset_cost_basis_impl() } fn reset_single_iteration_values(&mut self) { - if let Some(state) = self.state.as_mut() { - state.reset_single_iteration_values(); - } + self.reset_iteration_impl(); } } -impl Filtered for UTXOCohortVecs { +// --- CoreCohortMetrics: uses CoreRealizedState --- + +impl Filtered for UTXOCohortVecs { fn filter(&self) -> &Filter { &self.metrics.filter } } -impl DynCohortVecs for UTXOCohortVecs { +impl DynCohortVecs for UTXOCohortVecs { fn min_stateful_height_len(&self) -> usize { self.metrics.min_stateful_height_len() } fn reset_state_starting_height(&mut self) { - self.state_starting_height = Some(Height::ZERO); - if let Some(state) = self.state.as_mut() { - state.reset(); - } + self.reset_state_impl(); } - fn import_state(&mut self, starting_height: Height) -> Result { - if let Some(state) = self.state.as_mut() { - if let Some(mut prev_height) = starting_height.decremented() { - prev_height = state.import_at_or_before(prev_height)?; - - state.supply.value = self - .metrics - .supply - .total - .sats - .height - .collect_one(prev_height) - .unwrap(); - state.supply.utxo_count = *self - .metrics - .outputs - .utxo_count - .height - .collect_one(prev_height) - .unwrap(); - - state.restore_realized_cap(); - - let result = prev_height.incremented(); - self.state_starting_height = Some(result); - Ok(result) - } else { - self.state_starting_height = Some(Height::ZERO); - Ok(Height::ZERO) - } - } else { - self.state_starting_height = Some(starting_height); - Ok(starting_height) - } - } + impl_import_state!(); fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { self.metrics.validate_computed_versions(base_version) @@ -395,79 +380,36 @@ impl DynCohortVecs for UTXOCohortVecs { } fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> { - if let Some(state) = self.state.as_mut() { - state.write(height, cleanup)?; - } - Ok(()) + self.write_state_impl(height, cleanup) } 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(()) + self.reset_cost_basis_impl() } fn reset_single_iteration_values(&mut self) { - if let Some(state) = self.state.as_mut() { - state.reset_single_iteration_values(); - } + self.reset_iteration_impl(); } } -impl Filtered for UTXOCohortVecs { +// --- CompleteCohortMetrics: uses full RealizedState --- + +impl Filtered for UTXOCohortVecs { fn filter(&self) -> &Filter { &self.metrics.filter } } -impl DynCohortVecs for UTXOCohortVecs { +impl DynCohortVecs for UTXOCohortVecs { fn min_stateful_height_len(&self) -> usize { self.metrics.min_stateful_height_len() } fn reset_state_starting_height(&mut self) { - self.state_starting_height = Some(Height::ZERO); - if let Some(state) = self.state.as_mut() { - state.reset(); - } + self.reset_state_impl(); } - fn import_state(&mut self, starting_height: Height) -> Result { - if let Some(state) = self.state.as_mut() { - if let Some(mut prev_height) = starting_height.decremented() { - prev_height = state.import_at_or_before(prev_height)?; - - state.supply.value = self - .metrics - .supply - .total - .sats - .height - .collect_one(prev_height) - .unwrap(); - state.supply.utxo_count = *self - .metrics - .outputs - .utxo_count - .height - .collect_one(prev_height) - .unwrap(); - - state.restore_realized_cap(); - - let result = prev_height.incremented(); - self.state_starting_height = Some(result); - Ok(result) - } else { - self.state_starting_height = Some(Height::ZERO); - Ok(Height::ZERO) - } - } else { - self.state_starting_height = Some(starting_height); - Ok(starting_height) - } - } + impl_import_state!(); fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { self.metrics.validate_computed_versions(base_version) @@ -530,22 +472,14 @@ impl DynCohortVecs for UTXOCohortVecs { } fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> { - if let Some(state) = self.state.as_mut() { - state.write(height, cleanup)?; - } - Ok(()) + self.write_state_impl(height, cleanup) } 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(()) + self.reset_cost_basis_impl() } fn reset_single_iteration_values(&mut self) { - if let Some(state) = self.state.as_mut() { - state.reset_single_iteration_values(); - } + self.reset_iteration_impl(); } } diff --git a/crates/brk_computer/src/distribution/metrics/activity/base.rs b/crates/brk_computer/src/distribution/metrics/activity/base.rs index 980e0ffa3..ad1d62f08 100644 --- a/crates/brk_computer/src/distribution/metrics/activity/base.rs +++ b/crates/brk_computer/src/distribution/metrics/activity/base.rs @@ -81,18 +81,8 @@ impl ActivityMetrics { self.core .compute_from_stateful(starting_indexes, &core_refs, exit)?; - macro_rules! sum_others { - ($($field:tt).+) => { - self.$($field).+.compute_sum_of_others( - starting_indexes.height, - &others.iter().map(|v| &v.$($field).+).collect::>(), - exit, - )? - }; - } - - sum_others!(coinblocks_destroyed.height); - sum_others!(coindays_destroyed.height); + sum_others!(self, starting_indexes, others, exit; coinblocks_destroyed.height); + sum_others!(self, starting_indexes, others, exit; coindays_destroyed.height); Ok(()) } diff --git a/crates/brk_computer/src/distribution/metrics/cohort/adjusted.rs b/crates/brk_computer/src/distribution/metrics/cohort/adjusted.rs deleted file mode 100644 index 1e7478bef..000000000 --- a/crates/brk_computer/src/distribution/metrics/cohort/adjusted.rs +++ /dev/null @@ -1,143 +0,0 @@ -use brk_cohort::Filter; -use brk_error::Result; -use brk_traversable::Traversable; -use brk_types::{Cents, Dollars, Height, Indexes, Sats, Version}; -use rayon::prelude::*; -use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; - -use crate::{blocks, prices}; - -use crate::distribution::metrics::{ - ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase, - RealizedWithAdjusted, RelativeWithRelToAll, SupplyMetrics, UnrealizedBase, -}; - -/// Cohort metrics with adjusted realized (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 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 AdjustedCohortMetrics { - pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { - let supply = SupplyMetrics::forced_import(cfg)?; - let unrealized = UnrealizedBase::forced_import(cfg)?; - let realized = RealizedWithAdjusted::forced_import(cfg)?; - - let relative = RelativeWithRelToAll::forced_import(cfg)?; - - 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), - }) - } - - #[allow(clippy::too_many_arguments)] - pub(crate) fn compute_rest_part2( - &mut self, - blocks: &blocks::Vecs, - prices: &prices::Vecs, - starting_indexes: &Indexes, - height_to_market_cap: &impl ReadableVec, - up_to_1h_value_created: &impl ReadableVec, - up_to_1h_value_destroyed: &impl ReadableVec, - all_supply_sats: &impl ReadableVec, - exit: &Exit, - ) -> Result<()> { - self.realized.compute_rest_part2( - blocks, - prices, - starting_indexes, - &self.supply.total.btc.height, - height_to_market_cap, - up_to_1h_value_created, - up_to_1h_value_destroyed, - exit, - )?; - - self.relative.compute( - starting_indexes.height, - &self.unrealized, - &self.realized.base, - &self.supply.total.sats.height, - height_to_market_cap, - all_supply_sats, - exit, - )?; - - Ok(()) - } -} diff --git a/crates/brk_computer/src/distribution/metrics/cohort/all.rs b/crates/brk_computer/src/distribution/metrics/cohort/all.rs index 8a3d256e1..e6383b241 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/all.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/all.rs @@ -5,16 +5,16 @@ use brk_types::{Cents, Dollars, Height, Indexes, Version}; use rayon::prelude::*; use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; -use crate::{blocks, distribution::state::CohortState, prices}; +use crate::{blocks, distribution::state::{CohortState, RealizedState}, prices}; use crate::distribution::metrics::{ ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig, - OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeForAll, SupplyMetrics, - UnrealizedBase, + OutputsMetrics, RealizedAdjusted, RealizedBase, RealizedWithExtended, RelativeForAll, + SupplyMetrics, UnrealizedBase, }; -/// All-cohort metrics: extended + adjusted realized, extended cost basis, -/// relative for-all (no rel_to_all). +/// All-cohort metrics: extended realized + adjusted (as composable add-on), +/// extended cost basis, relative for-all (no rel_to_all). /// Used by: the "all" cohort. #[derive(Traversable)] pub struct AllCohortMetrics { @@ -23,83 +23,14 @@ pub struct AllCohortMetrics { pub supply: Box>, pub outputs: Box>, pub activity: Box>, - pub realized: Box>, + pub realized: Box>, pub cost_basis: Box>, pub unrealized: Box>, + pub adjusted: 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, - is_day_boundary: bool, - ) -> Result<()> { - self.compute_and_push_unrealized_base(height, height_price, state)?; - self.cost_basis - .extended - .truncate_push_percentiles(height, state, is_day_boundary)?; - 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.collect_vecs_mut()); - vecs - } -} +impl_cohort_metrics_base!(AllCohortMetrics, extended_cost_basis); impl AllCohortMetrics { /// Import the "all" cohort metrics with a pre-imported supply. @@ -111,7 +42,8 @@ impl AllCohortMetrics { supply: SupplyMetrics, ) -> Result { let unrealized = UnrealizedBase::forced_import(cfg)?; - let realized = RealizedWithExtendedAdjusted::forced_import(cfg)?; + let realized = RealizedWithExtended::forced_import(cfg)?; + let adjusted = RealizedAdjusted::forced_import(cfg)?; let relative = RelativeForAll::forced_import(cfg)?; @@ -123,6 +55,7 @@ impl AllCohortMetrics { realized: Box::new(realized), cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?), unrealized: Box::new(unrealized), + adjusted: Box::new(adjusted), relative: Box::new(relative), }) } @@ -144,6 +77,14 @@ impl AllCohortMetrics { starting_indexes, &self.supply.total.btc.height, height_to_market_cap, + exit, + )?; + + self.adjusted.compute_rest_part2( + blocks, + starting_indexes, + &self.realized.value_created.height, + &self.realized.value_destroyed.height, up_to_1h_value_created, up_to_1h_value_destroyed, exit, diff --git a/crates/brk_computer/src/distribution/metrics/cohort/basic.rs b/crates/brk_computer/src/distribution/metrics/cohort/basic.rs index e573104b7..7a3806e5c 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/basic.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/basic.rs @@ -27,62 +27,7 @@ pub struct BasicCohortMetrics { 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 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_cohort_metrics_base!(BasicCohortMetrics, base_cost_basis); impl BasicCohortMetrics { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { @@ -104,10 +49,6 @@ impl BasicCohortMetrics { }) } - 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, @@ -139,28 +80,4 @@ impl BasicCohortMetrics { Ok(()) } - pub(crate) fn compute_from_stateful( - &mut self, - starting_indexes: &Indexes, - others: &[&Self], - exit: &Exit, - ) -> Result<()> { - macro_rules! aggregate { - ($field:ident) => { - self.$field.compute_from_stateful( - starting_indexes, - &others.iter().map(|v| &*v.$field).collect::>(), - exit, - )? - }; - } - - aggregate!(supply); - aggregate!(outputs); - aggregate!(activity); - aggregate!(realized); - aggregate!(unrealized); - aggregate!(cost_basis); - Ok(()) - } } diff --git a/crates/brk_computer/src/distribution/metrics/cohort/complete.rs b/crates/brk_computer/src/distribution/metrics/cohort/complete.rs index 2ba7462c3..c82173536 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/complete.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/complete.rs @@ -78,10 +78,10 @@ impl CompleteCohortMetrics { } /// Aggregate Complete-tier metrics from Source cohort refs. - pub(crate) fn compute_from_sources( + pub(crate) fn compute_from_sources( &mut self, starting_indexes: &Indexes, - others: &[&dyn CohortMetricsBase], + others: &[&T], exit: &Exit, ) -> Result<()> { // Supply, outputs, activity: use their existing compute_from_stateful diff --git a/crates/brk_computer/src/distribution/metrics/cohort/extended.rs b/crates/brk_computer/src/distribution/metrics/cohort/extended.rs index 0416a9a3f..c8eaad46f 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/extended.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/extended.rs @@ -5,7 +5,7 @@ use brk_types::{Cents, Dollars, Height, Indexes, Sats, Version}; use rayon::prelude::*; use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; -use crate::{blocks, distribution::state::CohortState, prices}; +use crate::{blocks, distribution::state::{CohortState, RealizedState}, prices}; use crate::distribution::metrics::{ ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig, @@ -28,78 +28,7 @@ pub struct ExtendedCohortMetrics { 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, - is_day_boundary: bool, - ) -> Result<()> { - self.compute_and_push_unrealized_base(height, height_price, state)?; - self.cost_basis - .extended - .truncate_push_percentiles(height, state, is_day_boundary)?; - 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.collect_vecs_mut()); - vecs - } -} +impl_cohort_metrics_base!(ExtendedCohortMetrics, extended_cost_basis); impl ExtendedCohortMetrics { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { diff --git a/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs b/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs index 31139aa6d..60fd89c13 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/extended_adjusted.rs @@ -2,121 +2,40 @@ use brk_cohort::Filter; use brk_error::Result; use brk_traversable::Traversable; use brk_types::{Cents, Dollars, Height, Indexes, Sats, Version}; -use rayon::prelude::*; +use derive_more::{Deref, DerefMut}; use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode}; -use crate::{blocks, distribution::state::CohortState, prices}; +use crate::{blocks, distribution::state::{CohortState, RealizedState}, prices}; use crate::distribution::metrics::{ - ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig, - OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeWithExtended, - SupplyMetrics, UnrealizedBase, + ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedAdjusted, + RealizedBase, SupplyMetrics, UnrealizedBase, }; +use super::ExtendedCohortMetrics; + /// Cohort metrics with extended + adjusted realized, extended cost basis. +/// Wraps `ExtendedCohortMetrics` and adds adjusted SOPR as a composable add-on. /// Used by: sth cohort. -#[derive(Traversable)] +#[derive(Deref, DerefMut, 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>, + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub inner: ExtendedCohortMetrics, + #[traversable(flatten)] + pub adjusted: 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, - is_day_boundary: bool, - ) -> Result<()> { - self.compute_and_push_unrealized_base(height, height_price, state)?; - self.cost_basis - .extended - .truncate_push_percentiles(height, state, is_day_boundary)?; - 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.collect_vecs_mut()); - vecs - } -} +impl_cohort_metrics_base!(ExtendedAdjustedCohortMetrics, deref_extended_cost_basis); impl ExtendedAdjustedCohortMetrics { pub(crate) fn forced_import(cfg: &ImportConfig) -> Result { - let supply = SupplyMetrics::forced_import(cfg)?; - let unrealized = UnrealizedBase::forced_import(cfg)?; - let realized = RealizedWithExtendedAdjusted::forced_import(cfg)?; - - let relative = RelativeWithExtended::forced_import(cfg)?; - + let inner = ExtendedCohortMetrics::forced_import(cfg)?; + let adjusted = RealizedAdjusted::forced_import(cfg)?; 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), + inner, + adjusted: Box::new(adjusted), }) } @@ -132,25 +51,22 @@ impl ExtendedAdjustedCohortMetrics { all_supply_sats: &impl ReadableVec, exit: &Exit, ) -> Result<()> { - self.realized.compute_rest_part2( + self.inner.compute_rest_part2( blocks, prices, starting_indexes, - &self.supply.total.btc.height, height_to_market_cap, - up_to_1h_value_created, - up_to_1h_value_destroyed, + all_supply_sats, exit, )?; - self.relative.compute( - starting_indexes.height, - &self.unrealized, - &self.realized.base, - &self.supply.total.sats.height, - height_to_market_cap, - all_supply_sats, - &self.supply.total.usd.height, + self.adjusted.compute_rest_part2( + blocks, + starting_indexes, + &self.inner.realized.value_created.height, + &self.inner.realized.value_destroyed.height, + up_to_1h_value_created, + up_to_1h_value_destroyed, exit, )?; diff --git a/crates/brk_computer/src/distribution/metrics/cohort/mod.rs b/crates/brk_computer/src/distribution/metrics/cohort/mod.rs index 0cda51957..44a3751a5 100644 --- a/crates/brk_computer/src/distribution/metrics/cohort/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/cohort/mod.rs @@ -1,4 +1,3 @@ -mod adjusted; mod all; mod basic; mod complete; @@ -7,7 +6,6 @@ mod extended; mod extended_adjusted; mod minimal; -pub use adjusted::*; pub use all::*; pub use basic::*; pub use complete::*; diff --git a/crates/brk_computer/src/distribution/metrics/cost_basis/base.rs b/crates/brk_computer/src/distribution/metrics/cost_basis/base.rs index ee290f3df..42a9bcd63 100644 --- a/crates/brk_computer/src/distribution/metrics/cost_basis/base.rs +++ b/crates/brk_computer/src/distribution/metrics/cost_basis/base.rs @@ -4,7 +4,7 @@ use brk_types::{Cents, Height, Indexes, Version}; use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec}; use crate::{ - distribution::state::CohortState, + distribution::state::{CohortState, RealizedState}, internal::{ComputedFromHeight, Price}, }; @@ -35,7 +35,7 @@ impl CostBasisBase { pub(crate) fn truncate_push_minmax( &mut self, height: Height, - state: &CohortState, + state: &CohortState, ) -> Result<()> { self.min.cents.height.truncate_push( height, diff --git a/crates/brk_computer/src/distribution/metrics/cost_basis/extended.rs b/crates/brk_computer/src/distribution/metrics/cost_basis/extended.rs index 7691e4c07..15b148774 100644 --- a/crates/brk_computer/src/distribution/metrics/cost_basis/extended.rs +++ b/crates/brk_computer/src/distribution/metrics/cost_basis/extended.rs @@ -4,7 +4,7 @@ use brk_types::{Cents, Height, Version}; use vecdb::{AnyStoredVec, Rw, StorageMode}; use crate::{ - distribution::state::CohortState, + distribution::state::{CohortState, RealizedState}, internal::{PERCENTILES_LEN, PercentilesVecs}, }; @@ -41,7 +41,7 @@ impl CostBasisExtended { pub(crate) fn truncate_push_percentiles( &mut self, height: Height, - state: &mut CohortState, + state: &mut CohortState, is_day_boundary: bool, ) -> Result<()> { let computed = if is_day_boundary { diff --git a/crates/brk_computer/src/distribution/metrics/mod.rs b/crates/brk_computer/src/distribution/metrics/mod.rs index 20bdc44e0..36c881297 100644 --- a/crates/brk_computer/src/distribution/metrics/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/mod.rs @@ -1,4 +1,146 @@ +/// Aggregate a field by summing the same field across `others`. +macro_rules! sum_others { + ($self_:ident, $si:ident, $others:ident, $exit:ident; $($field:tt).+) => { + $self_.$($field).+.compute_sum_of_others( + $si.height, + &$others.iter().map(|v| &v.$($field).+).collect::>(), + $exit, + )? + }; +} + mod activity; + +/// DRY macro for `CohortMetricsBase` impl on cohort metric types. +/// +/// All types share the same 13 accessor methods and common `collect_all_vecs_mut` shape. +/// Two variants handle the cost basis difference: +/// +/// - `base_cost_basis`: `CostBasisBase` only (no percentiles, no cost_basis version check) +/// - `extended_cost_basis`: `CostBasisWithExtended` (percentiles + cost_basis version check) +/// - `deref_extended_cost_basis`: Deref wrapper delegating to `self.inner` (avoids DerefMut borrow conflicts) +macro_rules! impl_cohort_metrics_base { + ($type:ident, base_cost_basis) => { + impl CohortMetricsBase for $type { + impl_cohort_metrics_base!(@accessors); + + 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 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 + } + } + }; + + ($type:ident, extended_cost_basis) => { + impl CohortMetricsBase for $type { + impl_cohort_metrics_base!(@accessors); + + 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, + is_day_boundary: bool, + ) -> Result<()> { + self.compute_and_push_unrealized_base(height, height_price, state)?; + self.cost_basis + .extended + .truncate_push_percentiles(height, state, is_day_boundary)?; + 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.collect_vecs_mut()); + vecs + } + } + }; + + ($type:ident, deref_extended_cost_basis) => { + impl CohortMetricsBase for $type { + impl_cohort_metrics_base!(@deref_accessors); + + fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> { + self.inner.validate_computed_versions(base_version) + } + + fn compute_then_truncate_push_unrealized_states( + &mut self, + height: Height, + height_price: Cents, + state: &mut CohortState, + is_day_boundary: bool, + ) -> Result<()> { + self.inner.compute_then_truncate_push_unrealized_states( + height, height_price, state, is_day_boundary, + ) + } + + fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> { + self.inner.collect_all_vecs_mut() + } + } + }; + + (@accessors) => { + 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 } + }; + + (@deref_accessors) => { + fn filter(&self) -> &Filter { self.inner.filter() } + fn supply(&self) -> &SupplyMetrics { self.inner.supply() } + fn supply_mut(&mut self) -> &mut SupplyMetrics { self.inner.supply_mut() } + fn outputs(&self) -> &OutputsMetrics { self.inner.outputs() } + fn outputs_mut(&mut self) -> &mut OutputsMetrics { self.inner.outputs_mut() } + fn activity(&self) -> &ActivityMetrics { self.inner.activity() } + fn activity_mut(&mut self) -> &mut ActivityMetrics { self.inner.activity_mut() } + fn realized_base(&self) -> &RealizedBase { self.inner.realized_base() } + fn realized_base_mut(&mut self) -> &mut RealizedBase { self.inner.realized_base_mut() } + fn unrealized_base(&self) -> &UnrealizedBase { self.inner.unrealized_base() } + fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { self.inner.unrealized_base_mut() } + fn cost_basis_base(&self) -> &CostBasisBase { self.inner.cost_basis_base() } + fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { self.inner.cost_basis_base_mut() } + }; +} + mod cohort; mod config; mod cost_basis; @@ -23,7 +165,7 @@ use brk_error::Result; use brk_types::{Cents, Height, Indexes, Version}; use vecdb::{AnyStoredVec, Exit}; -use crate::{blocks, distribution::state::CohortState, prices}; +use crate::{blocks, distribution::state::{CohortState, RealizedState}, prices}; pub trait CohortMetricsBase: Send + Sync { fn filter(&self) -> &Filter; @@ -47,7 +189,7 @@ pub trait CohortMetricsBase: Send + Sync { &mut self, height: Height, height_price: Cents, - state: &mut CohortState, + state: &mut CohortState, ) -> Result<()> { state.apply_pending(); self.cost_basis_base_mut() @@ -63,7 +205,7 @@ pub trait CohortMetricsBase: Send + Sync { &mut self, height: Height, height_price: Cents, - state: &mut CohortState, + state: &mut CohortState, _is_day_boundary: bool, ) -> Result<()> { self.compute_and_push_unrealized_base(height, height_price, state) @@ -81,7 +223,7 @@ pub trait CohortMetricsBase: Send + Sync { .min(self.cost_basis_base().min_stateful_height_len()) } - fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { + fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> { self.supply_mut() .truncate_push(height, state.supply.value)?; self.outputs_mut() @@ -97,39 +239,11 @@ pub trait CohortMetricsBase: Send + Sync { Ok(()) } - /// Compute net_sentiment.height as capital-weighted average of component cohorts (same type). - fn compute_net_sentiment_from_others( + /// Compute net_sentiment.height as capital-weighted average of component cohorts. + fn compute_net_sentiment_from_others( &mut self, starting_indexes: &Indexes, - others: &[&Self], - exit: &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.cents.height) - .collect(); - - self.unrealized_base_mut() - .net_sentiment - .cents - .height - .compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?; - - Ok(()) - } - - /// Compute net_sentiment.height as capital-weighted average from heterogeneous sources. - fn compute_net_sentiment_from_others_dyn( - &mut self, - starting_indexes: &Indexes, - others: &[&dyn CohortMetricsBase], + others: &[&T], exit: &Exit, ) -> Result<()> { let weights: Vec<_> = others @@ -196,17 +310,13 @@ pub trait CohortMetricsBase: Send + Sync { 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( + /// Compute aggregate base metrics from source cohorts. + fn compute_base_from_others( &mut self, starting_indexes: &Indexes, - others: &[&dyn CohortMetricsBase], + others: &[&T], exit: &Exit, - ) -> Result<()> - where - Self: Sized, - { + ) -> Result<()> { macro_rules! aggregate { ($self_mut:ident, $accessor:ident) => { self.$self_mut().compute_from_stateful( diff --git a/crates/brk_computer/src/distribution/metrics/realized/adjusted.rs b/crates/brk_computer/src/distribution/metrics/realized/adjusted.rs index 82ee76834..611a91c95 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/adjusted.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/adjusted.rs @@ -38,7 +38,7 @@ impl RealizedAdjusted { } #[allow(clippy::too_many_arguments)] - pub(crate) fn compute_rest_part2_adj( + pub(crate) fn compute_rest_part2( &mut self, blocks: &blocks::Vecs, starting_indexes: &Indexes, diff --git a/crates/brk_computer/src/distribution/metrics/realized/complete.rs b/crates/brk_computer/src/distribution/metrics/realized/complete.rs index dc869c83d..eb10d92f7 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/complete.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/complete.rs @@ -179,22 +179,12 @@ impl RealizedComplete { self.core .compute_from_stateful(starting_indexes, &core_refs, exit)?; - macro_rules! sum_others { - ($($field:tt).+) => { - self.$($field).+.compute_sum_of_others( - starting_indexes.height, - &others.iter().map(|v| &v.$($field).+).collect::>(), - exit, - )? - }; - } - - sum_others!(profit_value_created.height); - sum_others!(profit_value_destroyed.height); - sum_others!(loss_value_created.height); - sum_others!(loss_value_destroyed.height); - sum_others!(sent_in_profit.base.sats.height); - sum_others!(sent_in_loss.base.sats.height); + sum_others!(self, starting_indexes, others, exit; profit_value_created.height); + sum_others!(self, starting_indexes, others, exit; profit_value_destroyed.height); + sum_others!(self, starting_indexes, others, exit; loss_value_created.height); + sum_others!(self, starting_indexes, others, exit; loss_value_destroyed.height); + sum_others!(self, starting_indexes, others, exit; sent_in_profit.base.sats.height); + sum_others!(self, starting_indexes, others, exit; sent_in_loss.base.sats.height); Ok(()) } diff --git a/crates/brk_computer/src/distribution/metrics/realized/core.rs b/crates/brk_computer/src/distribution/metrics/realized/core.rs index ce29199cc..a9be503ba 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/core.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/core.rs @@ -10,7 +10,7 @@ use vecdb::{ use crate::{ blocks, - distribution::state::RealizedState, + distribution::state::RealizedOps, internal::{ CentsUnsignedToDollars, ComputedFromHeight, ComputedFromHeightCumulative, ComputedFromHeightRatio, FiatFromHeight, Identity, LazyFromHeight, @@ -122,7 +122,7 @@ impl CoreRealized { .min(self.realized_loss.height.len()) } - pub(crate) fn truncate_push(&mut self, height: Height, state: &RealizedState) -> Result<()> { + pub(crate) fn truncate_push(&mut self, height: Height, state: &impl RealizedOps) -> Result<()> { self.realized_cap_cents .height .truncate_push(height, state.cap())?; @@ -149,19 +149,9 @@ impl CoreRealized { others: &[&Self], exit: &Exit, ) -> Result<()> { - macro_rules! sum_others { - ($($field:tt).+) => { - self.$($field).+.compute_sum_of_others( - starting_indexes.height, - &others.iter().map(|v| &v.$($field).+).collect::>(), - exit, - )? - }; - } - - sum_others!(realized_cap_cents.height); - sum_others!(realized_profit.height); - sum_others!(realized_loss.height); + sum_others!(self, starting_indexes, others, exit; realized_cap_cents.height); + sum_others!(self, starting_indexes, others, exit; realized_profit.height); + sum_others!(self, starting_indexes, others, exit; realized_loss.height); Ok(()) } diff --git a/crates/brk_computer/src/distribution/metrics/realized/extended.rs b/crates/brk_computer/src/distribution/metrics/realized/extended.rs index d86cf6f70..5f3fab9c9 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/extended.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/extended.rs @@ -6,9 +6,10 @@ use vecdb::{Exit, ReadableVec, Rw, StorageMode}; use crate::{ blocks, internal::{ - ComputedFromHeightRatioPercentiles, ComputedFromHeightRatioStdDevBands, - PercentFromHeight, RatioCents64, RatioDollarsBp32, RollingWindows, + ComputedFromHeightRatioFull, PercentFromHeight, RatioCents64, RatioDollarsBp32, + RollingWindows, }, + prices, }; use crate::distribution::metrics::ImportConfig; @@ -24,10 +25,8 @@ pub struct RealizedExtended { pub realized_profit_to_loss_ratio: RollingWindows, - pub realized_price_ratio_percentiles: ComputedFromHeightRatioPercentiles, - pub realized_price_ratio_std_dev: ComputedFromHeightRatioStdDevBands, - pub investor_price_ratio_percentiles: ComputedFromHeightRatioPercentiles, - pub investor_price_ratio_std_dev: ComputedFromHeightRatioStdDevBands, + pub realized_price_ratio: ComputedFromHeightRatioFull, + pub investor_price_ratio: ComputedFromHeightRatioFull, } impl RealizedExtended { @@ -39,27 +38,13 @@ impl RealizedExtended { realized_loss_sum: cfg.import_rolling("realized_loss", Version::ONE)?, realized_profit_to_loss_ratio: cfg .import_rolling("realized_profit_to_loss_ratio", Version::ONE)?, - realized_price_ratio_percentiles: - ComputedFromHeightRatioPercentiles::forced_import( - cfg.db, - &cfg.name("realized_price"), - cfg.version + Version::ONE, - cfg.indexes, - )?, - realized_price_ratio_std_dev: ComputedFromHeightRatioStdDevBands::forced_import( + realized_price_ratio: ComputedFromHeightRatioFull::forced_import( cfg.db, &cfg.name("realized_price"), cfg.version + Version::ONE, cfg.indexes, )?, - investor_price_ratio_percentiles: - ComputedFromHeightRatioPercentiles::forced_import( - cfg.db, - &cfg.name("investor_price"), - cfg.version, - cfg.indexes, - )?, - investor_price_ratio_std_dev: ComputedFromHeightRatioStdDevBands::forced_import( + investor_price_ratio: ComputedFromHeightRatioFull::forced_import( cfg.db, &cfg.name("investor_price"), cfg.version, @@ -69,10 +54,11 @@ impl RealizedExtended { } #[allow(clippy::too_many_arguments)] - pub(crate) fn compute_rest_part2_ext( + pub(crate) fn compute_rest_part2( &mut self, base: &RealizedBase, blocks: &blocks::Vecs, + prices: &prices::Vecs, starting_indexes: &Indexes, height_to_market_cap: &impl ReadableVec, exit: &Exit, @@ -117,35 +103,21 @@ impl RealizedExtended { )?; } - // Realized price ratio: percentiles + stddev - self.realized_price_ratio_percentiles.compute( + // Realized price: ratio + percentiles + stddev bands + self.realized_price_ratio.compute_rest( blocks, + prices, starting_indexes, exit, - &base.realized_price_ratio.ratio.height, - &base.realized_price.cents.height, - )?; - self.realized_price_ratio_std_dev.compute( - blocks, - starting_indexes, - exit, - &base.realized_price_ratio.ratio.height, &base.realized_price.cents.height, )?; - // Investor price ratio: percentiles + stddev - self.investor_price_ratio_percentiles.compute( + // Investor price: ratio + percentiles + stddev bands + self.investor_price_ratio.compute_rest( blocks, + prices, starting_indexes, exit, - &base.investor_price_ratio.ratio.height, - &base.investor_price.cents.height, - )?; - self.investor_price_ratio_std_dev.compute( - blocks, - starting_indexes, - exit, - &base.investor_price_ratio.ratio.height, &base.investor_price.cents.height, )?; diff --git a/crates/brk_computer/src/distribution/metrics/realized/mod.rs b/crates/brk_computer/src/distribution/metrics/realized/mod.rs index 475699475..2c32e35ab 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/mod.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/mod.rs @@ -4,9 +4,7 @@ mod complete; mod core; mod extended; -mod with_adjusted; mod with_extended; -mod with_extended_adjusted; pub use adjusted::*; pub use base::*; @@ -14,6 +12,4 @@ pub use complete::*; pub use core::*; 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 deleted file mode 100644 index 80ec6fb3b..000000000 --- a/crates/brk_computer/src/distribution/metrics/realized/with_adjusted.rs +++ /dev/null @@ -1,64 +0,0 @@ -use brk_error::Result; -use brk_traversable::Traversable; -use brk_types::{Bitcoin, Cents, Dollars, Height, Indexes}; -use derive_more::{Deref, DerefMut}; -use vecdb::{Exit, ReadableVec, Rw, StorageMode}; - -use crate::{blocks, prices}; - -use crate::distribution::metrics::ImportConfig; - -use super::{RealizedAdjusted, RealizedBase}; - -/// Realized metrics with guaranteed adjusted (no Option). -#[derive(Deref, DerefMut, Traversable)] -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) -> Result { - let base = RealizedBase::forced_import(cfg)?; - let adjusted = RealizedAdjusted::forced_import(cfg)?; - 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: &Indexes, - height_to_supply: &impl ReadableVec, - height_to_market_cap: &impl ReadableVec, - up_to_1h_value_created: &impl ReadableVec, - up_to_1h_value_destroyed: &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, - &self.base.value_created.height, - &self.base.value_destroyed.height, - up_to_1h_value_created, - up_to_1h_value_destroyed, - 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 index 24aee0bcc..3a0c63574 100644 --- a/crates/brk_computer/src/distribution/metrics/realized/with_extended.rs +++ b/crates/brk_computer/src/distribution/metrics/realized/with_extended.rs @@ -47,9 +47,10 @@ impl RealizedWithExtended { exit, )?; - self.extended.compute_rest_part2_ext( + self.extended.compute_rest_part2( &self.base, blocks, + prices, starting_indexes, height_to_market_cap, exit, 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 deleted file mode 100644 index 45a0a9ee8..000000000 --- a/crates/brk_computer/src/distribution/metrics/realized/with_extended_adjusted.rs +++ /dev/null @@ -1,79 +0,0 @@ -use brk_error::Result; -use brk_traversable::Traversable; -use brk_types::{Bitcoin, Cents, Dollars, Height, Indexes}; -use derive_more::{Deref, DerefMut}; -use vecdb::{Exit, ReadableVec, Rw, StorageMode}; - -use crate::{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)] -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) -> Result { - let base = RealizedBase::forced_import(cfg)?; - let extended = RealizedExtended::forced_import(cfg)?; - let adjusted = RealizedAdjusted::forced_import(cfg)?; - 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: &Indexes, - height_to_supply: &impl ReadableVec, - height_to_market_cap: &impl ReadableVec, - up_to_1h_value_created: &impl ReadableVec, - up_to_1h_value_destroyed: &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, - &self.base.value_created.height, - &self.base.value_destroyed.height, - up_to_1h_value_created, - up_to_1h_value_destroyed, - exit, - )?; - - Ok(()) - } -} diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/base.rs b/crates/brk_computer/src/distribution/metrics/unrealized/base.rs index 1e59df5dd..53def2376 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/base.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/base.rs @@ -150,18 +150,8 @@ impl UnrealizedBase { .compute_from_stateful(starting_indexes, &complete_refs, exit)?; // Source-only: invested_capital - macro_rules! sum_others { - ($($field:tt).+) => { - self.$($field).+.compute_sum_of_others( - starting_indexes.height, - &others.iter().map(|v| &v.$($field).+).collect::>(), - exit, - )? - }; - } - - sum_others!(invested_capital_in_profit.cents.height); - sum_others!(invested_capital_in_loss.cents.height); + sum_others!(self, starting_indexes, others, exit; invested_capital_in_profit.cents.height); + sum_others!(self, starting_indexes, others, exit; invested_capital_in_loss.cents.height); // Source-only: raw BytesVec aggregation let start = self diff --git a/crates/brk_computer/src/distribution/metrics/unrealized/complete.rs b/crates/brk_computer/src/distribution/metrics/unrealized/complete.rs index c9b27fbf2..e357a4901 100644 --- a/crates/brk_computer/src/distribution/metrics/unrealized/complete.rs +++ b/crates/brk_computer/src/distribution/metrics/unrealized/complete.rs @@ -112,20 +112,10 @@ impl UnrealizedComplete { others: &[&Self], exit: &Exit, ) -> Result<()> { - macro_rules! sum_others { - ($($field:tt).+) => { - self.$($field).+.compute_sum_of_others( - starting_indexes.height, - &others.iter().map(|v| &v.$($field).+).collect::>(), - exit, - )? - }; - } - - sum_others!(supply_in_profit.sats.height); - sum_others!(supply_in_loss.sats.height); - sum_others!(unrealized_profit.cents.height); - sum_others!(unrealized_loss.cents.height); + sum_others!(self, starting_indexes, others, exit; supply_in_profit.sats.height); + sum_others!(self, starting_indexes, others, exit; supply_in_loss.sats.height); + sum_others!(self, starting_indexes, others, exit; unrealized_profit.cents.height); + sum_others!(self, starting_indexes, others, exit; unrealized_loss.cents.height); Ok(()) } diff --git a/crates/brk_computer/src/distribution/state/cohort/address.rs b/crates/brk_computer/src/distribution/state/cohort/address.rs index 1c56fbdd6..2b35634f0 100644 --- a/crates/brk_computer/src/distribution/state/cohort/address.rs +++ b/crates/brk_computer/src/distribution/state/cohort/address.rs @@ -4,17 +4,18 @@ use brk_error::Result; use brk_types::{Age, Cents, FundedAddressData, Sats, SupplyState}; use vecdb::unlikely; -use super::{super::cost_basis::RealizedState, base::CohortState}; +use super::super::cost_basis::RealizedOps; +use super::base::CohortState; /// Significant digits for address cost basis prices (after rounding to dollars). const COST_BASIS_PRICE_DIGITS: i32 = 4; -pub struct AddressCohortState { +pub struct AddressCohortState { pub addr_count: u64, - pub inner: CohortState, + pub inner: CohortState, } -impl AddressCohortState { +impl AddressCohortState { pub(crate) fn new(path: &Path, name: &str) -> Self { Self { addr_count: 0, @@ -29,7 +30,7 @@ impl AddressCohortState { self.inner.sent = Sats::ZERO; self.inner.satblocks_destroyed = Sats::ZERO; self.inner.satdays_destroyed = Sats::ZERO; - self.inner.realized = RealizedState::default(); + self.inner.realized = R::default(); } pub(crate) fn send( diff --git a/crates/brk_computer/src/distribution/state/cohort/base.rs b/crates/brk_computer/src/distribution/state/cohort/base.rs index 87a5ba964..8fc6d0e7c 100644 --- a/crates/brk_computer/src/distribution/state/cohort/base.rs +++ b/crates/brk_computer/src/distribution/state/cohort/base.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, path::Path}; use brk_error::Result; use brk_types::{Age, Cents, CentsCompact, CentsSats, CentsSquaredSats, CostBasisSnapshot, Height, Sats, SupplyState}; -use super::super::cost_basis::{CostBasisData, Percentiles, RealizedState, UnrealizedState}; +use super::super::cost_basis::{CostBasisData, Percentiles, RealizedOps, UnrealizedState}; pub struct SendPrecomputed { pub sats: Sats, @@ -15,20 +15,54 @@ pub struct SendPrecomputed { pub prev_investor_cap: CentsSquaredSats, } -pub struct CohortState { +impl SendPrecomputed { + /// Pre-compute values for send_utxo when the same supply/prices are shared + /// across multiple cohorts (age_range, epoch, class). + pub(crate) fn new( + supply: &SupplyState, + current_price: Cents, + prev_price: Cents, + ath: Cents, + age: Age, + ) -> Option { + if supply.utxo_count == 0 || supply.value == Sats::ZERO { + return None; + } + let sats = supply.value; + let current_ps = CentsSats::from_price_sats(current_price, sats); + let prev_ps = CentsSats::from_price_sats(prev_price, sats); + let ath_ps = if ath == current_price { + current_ps + } else { + CentsSats::from_price_sats(ath, sats) + }; + let prev_investor_cap = prev_ps.to_investor_cap(prev_price); + Some(Self { + sats, + prev_price, + age, + current_ps, + prev_ps, + ath_ps, + prev_investor_cap, + }) + } +} + +pub struct CohortState { pub supply: SupplyState, - pub realized: RealizedState, + pub realized: R, pub sent: Sats, pub satblocks_destroyed: Sats, pub satdays_destroyed: Sats, cost_basis_data: CostBasisData, } -impl CohortState { +impl CohortState { pub(crate) fn new(path: &Path, name: &str) -> Self { Self { supply: SupplyState::default(), - realized: RealizedState::default(), + realized: R::default(), sent: Sats::ZERO, satblocks_destroyed: Sats::ZERO, satdays_destroyed: Sats::ZERO, @@ -47,7 +81,6 @@ impl CohortState { } /// Restore realized cap from cost_basis_data after import. - /// Uses the exact persisted values instead of recomputing from the map. pub(crate) fn restore_realized_cap(&mut self) { self.realized.set_cap_raw(self.cost_basis_data.cap_raw()); self.realized @@ -170,38 +203,6 @@ impl CohortState { } } - /// Pre-computed values for send_utxo when the same supply/prices are shared - /// across multiple cohorts (age_range, epoch, year). - pub(crate) fn precompute_send( - supply: &SupplyState, - current_price: Cents, - prev_price: Cents, - ath: Cents, - age: Age, - ) -> Option { - if supply.utxo_count == 0 || supply.value == Sats::ZERO { - return None; - } - let sats = supply.value; - let current_ps = CentsSats::from_price_sats(current_price, sats); - let prev_ps = CentsSats::from_price_sats(prev_price, sats); - let ath_ps = if ath == current_price { - current_ps - } else { - CentsSats::from_price_sats(ath, sats) - }; - let prev_investor_cap = prev_ps.to_investor_cap(prev_price); - Some(SendPrecomputed { - sats, - prev_price, - age, - current_ps, - prev_ps, - ath_ps, - prev_investor_cap, - }) - } - pub(crate) fn send_utxo_precomputed( &mut self, supply: &SupplyState, @@ -227,7 +228,7 @@ impl CohortState { ath: Cents, age: Age, ) { - if let Some(pre) = Self::precompute_send(supply, current_price, prev_price, ath, age) { + if let Some(pre) = SendPrecomputed::new(supply, current_price, prev_price, ath, age) { self.send_utxo_precomputed(supply, &pre); } else if supply.utxo_count > 0 { self.supply -= supply; diff --git a/crates/brk_computer/src/distribution/state/cohort/utxo.rs b/crates/brk_computer/src/distribution/state/cohort/utxo.rs index be104fa98..52f8a2b39 100644 --- a/crates/brk_computer/src/distribution/state/cohort/utxo.rs +++ b/crates/brk_computer/src/distribution/state/cohort/utxo.rs @@ -4,12 +4,13 @@ use brk_error::Result; use brk_types::{Sats, SupplyState}; use derive_more::{Deref, DerefMut}; -use super::{super::cost_basis::RealizedState, base::CohortState}; +use super::super::cost_basis::RealizedOps; +use super::base::CohortState; #[derive(Deref, DerefMut)] -pub struct UTXOCohortState(CohortState); +pub struct UTXOCohortState(pub(crate) CohortState); -impl UTXOCohortState { +impl UTXOCohortState { pub(crate) fn new(path: &Path, name: &str) -> Self { Self(CohortState::new(path, name)) } @@ -24,6 +25,6 @@ impl UTXOCohortState { self.0.sent = Sats::ZERO; self.0.satblocks_destroyed = Sats::ZERO; self.0.satdays_destroyed = Sats::ZERO; - self.0.realized = RealizedState::default(); + self.0.realized = R::default(); } } diff --git a/crates/brk_computer/src/distribution/state/cost_basis/realized.rs b/crates/brk_computer/src/distribution/state/cost_basis/realized.rs index 2aef95c9b..277fe7685 100644 --- a/crates/brk_computer/src/distribution/state/cost_basis/realized.rs +++ b/crates/brk_computer/src/distribution/state/cost_basis/realized.rs @@ -2,19 +2,131 @@ use std::cmp::Ordering; use brk_types::{Cents, CentsSats, CentsSquaredSats, Sats}; -/// Realized state using u128 for raw cent*sat values internally. -/// This avoids overflow and defers division to output time for efficiency. +/// Trait for realized state operations, implemented by both Core and Full variants. +/// Core skips extra fields (value_created/destroyed, peak_regret, sent_in_profit/loss, investor_cap). +pub trait RealizedOps: Default + Clone + Send + Sync + 'static { + fn cap(&self) -> Cents; + fn profit(&self) -> Cents; + fn loss(&self) -> Cents; + fn set_cap_raw(&mut self, cap_raw: CentsSats); + fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats); + fn reset_single_iteration_values(&mut self); + fn increment(&mut self, price: Cents, sats: Sats); + fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats); + fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats); + fn receive(&mut self, price: Cents, sats: Sats) { + self.increment(price, sats); + } + fn send( + &mut self, + sats: Sats, + current_ps: CentsSats, + prev_ps: CentsSats, + ath_ps: CentsSats, + prev_investor_cap: CentsSquaredSats, + ); +} + +/// Core realized state: only cap, profit, loss (48 bytes). +/// Used by CoreCohortMetrics and MinimalCohortMetrics cohorts +/// (epoch, class, amount_range, type_ — ~50 separate cohorts). +#[derive(Debug, Default, Clone)] +pub struct CoreRealizedState { + cap_raw: u128, + profit_raw: u128, + loss_raw: u128, +} + +impl RealizedOps for CoreRealizedState { + #[inline] + fn cap(&self) -> Cents { + if self.cap_raw == 0 { + return Cents::ZERO; + } + Cents::new((self.cap_raw / Sats::ONE_BTC_U128) as u64) + } + + #[inline] + fn profit(&self) -> Cents { + if self.profit_raw == 0 { + return Cents::ZERO; + } + Cents::new((self.profit_raw / Sats::ONE_BTC_U128) as u64) + } + + #[inline] + fn loss(&self) -> Cents { + if self.loss_raw == 0 { + return Cents::ZERO; + } + Cents::new((self.loss_raw / Sats::ONE_BTC_U128) as u64) + } + + #[inline] + fn set_cap_raw(&mut self, cap_raw: CentsSats) { + self.cap_raw = cap_raw.inner(); + } + + #[inline] + fn set_investor_cap_raw(&mut self, _investor_cap_raw: CentsSquaredSats) { + // no-op for Core + } + + #[inline] + fn reset_single_iteration_values(&mut self) { + self.profit_raw = 0; + self.loss_raw = 0; + } + + #[inline] + fn increment(&mut self, price: Cents, sats: Sats) { + if sats.is_zero() { + return; + } + let price_sats = CentsSats::from_price_sats(price, sats); + self.cap_raw += price_sats.as_u128(); + } + + #[inline] + fn increment_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) { + self.cap_raw += price_sats.as_u128(); + } + + #[inline] + fn decrement_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) { + self.cap_raw -= price_sats.as_u128(); + } + + #[inline] + fn send( + &mut self, + _sats: Sats, + current_ps: CentsSats, + prev_ps: CentsSats, + _ath_ps: CentsSats, + _prev_investor_cap: CentsSquaredSats, + ) { + match current_ps.cmp(&prev_ps) { + Ordering::Greater => { + self.profit_raw += (current_ps - prev_ps).as_u128(); + } + Ordering::Less => { + self.loss_raw += (prev_ps - current_ps).as_u128(); + } + Ordering::Equal => {} + } + self.cap_raw -= prev_ps.as_u128(); + } +} + +/// Full realized state (~160 bytes). +/// Used by BasicCohortMetrics and CompleteCohortMetrics cohorts +/// (age_range — 21 separate cohorts). #[derive(Debug, Default, Clone)] pub struct RealizedState { - /// Raw realized cap: Σ(price × sats) - cap_raw: u128, + core: CoreRealizedState, /// Raw investor cap: Σ(price² × sats) - /// investor_price = investor_cap_raw / cap_raw (gives cents directly) investor_cap_raw: CentsSquaredSats, - /// Raw realized profit (cents * sats) - profit_raw: u128, - /// Raw realized loss (cents * sats) - loss_raw: u128, /// sell_price × sats for profit cases profit_value_created_raw: u128, /// cost_basis × sats for profit cases @@ -31,43 +143,120 @@ pub struct RealizedState { sent_in_loss: Sats, } -impl RealizedState { - /// Get realized cap as CentsUnsigned (divides by ONE_BTC). +impl RealizedOps for RealizedState { #[inline] - pub(crate) fn cap(&self) -> Cents { - if self.cap_raw == 0 { - return Cents::ZERO; - } - Cents::new((self.cap_raw / Sats::ONE_BTC_U128) as u64) + fn cap(&self) -> Cents { + self.core.cap() } - /// Set cap_raw directly from persisted value. #[inline] - pub(crate) fn set_cap_raw(&mut self, cap_raw: CentsSats) { - self.cap_raw = cap_raw.inner(); + fn profit(&self) -> Cents { + self.core.profit() } - /// Set investor_cap_raw directly from persisted value. #[inline] - pub(crate) fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats) { + fn loss(&self) -> Cents { + self.core.loss() + } + + #[inline] + fn set_cap_raw(&mut self, cap_raw: CentsSats) { + self.core.set_cap_raw(cap_raw); + } + + #[inline] + fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats) { self.investor_cap_raw = investor_cap_raw; } + #[inline] + fn reset_single_iteration_values(&mut self) { + self.core.reset_single_iteration_values(); + self.profit_value_created_raw = 0; + self.profit_value_destroyed_raw = 0; + self.loss_value_created_raw = 0; + self.loss_value_destroyed_raw = 0; + self.peak_regret_raw = 0; + self.sent_in_profit = Sats::ZERO; + self.sent_in_loss = Sats::ZERO; + } + + #[inline] + fn increment(&mut self, price: Cents, sats: Sats) { + if sats.is_zero() { + return; + } + let price_sats = CentsSats::from_price_sats(price, sats); + self.core.cap_raw += price_sats.as_u128(); + self.investor_cap_raw += price_sats.to_investor_cap(price); + } + + #[inline] + fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) { + self.core.cap_raw += price_sats.as_u128(); + self.investor_cap_raw += investor_cap; + } + + #[inline] + fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) { + self.core.cap_raw -= price_sats.as_u128(); + self.investor_cap_raw -= investor_cap; + } + + #[inline] + fn send( + &mut self, + sats: Sats, + current_ps: CentsSats, + prev_ps: CentsSats, + ath_ps: CentsSats, + prev_investor_cap: CentsSquaredSats, + ) { + match current_ps.cmp(&prev_ps) { + Ordering::Greater => { + self.core.profit_raw += (current_ps - prev_ps).as_u128(); + self.profit_value_created_raw += current_ps.as_u128(); + self.profit_value_destroyed_raw += prev_ps.as_u128(); + self.sent_in_profit += sats; + } + Ordering::Less => { + self.core.loss_raw += (prev_ps - current_ps).as_u128(); + self.loss_value_created_raw += current_ps.as_u128(); + self.loss_value_destroyed_raw += prev_ps.as_u128(); + self.sent_in_loss += sats; + } + Ordering::Equal => { + // Break-even: count as profit side (arbitrary but consistent) + self.profit_value_created_raw += current_ps.as_u128(); + self.profit_value_destroyed_raw += prev_ps.as_u128(); + self.sent_in_profit += sats; + } + } + + // Track peak regret: (peak - sell_price) × sats + self.peak_regret_raw += (ath_ps - current_ps).as_u128(); + + // Inline decrement to avoid recomputation + self.core.cap_raw -= prev_ps.as_u128(); + self.investor_cap_raw -= prev_investor_cap; + } +} + +impl RealizedState { /// Get investor price as CentsUnsigned. /// investor_price = Σ(price² × sats) / Σ(price × sats) - /// This is the dollar-weighted average acquisition price. #[inline] pub(crate) fn investor_price(&self) -> Cents { - if self.cap_raw == 0 { + if self.core.cap_raw == 0 { return Cents::ZERO; } - Cents::new((self.investor_cap_raw / self.cap_raw) as u64) + Cents::new((self.investor_cap_raw / self.core.cap_raw) as u64) } /// Get raw realized cap for aggregation. #[inline] pub(crate) fn cap_raw(&self) -> CentsSats { - CentsSats::new(self.cap_raw) + CentsSats::new(self.core.cap_raw) } /// Get raw investor cap for aggregation. @@ -76,24 +265,6 @@ impl RealizedState { self.investor_cap_raw } - /// Get realized profit as CentsUnsigned. - #[inline] - pub(crate) fn profit(&self) -> Cents { - if self.profit_raw == 0 { - return Cents::ZERO; - } - Cents::new((self.profit_raw / Sats::ONE_BTC_U128) as u64) - } - - /// Get realized loss as CentsUnsigned. - #[inline] - pub(crate) fn loss(&self) -> Cents { - if self.loss_raw == 0 { - return Cents::ZERO; - } - Cents::new((self.loss_raw / Sats::ONE_BTC_U128) as u64) - } - /// Get profit value created as CentsUnsigned (sell_price × sats for profit cases). #[inline] pub(crate) fn profit_value_created(&self) -> Cents { @@ -104,7 +275,6 @@ impl RealizedState { } /// Get profit value destroyed as CentsUnsigned (cost_basis × sats for profit cases). - /// This is also known as profit_flow. #[inline] pub(crate) fn profit_value_destroyed(&self) -> Cents { if self.profit_value_destroyed_raw == 0 { @@ -123,7 +293,6 @@ impl RealizedState { } /// Get loss value destroyed as CentsUnsigned (cost_basis × sats for loss cases). - /// This is also known as capitulation_flow. #[inline] pub(crate) fn loss_value_destroyed(&self) -> Cents { if self.loss_value_destroyed_raw == 0 { @@ -133,8 +302,6 @@ impl RealizedState { } /// Get realized peak regret as CentsUnsigned. - /// This is Σ((peak - sell_price) × sats) - how much more could have been made - /// by selling at peak instead of when actually sold. #[inline] pub(crate) fn peak_regret(&self) -> Cents { if self.peak_regret_raw == 0 { @@ -154,93 +321,4 @@ impl RealizedState { pub(crate) fn sent_in_loss(&self) -> Sats { self.sent_in_loss } - - pub(crate) fn reset_single_iteration_values(&mut self) { - self.profit_raw = 0; - self.loss_raw = 0; - self.profit_value_created_raw = 0; - self.profit_value_destroyed_raw = 0; - self.loss_value_created_raw = 0; - self.loss_value_destroyed_raw = 0; - self.peak_regret_raw = 0; - self.sent_in_profit = Sats::ZERO; - self.sent_in_loss = Sats::ZERO; - } - - /// Increment using pre-computed values (for UTXO path) - #[inline] - pub(crate) fn increment(&mut self, price: Cents, sats: Sats) { - if sats.is_zero() { - return; - } - let price_sats = CentsSats::from_price_sats(price, sats); - self.cap_raw += price_sats.as_u128(); - self.investor_cap_raw += price_sats.to_investor_cap(price); - } - - /// Increment using pre-computed snapshot values (for address path) - #[inline] - pub(crate) fn increment_snapshot( - &mut self, - price_sats: CentsSats, - investor_cap: CentsSquaredSats, - ) { - self.cap_raw += price_sats.as_u128(); - self.investor_cap_raw += investor_cap; - } - - /// Decrement using pre-computed snapshot values (for address path) - #[inline] - pub(crate) fn decrement_snapshot( - &mut self, - price_sats: CentsSats, - investor_cap: CentsSquaredSats, - ) { - self.cap_raw -= price_sats.as_u128(); - self.investor_cap_raw -= investor_cap; - } - - #[inline] - pub(crate) fn receive(&mut self, price: Cents, sats: Sats) { - self.increment(price, sats); - } - - /// Send with pre-computed typed values. Inlines decrement to avoid recomputation. - #[inline] - pub(crate) fn send( - &mut self, - sats: Sats, - current_ps: CentsSats, - prev_ps: CentsSats, - ath_ps: CentsSats, - prev_investor_cap: CentsSquaredSats, - ) { - match current_ps.cmp(&prev_ps) { - Ordering::Greater => { - self.profit_raw += (current_ps - prev_ps).as_u128(); - self.profit_value_created_raw += current_ps.as_u128(); - self.profit_value_destroyed_raw += prev_ps.as_u128(); - self.sent_in_profit += sats; - } - Ordering::Less => { - self.loss_raw += (prev_ps - current_ps).as_u128(); - self.loss_value_created_raw += current_ps.as_u128(); - self.loss_value_destroyed_raw += prev_ps.as_u128(); - self.sent_in_loss += sats; - } - Ordering::Equal => { - // Break-even: count as profit side (arbitrary but consistent) - self.profit_value_created_raw += current_ps.as_u128(); - self.profit_value_destroyed_raw += prev_ps.as_u128(); - self.sent_in_profit += sats; - } - } - - // Track peak regret: (peak - sell_price) × sats - self.peak_regret_raw += (ath_ps - current_ps).as_u128(); - - // Inline decrement to avoid recomputation - self.cap_raw -= prev_ps.as_u128(); - self.investor_cap_raw -= prev_investor_cap; - } } diff --git a/crates/brk_computer/src/outputs/spent/compute.rs b/crates/brk_computer/src/outputs/spent/compute.rs index b5382a6b1..8a1c0cb5e 100644 --- a/crates/brk_computer/src/outputs/spent/compute.rs +++ b/crates/brk_computer/src/outputs/spent/compute.rs @@ -38,12 +38,13 @@ impl Vecs { // Find min_height via binary search (first_txoutindex is monotonically non-decreasing) let first_txoutindex_vec = &indexer.vecs.outputs.first_txoutindex; - let total_heights = target_height.to_usize() + 1; let min_height = if min_txoutindex == 0 { Height::ZERO + } else if min_txoutindex >= starting_indexes.txoutindex.to_usize() { + starting_indexes.height } else { let mut lo = 0usize; - let mut hi = total_heights; + let mut hi = starting_indexes.height.to_usize() + 1; while lo < hi { let mid = lo + (hi - lo) / 2; if first_txoutindex_vec.collect_one_at(mid).unwrap().to_usize() <= min_txoutindex { diff --git a/crates/brk_traversable_derive/src/lib.rs b/crates/brk_traversable_derive/src/lib.rs index b70f7e1ef..0466847e3 100644 --- a/crates/brk_traversable_derive/src/lib.rs +++ b/crates/brk_traversable_derive/src/lib.rs @@ -612,10 +612,7 @@ fn gen_read_only_clone(input: &DeriveInput) -> proc_macro2::TokenStream { return gen_read_only_clone_for_m(name, generics, data, mode_param); } - // Collect generic type params that have NO trait bounds. - // Container types (ByDcaClass, Price) have unbounded params. - // Leaf types (LazyPercentiles) have bounded params. - // Only generate ReadOnlyClone for container-like types (all params unbounded). + // Collect all generic type params. let type_params: Vec<&syn::TypeParam> = generics .params .iter() @@ -629,31 +626,43 @@ fn gen_read_only_clone(input: &DeriveInput) -> proc_macro2::TokenStream { return quote! {}; } - // If any type param has bounds (inline or in where clause), skip — - // this is a leaf/computation type, not a container. - if type_params.iter().any(|tp| !tp.bounds.is_empty()) { + // Determine which type params have bounds (inline or via where clause). + // Bounded params are "leaf" params — kept as-is in the ReadOnly target. + // Unbounded params are "container" params — mapped through ReadOnlyClone. + let where_bounded: Vec<&syn::Ident> = if let Some(where_clause) = &generics.where_clause { + where_clause + .predicates + .iter() + .filter_map(|pred| { + if let syn::WherePredicate::Type(pt) = pred + && let Type::Path(tp) = &pt.bounded_ty + && let Some(seg) = tp.path.segments.first() + { + type_params + .iter() + .find(|p| p.ident == seg.ident) + .map(|p| &p.ident) + } else { + None + } + }) + .collect() + } else { + Vec::new() + }; + + let container_params: Vec<&syn::Ident> = type_params + .iter() + .filter(|tp| tp.bounds.is_empty() && !where_bounded.contains(&&tp.ident)) + .map(|tp| &tp.ident) + .collect(); + + // If no container params, this is a pure leaf type — skip. + if container_params.is_empty() { return quote! {}; } - // Also check where clause for bounds on any type param. - if let Some(where_clause) = &generics.where_clause { - let param_names: Vec<&syn::Ident> = type_params.iter().map(|tp| &tp.ident).collect(); - let has_where_bounds = where_clause.predicates.iter().any(|pred| { - if let syn::WherePredicate::Type(pt) = pred - && let Type::Path(tp) = &pt.bounded_ty - && let Some(seg) = tp.path.segments.first() - { - return param_names.iter().any(|p| seg.ident == **p); - } - false - }); - if has_where_bounds { - return quote! {}; - } - } - let param_idents: Vec<&syn::Ident> = type_params.iter().map(|tp| &tp.ident).collect(); - - gen_read_only_clone_for_generics(name, generics, data, ¶m_idents) + gen_read_only_clone_for_generics(name, generics, data, &container_params) } /// Generate `ReadOnlyClone` for types with `M: StorageMode`. @@ -789,33 +798,40 @@ fn is_field_skipped(field: &syn::Field) -> bool { /// Generate `ReadOnlyClone` for types with generic type params but no `M: StorageMode`. /// -/// Each generic type param T gets a `ReadOnlyClone` bound. -/// `type ReadOnly = Self` for each type param. -/// Fields containing any type param use `.read_only_clone()`, others use `.clone()`. +/// `container_params` are unbounded type params that get `ReadOnlyClone` bounds and are +/// mapped to `T::ReadOnly` in the target type. +/// Bounded type params (leaf params) are kept as-is — they don't change across storage modes. +/// Fields containing container params use `.read_only_clone()`, others use `.clone()`. fn gen_read_only_clone_for_generics( name: &syn::Ident, generics: &syn::Generics, data: &syn::DataStruct, - type_params: &[&syn::Ident], + container_params: &[&syn::Ident], ) -> proc_macro2::TokenStream { - // Check if any field actually references a type param (otherwise skip). - let has_generic_field = match &data.fields { - Fields::Named(named) => named - .named - .iter() - .any(|f| type_params.iter().any(|tp| type_contains_ident(&f.ty, tp))), - Fields::Unnamed(unnamed) => unnamed - .unnamed - .iter() - .any(|f| type_params.iter().any(|tp| type_contains_ident(&f.ty, tp))), + // Check if any non-skipped field references a container param (otherwise skip). + let has_container_field = match &data.fields { + Fields::Named(named) => named.named.iter().any(|f| { + !is_field_skipped(f) + && container_params + .iter() + .any(|tp| type_contains_ident(&f.ty, tp)) + }), + Fields::Unnamed(unnamed) => unnamed.unnamed.iter().any(|f| { + !is_field_skipped(f) + && container_params + .iter() + .any(|tp| type_contains_ident(&f.ty, tp)) + }), Fields::Unit => false, }; - if !has_generic_field { + if !has_container_field { return quote! {}; } - // Impl generics: add ReadOnlyClone bound to type params. + let is_container = |ident: &syn::Ident| container_params.iter().any(|cp| *cp == ident); + + // Impl generics: add ReadOnlyClone bound to container params, keep bounds for leaf params. let impl_params: Vec = generics .params .iter() @@ -823,10 +839,12 @@ fn gen_read_only_clone_for_generics( syn::GenericParam::Type(tp) => { let ident = &tp.ident; let bounds = &tp.bounds; - if bounds.is_empty() { + if is_container(ident) { quote! { #ident: vecdb::ReadOnlyClone } + } else if bounds.is_empty() { + quote! { #ident } } else { - quote! { #ident: #bounds + vecdb::ReadOnlyClone } + quote! { #ident: #bounds } } } syn::GenericParam::Lifetime(lt) => quote! { #lt }, @@ -858,14 +876,18 @@ fn gen_read_only_clone_for_generics( }) .collect(); - // ReadOnly type args: replace each type param T with ::ReadOnly. + // ReadOnly type args: map container params to ReadOnly, keep leaf params as-is. let ro_ty_args: Vec = generics .params .iter() .map(|p| match p { syn::GenericParam::Type(tp) => { let id = &tp.ident; - quote! { <#id as vecdb::ReadOnlyClone>::ReadOnly } + if is_container(id) { + quote! { <#id as vecdb::ReadOnlyClone>::ReadOnly } + } else { + quote! { #id } + } } syn::GenericParam::Lifetime(lt) => { let lt = <.lifetime; @@ -880,9 +902,9 @@ fn gen_read_only_clone_for_generics( let where_clause = &generics.where_clause; - // Field-level: if field type contains any type param → read_only_clone, else → clone. - let field_contains_any_param = - |ty: &Type| type_params.iter().any(|tp| type_contains_ident(ty, tp)); + // Field-level: if field type contains any container param → read_only_clone, else → clone. + let field_contains_container_param = + |ty: &Type| container_params.iter().any(|tp| type_contains_ident(ty, tp)); let body = match &data.fields { Fields::Named(named) => { @@ -893,7 +915,7 @@ fn gen_read_only_clone_for_generics( let field_name = f.ident.as_ref().unwrap(); if is_field_skipped(f) { quote! { #field_name: Default::default() } - } else if field_contains_any_param(&f.ty) { + } else if field_contains_container_param(&f.ty) { if is_box_type(&f.ty) { quote! { #field_name: Box::new(vecdb::ReadOnlyClone::read_only_clone(&*self.#field_name)) } } else { @@ -915,7 +937,7 @@ fn gen_read_only_clone_for_generics( let idx = syn::Index::from(i); if is_field_skipped(f) { quote! { Default::default() } - } else if field_contains_any_param(&f.ty) { + } else if field_contains_container_param(&f.ty) { if is_box_type(&f.ty) { quote! { Box::new(vecdb::ReadOnlyClone::read_only_clone(&*self.#idx)) } } else {