global: snapshot

This commit is contained in:
nym21
2026-03-07 22:52:51 +01:00
parent 90f2d64019
commit 81ab1886d1
18 changed files with 703 additions and 1008 deletions

View File

@@ -6,7 +6,7 @@ use brk_types::{Cents, CentsCompact, CostBasisDistribution, Date, Height, Sats};
use crate::internal::{PERCENTILES, PERCENTILES_LEN};
use crate::distribution::metrics::{CohortMetricsBase, CostBasisExtended};
use crate::distribution::metrics::{CohortMetricsBase, CostBasis};
use super::groups::UTXOCohorts;
@@ -16,11 +16,14 @@ const COST_BASIS_PRICE_DIGITS: i32 = 5;
pub(super) struct CachedPercentiles {
sat_result: [Cents; PERCENTILES_LEN],
usd_result: [Cents; PERCENTILES_LEN],
min_price: Cents,
max_price: Cents,
}
impl CachedPercentiles {
fn push(&self, height: Height, ext: &mut CostBasisExtended) -> Result<()> {
ext.push_arrays(height, &self.sat_result, &self.usd_result)
fn push(&self, height: Height, cost_basis: &mut CostBasis) -> Result<()> {
cost_basis.truncate_push_minmax(height, self.min_price, self.max_price)?;
cost_basis.truncate_push_percentiles(height, &self.sat_result, &self.usd_result)
}
}
@@ -114,13 +117,13 @@ impl UTXOCohorts {
self.percentile_cache
.all
.push(height, &mut self.all.metrics.cost_basis.extended)?;
.push(height, &mut self.all.metrics.cost_basis)?;
self.percentile_cache
.sth
.push(height, &mut self.sth.metrics.cost_basis.extended)?;
.push(height, &mut self.sth.metrics.cost_basis)?;
self.percentile_cache
.lth
.push(height, &mut self.lth.metrics.cost_basis.extended)?;
.push(height, &mut self.lth.metrics.cost_basis)?;
// Serialize full distribution at day boundaries
if let Some(date) = date_opt {
@@ -136,13 +139,13 @@ impl UTXOCohorts {
fn push_cached_percentiles(&mut self, height: Height) -> Result<()> {
self.percentile_cache
.all
.push(height, &mut self.all.metrics.cost_basis.extended)?;
.push(height, &mut self.all.metrics.cost_basis)?;
self.percentile_cache
.sth
.push(height, &mut self.sth.metrics.cost_basis.extended)?;
.push(height, &mut self.sth.metrics.cost_basis)?;
self.percentile_cache
.lth
.push(height, &mut self.lth.metrics.cost_basis.extended)?;
.push(height, &mut self.lth.metrics.cost_basis)?;
Ok(())
}
}
@@ -269,6 +272,8 @@ struct PercTarget {
usd_result: [Cents; PERCENTILES_LEN],
price_sats: u64,
price_usd: u128,
min_price: Cents,
max_price: Cents,
merged: Vec<(CentsCompact, Sats)>,
}
@@ -295,6 +300,8 @@ impl PercTarget {
usd_result: [Cents::ZERO; PERCENTILES_LEN],
price_sats: 0,
price_usd: 0,
min_price: Cents::ZERO,
max_price: Cents::ZERO,
merged: Vec::with_capacity(merged_cap),
}
}
@@ -303,6 +310,8 @@ impl PercTarget {
CachedPercentiles {
sat_result: self.sat_result,
usd_result: self.usd_result,
min_price: self.min_price,
max_price: self.max_price,
}
}
@@ -313,14 +322,21 @@ impl PercTarget {
}
fn finalize_price(&mut self, price: Cents, collect_merged: bool) {
if collect_merged && self.price_sats > 0 {
let rounded: CentsCompact = price.round_to_dollar(COST_BASIS_PRICE_DIGITS).into();
if let Some((lp, ls)) = self.merged.last_mut()
&& *lp == rounded
{
*ls += Sats::from(self.price_sats);
} else {
self.merged.push((rounded, Sats::from(self.price_sats)));
if self.price_sats > 0 {
if self.min_price == Cents::ZERO {
self.min_price = price;
}
self.max_price = price;
if collect_merged {
let rounded: CentsCompact = price.round_to_dollar(COST_BASIS_PRICE_DIGITS).into();
if let Some((lp, ls)) = self.merged.last_mut()
&& *lp == rounded
{
*ls += Sats::from(self.price_sats);
} else {
self.merged.push((rounded, Sats::from(self.price_sats)));
}
}
}

View File

@@ -136,14 +136,13 @@ impl<M: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<M> {
&mut self,
height: Height,
height_price: Cents,
is_day_boundary: bool,
_is_day_boundary: bool,
) -> Result<()> {
if let Some(state) = self.state.as_mut() {
self.metrics.compute_then_truncate_push_unrealized_states(
self.metrics.compute_and_push_unrealized(
height,
height_price,
state,
is_day_boundary,
)?;
}
Ok(())

View File

@@ -13,7 +13,7 @@ use crate::{blocks, prices};
use crate::internal::{ComputedFromHeight, RollingDeltaExcept1m};
use crate::distribution::metrics::{
ActivityFull, CohortMetricsBase, CostBasisWithExtended, ImportConfig, OutputsMetrics,
ActivityFull, CohortMetricsBase, CostBasis, ImportConfig, OutputsMetrics,
RealizedAdjusted, RealizedFull, RelativeForAll, SupplyMetrics, UnrealizedFull,
};
@@ -28,7 +28,7 @@ pub struct AllCohortMetrics<M: StorageMode = Rw> {
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityFull<M>>,
pub realized: Box<RealizedFull<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub cost_basis: Box<CostBasis<M>>,
pub unrealized: Box<UnrealizedFull<M>>,
pub adjusted: Box<RealizedAdjusted<M>>,
pub relative: Box<RelativeForAll<M>>,
@@ -43,10 +43,26 @@ impl CohortMetricsBase for AllCohortMetrics {
type ActivityVecs = ActivityFull;
type RealizedVecs = RealizedFull;
type UnrealizedVecs = UnrealizedFull;
type CostBasisVecs = CostBasisWithExtended;
impl_cohort_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 min_stateful_height_len(&self) -> usize {
self.supply
.min_len()
.min(self.outputs.min_len())
.min(self.activity.min_len())
.min(self.realized.min_stateful_height_len())
.min(self.unrealized.min_stateful_height_len())
.min(self.cost_basis.min_stateful_height_len())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.collect_vecs_mut());
@@ -82,7 +98,7 @@ impl AllCohortMetrics {
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityFull::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?),
cost_basis: Box::new(CostBasis::forced_import(cfg)?),
unrealized: Box::new(unrealized),
adjusted: Box::new(adjusted),
relative: Box::new(relative),

View File

@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{blocks, prices};
use crate::distribution::metrics::{
ActivityBase, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase,
ActivityBase, CohortMetricsBase, ImportConfig, OutputsMetrics, RealizedBase,
RelativeToAll, SupplyMetrics, UnrealizedBase,
};
@@ -21,7 +21,6 @@ pub struct BasicCohortMetrics<M: StorageMode = Rw> {
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityBase<M>>,
pub realized: Box<RealizedBase<M>>,
pub cost_basis: Box<CostBasisBase<M>>,
pub unrealized: Box<UnrealizedBase<M>>,
pub relative: Box<RelativeToAll<M>>,
}
@@ -30,7 +29,6 @@ impl CohortMetricsBase for BasicCohortMetrics {
type ActivityVecs = ActivityBase;
type RealizedVecs = RealizedBase;
type UnrealizedVecs = UnrealizedBase;
type CostBasisVecs = CostBasisBase;
impl_cohort_accessors!();
@@ -40,7 +38,6 @@ impl CohortMetricsBase for BasicCohortMetrics {
vecs.extend(self.outputs.collect_vecs_mut());
vecs.extend(self.activity.collect_vecs_mut());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.collect_vecs_mut());
vecs.extend(self.unrealized.collect_vecs_mut());
vecs
}
@@ -60,7 +57,6 @@ impl BasicCohortMetrics {
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityBase::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisBase::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
})

View File

@@ -12,7 +12,7 @@ use crate::{blocks, prices};
use crate::internal::{ComputedFromHeight, RollingDeltaExcept1m};
use crate::distribution::metrics::{
ActivityFull, CohortMetricsBase, CostBasisWithExtended, ImportConfig, OutputsMetrics,
ActivityFull, CohortMetricsBase, CostBasis, ImportConfig, OutputsMetrics,
RealizedFull, RelativeWithExtended, SupplyMetrics, UnrealizedFull,
};
@@ -26,7 +26,7 @@ pub struct ExtendedCohortMetrics<M: StorageMode = Rw> {
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityFull<M>>,
pub realized: Box<RealizedFull<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub cost_basis: Box<CostBasis<M>>,
pub unrealized: Box<UnrealizedFull<M>>,
pub relative: Box<RelativeWithExtended<M>>,
pub dormancy: ComputedFromHeight<StoredF32, M>,
@@ -40,10 +40,26 @@ impl CohortMetricsBase for ExtendedCohortMetrics {
type ActivityVecs = ActivityFull;
type RealizedVecs = RealizedFull;
type UnrealizedVecs = UnrealizedFull;
type CostBasisVecs = CostBasisWithExtended;
impl_cohort_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 min_stateful_height_len(&self) -> usize {
self.supply
.min_len()
.min(self.outputs.min_len())
.min(self.activity.min_len())
.min(self.realized.min_stateful_height_len())
.min(self.unrealized.min_stateful_height_len())
.min(self.cost_basis.min_stateful_height_len())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.collect_vecs_mut());
@@ -72,7 +88,7 @@ impl ExtendedCohortMetrics {
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityFull::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?),
cost_basis: Box::new(CostBasis::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
dormancy: cfg.import("dormancy", Version::ONE)?,

View File

@@ -1,13 +1,13 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Indexes, Sats};
use brk_types::{Cents, Dollars, Height, Indexes, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{blocks, prices};
use crate::distribution::metrics::{
ActivityFull, CohortMetricsBase, CostBasisWithExtended, ImportConfig, RealizedAdjusted,
ActivityFull, CohortMetricsBase, ImportConfig, RealizedAdjusted,
RealizedFull, UnrealizedFull,
};
@@ -30,10 +30,17 @@ impl CohortMetricsBase for ExtendedAdjustedCohortMetrics {
type ActivityVecs = ActivityFull;
type RealizedVecs = RealizedFull;
type UnrealizedVecs = UnrealizedFull;
type CostBasisVecs = CostBasisWithExtended;
impl_cohort_accessors!();
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.inner.validate_computed_versions(base_version)
}
fn min_stateful_height_len(&self) -> usize {
self.inner.min_stateful_height_len()
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
self.inner.collect_all_vecs_mut()
}

View File

@@ -1,88 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Indexes, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{
distribution::state::{CohortState, RealizedState},
internal::{ComputedFromHeight, Price},
};
use crate::distribution::metrics::ImportConfig;
/// Base cost basis metrics (always computed).
#[derive(Traversable)]
pub struct CostBasisBase<M: StorageMode = Rw> {
/// Minimum cost basis for any UTXO at this height
pub min: Price<ComputedFromHeight<Cents, M>>,
/// Maximum cost basis for any UTXO at this height
pub max: Price<ComputedFromHeight<Cents, M>>,
}
impl CostBasisBase {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
min: cfg.import("cost_basis_min", Version::ZERO)?,
max: cfg.import("cost_basis_max", Version::ZERO)?,
})
}
pub(crate) fn min_stateful_height_len(&self) -> usize {
self.min.cents.height.len().min(self.max.cents.height.len())
}
pub(crate) fn truncate_push_minmax(
&mut self,
height: Height,
state: &CohortState<RealizedState>,
) -> Result<()> {
self.min.cents.height.truncate_push(
height,
state
.cost_basis_data_first_key_value()
.map(|(cents, _)| cents)
.unwrap_or(Cents::ZERO),
)?;
self.max.cents.height.truncate_push(
height,
state
.cost_basis_data_last_key_value()
.map(|(cents, _)| cents)
.unwrap_or(Cents::ZERO),
)?;
Ok(())
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.min.cents.height as &mut dyn AnyStoredVec,
&mut self.max.cents.height,
]
}
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.min.cents.height.compute_min_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.min.cents.height)
.collect::<Vec<_>>(),
exit,
)?;
self.max.cents.height.compute_max_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.max.cents.height)
.collect::<Vec<_>>(),
exit,
)?;
Ok(())
}
}

View File

@@ -1,102 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Version};
use vecdb::{AnyStoredVec, Rw, StorageMode};
use crate::{
distribution::state::{CohortState, RealizedState},
internal::{PERCENTILES_LEN, PercentilesVecs},
};
use crate::distribution::metrics::ImportConfig;
/// Extended cost basis metrics (only for extended cohorts).
#[derive(Traversable)]
pub struct CostBasisExtended<M: StorageMode = Rw> {
/// Cost basis percentiles (sat-weighted)
pub percentiles: PercentilesVecs<M>,
/// Invested capital percentiles (USD-weighted)
pub invested_capital: PercentilesVecs<M>,
}
impl CostBasisExtended {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
percentiles: PercentilesVecs::forced_import(
cfg.db,
&cfg.name("cost_basis"),
cfg.version,
cfg.indexes,
)?,
invested_capital: PercentilesVecs::forced_import(
cfg.db,
&cfg.name("invested_capital"),
cfg.version,
cfg.indexes,
)?,
})
}
pub(crate) fn truncate_push_percentiles(
&mut self,
height: Height,
state: &mut CohortState<RealizedState>,
is_day_boundary: bool,
) -> Result<()> {
let computed = if is_day_boundary {
state.compute_percentiles()
} else {
state.cached_percentiles()
};
let sat_prices = computed
.as_ref()
.map(|p| p.sat_weighted)
.unwrap_or([Cents::ZERO; PERCENTILES_LEN]);
let usd_prices = computed
.as_ref()
.map(|p| p.usd_weighted)
.unwrap_or([Cents::ZERO; PERCENTILES_LEN]);
self.push_arrays(height, &sat_prices, &usd_prices)
}
/// Push pre-computed percentile arrays.
/// Shared by both individual cohort and aggregate (K-way merge) paths.
pub(crate) fn push_arrays(
&mut self,
height: Height,
sat_prices: &[Cents; PERCENTILES_LEN],
usd_prices: &[Cents; PERCENTILES_LEN],
) -> Result<()> {
self.percentiles.truncate_push(height, sat_prices)?;
self.invested_capital.truncate_push(height, usd_prices)?;
Ok(())
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(
self.percentiles
.vecs
.iter_mut()
.map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
);
vecs.extend(
self.invested_capital
.vecs
.iter_mut()
.map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
);
vecs
}
pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.percentiles
.validate_computed_version_or_reset(base_version)?;
self.invested_capital
.validate_computed_version_or_reset(base_version)?;
Ok(())
}
}

View File

@@ -1,51 +1,93 @@
mod base;
mod extended;
mod with_extended;
pub use base::CostBasisBase;
pub use extended::CostBasisExtended;
pub use with_extended::CostBasisWithExtended;
use brk_error::Result;
use brk_types::{Height, Version};
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Version};
use vecdb::{AnyStoredVec, AnyVec, Rw, StorageMode, WritableVec};
use crate::distribution::state::{CohortState, RealizedState};
use crate::internal::{ComputedFromHeight, PercentilesVecs, Price, PERCENTILES_LEN};
/// Polymorphic dispatch for cost basis metric types.
///
/// `CostBasisBase` has no version validation or percentiles (no-op defaults).
/// `CostBasisWithExtended` validates versions and pushes percentiles.
pub trait CostBasisLike: Send + Sync {
fn as_base(&self) -> &CostBasisBase;
fn as_base_mut(&mut self) -> &mut CostBasisBase;
fn validate_computed_versions(&mut self, _base_version: Version) -> Result<()> { Ok(()) }
fn truncate_push_percentiles(
&mut self,
_height: Height,
_state: &mut CohortState<RealizedState>,
_is_day_boundary: bool,
) -> Result<()> {
Ok(())
}
use super::ImportConfig;
/// Cost basis metrics: min/max + percentiles.
/// Used by all/sth/lth cohorts only.
#[derive(Traversable)]
pub struct CostBasis<M: StorageMode = Rw> {
pub min: Price<ComputedFromHeight<Cents, M>>,
pub max: Price<ComputedFromHeight<Cents, M>>,
pub percentiles: PercentilesVecs<M>,
pub invested_capital: PercentilesVecs<M>,
}
impl CostBasisLike for CostBasisBase {
fn as_base(&self) -> &CostBasisBase { self }
fn as_base_mut(&mut self) -> &mut CostBasisBase { self }
}
impl CostBasisLike for CostBasisWithExtended {
fn as_base(&self) -> &CostBasisBase { &self.base }
fn as_base_mut(&mut self) -> &mut CostBasisBase { &mut self.base }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.extended.validate_computed_versions(base_version)
impl CostBasis {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
min: cfg.import("cost_basis_min", Version::ZERO)?,
max: cfg.import("cost_basis_max", Version::ZERO)?,
percentiles: PercentilesVecs::forced_import(
cfg.db,
&cfg.name("cost_basis"),
cfg.version,
cfg.indexes,
)?,
invested_capital: PercentilesVecs::forced_import(
cfg.db,
&cfg.name("invested_capital"),
cfg.version,
cfg.indexes,
)?,
})
}
fn truncate_push_percentiles(
pub(crate) fn min_stateful_height_len(&self) -> usize {
self.min.cents.height.len().min(self.max.cents.height.len())
}
pub(crate) fn truncate_push_minmax(
&mut self,
height: Height,
state: &mut CohortState<RealizedState>,
is_day_boundary: bool,
min_price: Cents,
max_price: Cents,
) -> Result<()> {
self.extended.truncate_push_percentiles(height, state, is_day_boundary)
self.min.cents.height.truncate_push(height, min_price)?;
self.max.cents.height.truncate_push(height, max_price)?;
Ok(())
}
pub(crate) fn truncate_push_percentiles(
&mut self,
height: Height,
sat_prices: &[Cents; PERCENTILES_LEN],
usd_prices: &[Cents; PERCENTILES_LEN],
) -> Result<()> {
self.percentiles.truncate_push(height, sat_prices)?;
self.invested_capital.truncate_push(height, usd_prices)?;
Ok(())
}
pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.percentiles
.validate_computed_version_or_reset(base_version)?;
self.invested_capital
.validate_computed_version_or_reset(base_version)?;
Ok(())
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = vec![
&mut self.min.cents.height,
&mut self.max.cents.height,
];
vecs.extend(
self.percentiles
.vecs
.iter_mut()
.map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
);
vecs.extend(
self.invested_capital
.vecs
.iter_mut()
.map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
);
vecs
}
}

View File

@@ -1,34 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Rw, StorageMode};
use crate::distribution::metrics::ImportConfig;
use super::{CostBasisBase, CostBasisExtended};
/// Cost basis metrics with guaranteed extended (no Option).
#[derive(Deref, DerefMut, Traversable)]
pub struct CostBasisWithExtended<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: CostBasisBase<M>,
#[traversable(flatten)]
pub extended: CostBasisExtended<M>,
}
impl CostBasisWithExtended {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
base: CostBasisBase::forced_import(cfg)?,
extended: CostBasisExtended::forced_import(cfg)?,
})
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = self.base.collect_vecs_mut();
vecs.extend(self.extended.collect_vecs_mut());
vecs
}
}

View File

@@ -14,7 +14,7 @@ mod activity;
/// Accessor methods for `CohortMetricsBase` implementations.
///
/// All cohort metric types share the same field names (`filter`, `supply`, `outputs`,
/// `activity`, `realized`, `unrealized`, `cost_basis`). For wrapper types like
/// `activity`, `realized`, `unrealized`). For wrapper types like
/// `ExtendedAdjustedCohortMetrics`, Rust's auto-deref resolves these through `Deref`.
macro_rules! impl_cohort_accessors {
() => {
@@ -29,8 +29,6 @@ macro_rules! impl_cohort_accessors {
fn realized_mut(&mut self) -> &mut Self::RealizedVecs { &mut self.realized }
fn unrealized(&self) -> &Self::UnrealizedVecs { &self.unrealized }
fn unrealized_mut(&mut self) -> &mut Self::UnrealizedVecs { &mut self.unrealized }
fn cost_basis(&self) -> &Self::CostBasisVecs { &self.cost_basis }
fn cost_basis_mut(&mut self) -> &mut Self::CostBasisVecs { &mut self.cost_basis }
};
}
@@ -49,7 +47,7 @@ pub use cohort::{
ExtendedCohortMetrics, MinimalCohortMetrics,
};
pub use config::ImportConfig;
pub use cost_basis::{CostBasisBase, CostBasisExtended, CostBasisLike, CostBasisWithExtended};
pub use cost_basis::CostBasis;
pub use outputs::OutputsMetrics;
pub use realized::{
RealizedAdjusted, RealizedBase, RealizedCore, RealizedFull, RealizedLike, RealizedMinimal,
@@ -94,7 +92,6 @@ pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState> + Send
type ActivityVecs: ActivityLike;
type RealizedVecs: RealizedLike;
type UnrealizedVecs: UnrealizedLike;
type CostBasisVecs: CostBasisLike;
fn filter(&self) -> &Filter;
fn supply(&self) -> &SupplyMetrics;
@@ -107,8 +104,6 @@ pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState> + Send
fn realized_mut(&mut self) -> &mut Self::RealizedVecs;
fn unrealized(&self) -> &Self::UnrealizedVecs;
fn unrealized_mut(&mut self) -> &mut Self::UnrealizedVecs;
fn cost_basis(&self) -> &Self::CostBasisVecs;
fn cost_basis_mut(&mut self) -> &mut Self::CostBasisVecs;
/// Convenience: access activity as `&ActivityBase` (via `ActivityLike::as_base`).
fn activity_base(&self) -> &ActivityBase { self.activity().as_base() }
@@ -122,46 +117,26 @@ pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState> + Send
fn unrealized_base(&self) -> &UnrealizedBase { self.unrealized().as_base() }
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { self.unrealized_mut().as_base_mut() }
/// Convenience: access cost basis as `&CostBasisBase` (via `CostBasisLike::as_base`).
fn cost_basis_base(&self) -> &CostBasisBase { self.cost_basis().as_base() }
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { self.cost_basis_mut().as_base_mut() }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply_mut().validate_computed_versions(base_version)?;
self.activity_mut().validate_computed_versions(base_version)?;
self.cost_basis_mut().validate_computed_versions(base_version)?;
Ok(())
}
/// Apply pending, push min/max cost basis, compute and push unrealized state.
fn compute_and_push_unrealized_base(
/// Apply pending state, compute and push unrealized state.
fn compute_and_push_unrealized(
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState<RealizedState>,
) -> Result<()> {
state.apply_pending();
self.cost_basis_base_mut()
.truncate_push_minmax(height, state)?;
let unrealized_state = state.compute_unrealized_state(height_price);
self.unrealized_mut()
.truncate_push(height, &unrealized_state)?;
Ok(())
}
/// Compute and push unrealized states + cost basis percentiles (no-op for base types).
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState<RealizedState>,
is_day_boundary: bool,
) -> Result<()> {
self.compute_and_push_unrealized_base(height, height_price, state)?;
self.cost_basis_mut().truncate_push_percentiles(height, state, is_day_boundary)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec>;
fn min_stateful_height_len(&self) -> usize {
@@ -171,7 +146,6 @@ pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState> + Send
.min(self.activity().min_len())
.min(self.realized().min_stateful_height_len())
.min(self.unrealized().min_stateful_height_len())
.min(self.cost_basis_base().min_stateful_height_len())
}
fn truncate_push(&mut self, height: Height, state: &CohortState<RealizedState>) -> Result<()> {
@@ -258,11 +232,6 @@ pub trait CohortMetricsBase: CohortMetricsState<Realized = RealizedState> + Send
&others.iter().map(|v| v.unrealized_base()).collect::<Vec<_>>(),
exit,
)?;
self.cost_basis_base_mut().compute_from_stateful(
starting_indexes,
&others.iter().map(|v| v.cost_basis_base()).collect::<Vec<_>>(),
exit,
)?;
Ok(())
}
}

View File

@@ -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, RealizedOps, UnrealizedState};
use super::super::cost_basis::{CostBasisData, RealizedOps, UnrealizedState};
pub struct SendPrecomputed {
pub sats: Sats,
@@ -97,18 +97,6 @@ impl<R: RealizedOps> CohortState<R> {
self.cost_basis_data.apply_pending();
}
pub(crate) fn cost_basis_data_first_key_value(&self) -> Option<(Cents, &Sats)> {
self.cost_basis_data
.first_key_value()
.map(|(k, v)| (k.into(), v))
}
pub(crate) fn cost_basis_data_last_key_value(&self) -> Option<(Cents, &Sats)> {
self.cost_basis_data
.last_key_value()
.map(|(k, v)| (k.into(), v))
}
pub(crate) fn reset_single_iteration_values(&mut self) {
self.sent = Sats::ZERO;
self.satdays_destroyed = Sats::ZERO;
@@ -288,14 +276,6 @@ impl<R: RealizedOps> CohortState<R> {
}
}
pub(crate) fn compute_percentiles(&mut self) -> Option<Percentiles> {
self.cost_basis_data.compute_percentiles()
}
pub(crate) fn cached_percentiles(&self) -> Option<Percentiles> {
self.cost_basis_data.cached_percentiles()
}
pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState {
self.cost_basis_data.compute_unrealized_state(height_price)
}

View File

@@ -11,7 +11,7 @@ use brk_types::{
use rustc_hash::FxHashMap;
use vecdb::{Bytes, unlikely};
use super::{CachedUnrealizedState, Percentiles, UnrealizedState};
use super::{CachedUnrealizedState, UnrealizedState};
/// Type alias for the price-to-sats map used in cost basis data.
pub(super) type CostBasisMap = BTreeMap<CentsCompact, Sats>;
@@ -31,8 +31,6 @@ pub struct CostBasisData {
pending: FxHashMap<CentsCompact, (Sats, Sats)>,
pending_raw: PendingRaw,
cache: Option<CachedUnrealizedState>,
percentiles_dirty: bool,
cached_percentiles: Option<Percentiles>,
rounding_digits: Option<i32>,
/// Monotonically increasing counter, bumped on each apply_pending with actual changes.
generation: u64,
@@ -48,8 +46,6 @@ impl CostBasisData {
pending: FxHashMap::default(),
pending_raw: PendingRaw::default(),
cache: None,
percentiles_dirty: true,
cached_percentiles: None,
rounding_digits: None,
generation: 0,
}
@@ -77,8 +73,6 @@ impl CostBasisData {
self.pending.clear();
self.pending_raw = PendingRaw::default();
self.cache = None;
self.percentiles_dirty = true;
self.cached_percentiles = None;
Ok(height)
}
@@ -105,28 +99,6 @@ impl CostBasisData {
self.pending.is_empty() && self.state.as_ref().unwrap().base.map.is_empty()
}
pub(crate) fn first_key_value(&self) -> Option<(CentsCompact, &Sats)> {
self.assert_pending_empty();
self.state
.as_ref()
.unwrap()
.base
.map
.first_key_value()
.map(|(&k, v)| (k, v))
}
pub(crate) fn last_key_value(&self) -> Option<(CentsCompact, &Sats)> {
self.assert_pending_empty();
self.state
.as_ref()
.unwrap()
.base
.map
.last_key_value()
.map(|(&k, v)| (k, v))
}
/// Get the exact cap_raw value (not recomputed from map).
pub(crate) fn cap_raw(&self) -> CentsSats {
self.assert_pending_empty();
@@ -184,7 +156,6 @@ impl CostBasisData {
return;
}
self.generation = self.generation.wrapping_add(1);
self.percentiles_dirty = true;
let map = &mut self.state.as_mut().unwrap().base.map;
for (cents, (inc, dec)) in self.pending.drain() {
match map.entry(cents) {
@@ -273,23 +244,6 @@ impl CostBasisData {
self.pending.clear();
self.pending_raw = PendingRaw::default();
self.cache = None;
self.percentiles_dirty = true;
self.cached_percentiles = None;
}
pub(crate) fn cached_percentiles(&self) -> Option<Percentiles> {
self.cached_percentiles
}
pub(crate) fn compute_percentiles(&mut self) -> Option<Percentiles> {
self.assert_pending_empty();
if !self.percentiles_dirty {
return self.cached_percentiles;
}
self.cached_percentiles =
Percentiles::compute_from_map(&self.state.as_ref().unwrap().base.map);
self.percentiles_dirty = false;
self.cached_percentiles
}
pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState {

View File

@@ -1,10 +1,8 @@
mod data;
mod percentiles;
mod realized;
mod unrealized;
pub use data::*;
pub use percentiles::*;
pub use realized::*;
pub use unrealized::UnrealizedState;

View File

@@ -1,69 +0,0 @@
use brk_types::Cents;
use crate::internal::{PERCENTILES, PERCENTILES_LEN};
use super::CostBasisMap;
#[derive(Clone, Copy, Debug)]
pub struct Percentiles {
/// Sat-weighted: percentiles by coin count
pub sat_weighted: [Cents; PERCENTILES_LEN],
/// USD-weighted: percentiles by invested capital (sats × price)
pub usd_weighted: [Cents; PERCENTILES_LEN],
}
impl Percentiles {
/// Compute both sat-weighted and USD-weighted percentiles in two passes over the BTreeMap.
/// Avoids intermediate Vec allocation by iterating the map directly.
pub(crate) fn compute_from_map(map: &CostBasisMap) -> Option<Self> {
if map.is_empty() {
return None;
}
// First pass: compute totals
let mut total_sats: u64 = 0;
let mut total_usd: u128 = 0;
for (&cents, &sats) in map.iter() {
total_sats += u64::from(sats);
total_usd += cents.as_u128() * sats.as_u128();
}
if total_sats == 0 {
return None;
}
// Precompute targets to avoid repeated multiplication in the inner loop
let sat_targets: [u64; PERCENTILES_LEN] =
PERCENTILES.map(|p| total_sats * u64::from(p) / 100);
let usd_targets: [u128; PERCENTILES_LEN] =
PERCENTILES.map(|p| total_usd * u128::from(p) / 100);
let mut sat_weighted = [Cents::ZERO; PERCENTILES_LEN];
let mut usd_weighted = [Cents::ZERO; PERCENTILES_LEN];
let mut cumsum_sats: u64 = 0;
let mut cumsum_usd: u128 = 0;
let mut sat_idx = 0;
let mut usd_idx = 0;
// Second pass: compute percentiles
for (&cents, &sats) in map.iter() {
cumsum_sats += u64::from(sats);
cumsum_usd += cents.as_u128() * sats.as_u128();
while sat_idx < PERCENTILES_LEN && cumsum_sats >= sat_targets[sat_idx] {
sat_weighted[sat_idx] = cents.into();
sat_idx += 1;
}
while usd_idx < PERCENTILES_LEN && cumsum_usd >= usd_targets[usd_idx] {
usd_weighted[usd_idx] = cents.into();
usd_idx += 1;
}
}
Some(Self {
sat_weighted,
usd_weighted,
})
}
}