computer: distribution: replace Option but distinct structs

This commit is contained in:
nym21
2026-02-25 14:57:20 +01:00
parent f74115c6e2
commit 9e4fe62de2
53 changed files with 3398 additions and 1853 deletions

View File

@@ -8,7 +8,8 @@ use crate::internal::{ComputedFromHeightSumCum, ConstantVecs, RollingWindows, Wi
pub struct Vecs<M: StorageMode = Rw> {
pub block_count_target: ConstantVecs<StoredU64>,
pub block_count: ComputedFromHeightSumCum<StoredU32, M>,
// Rolling window starts (height-indexed only, no date aggregation needed)
pub block_count_sum: RollingWindows<StoredU32, M>,
pub height_24h_ago: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub height_3d_ago: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub height_1w_ago: M::Stored<EagerVec<PcoVec<Height, Height>>>,
@@ -39,8 +40,6 @@ pub struct Vecs<M: StorageMode = Rw> {
pub height_6y_ago: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub height_8y_ago: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub height_10y_ago: M::Stored<EagerVec<PcoVec<Height, Height>>>,
// Rolling window block counts
pub block_count_sum: RollingWindows<StoredU32, M>,
}
impl Vecs {

View File

@@ -7,7 +7,6 @@ use crate::internal::{ComputedFromHeightLast, ComputedHeightDerivedLast};
/// Difficulty metrics: raw difficulty, derived stats, adjustment, and countdown
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
/// Raw difficulty with day1/period stats - merges with indexer's raw
pub raw: ComputedHeightDerivedLast<StoredF64>,
pub as_hash: ComputedFromHeightLast<StoredF32, M>,
pub adjustment: ComputedFromHeightLast<StoredF32, M>,

View File

@@ -12,7 +12,7 @@ use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, distribution::DynCohortVecs, indexes, prices};
use crate::distribution::metrics::SupplyMetrics;
use crate::distribution::metrics::{CohortMetricsBase, SupplyMetrics};
use super::{super::traits::CohortVecs, vecs::AddressCohortVecs};
@@ -33,7 +33,7 @@ impl AddressCohorts {
indexes: &indexes::Vecs,
prices: &prices::Vecs,
states_path: &Path,
all_supply: Option<&SupplyMetrics>,
all_supply: &SupplyMetrics,
) -> Result<Self> {
let v = version + VERSION;
@@ -140,7 +140,7 @@ impl AddressCohorts {
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: Option<&HM>,
height_to_market_cap: &HM,
exit: &Exit,
) -> Result<()>
where
@@ -198,12 +198,8 @@ impl AddressCohorts {
/// Reset cost_basis_data for all separate cohorts (called during fresh start).
pub(crate) fn reset_separate_cost_basis_data(&mut self) -> Result<()> {
self.par_iter_separate_mut().try_for_each(|v| {
if let Some(state) = v.state.as_mut() {
state.reset_cost_basis_data_if_needed()?;
}
Ok(())
})
self.par_iter_separate_mut()
.try_for_each(|v| v.reset_cost_basis_data_if_needed())
}
/// Validate computed versions for all separate cohorts.

View File

@@ -15,7 +15,7 @@ use crate::{
prices,
};
use crate::distribution::metrics::{CohortMetrics, ImportConfig, SupplyMetrics};
use crate::distribution::metrics::{BasicCohortMetrics, CohortMetricsBase, ImportConfig, SupplyMetrics};
use super::super::traits::{CohortVecs, DynCohortVecs};
@@ -33,7 +33,7 @@ pub struct AddressCohortVecs<M: StorageMode = Rw> {
/// Metric vectors
#[traversable(flatten)]
pub metrics: CohortMetrics<M>,
pub metrics: BasicCohortMetrics<M>,
pub addr_count: ComputedFromHeightLast<StoredU64, M>,
pub addr_count_30d_change: ComputedFromHeightLast<StoredF64, M>,
@@ -43,7 +43,7 @@ impl AddressCohortVecs {
/// Import address cohort from database.
///
/// `all_supply` is the supply metrics from the "all" cohort, used as global
/// sources for `*_rel_to_market_cap` ratios. Pass `None` if not available.
/// sources for `*_rel_to_market_cap` ratios.
#[allow(clippy::too_many_arguments)]
pub(crate) fn forced_import(
db: &Database,
@@ -53,7 +53,7 @@ impl AddressCohortVecs {
indexes: &indexes::Vecs,
prices: &prices::Vecs,
states_path: Option<&Path>,
all_supply: Option<&SupplyMetrics>,
all_supply: &SupplyMetrics,
) -> Result<Self> {
let full_name = CohortContext::Address.full_name(&filter, name);
@@ -65,7 +65,6 @@ impl AddressCohortVecs {
version,
indexes,
prices,
up_to_1h_realized: None,
};
Ok(Self {
@@ -74,7 +73,7 @@ impl AddressCohortVecs {
state: states_path
.map(|path| Box::new(AddressCohortState::new(path, &full_name))),
metrics: CohortMetrics::forced_import(&cfg, all_supply)?,
metrics: BasicCohortMetrics::forced_import(&cfg, all_supply)?,
addr_count: ComputedFromHeightLast::forced_import(
db,
@@ -227,6 +226,35 @@ impl DynCohortVecs for AddressCohortVecs {
.compute_rest_part1(blocks, prices, starting_indexes, exit)?;
Ok(())
}
fn compute_net_sentiment_height(
&mut self,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
self.metrics
.compute_net_sentiment_height(starting_indexes, exit)
}
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.inner.write(height, cleanup)?;
}
Ok(())
}
fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.inner.reset_cost_basis_data_if_needed()?;
}
Ok(())
}
fn reset_single_iteration_values(&mut self) {
if let Some(state) = self.state.as_mut() {
state.inner.reset_single_iteration_values();
}
}
}
impl CohortVecs for AddressCohortVecs {
@@ -258,7 +286,7 @@ impl CohortVecs for AddressCohortVecs {
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: Option<&impl ReadableVec<Height, Dollars>>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.metrics.compute_rest_part2(

View File

@@ -38,9 +38,27 @@ pub trait DynCohortVecs: Send + Sync {
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()>;
/// Compute net_sentiment.height for separate cohorts (greed - pain).
fn compute_net_sentiment_height(
&mut self,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()>;
/// Write state checkpoint to disk.
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()>;
/// Reset cost basis data (called during fresh start).
fn reset_cost_basis_data_if_needed(&mut self) -> Result<()>;
/// Reset per-block iteration values.
fn reset_single_iteration_values(&mut self);
}
/// Static dispatch trait for cohort vectors with additional methods.
///
/// Used by address cohorts where all cohorts share the same concrete type.
pub trait CohortVecs: DynCohortVecs {
/// Compute aggregate cohort from component cohorts.
fn compute_from_stateful(
@@ -56,7 +74,7 @@ pub trait CohortVecs: DynCohortVecs {
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: Option<&impl ReadableVec<Height, Dollars>>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()>;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
use brk_types::{Cents, Height, Timestamp};
use vecdb::Rw;
use crate::distribution::state::Transacted;
use super::groups::UTXOCohorts;
impl UTXOCohorts {
impl UTXOCohorts<Rw> {
/// Process received outputs for this block.
///
/// New UTXOs are added to:
@@ -23,15 +24,9 @@ impl UTXOCohorts {
let supply_state = received.spendable_supply;
// New UTXOs go into up_to_1h, current epoch, and current year
[
&mut self.0.age_range.up_to_1h,
self.0.epoch.mut_vec_from_height(height),
self.0.year.mut_vec_from_timestamp(timestamp),
]
.into_iter()
.for_each(|v| {
v.state.as_mut().unwrap().receive_utxo(&supply_state, price);
});
self.age_range.up_to_1h.state.as_mut().unwrap().receive_utxo(&supply_state, price);
self.epoch.mut_vec_from_height(height).state.as_mut().unwrap().receive_utxo(&supply_state, price);
self.year.mut_vec_from_timestamp(timestamp).state.as_mut().unwrap().receive_utxo(&supply_state, price);
// Update output type cohorts
self.type_

View File

@@ -1,6 +1,6 @@
use brk_types::{Age, Height};
use rustc_hash::FxHashMap;
use vecdb::VecIndex;
use vecdb::{Rw, VecIndex};
use crate::distribution::{
compute::PriceRangeMax,
@@ -9,7 +9,7 @@ use crate::distribution::{
use super::groups::UTXOCohorts;
impl UTXOCohorts {
impl UTXOCohorts<Rw> {
/// Process spent inputs for this block.
///
/// Each input references a UTXO created at some previous height.
@@ -47,7 +47,7 @@ impl UTXOCohorts {
let peak_price = price_range_max.max_between(receive_height, send_height);
// Update age range cohort (direct index lookup)
self.0.age_range.get_mut(age).state.as_mut().unwrap().send_utxo(
self.age_range.get_mut(age).state.as_mut().unwrap().send_utxo(
&sent.spendable_supply,
current_price,
prev_price,
@@ -56,8 +56,7 @@ impl UTXOCohorts {
);
// Update epoch cohort (direct lookup by height)
self.0
.epoch
self.epoch
.mut_vec_from_height(receive_height)
.state
.as_mut().unwrap()
@@ -70,8 +69,7 @@ impl UTXOCohorts {
);
// Update year cohort (direct lookup by timestamp)
self.0
.year
self.year
.mut_vec_from_timestamp(block_state.timestamp)
.state
.as_mut().unwrap()
@@ -88,7 +86,7 @@ impl UTXOCohorts {
.spendable
.iter_typed()
.for_each(|(output_type, supply_state)| {
self.0.type_.get_mut(output_type).state.as_mut().unwrap().send_utxo(
self.type_.get_mut(output_type).state.as_mut().unwrap().send_utxo(
supply_state,
current_price,
prev_price,
@@ -101,7 +99,7 @@ impl UTXOCohorts {
sent.by_size_group
.iter_typed()
.for_each(|(group, supply_state)| {
self.0.amount_range.get_mut(group).state.as_mut().unwrap().send_utxo(
self.amount_range.get_mut(group).state.as_mut().unwrap().send_utxo(
supply_state,
current_price,
prev_price,

View File

@@ -1,11 +1,12 @@
use brk_cohort::AGE_BOUNDARIES;
use brk_types::{ONE_HOUR_IN_SEC, Timestamp};
use vecdb::Rw;
use crate::distribution::state::BlockState;
use super::groups::UTXOCohorts;
impl UTXOCohorts {
impl UTXOCohorts<Rw> {
/// Handle age transitions when processing a new block.
///
/// UTXOs age with each block. When they cross hour boundaries,
@@ -32,7 +33,7 @@ impl UTXOCohorts {
// Cohort i covers hours [BOUNDARIES[i-1], BOUNDARIES[i])
// Cohort 0 covers [0, 1) hours
// Cohort 20 covers [15*365*24, infinity) hours
let mut age_cohorts: Vec<_> = self.0.age_range.iter_mut().map(|v| &mut v.state).collect();
let mut age_cohorts: Vec<_> = self.age_range.iter_mut().map(|v| &mut v.state).collect();
// For each boundary (in hours), find blocks that just crossed it
for (boundary_idx, &boundary_hours) in AGE_BOUNDARIES.iter().enumerate() {

View File

@@ -1,22 +1,23 @@
use std::path::Path;
use brk_cohort::{CohortContext, Filter, Filtered, StateLevel};
use brk_cohort::{Filter, Filtered};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use brk_types::{Cents, Height, Version};
use vecdb::{Exit, ReadableVec};
use crate::{ComputeIndexes, blocks, distribution::state::UTXOCohortState, indexes, prices};
use crate::{ComputeIndexes, blocks, distribution::state::UTXOCohortState, prices};
use crate::distribution::metrics::{CohortMetrics, ImportConfig, RealizedMetrics, SupplyMetrics};
use crate::distribution::metrics::CohortMetricsBase;
use super::super::traits::{CohortVecs, DynCohortVecs};
use super::super::traits::DynCohortVecs;
/// UTXO cohort with metrics and optional runtime state.
///
/// Generic over the metrics type to support different cohort configurations
/// (e.g. AllCohortMetrics, ExtendedCohortMetrics, BasicCohortMetrics, etc.)
#[derive(Traversable)]
pub struct UTXOCohortVecs<M: StorageMode = Rw> {
pub struct UTXOCohortVecs<Metrics> {
/// Starting height when state was imported
#[traversable(skip)]
state_starting_height: Option<Height>,
/// Runtime state for block-by-block processing (separate cohorts only)
@@ -25,85 +26,28 @@ pub struct UTXOCohortVecs<M: StorageMode = Rw> {
/// Metric vectors
#[traversable(flatten)]
pub metrics: CohortMetrics<M>,
pub metrics: Metrics,
}
impl UTXOCohortVecs {
/// Import UTXO cohort from database.
///
/// `all_supply` is the supply metrics from the "all" cohort, used as global
/// sources for `*_rel_to_market_cap` ratios. Pass `None` for the "all" cohort itself.
///
/// `up_to_1h_realized` is used for cohorts where `compute_adjusted()` is true,
/// to create lazy adjusted vecs: adjusted = cohort - up_to_1h.
#[allow(clippy::too_many_arguments)]
pub(crate) fn forced_import(
db: &Database,
filter: Filter,
name: &str,
version: Version,
indexes: &indexes::Vecs,
prices: &prices::Vecs,
states_path: &Path,
state_level: StateLevel,
all_supply: Option<&SupplyMetrics>,
up_to_1h_realized: Option<&RealizedMetrics>,
) -> Result<Self> {
let full_name = CohortContext::Utxo.full_name(&filter, name);
let cfg = ImportConfig {
db,
filter,
full_name: &full_name,
context: CohortContext::Utxo,
version,
indexes,
prices,
up_to_1h_realized,
};
Ok(Self {
impl<Metrics> UTXOCohortVecs<Metrics> {
/// Create a new UTXOCohortVecs with state and metrics.
pub(crate) fn new(state: Option<Box<UTXOCohortState>>, metrics: Metrics) -> Self {
Self {
state_starting_height: None,
state: if state_level.is_full() {
Some(Box::new(UTXOCohortState::new(states_path, &full_name)))
} else {
None
},
metrics: CohortMetrics::forced_import(&cfg, all_supply)?,
})
}
/// Reset state starting height to zero and reset state values.
pub(crate) fn reset_state_starting_height(&mut self) {
self.state_starting_height = Some(Height::ZERO);
if let Some(state) = self.state.as_mut() {
state.reset();
state,
metrics,
}
}
/// Returns a parallel iterator over all vecs for parallel writing.
pub(crate) fn par_iter_vecs_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.metrics.par_iter_mut()
}
/// Commit state to disk (separate from vec writes for parallelization).
pub(crate) fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.write(height, cleanup)?;
}
Ok(())
}
}
impl Filtered for UTXOCohortVecs {
impl<Metrics: CohortMetricsBase + Traversable> Filtered for UTXOCohortVecs<Metrics> {
fn filter(&self) -> &Filter {
&self.metrics.filter
self.metrics.filter()
}
}
impl DynCohortVecs for UTXOCohortVecs {
impl<Metrics: CohortMetricsBase + Traversable> DynCohortVecs for UTXOCohortVecs<Metrics> {
fn min_stateful_height_len(&self) -> usize {
self.metrics.min_stateful_height_len()
}
@@ -116,18 +60,13 @@ impl DynCohortVecs for UTXOCohortVecs {
}
fn import_state(&mut self, starting_height: Height) -> Result<Height> {
// Import state from runtime state if present
if let Some(state) = self.state.as_mut() {
// State files are saved AT height H, so to resume at H+1 we need to import at H
// Decrement first, then increment result to match expected starting_height
if let Some(mut prev_height) = starting_height.decremented() {
// Import cost_basis_data state file (may adjust prev_height to actual file found)
prev_height = state.import_at_or_before(prev_height)?;
// Restore supply state from height-indexed vectors
state.supply.value = self
.metrics
.supply
.supply()
.total
.sats
.height
@@ -135,20 +74,18 @@ impl DynCohortVecs for UTXOCohortVecs {
.unwrap();
state.supply.utxo_count = *self
.metrics
.outputs
.outputs()
.utxo_count
.height
.collect_one(prev_height)
.unwrap();
// Restore realized cap from persisted exact values
state.restore_realized_cap();
let result = prev_height.incremented();
self.state_starting_height = Some(result);
Ok(result)
} else {
// starting_height is 0, nothing to import
self.state_starting_height = Some(Height::ZERO);
Ok(Height::ZERO)
}
@@ -167,7 +104,6 @@ impl DynCohortVecs for UTXOCohortVecs {
return Ok(());
}
// Push from state to metrics
if let Some(state) = self.state.as_ref() {
self.metrics.truncate_push(height, state)?;
}
@@ -181,11 +117,8 @@ impl DynCohortVecs for UTXOCohortVecs {
height_price: Cents,
) -> Result<()> {
if let Some(state) = self.state.as_mut() {
self.metrics.compute_then_truncate_push_unrealized_states(
height,
height_price,
state,
)?;
self.metrics
.compute_then_truncate_push_unrealized_states(height, height_price, state)?;
}
Ok(())
}
@@ -200,36 +133,33 @@ impl DynCohortVecs for UTXOCohortVecs {
self.metrics
.compute_rest_part1(blocks, prices, starting_indexes, exit)
}
}
impl CohortVecs for UTXOCohortVecs {
fn compute_from_stateful(
fn compute_net_sentiment_height(
&mut self,
starting_indexes: &ComputeIndexes,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.metrics.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &v.metrics).collect::<Vec<_>>(),
exit,
)
self.metrics
.compute_net_sentiment_height(starting_indexes, exit)
}
fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: Option<&impl ReadableVec<Height, Dollars>>,
exit: &Exit,
) -> Result<()> {
self.metrics.compute_rest_part2(
blocks,
prices,
starting_indexes,
height_to_market_cap,
exit,
)
fn write_state(&mut self, height: Height, cleanup: bool) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.write(height, cleanup)?;
}
Ok(())
}
fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
if let Some(state) = self.state.as_mut() {
state.reset_cost_basis_data_if_needed()?;
}
Ok(())
}
fn reset_single_iteration_values(&mut self) {
if let Some(state) = self.state.as_mut() {
state.reset_single_iteration_values();
}
}
}

View File

@@ -54,7 +54,7 @@ pub(crate) fn compute_rest_part2<HM>(
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: Option<&HM>,
height_to_market_cap: &HM,
exit: &Exit,
) -> Result<()>
where

View File

@@ -520,17 +520,13 @@ pub(crate) fn process_blocks(
/// Reset per-block values for all separate cohorts.
fn reset_block_values(utxo_cohorts: &mut UTXOCohorts, address_cohorts: &mut AddressCohorts) {
utxo_cohorts.iter_separate_mut().for_each(|v| {
if let Some(state) = v.state.as_mut() {
state.reset_single_iteration_values();
}
});
utxo_cohorts
.iter_separate_mut()
.for_each(|v| v.reset_single_iteration_values());
address_cohorts.iter_separate_mut().for_each(|v| {
if let Some(state) = v.state.as_mut() {
state.inner.reset_single_iteration_values();
}
});
address_cohorts
.iter_separate_mut()
.for_each(|v| v.reset_single_iteration_values());
}
/// Push cohort states to height-indexed vectors.

View File

@@ -0,0 +1,122 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase,
RealizedWithAdjusted, RelativeWithPeakRegret, SupplyMetrics, UnrealizedBase,
UnrealizedWithPeakRegret,
};
/// Cohort metrics with adjusted realized + peak regret (no extended).
/// Used by: max_age cohorts.
#[derive(Traversable)]
pub struct AdjustedCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
pub supply: Box<SupplyMetrics<M>>,
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithAdjusted<M>>,
pub cost_basis: Box<CostBasisBase<M>>,
pub unrealized: Box<UnrealizedWithPeakRegret<M>>,
pub relative: Box<RelativeWithPeakRegret>,
}
impl CohortMetricsBase for AdjustedCohortMetrics {
fn filter(&self) -> &Filter { &self.filter }
fn supply(&self) -> &SupplyMetrics { &self.supply }
fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply }
fn outputs(&self) -> &OutputsMetrics { &self.outputs }
fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs }
fn activity(&self) -> &ActivityMetrics { &self.activity }
fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity }
fn realized_base(&self) -> &RealizedBase { &self.realized }
fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized }
fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized }
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized }
fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis }
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self, height: Height, height_price: Cents, state: &mut CohortState,
) -> Result<()> {
state.apply_pending();
self.cost_basis.truncate_push_minmax(height, state)?;
let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None);
self.unrealized.base.truncate_push(height, &height_unrealized_state)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.collect_vecs_mut());
vecs.extend(self.unrealized.base.collect_vecs_mut());
vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut());
vecs
}
}
impl AdjustedCohortMetrics {
pub(crate) fn forced_import(
cfg: &ImportConfig,
all_supply: &SupplyMetrics,
up_to_1h: &RealizedBase,
) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?;
let realized = RealizedWithAdjusted::forced_import(cfg, up_to_1h)?;
let relative = RelativeWithPeakRegret::forced_import(
cfg,
&unrealized.base,
&supply,
all_supply,
&realized.base,
&unrealized.peak_regret_ext.peak_regret,
);
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityMetrics::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisBase::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
})
}
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
exit,
)
}
}

View File

@@ -0,0 +1,128 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig,
OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeForAll, SupplyMetrics,
UnrealizedBase, UnrealizedWithPeakRegret,
};
/// All-cohort metrics: extended + adjusted realized, extended cost basis,
/// peak regret, relative for-all (no rel_to_all).
/// Used by: the "all" cohort.
#[derive(Traversable)]
pub struct AllCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
pub supply: Box<SupplyMetrics<M>>,
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithExtendedAdjusted<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub unrealized: Box<UnrealizedWithPeakRegret<M>>,
pub relative: Box<RelativeForAll>,
}
impl CohortMetricsBase for AllCohortMetrics {
fn filter(&self) -> &Filter { &self.filter }
fn supply(&self) -> &SupplyMetrics { &self.supply }
fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply }
fn outputs(&self) -> &OutputsMetrics { &self.outputs }
fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs }
fn activity(&self) -> &ActivityMetrics { &self.activity }
fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity }
fn realized_base(&self) -> &RealizedBase { &self.realized }
fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized }
fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized }
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized }
fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis }
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
self.cost_basis.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self, height: Height, height_price: Cents, state: &mut CohortState,
) -> Result<()> {
state.apply_pending();
self.cost_basis.truncate_push_minmax(height, state)?;
let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None);
self.unrealized.base.truncate_push(height, &height_unrealized_state)?;
let spot = height_price.to_dollars();
self.cost_basis.extended.truncate_push_percentiles(height, state, spot)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.base.collect_vecs_mut());
vecs.extend(self.cost_basis.extended.collect_vecs_mut());
vecs.extend(self.unrealized.base.collect_vecs_mut());
vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut());
vecs
}
}
impl AllCohortMetrics {
/// Import the "all" cohort metrics with a pre-imported supply.
///
/// Supply is imported first (before other cohorts) so it can be used as `all_supply`
/// reference for relative metric lazy vecs in other cohorts.
pub(crate) fn forced_import_with_supply(
cfg: &ImportConfig,
supply: SupplyMetrics,
up_to_1h: &RealizedBase,
) -> Result<Self> {
let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?;
let realized = RealizedWithExtendedAdjusted::forced_import(cfg, up_to_1h)?;
let relative = RelativeForAll::forced_import(
cfg,
&unrealized.base,
&supply,
&realized.base,
&unrealized.peak_regret_ext.peak_regret,
);
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityMetrics::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
})
}
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
exit,
)
}
}

View File

@@ -0,0 +1,155 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase,
RelativeWithRelToAll, SupplyMetrics, UnrealizedBase,
};
/// Basic cohort metrics: no extensions, with relative (rel_to_all).
/// Used by: epoch, year, type (spendable), amount, address cohorts.
#[derive(Traversable)]
pub struct BasicCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
pub supply: Box<SupplyMetrics<M>>,
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedBase<M>>,
pub cost_basis: Box<CostBasisBase<M>>,
pub unrealized: Box<UnrealizedBase<M>>,
pub relative: Box<RelativeWithRelToAll>,
}
impl CohortMetricsBase for BasicCohortMetrics {
fn filter(&self) -> &Filter { &self.filter }
fn supply(&self) -> &SupplyMetrics { &self.supply }
fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply }
fn outputs(&self) -> &OutputsMetrics { &self.outputs }
fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs }
fn activity(&self) -> &ActivityMetrics { &self.activity }
fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity }
fn realized_base(&self) -> &RealizedBase { &self.realized }
fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized }
fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized }
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized }
fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis }
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self, height: Height, height_price: Cents, state: &mut CohortState,
) -> Result<()> {
state.apply_pending();
self.cost_basis.truncate_push_minmax(height, state)?;
let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None);
self.unrealized.truncate_push(height, &height_unrealized_state)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.collect_vecs_mut());
vecs.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
impl BasicCohortMetrics {
pub(crate) fn forced_import(
cfg: &ImportConfig,
all_supply: &SupplyMetrics,
) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedBase::forced_import(cfg)?;
let realized = RealizedBase::forced_import(cfg)?;
let relative = RelativeWithRelToAll::forced_import(
cfg, &unrealized, &supply, all_supply, &realized,
);
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityMetrics::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisBase::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
})
}
pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.collect_all_vecs_mut().into_par_iter()
}
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2_base(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
exit,
)
}
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &ComputeIndexes,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.supply.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.supply).collect::<Vec<_>>(),
exit,
)?;
self.outputs.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.outputs).collect::<Vec<_>>(),
exit,
)?;
self.activity.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.activity).collect::<Vec<_>>(),
exit,
)?;
self.realized.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.realized).collect::<Vec<_>>(),
exit,
)?;
self.unrealized.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.unrealized).collect::<Vec<_>>(),
exit,
)?;
self.cost_basis.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.cost_basis).collect::<Vec<_>>(),
exit,
)?;
Ok(())
}
}

View File

@@ -0,0 +1,125 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig,
OutputsMetrics, RealizedBase, RealizedWithExtended, RelativeWithExtended, SupplyMetrics,
UnrealizedBase, UnrealizedWithPeakRegret,
};
/// Cohort metrics with extended realized + extended cost basis + peak regret (no adjusted).
/// Used by: lth, age_range cohorts.
#[derive(Traversable)]
pub struct ExtendedCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
pub supply: Box<SupplyMetrics<M>>,
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithExtended<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub unrealized: Box<UnrealizedWithPeakRegret<M>>,
pub relative: Box<RelativeWithExtended>,
}
impl CohortMetricsBase for ExtendedCohortMetrics {
fn filter(&self) -> &Filter { &self.filter }
fn supply(&self) -> &SupplyMetrics { &self.supply }
fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply }
fn outputs(&self) -> &OutputsMetrics { &self.outputs }
fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs }
fn activity(&self) -> &ActivityMetrics { &self.activity }
fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity }
fn realized_base(&self) -> &RealizedBase { &self.realized }
fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized }
fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized }
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized }
fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis }
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
self.cost_basis.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self, height: Height, height_price: Cents, state: &mut CohortState,
) -> Result<()> {
state.apply_pending();
self.cost_basis.truncate_push_minmax(height, state)?;
let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None);
self.unrealized.base.truncate_push(height, &height_unrealized_state)?;
let spot = height_price.to_dollars();
self.cost_basis.extended.truncate_push_percentiles(height, state, spot)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.base.collect_vecs_mut());
vecs.extend(self.cost_basis.extended.collect_vecs_mut());
vecs.extend(self.unrealized.base.collect_vecs_mut());
vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut());
vecs
}
}
impl ExtendedCohortMetrics {
pub(crate) fn forced_import(
cfg: &ImportConfig,
all_supply: &SupplyMetrics,
) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?;
let realized = RealizedWithExtended::forced_import(cfg)?;
let relative = RelativeWithExtended::forced_import(
cfg,
&unrealized.base,
&supply,
all_supply,
&realized.base,
&unrealized.peak_regret_ext.peak_regret,
);
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityMetrics::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
})
}
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
exit,
)
}
}

View File

@@ -0,0 +1,125 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig,
OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeWithExtended,
SupplyMetrics, UnrealizedBase, UnrealizedWithPeakRegret,
};
/// Cohort metrics with extended + adjusted realized, extended cost basis, peak regret.
/// Used by: sth cohort.
#[derive(Traversable)]
pub struct ExtendedAdjustedCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
pub supply: Box<SupplyMetrics<M>>,
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithExtendedAdjusted<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub unrealized: Box<UnrealizedWithPeakRegret<M>>,
pub relative: Box<RelativeWithExtended>,
}
impl CohortMetricsBase for ExtendedAdjustedCohortMetrics {
fn filter(&self) -> &Filter { &self.filter }
fn supply(&self) -> &SupplyMetrics { &self.supply }
fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply }
fn outputs(&self) -> &OutputsMetrics { &self.outputs }
fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs }
fn activity(&self) -> &ActivityMetrics { &self.activity }
fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity }
fn realized_base(&self) -> &RealizedBase { &self.realized }
fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized }
fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized }
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized }
fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis }
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
self.cost_basis.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self, height: Height, height_price: Cents, state: &mut CohortState,
) -> Result<()> {
state.apply_pending();
self.cost_basis.truncate_push_minmax(height, state)?;
let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None);
self.unrealized.base.truncate_push(height, &height_unrealized_state)?;
let spot = height_price.to_dollars();
self.cost_basis.extended.truncate_push_percentiles(height, state, spot)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.base.collect_vecs_mut());
vecs.extend(self.cost_basis.extended.collect_vecs_mut());
vecs.extend(self.unrealized.base.collect_vecs_mut());
vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut());
vecs
}
}
impl ExtendedAdjustedCohortMetrics {
pub(crate) fn forced_import(
cfg: &ImportConfig,
all_supply: &SupplyMetrics,
up_to_1h: &RealizedBase,
) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?;
let realized = RealizedWithExtendedAdjusted::forced_import(cfg, up_to_1h)?;
let relative = RelativeWithExtended::forced_import(
cfg,
&unrealized.base,
&supply,
all_supply,
&realized.base,
&unrealized.peak_regret_ext.peak_regret,
);
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityMetrics::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisWithExtended::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
})
}
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
exit,
)
}
}

View File

@@ -0,0 +1,15 @@
mod adjusted;
mod all;
mod basic;
mod extended;
mod extended_adjusted;
mod peak_regret;
pub use adjusted::*;
pub use all::*;
pub use basic::*;
pub use extended::*;
pub use extended_adjusted::*;
pub use peak_regret::*;

View File

@@ -0,0 +1,120 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase,
RelativeWithPeakRegret, SupplyMetrics, UnrealizedBase, UnrealizedWithPeakRegret,
};
/// Cohort metrics with peak regret unrealized + relative (no extended, no adjusted).
/// Used by: min_age cohorts.
#[derive(Traversable)]
pub struct PeakRegretCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
pub supply: Box<SupplyMetrics<M>>,
pub outputs: Box<OutputsMetrics<M>>,
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedBase<M>>,
pub cost_basis: Box<CostBasisBase<M>>,
pub unrealized: Box<UnrealizedWithPeakRegret<M>>,
pub relative: Box<RelativeWithPeakRegret>,
}
impl CohortMetricsBase for PeakRegretCohortMetrics {
fn filter(&self) -> &Filter { &self.filter }
fn supply(&self) -> &SupplyMetrics { &self.supply }
fn supply_mut(&mut self) -> &mut SupplyMetrics { &mut self.supply }
fn outputs(&self) -> &OutputsMetrics { &self.outputs }
fn outputs_mut(&mut self) -> &mut OutputsMetrics { &mut self.outputs }
fn activity(&self) -> &ActivityMetrics { &self.activity }
fn activity_mut(&mut self) -> &mut ActivityMetrics { &mut self.activity }
fn realized_base(&self) -> &RealizedBase { &self.realized }
fn realized_base_mut(&mut self) -> &mut RealizedBase { &mut self.realized }
fn unrealized_base(&self) -> &UnrealizedBase { &self.unrealized }
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase { &mut self.unrealized }
fn cost_basis_base(&self) -> &CostBasisBase { &self.cost_basis }
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase { &mut self.cost_basis }
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
Ok(())
}
fn compute_then_truncate_push_unrealized_states(
&mut self, height: Height, height_price: Cents, state: &mut CohortState,
) -> Result<()> {
state.apply_pending();
self.cost_basis.truncate_push_minmax(height, state)?;
let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None);
self.unrealized.base.truncate_push(height, &height_unrealized_state)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.collect_vecs_mut());
vecs.extend(self.cost_basis.collect_vecs_mut());
vecs.extend(self.unrealized.base.collect_vecs_mut());
vecs.extend(self.unrealized.peak_regret_ext.collect_vecs_mut());
vecs
}
}
impl PeakRegretCohortMetrics {
pub(crate) fn forced_import(
cfg: &ImportConfig,
all_supply: &SupplyMetrics,
) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?;
let realized = RealizedBase::forced_import(cfg)?;
let relative = RelativeWithPeakRegret::forced_import(
cfg,
&unrealized.base,
&supply,
all_supply,
&realized,
&unrealized.peak_regret_ext.peak_regret,
);
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
outputs: Box::new(OutputsMetrics::forced_import(cfg)?),
activity: Box::new(ActivityMetrics::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisBase::forced_import(cfg)?),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
})
}
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2_base(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
exit,
)
}
}

View File

@@ -4,8 +4,6 @@ use vecdb::Database;
use crate::{indexes, prices};
use super::RealizedMetrics;
/// Configuration for importing metrics.
pub struct ImportConfig<'a> {
pub db: &'a Database,
@@ -15,9 +13,6 @@ pub struct ImportConfig<'a> {
pub version: Version,
pub indexes: &'a indexes::Vecs,
pub prices: &'a prices::Vecs,
/// Source for lazy adjusted computation: adjusted = cohort - up_to_1h.
/// Required for cohorts where `compute_adjusted()` is true.
pub up_to_1h_realized: Option<&'a RealizedMetrics>,
}
impl<'a> ImportConfig<'a> {
@@ -26,21 +21,6 @@ impl<'a> ImportConfig<'a> {
self.filter.is_extended(self.context)
}
/// Whether to compute relative-to-all metrics.
pub(crate) fn compute_rel_to_all(&self) -> bool {
self.filter.compute_rel_to_all()
}
/// Whether to compute adjusted metrics (SOPR, etc.).
pub(crate) fn compute_adjusted(&self) -> bool {
self.filter.compute_adjusted(self.context)
}
/// Whether to compute relative metrics (invested capital %, NUPL ratios, etc.).
pub(crate) fn compute_relative(&self) -> bool {
self.filter.compute_relative()
}
/// Get full metric name with filter prefix.
pub(crate) fn name(&self, suffix: &str) -> String {
if self.full_name.is_empty() {
@@ -52,12 +32,4 @@ impl<'a> ImportConfig<'a> {
}
}
/// Whether this cohort needs peak_regret metric.
/// True for UTXO cohorts with age-based filters (all, term, time).
/// age_range cohorts compute directly, others aggregate from age_range.
pub(crate) fn compute_peak_regret(&self) -> bool {
matches!(self.context, CohortContext::Utxo)
&& matches!(self.filter, Filter::All | Filter::Term(_) | Filter::Time(_))
}
}

View File

@@ -1,248 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, StoredF32, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{
ComputeIndexes,
distribution::state::CohortState,
internal::{
ComputedFromHeightLast, PERCENTILES_LEN, Price, PriceFromHeight, PercentilesVecs,
compute_spot_percentile_rank,
},
};
use super::ImportConfig;
/// Cost basis metrics.
#[derive(Traversable)]
pub struct CostBasisMetrics<M: StorageMode = Rw> {
/// Minimum cost basis for any UTXO at this height
pub min: Price<ComputedFromHeightLast<Dollars, M>>,
/// Maximum cost basis for any UTXO at this height
pub max: Price<ComputedFromHeightLast<Dollars, M>>,
/// Cost basis percentiles (sat-weighted)
pub percentiles: Option<PercentilesVecs<M>>,
/// Invested capital percentiles (USD-weighted)
pub invested_capital: Option<PercentilesVecs<M>>,
/// What percentile of cost basis is below spot (sat-weighted)
pub spot_cost_basis_percentile: Option<ComputedFromHeightLast<StoredF32, M>>,
/// What percentile of invested capital is below spot (USD-weighted)
pub spot_invested_capital_percentile: Option<ComputedFromHeightLast<StoredF32, M>>,
}
impl CostBasisMetrics {
/// Import cost basis metrics from database.
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let extended = cfg.extended();
Ok(Self {
min: PriceFromHeight::forced_import(
cfg.db,
&cfg.name("min_cost_basis"),
cfg.version,
cfg.indexes,
)?,
max: PriceFromHeight::forced_import(
cfg.db,
&cfg.name("max_cost_basis"),
cfg.version,
cfg.indexes,
)?,
percentiles: extended
.then(|| {
PercentilesVecs::forced_import(
cfg.db,
&cfg.name("cost_basis"),
cfg.version,
cfg.indexes,
true,
)
})
.transpose()?,
invested_capital: extended
.then(|| {
PercentilesVecs::forced_import(
cfg.db,
&cfg.name("invested_capital"),
cfg.version,
cfg.indexes,
true,
)
})
.transpose()?,
spot_cost_basis_percentile: extended
.then(|| {
ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("spot_cost_basis_percentile"),
cfg.version,
cfg.indexes,
)
})
.transpose()?,
spot_invested_capital_percentile: extended
.then(|| {
ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("spot_invested_capital_percentile"),
cfg.version,
cfg.indexes,
)
})
.transpose()?,
})
}
/// Get minimum length across height-indexed vectors written in block loop.
pub(crate) fn min_stateful_height_len(&self) -> usize {
let mut min = self.min.height.len().min(self.max.height.len());
if let Some(v) = &self.spot_cost_basis_percentile {
min = min.min(v.height.len());
}
if let Some(v) = &self.spot_invested_capital_percentile {
min = min.min(v.height.len());
}
if let Some(p) = &self.percentiles {
min = min.min(p.min_stateful_height_len());
}
if let Some(p) = &self.invested_capital {
min = min.min(p.min_stateful_height_len());
}
min
}
/// Push min/max cost basis from state.
pub(crate) fn truncate_push_minmax(&mut self, height: Height, state: &CohortState) -> Result<()> {
self.min.height.truncate_push(
height,
state
.cost_basis_data_first_key_value()
.map(|(cents, _)| cents.into())
.unwrap_or(Dollars::NAN),
)?;
self.max.height.truncate_push(
height,
state
.cost_basis_data_last_key_value()
.map(|(cents, _)| cents.into())
.unwrap_or(Dollars::NAN),
)?;
Ok(())
}
/// Push cost basis percentiles and spot ranks at every height.
pub(crate) fn truncate_push_percentiles(
&mut self,
height: Height,
state: &mut CohortState,
spot: Dollars,
) -> Result<()> {
let computed = state.compute_percentiles();
// Sat-weighted percentiles and spot rank
let sat_prices = computed
.as_ref()
.map(|p| p.sat_weighted.map(|c| c.to_dollars()))
.unwrap_or([Dollars::NAN; PERCENTILES_LEN]);
if let Some(percentiles) = self.percentiles.as_mut() {
percentiles.truncate_push(height, &sat_prices)?;
}
if let Some(spot_pct) = self.spot_cost_basis_percentile.as_mut() {
let rank = compute_spot_percentile_rank(&sat_prices, spot);
spot_pct.height.truncate_push(height, rank)?;
}
// USD-weighted percentiles and spot rank
let usd_prices = computed
.as_ref()
.map(|p| p.usd_weighted.map(|c| c.to_dollars()))
.unwrap_or([Dollars::NAN; PERCENTILES_LEN]);
if let Some(invested_capital) = self.invested_capital.as_mut() {
invested_capital.truncate_push(height, &usd_prices)?;
}
if let Some(spot_pct) = self.spot_invested_capital_percentile.as_mut() {
let rank = compute_spot_percentile_rank(&usd_prices, spot);
spot_pct.height.truncate_push(height, rank)?;
}
Ok(())
}
/// Returns a parallel iterator over all vecs for parallel writing.
pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = vec![&mut self.min.height, &mut self.max.height];
if let Some(percentiles) = self.percentiles.as_mut() {
vecs.extend(
percentiles
.vecs
.iter_mut()
.flatten()
.map(|v| &mut v.height as &mut dyn AnyStoredVec),
);
}
if let Some(invested_capital) = self.invested_capital.as_mut() {
vecs.extend(
invested_capital
.vecs
.iter_mut()
.flatten()
.map(|v| &mut v.height as &mut dyn AnyStoredVec),
);
}
if let Some(v) = self.spot_cost_basis_percentile.as_mut() {
vecs.push(&mut v.height);
}
if let Some(v) = self.spot_invested_capital_percentile.as_mut() {
vecs.push(&mut v.height);
}
vecs.into_par_iter()
}
/// Validate computed versions or reset if mismatched.
pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
if let Some(percentiles) = self.percentiles.as_mut() {
percentiles.validate_computed_version_or_reset(base_version)?;
}
if let Some(invested_capital) = self.invested_capital.as_mut() {
invested_capital.validate_computed_version_or_reset(base_version)?;
}
if let Some(v) = self.spot_cost_basis_percentile.as_mut() {
v.height
.validate_computed_version_or_reset(base_version)?;
}
if let Some(v) = self.spot_invested_capital_percentile.as_mut() {
v.height
.validate_computed_version_or_reset(base_version)?;
}
Ok(())
}
/// Compute aggregate values from separate cohorts.
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &ComputeIndexes,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.min.height.compute_min_of_others(
starting_indexes.height,
&others.iter().map(|v| &v.min.height).collect::<Vec<_>>(),
exit,
)?;
self.max.height.compute_max_of_others(
starting_indexes.height,
&others.iter().map(|v| &v.max.height).collect::<Vec<_>>(),
exit,
)?;
Ok(())
}
}

View File

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

View File

@@ -0,0 +1,130 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, StoredF32, Version};
use vecdb::{AnyStoredVec, Rw, StorageMode, WritableVec};
use crate::{
distribution::state::CohortState,
internal::{
ComputedFromHeightLast, PERCENTILES_LEN, PercentilesVecs, compute_spot_percentile_rank,
},
};
use crate::distribution::metrics::ImportConfig;
/// Extended cost basis metrics (only for extended cohorts).
#[derive(Traversable)]
pub struct CostBasisExtended<M: StorageMode = Rw> {
/// Cost basis percentiles (sat-weighted)
pub percentiles: PercentilesVecs<M>,
/// Invested capital percentiles (USD-weighted)
pub invested_capital: PercentilesVecs<M>,
/// What percentile of cost basis is below spot (sat-weighted)
pub spot_cost_basis_percentile: ComputedFromHeightLast<StoredF32, M>,
/// What percentile of invested capital is below spot (USD-weighted)
pub spot_invested_capital_percentile: ComputedFromHeightLast<StoredF32, 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,
true,
)?,
invested_capital: PercentilesVecs::forced_import(
cfg.db,
&cfg.name("invested_capital"),
cfg.version,
cfg.indexes,
true,
)?,
spot_cost_basis_percentile: ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("spot_cost_basis_percentile"),
cfg.version,
cfg.indexes,
)?,
spot_invested_capital_percentile: ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("spot_invested_capital_percentile"),
cfg.version,
cfg.indexes,
)?,
})
}
pub(crate) fn truncate_push_percentiles(
&mut self,
height: Height,
state: &mut CohortState,
spot: Dollars,
) -> Result<()> {
let computed = state.compute_percentiles();
let sat_prices = computed
.as_ref()
.map(|p| p.sat_weighted.map(|c| c.to_dollars()))
.unwrap_or([Dollars::NAN; PERCENTILES_LEN]);
self.percentiles.truncate_push(height, &sat_prices)?;
let rank = compute_spot_percentile_rank(&sat_prices, spot);
self.spot_cost_basis_percentile
.height
.truncate_push(height, rank)?;
let usd_prices = computed
.as_ref()
.map(|p| p.usd_weighted.map(|c| c.to_dollars()))
.unwrap_or([Dollars::NAN; PERCENTILES_LEN]);
self.invested_capital.truncate_push(height, &usd_prices)?;
let rank = compute_spot_percentile_rank(&usd_prices, spot);
self.spot_invested_capital_percentile
.height
.truncate_push(height, rank)?;
Ok(())
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(
self.percentiles
.vecs
.iter_mut()
.flatten()
.map(|v| &mut v.height as &mut dyn AnyStoredVec),
);
vecs.extend(
self.invested_capital
.vecs
.iter_mut()
.flatten()
.map(|v| &mut v.height as &mut dyn AnyStoredVec),
);
vecs.push(&mut self.spot_cost_basis_percentile.height);
vecs.push(&mut self.spot_invested_capital_percentile.height);
vecs
}
pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.percentiles
.validate_computed_version_or_reset(base_version)?;
self.invested_capital
.validate_computed_version_or_reset(base_version)?;
self.spot_cost_basis_percentile
.height
.validate_computed_version_or_reset(base_version)?;
self.spot_invested_capital_percentile
.height
.validate_computed_version_or_reset(base_version)?;
Ok(())
}
}

View File

@@ -0,0 +1,9 @@
mod base;
mod extended;
mod with_extended;
pub use base::*;
pub use extended::*;
pub use with_extended::*;

View File

@@ -0,0 +1,35 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use derive_more::{Deref, DerefMut};
use vecdb::{Rw, StorageMode};
use crate::distribution::metrics::ImportConfig;
use super::{CostBasisBase, CostBasisExtended};
/// Cost basis metrics with guaranteed extended (no Option).
#[derive(Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct CostBasisWithExtended<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 validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.extended.validate_computed_versions(base_version)
}
}

View File

@@ -1,4 +1,5 @@
mod activity;
mod cohort;
mod config;
mod cost_basis;
mod outputs;
@@ -8,6 +9,7 @@ mod supply;
mod unrealized;
pub use activity::*;
pub use cohort::*;
pub use config::*;
pub use cost_basis::*;
pub use outputs::*;
@@ -18,220 +20,123 @@ pub use unrealized::*;
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use brk_types::{Cents, Height, Version};
use vecdb::{AnyStoredVec, Exit};
use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
/// All metrics for a cohort, organized by category.
#[derive(Traversable)]
pub struct CohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
pub filter: Filter,
/// Trait defining the interface for cohort metrics containers.
///
/// Provides typed accessor methods for base sub-metric components, default
/// implementations for shared operations that only use base fields, and
/// required methods for operations that vary by extension level.
pub trait CohortMetricsBase: Send + Sync {
fn filter(&self) -> &Filter;
fn supply(&self) -> &SupplyMetrics;
fn supply_mut(&mut self) -> &mut SupplyMetrics;
fn outputs(&self) -> &OutputsMetrics;
fn outputs_mut(&mut self) -> &mut OutputsMetrics;
fn activity(&self) -> &ActivityMetrics;
fn activity_mut(&mut self) -> &mut ActivityMetrics;
fn realized_base(&self) -> &RealizedBase;
fn realized_base_mut(&mut self) -> &mut RealizedBase;
fn unrealized_base(&self) -> &UnrealizedBase;
fn unrealized_base_mut(&mut self) -> &mut UnrealizedBase;
fn cost_basis_base(&self) -> &CostBasisBase;
fn cost_basis_base_mut(&mut self) -> &mut CostBasisBase;
/// Supply metrics (always computed)
pub supply: Box<SupplyMetrics<M>>,
// === Required methods (vary by extension level) ===
/// Output metrics - UTXO count (always computed)
pub outputs: Box<OutputsMetrics<M>>,
/// Validate computed versions against base version.
/// Extended types also validate cost_basis extended versions.
fn validate_computed_versions(&mut self, base_version: Version) -> Result<()>;
/// Transaction activity (always computed)
pub activity: Box<ActivityMetrics<M>>,
/// Compute and push unrealized states.
/// Extended types also push cost_basis percentiles.
fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState,
) -> Result<()>;
/// Realized cap and profit/loss
pub realized: Box<RealizedMetrics<M>>,
/// Collect all stored vecs for parallel writing.
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec>;
/// Unrealized profit/loss
pub unrealized: Box<UnrealizedMetrics<M>>,
/// Cost basis metrics
pub cost_basis: Box<CostBasisMetrics<M>>,
/// Relative metrics (not all cohorts compute this)
pub relative: Option<Box<RelativeMetrics>>,
}
impl CohortMetrics {
/// Import all metrics from database.
///
/// `all_supply` is the supply metrics from the "all" cohort, used as global
/// sources for `*_rel_to_market_cap` and `*_rel_to_circulating_supply` ratios.
/// Pass `None` for the "all" cohort itself.
pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: Option<&SupplyMetrics>) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let outputs = OutputsMetrics::forced_import(cfg)?;
let unrealized = UnrealizedMetrics::forced_import(cfg)?;
let realized = RealizedMetrics::forced_import(cfg)?;
let relative = cfg
.compute_relative()
.then(|| {
RelativeMetrics::forced_import(
cfg,
&unrealized,
&supply,
all_supply,
Some(&realized),
)
})
.transpose()?;
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
outputs: Box::new(outputs),
activity: Box::new(ActivityMetrics::forced_import(cfg)?),
realized: Box::new(realized),
cost_basis: Box::new(CostBasisMetrics::forced_import(cfg)?),
relative: relative.map(Box::new),
unrealized: Box::new(unrealized),
})
}
// === Default methods (shared across all cohort metric types, use base fields only) ===
/// Get minimum length across height-indexed vectors written in block loop.
pub(crate) fn min_stateful_height_len(&self) -> usize {
self.supply
fn min_stateful_height_len(&self) -> usize {
self.supply()
.min_len()
.min(self.outputs.min_len())
.min(self.activity.min_len())
.min(self.realized.min_stateful_height_len())
.min(self.unrealized.min_stateful_height_len())
.min(self.cost_basis.min_stateful_height_len())
.min(self.outputs().min_len())
.min(self.activity().min_len())
.min(self.realized_base().min_stateful_height_len())
.min(self.unrealized_base().min_stateful_height_len())
.min(self.cost_basis_base().min_stateful_height_len())
}
/// Push state values to height-indexed vectors.
pub(crate) fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> {
self.supply.truncate_push(height, state.supply.value)?;
self.outputs
fn truncate_push(&mut self, height: Height, state: &CohortState) -> Result<()> {
self.supply_mut()
.truncate_push(height, state.supply.value)?;
self.outputs_mut()
.truncate_push(height, state.supply.utxo_count)?;
self.activity.truncate_push(
self.activity_mut().truncate_push(
height,
state.sent,
state.satblocks_destroyed,
state.satdays_destroyed,
)?;
self.realized.truncate_push(height, &state.realized)?;
self.realized_base_mut()
.truncate_push(height, &state.realized)?;
Ok(())
}
/// Returns a parallel iterator over all vecs for parallel writing.
pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
vecs.extend(self.supply.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.outputs.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.activity.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.realized.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.unrealized.par_iter_mut().collect::<Vec<_>>());
vecs.extend(self.cost_basis.par_iter_mut().collect::<Vec<_>>());
vecs.into_par_iter()
}
/// Validate computed versions against base version.
pub(crate) fn validate_computed_versions(&mut self, base_version: Version) -> Result<()> {
self.supply.validate_computed_versions(base_version)?;
self.activity.validate_computed_versions(base_version)?;
self.realized.validate_computed_versions(base_version)?;
self.cost_basis.validate_computed_versions(base_version)?;
Ok(())
}
/// Compute and push unrealized states and percentiles.
pub(crate) fn compute_then_truncate_push_unrealized_states(
&mut self,
height: Height,
height_price: Cents,
state: &mut CohortState,
) -> Result<()> {
// Apply pending updates before reading
state.apply_pending();
self.cost_basis.truncate_push_minmax(height, state)?;
let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None);
self.unrealized
.truncate_push(height, &height_unrealized_state)?;
let spot = height_price.to_dollars();
self.cost_basis
.truncate_push_percentiles(height, state, spot)?;
Ok(())
}
/// Compute aggregate cohort values from separate cohorts.
pub(crate) fn compute_from_stateful(
/// Compute net_sentiment.height as capital-weighted average of component cohorts (same type).
fn compute_net_sentiment_from_others(
&mut self,
starting_indexes: &ComputeIndexes,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.supply.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.supply).collect::<Vec<_>>(),
exit,
)?;
self.outputs.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.outputs).collect::<Vec<_>>(),
exit,
)?;
self.activity.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.activity).collect::<Vec<_>>(),
exit,
)?;
) -> Result<()>
where
Self: Sized,
{
let weights: Vec<_> = others
.iter()
.map(|o| &o.realized_base().realized_cap.height)
.collect();
let values: Vec<_> = others
.iter()
.map(|o| &o.unrealized_base().net_sentiment.height)
.collect();
self.realized.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.realized).collect::<Vec<_>>(),
exit,
)?;
self.unrealized.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.unrealized).collect::<Vec<_>>(),
exit,
)?;
self.cost_basis.compute_from_stateful(
starting_indexes,
&others.iter().map(|v| &*v.cost_basis).collect::<Vec<_>>(),
exit,
)?;
self.unrealized_base_mut()
.net_sentiment
.height
.compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?;
Ok(())
}
/// Compute net_sentiment.height as capital-weighted average of component cohorts.
///
/// For aggregate cohorts, the simple greed-pain formula produces values outside
/// the range of components due to asymmetric weighting. This computes net_sentiment
/// as a proper weighted average using realized_cap as weight.
///
/// Only computes height; day1 derivation is done separately via compute_net_sentiment_rest.
pub(crate) fn compute_net_sentiment_from_others(
/// Compute net_sentiment.height as capital-weighted average from heterogeneous sources.
fn compute_net_sentiment_from_others_dyn(
&mut self,
starting_indexes: &ComputeIndexes,
others: &[&Self],
others: &[&dyn CohortMetricsBase],
exit: &Exit,
) -> Result<()> {
let weights: Vec<_> = others
.iter()
.map(|o| &o.realized.realized_cap.height)
.map(|o| &o.realized_base().realized_cap.height)
.collect();
let values: Vec<_> = others
.iter()
.map(|o| &o.unrealized.net_sentiment.height)
.map(|o| &o.unrealized_base().net_sentiment.height)
.collect();
self.unrealized
self.unrealized_base_mut()
.net_sentiment
.height
.compute_weighted_average_of_others(starting_indexes.height, &weights, &values, exit)?;
@@ -240,55 +145,81 @@ impl CohortMetrics {
}
/// First phase of computed metrics (indexes from height).
pub(crate) fn compute_rest_part1(
fn compute_rest_part1(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
self.supply.compute_rest_part1(blocks, starting_indexes, exit)?;
self.outputs.compute_rest(blocks, starting_indexes, exit)?;
self.activity.compute_rest_part1(blocks, starting_indexes, exit)?;
self.supply_mut()
.compute_rest_part1(blocks, starting_indexes, exit)?;
self.outputs_mut()
.compute_rest(blocks, starting_indexes, exit)?;
self.activity_mut()
.compute_rest_part1(blocks, starting_indexes, exit)?;
self.realized.compute_rest_part1(starting_indexes, exit)?;
self.realized_base_mut()
.compute_rest_part1(starting_indexes, exit)?;
self.unrealized
self.unrealized_base_mut()
.compute_rest(prices, starting_indexes, exit)?;
Ok(())
}
/// Second phase of computed metrics (ratios, relative values).
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: Option<&impl ReadableVec<Height, Dollars>>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
exit,
)?;
Ok(())
}
/// Compute net_sentiment.height for separate cohorts (greed - pain).
/// Called only for separate cohorts; aggregates compute via weighted average in compute_from_stateful.
pub(crate) fn compute_net_sentiment_height(
fn compute_net_sentiment_height(
&mut self,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
self.unrealized
self.unrealized_base_mut()
.compute_net_sentiment_height(starting_indexes, exit)?;
Ok(())
}
/// Compute aggregate base metrics from heterogeneous source cohorts.
/// Uses only base fields (supply, outputs, activity, realized_base, unrealized_base, cost_basis_base).
fn compute_base_from_others(
&mut self,
starting_indexes: &ComputeIndexes,
others: &[&dyn CohortMetricsBase],
exit: &Exit,
) -> Result<()>
where
Self: Sized,
{
self.supply_mut().compute_from_stateful(
starting_indexes,
&others.iter().map(|v| v.supply()).collect::<Vec<_>>(),
exit,
)?;
self.outputs_mut().compute_from_stateful(
starting_indexes,
&others.iter().map(|v| v.outputs()).collect::<Vec<_>>(),
exit,
)?;
self.activity_mut().compute_from_stateful(
starting_indexes,
&others.iter().map(|v| v.activity()).collect::<Vec<_>>(),
exit,
)?;
self.realized_base_mut().compute_from_stateful(
starting_indexes,
&others.iter().map(|v| v.realized_base()).collect::<Vec<_>>(),
exit,
)?;
self.unrealized_base_mut().compute_from_stateful(
starting_indexes,
&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

@@ -0,0 +1,170 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, StoredF64, Version};
use vecdb::{Exit, Ident, ReadableCloneableVec, Rw, StorageMode};
use crate::{
ComputeIndexes, blocks,
internal::{
ComputedFromHeightLast, DollarsMinus, LazyBinaryFromHeightLast,
LazyFromHeightLast, Ratio64,
},
};
use crate::distribution::metrics::ImportConfig;
use super::RealizedBase;
/// Adjusted realized metrics (only for adjusted cohorts: all, sth, max_age).
#[derive(Traversable)]
pub struct RealizedAdjusted<M: StorageMode = Rw> {
// === Adjusted Value (lazy: cohort - up_to_1h) ===
pub adjusted_value_created: LazyBinaryFromHeightLast<Dollars, Dollars, Dollars>,
pub adjusted_value_destroyed: LazyBinaryFromHeightLast<Dollars, Dollars, Dollars>,
// === Adjusted Value Created/Destroyed Rolling Sums ===
pub adjusted_value_created_24h: ComputedFromHeightLast<Dollars, M>,
pub adjusted_value_created_7d: ComputedFromHeightLast<Dollars, M>,
pub adjusted_value_created_30d: ComputedFromHeightLast<Dollars, M>,
pub adjusted_value_created_1y: ComputedFromHeightLast<Dollars, M>,
pub adjusted_value_destroyed_24h: ComputedFromHeightLast<Dollars, M>,
pub adjusted_value_destroyed_7d: ComputedFromHeightLast<Dollars, M>,
pub adjusted_value_destroyed_30d: ComputedFromHeightLast<Dollars, M>,
pub adjusted_value_destroyed_1y: ComputedFromHeightLast<Dollars, M>,
// === Adjusted SOPR (rolling window ratios) ===
pub adjusted_sopr: LazyFromHeightLast<StoredF64>,
pub adjusted_sopr_24h: LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>,
pub adjusted_sopr_7d: LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>,
pub adjusted_sopr_30d: LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>,
pub adjusted_sopr_1y: LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>,
pub adjusted_sopr_24h_7d_ema: ComputedFromHeightLast<StoredF64, M>,
pub adjusted_sopr_7d_ema: LazyFromHeightLast<StoredF64>,
pub adjusted_sopr_24h_30d_ema: ComputedFromHeightLast<StoredF64, M>,
pub adjusted_sopr_30d_ema: LazyFromHeightLast<StoredF64>,
}
impl RealizedAdjusted {
pub(crate) fn forced_import(
cfg: &ImportConfig,
base: &RealizedBase,
up_to_1h: &RealizedBase,
) -> Result<Self> {
let v1 = Version::ONE;
macro_rules! import_rolling {
($name:expr) => {
ComputedFromHeightLast::forced_import(cfg.db, &cfg.name($name), cfg.version + v1, cfg.indexes)?
};
}
let adjusted_value_created = LazyBinaryFromHeightLast::from_both_binary_block::<
DollarsMinus, Dollars, Dollars, Dollars, Dollars,
>(
&cfg.name("adjusted_value_created"),
cfg.version,
&base.value_created,
&up_to_1h.value_created,
);
let adjusted_value_destroyed = LazyBinaryFromHeightLast::from_both_binary_block::<
DollarsMinus, Dollars, Dollars, Dollars, Dollars,
>(
&cfg.name("adjusted_value_destroyed"),
cfg.version,
&base.value_destroyed,
&up_to_1h.value_destroyed,
);
let adjusted_value_created_24h = import_rolling!("adjusted_value_created_24h");
let adjusted_value_created_7d = import_rolling!("adjusted_value_created_7d");
let adjusted_value_created_30d = import_rolling!("adjusted_value_created_30d");
let adjusted_value_created_1y = import_rolling!("adjusted_value_created_1y");
let adjusted_value_destroyed_24h = import_rolling!("adjusted_value_destroyed_24h");
let adjusted_value_destroyed_7d = import_rolling!("adjusted_value_destroyed_7d");
let adjusted_value_destroyed_30d = import_rolling!("adjusted_value_destroyed_30d");
let adjusted_value_destroyed_1y = import_rolling!("adjusted_value_destroyed_1y");
let adjusted_sopr_24h = LazyBinaryFromHeightLast::from_computed_last::<Ratio64>(
&cfg.name("adjusted_sopr_24h"), cfg.version + v1, &adjusted_value_created_24h, &adjusted_value_destroyed_24h,
);
let adjusted_sopr_7d = LazyBinaryFromHeightLast::from_computed_last::<Ratio64>(
&cfg.name("adjusted_sopr_7d"), cfg.version + v1, &adjusted_value_created_7d, &adjusted_value_destroyed_7d,
);
let adjusted_sopr_30d = LazyBinaryFromHeightLast::from_computed_last::<Ratio64>(
&cfg.name("adjusted_sopr_30d"), cfg.version + v1, &adjusted_value_created_30d, &adjusted_value_destroyed_30d,
);
let adjusted_sopr_1y = LazyBinaryFromHeightLast::from_computed_last::<Ratio64>(
&cfg.name("adjusted_sopr_1y"), cfg.version + v1, &adjusted_value_created_1y, &adjusted_value_destroyed_1y,
);
let adjusted_sopr = LazyFromHeightLast::from_binary::<Ident, Dollars, Dollars>(
&cfg.name("adjusted_sopr"), cfg.version + v1, &adjusted_sopr_24h,
);
let adjusted_sopr_24h_7d_ema = import_rolling!("adjusted_sopr_24h_7d_ema");
let adjusted_sopr_7d_ema = LazyFromHeightLast::from_computed::<Ident>(
&cfg.name("adjusted_sopr_7d_ema"), cfg.version + v1,
adjusted_sopr_24h_7d_ema.height.read_only_boxed_clone(), &adjusted_sopr_24h_7d_ema,
);
let adjusted_sopr_24h_30d_ema = import_rolling!("adjusted_sopr_24h_30d_ema");
let adjusted_sopr_30d_ema = LazyFromHeightLast::from_computed::<Ident>(
&cfg.name("adjusted_sopr_30d_ema"), cfg.version + v1,
adjusted_sopr_24h_30d_ema.height.read_only_boxed_clone(), &adjusted_sopr_24h_30d_ema,
);
Ok(RealizedAdjusted {
adjusted_value_created,
adjusted_value_destroyed,
adjusted_value_created_24h,
adjusted_value_created_7d,
adjusted_value_created_30d,
adjusted_value_created_1y,
adjusted_value_destroyed_24h,
adjusted_value_destroyed_7d,
adjusted_value_destroyed_30d,
adjusted_value_destroyed_1y,
adjusted_sopr,
adjusted_sopr_24h,
adjusted_sopr_7d,
adjusted_sopr_30d,
adjusted_sopr_1y,
adjusted_sopr_24h_7d_ema,
adjusted_sopr_7d_ema,
adjusted_sopr_24h_30d_ema,
adjusted_sopr_30d_ema,
})
}
pub(crate) fn compute_rest_part2_adj(
&mut self,
blocks: &blocks::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// Adjusted value created/destroyed rolling sums
self.adjusted_value_created_24h.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &self.adjusted_value_created.height, exit)?;
self.adjusted_value_created_7d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &self.adjusted_value_created.height, exit)?;
self.adjusted_value_created_30d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &self.adjusted_value_created.height, exit)?;
self.adjusted_value_created_1y.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &self.adjusted_value_created.height, exit)?;
self.adjusted_value_destroyed_24h.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &self.adjusted_value_destroyed.height, exit)?;
self.adjusted_value_destroyed_7d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &self.adjusted_value_destroyed.height, exit)?;
self.adjusted_value_destroyed_30d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &self.adjusted_value_destroyed.height, exit)?;
self.adjusted_value_destroyed_1y.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &self.adjusted_value_destroyed.height, exit)?;
// Adjusted SOPR EMAs
self.adjusted_sopr_24h_7d_ema.height.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1w_ago,
&self.adjusted_sopr.height,
exit,
)?;
self.adjusted_sopr_24h_30d_ema.height.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1m_ago,
&self.adjusted_sopr.height,
exit,
)?;
Ok(())
}
}

View File

@@ -4,7 +4,6 @@ use brk_types::{
Bitcoin, Cents, CentsSats, CentsSquaredSats, Dollars, Height, StoredF32, StoredF64,
Version,
};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, AnyVec, BytesVec, Exit, WritableVec, Ident, ImportableVec,
ReadableCloneableVec, ReadableVec, Negate, Rw, StorageMode,
@@ -15,7 +14,7 @@ use crate::{
distribution::state::RealizedState,
internal::{
CentsUnsignedToDollars, ComputedFromHeightCum, ComputedFromHeightLast,
ComputedFromHeightRatio, DollarsMinus, DollarsPlus,
ComputedFromHeightRatio, DollarsPlus,
DollarsSquaredDivide, LazyBinaryFromHeightLast,
LazyBinaryPriceFromHeight, LazyComputedValueFromHeightCum, LazyFromHeightLast,
LazyPriceFromCents, PercentageDollarsF32, Price, PriceFromHeight,
@@ -24,36 +23,32 @@ use crate::{
prices,
};
use super::ImportConfig;
use crate::distribution::metrics::ImportConfig;
/// Realized cap and related metrics.
/// Base realized metrics (always computed).
#[derive(Traversable)]
pub struct RealizedMetrics<M: StorageMode = Rw> {
pub struct RealizedBase<M: StorageMode = Rw> {
// === Realized Cap ===
pub realized_cap_cents: ComputedFromHeightLast<Cents, M>,
pub realized_cap: LazyFromHeightLast<Dollars, Cents>,
pub realized_price: Price<ComputedFromHeightLast<Dollars, M>>,
pub realized_price_extra: ComputedFromHeightRatio<M>,
pub realized_cap_rel_to_own_market_cap: Option<ComputedFromHeightLast<StoredF32, M>>,
pub realized_cap_30d_delta: ComputedFromHeightLast<Dollars, M>,
// === Investor Price (dollar-weighted average acquisition price) ===
// === Investor Price ===
pub investor_price_cents: ComputedFromHeightLast<Cents, M>,
pub investor_price: LazyPriceFromCents,
pub investor_price_extra: ComputedFromHeightRatio<M>,
// === Floor/Ceiling Price Bands (lazy: realized²/investor, investor²/realized) ===
// === Floor/Ceiling Price Bands ===
pub lower_price_band: LazyBinaryPriceFromHeight,
pub upper_price_band: LazyBinaryPriceFromHeight,
// === Raw values for aggregation (needed to compute investor_price for aggregated cohorts) ===
/// Raw Σ(price × sats) for realized cap aggregation
// === Raw values for aggregation ===
pub cap_raw: M::Stored<BytesVec<Height, CentsSats>>,
/// Raw Σ(price² × sats) for investor_price aggregation
pub investor_cap_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
// === MVRV (Market Value to Realized Value) ===
// Proxy for realized_price_extra.ratio (close / realized_price = market_cap / realized_cap)
// === MVRV ===
pub mvrv: LazyFromHeightLast<StoredF32>,
// === Realized Profit/Loss ===
@@ -76,29 +71,13 @@ pub struct RealizedMetrics<M: StorageMode = Rw> {
// === Total Realized PnL ===
pub total_realized_pnl: LazyFromHeightLast<Dollars>,
// === Realized Profit/Loss Rolling Sums ===
pub realized_profit_24h: Option<ComputedFromHeightLast<Dollars, M>>,
pub realized_profit_7d: Option<ComputedFromHeightLast<Dollars, M>>,
pub realized_profit_30d: Option<ComputedFromHeightLast<Dollars, M>>,
pub realized_profit_1y: Option<ComputedFromHeightLast<Dollars, M>>,
pub realized_loss_24h: Option<ComputedFromHeightLast<Dollars, M>>,
pub realized_loss_7d: Option<ComputedFromHeightLast<Dollars, M>>,
pub realized_loss_30d: Option<ComputedFromHeightLast<Dollars, M>>,
pub realized_loss_1y: Option<ComputedFromHeightLast<Dollars, M>>,
// === Realized Profit to Loss Ratio (lazy from rolling sums) ===
pub realized_profit_to_loss_ratio_24h: Option<LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>>,
pub realized_profit_to_loss_ratio_7d: Option<LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>>,
pub realized_profit_to_loss_ratio_30d: Option<LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>>,
pub realized_profit_to_loss_ratio_1y: Option<LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>>,
// === Value Created/Destroyed Splits (stored) ===
pub profit_value_created: ComputedFromHeightLast<Dollars, M>,
pub profit_value_destroyed: ComputedFromHeightLast<Dollars, M>,
pub loss_value_created: ComputedFromHeightLast<Dollars, M>,
pub loss_value_destroyed: ComputedFromHeightLast<Dollars, M>,
// === Value Created/Destroyed Totals (lazy: profit + loss) ===
// === Value Created/Destroyed Totals (lazy) ===
pub value_created: LazyBinaryFromHeightLast<Dollars, Dollars, Dollars>,
pub value_destroyed: LazyBinaryFromHeightLast<Dollars, Dollars, Dollars>,
@@ -106,10 +85,6 @@ pub struct RealizedMetrics<M: StorageMode = Rw> {
pub capitulation_flow: LazyFromHeightLast<Dollars>,
pub profit_flow: LazyFromHeightLast<Dollars>,
// === Adjusted Value (lazy: cohort - up_to_1h) ===
pub adjusted_value_created: Option<LazyBinaryFromHeightLast<Dollars, Dollars, Dollars>>,
pub adjusted_value_destroyed: Option<LazyBinaryFromHeightLast<Dollars, Dollars, Dollars>>,
// === Value Created/Destroyed Rolling Sums ===
pub value_created_24h: ComputedFromHeightLast<Dollars, M>,
pub value_created_7d: ComputedFromHeightLast<Dollars, M>,
@@ -131,27 +106,6 @@ pub struct RealizedMetrics<M: StorageMode = Rw> {
pub sopr_24h_30d_ema: ComputedFromHeightLast<StoredF64, M>,
pub sopr_30d_ema: LazyFromHeightLast<StoredF64>,
// === Adjusted Value Created/Destroyed Rolling Sums ===
pub adjusted_value_created_24h: Option<ComputedFromHeightLast<Dollars, M>>,
pub adjusted_value_created_7d: Option<ComputedFromHeightLast<Dollars, M>>,
pub adjusted_value_created_30d: Option<ComputedFromHeightLast<Dollars, M>>,
pub adjusted_value_created_1y: Option<ComputedFromHeightLast<Dollars, M>>,
pub adjusted_value_destroyed_24h: Option<ComputedFromHeightLast<Dollars, M>>,
pub adjusted_value_destroyed_7d: Option<ComputedFromHeightLast<Dollars, M>>,
pub adjusted_value_destroyed_30d: Option<ComputedFromHeightLast<Dollars, M>>,
pub adjusted_value_destroyed_1y: Option<ComputedFromHeightLast<Dollars, M>>,
// === Adjusted SOPR (rolling window ratios) ===
pub adjusted_sopr: Option<LazyFromHeightLast<StoredF64>>,
pub adjusted_sopr_24h: Option<LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>>,
pub adjusted_sopr_7d: Option<LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>>,
pub adjusted_sopr_30d: Option<LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>>,
pub adjusted_sopr_1y: Option<LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>>,
pub adjusted_sopr_24h_7d_ema: Option<ComputedFromHeightLast<StoredF64, M>>,
pub adjusted_sopr_7d_ema: Option<LazyFromHeightLast<StoredF64>>,
pub adjusted_sopr_24h_30d_ema: Option<ComputedFromHeightLast<StoredF64, M>>,
pub adjusted_sopr_30d_ema: Option<LazyFromHeightLast<StoredF64>>,
// === Sell Side Risk Rolling Sum Intermediates ===
pub realized_value_24h: ComputedFromHeightLast<Dollars, M>,
pub realized_value_7d: ComputedFromHeightLast<Dollars, M>,
@@ -175,32 +129,23 @@ pub struct RealizedMetrics<M: StorageMode = Rw> {
pub net_realized_pnl_cumulative_30d_delta_rel_to_market_cap: ComputedFromHeightLast<StoredF32, M>,
// === Peak Regret ===
/// Realized peak regret: Σ((peak - sell_price) × sats)
/// where peak = max price during holding period.
/// "How much more could have been made by selling at peak instead"
pub peak_regret: ComputedFromHeightCum<Dollars, M>,
/// Peak regret as % of realized cap
pub peak_regret_rel_to_realized_cap: LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
// === Sent in Profit/Loss ===
/// Sats sent in profit (sats/btc/usd)
pub sent_in_profit: LazyComputedValueFromHeightCum<M>,
/// 14-day EMA of sent in profit (sats, btc, usd)
pub sent_in_profit_14d_ema: ValueEmaFromHeight<M>,
/// Sats sent in loss (sats/btc/usd)
pub sent_in_loss: LazyComputedValueFromHeightCum<M>,
/// 14-day EMA of sent in loss (sats, btc, usd)
pub sent_in_loss_14d_ema: ValueEmaFromHeight<M>,
}
impl RealizedMetrics {
/// Import realized metrics from database.
impl RealizedBase {
/// Import realized base metrics from database.
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let v1 = Version::ONE;
let v2 = Version::new(2);
let v3 = Version::new(3);
let extended = cfg.extended();
let compute_adjusted = cfg.compute_adjusted();
// Import combined types using forced_import which handles height + derived
let realized_cap_cents = ComputedFromHeightLast::forced_import(
@@ -273,7 +218,6 @@ impl RealizedMetrics {
cfg.indexes,
)?;
// realized_value is the source for total_realized_pnl (they're identical)
let realized_value = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("realized_value"),
@@ -281,7 +225,6 @@ impl RealizedMetrics {
cfg.indexes,
)?;
// total_realized_pnl is a lazy alias to realized_value
let total_realized_pnl = LazyFromHeightLast::from_computed::<Ident>(
&cfg.name("total_realized_pnl"),
cfg.version + v1,
@@ -289,7 +232,6 @@ impl RealizedMetrics {
&realized_value,
);
// Construct lazy ratio vecs
let realized_profit_rel_to_realized_cap =
LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::<PercentageDollarsF32, _>(
&cfg.name("realized_profit_rel_to_realized_cap"),
@@ -321,7 +263,6 @@ impl RealizedMetrics {
cfg.indexes,
)?;
// Investor price (dollar-weighted average acquisition price)
let investor_price_cents = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("investor_price_cents"),
@@ -344,7 +285,6 @@ impl RealizedMetrics {
extended,
)?;
// Floor price = realized² / investor (lower band)
let lower_price_band =
LazyBinaryPriceFromHeight::from_price_and_lazy_price::<DollarsSquaredDivide>(
&cfg.name("lower_price_band"),
@@ -353,7 +293,6 @@ impl RealizedMetrics {
&investor_price,
);
// Ceiling price = investor² / realized (upper band)
let upper_price_band =
LazyBinaryPriceFromHeight::from_lazy_price_and_price::<DollarsSquaredDivide>(
&cfg.name("upper_price_band"),
@@ -362,33 +301,28 @@ impl RealizedMetrics {
&realized_price,
);
// Raw values for aggregation
let cap_raw = BytesVec::forced_import(cfg.db, &cfg.name("cap_raw"), cfg.version)?;
let investor_cap_raw =
BytesVec::forced_import(cfg.db, &cfg.name("investor_cap_raw"), cfg.version)?;
// Import the 4 splits (stored)
let profit_value_created = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("profit_value_created"),
cfg.version,
cfg.indexes,
)?;
let profit_value_destroyed = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("profit_value_destroyed"),
cfg.version,
cfg.indexes,
)?;
let loss_value_created = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("loss_value_created"),
cfg.version,
cfg.indexes,
)?;
let loss_value_destroyed = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("loss_value_destroyed"),
@@ -396,14 +330,12 @@ impl RealizedMetrics {
cfg.indexes,
)?;
// Create lazy totals (profit + loss)
let value_created = LazyBinaryFromHeightLast::from_computed_last::<DollarsPlus>(
&cfg.name("value_created"),
cfg.version,
&profit_value_created,
&loss_value_created,
);
let value_destroyed = LazyBinaryFromHeightLast::from_computed_last::<DollarsPlus>(
&cfg.name("value_destroyed"),
cfg.version,
@@ -411,14 +343,12 @@ impl RealizedMetrics {
&loss_value_destroyed,
);
// Create lazy aliases
let capitulation_flow = LazyFromHeightLast::from_computed::<Ident>(
&cfg.name("capitulation_flow"),
cfg.version,
loss_value_destroyed.height.read_only_boxed_clone(),
&loss_value_destroyed,
);
let profit_flow = LazyFromHeightLast::from_computed::<Ident>(
&cfg.name("profit_flow"),
cfg.version,
@@ -426,41 +356,6 @@ impl RealizedMetrics {
&profit_value_destroyed,
);
// Create lazy adjusted vecs if compute_adjusted and up_to_1h is available
let adjusted_value_created =
(compute_adjusted && cfg.up_to_1h_realized.is_some()).then(|| {
let up_to_1h = cfg.up_to_1h_realized.unwrap();
LazyBinaryFromHeightLast::from_both_binary_block::<
DollarsMinus,
Dollars,
Dollars,
Dollars,
Dollars,
>(
&cfg.name("adjusted_value_created"),
cfg.version,
&value_created,
&up_to_1h.value_created,
)
});
let adjusted_value_destroyed =
(compute_adjusted && cfg.up_to_1h_realized.is_some()).then(|| {
let up_to_1h = cfg.up_to_1h_realized.unwrap();
LazyBinaryFromHeightLast::from_both_binary_block::<
DollarsMinus,
Dollars,
Dollars,
Dollars,
Dollars,
>(
&cfg.name("adjusted_value_destroyed"),
cfg.version,
&value_destroyed,
&up_to_1h.value_destroyed,
)
});
// Create realized_price_extra first so we can reference its ratio for MVRV proxy
let realized_price_extra = ComputedFromHeightRatio::forced_import(
cfg.db,
&cfg.name("realized_price"),
@@ -470,8 +365,6 @@ impl RealizedMetrics {
extended,
)?;
// MVRV is a lazy proxy for realized_price_extra.ratio
// ratio = close / realized_price = market_cap / realized_cap = MVRV
let mvrv = LazyFromHeightLast::from_computed::<StoredF32Identity>(
&cfg.name("mvrv"),
cfg.version,
@@ -479,17 +372,12 @@ impl RealizedMetrics {
&realized_price_extra.ratio,
);
// === Rolling sum intermediates (must be imported before lazy ratios reference them) ===
// === Rolling sum intermediates ===
macro_rules! import_rolling {
($name:expr) => {
ComputedFromHeightLast::forced_import(cfg.db, &cfg.name($name), cfg.version + v1, cfg.indexes)?
};
}
macro_rules! import_rolling_opt {
($cond:expr, $name:expr) => {
$cond.then(|| ComputedFromHeightLast::forced_import(cfg.db, &cfg.name($name), cfg.version + v1, cfg.indexes)).transpose()?
};
}
let value_created_24h = import_rolling!("value_created_24h");
let value_created_7d = import_rolling!("value_created_7d");
@@ -500,30 +388,12 @@ impl RealizedMetrics {
let value_destroyed_30d = import_rolling!("value_destroyed_30d");
let value_destroyed_1y = import_rolling!("value_destroyed_1y");
let adjusted_value_created_24h = import_rolling_opt!(compute_adjusted, "adjusted_value_created_24h");
let adjusted_value_created_7d = import_rolling_opt!(compute_adjusted, "adjusted_value_created_7d");
let adjusted_value_created_30d = import_rolling_opt!(compute_adjusted, "adjusted_value_created_30d");
let adjusted_value_created_1y = import_rolling_opt!(compute_adjusted, "adjusted_value_created_1y");
let adjusted_value_destroyed_24h = import_rolling_opt!(compute_adjusted, "adjusted_value_destroyed_24h");
let adjusted_value_destroyed_7d = import_rolling_opt!(compute_adjusted, "adjusted_value_destroyed_7d");
let adjusted_value_destroyed_30d = import_rolling_opt!(compute_adjusted, "adjusted_value_destroyed_30d");
let adjusted_value_destroyed_1y = import_rolling_opt!(compute_adjusted, "adjusted_value_destroyed_1y");
let realized_value_24h = import_rolling!("realized_value_24h");
let realized_value_7d = import_rolling!("realized_value_7d");
let realized_value_30d = import_rolling!("realized_value_30d");
let realized_value_1y = import_rolling!("realized_value_1y");
let realized_profit_24h = import_rolling_opt!(extended, "realized_profit_24h");
let realized_profit_7d = import_rolling_opt!(extended, "realized_profit_7d");
let realized_profit_30d = import_rolling_opt!(extended, "realized_profit_30d");
let realized_profit_1y = import_rolling_opt!(extended, "realized_profit_1y");
let realized_loss_24h = import_rolling_opt!(extended, "realized_loss_24h");
let realized_loss_7d = import_rolling_opt!(extended, "realized_loss_7d");
let realized_loss_30d = import_rolling_opt!(extended, "realized_loss_30d");
let realized_loss_1y = import_rolling_opt!(extended, "realized_loss_1y");
// === Rolling window lazy ratios (from rolling sum intermediates) ===
// === Rolling window lazy ratios ===
let sopr_24h = LazyBinaryFromHeightLast::from_computed_last::<Ratio64>(
&cfg.name("sopr_24h"), cfg.version + v1, &value_created_24h, &value_destroyed_24h,
);
@@ -540,26 +410,6 @@ impl RealizedMetrics {
&cfg.name("sopr"), cfg.version + v1, &sopr_24h,
);
macro_rules! lazy_binary_from_opt_last {
($transform:ty, $name:expr, $s1:expr, $s2:expr) => {
($s1.is_some() && $s2.is_some()).then(|| {
LazyBinaryFromHeightLast::from_computed_last::<$transform>(
&cfg.name($name), cfg.version + v1,
$s1.as_ref().unwrap(), $s2.as_ref().unwrap(),
)
})
};
}
let adjusted_sopr_24h = lazy_binary_from_opt_last!(Ratio64, "adjusted_sopr_24h", adjusted_value_created_24h, adjusted_value_destroyed_24h);
let adjusted_sopr_7d = lazy_binary_from_opt_last!(Ratio64, "adjusted_sopr_7d", adjusted_value_created_7d, adjusted_value_destroyed_7d);
let adjusted_sopr_30d = lazy_binary_from_opt_last!(Ratio64, "adjusted_sopr_30d", adjusted_value_created_30d, adjusted_value_destroyed_30d);
let adjusted_sopr_1y = lazy_binary_from_opt_last!(Ratio64, "adjusted_sopr_1y", adjusted_value_created_1y, adjusted_value_destroyed_1y);
let adjusted_sopr = adjusted_sopr_24h.as_ref().map(|sopr_24h| {
LazyFromHeightLast::from_binary::<Ident, Dollars, Dollars>(
&cfg.name("adjusted_sopr"), cfg.version + v1, sopr_24h,
)
});
let sell_side_risk_ratio_24h = LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::<PercentageDollarsF32, _>(
&cfg.name("sell_side_risk_ratio_24h"), cfg.version + v1, &realized_value_24h, &realized_cap,
);
@@ -576,11 +426,6 @@ impl RealizedMetrics {
&cfg.name("sell_side_risk_ratio"), cfg.version + v1, &sell_side_risk_ratio_24h,
);
let realized_profit_to_loss_ratio_24h = lazy_binary_from_opt_last!(Ratio64, "realized_profit_to_loss_ratio_24h", realized_profit_24h, realized_loss_24h);
let realized_profit_to_loss_ratio_7d = lazy_binary_from_opt_last!(Ratio64, "realized_profit_to_loss_ratio_7d", realized_profit_7d, realized_loss_7d);
let realized_profit_to_loss_ratio_30d = lazy_binary_from_opt_last!(Ratio64, "realized_profit_to_loss_ratio_30d", realized_profit_30d, realized_loss_30d);
let realized_profit_to_loss_ratio_1y = lazy_binary_from_opt_last!(Ratio64, "realized_profit_to_loss_ratio_1y", realized_profit_1y, realized_loss_1y);
// === EMA imports + identity aliases ===
let sopr_24h_7d_ema = import_rolling!("sopr_24h_7d_ema");
let sopr_7d_ema = LazyFromHeightLast::from_computed::<Ident>(
@@ -593,21 +438,6 @@ impl RealizedMetrics {
sopr_24h_30d_ema.height.read_only_boxed_clone(), &sopr_24h_30d_ema,
);
let adjusted_sopr_24h_7d_ema = import_rolling_opt!(compute_adjusted, "adjusted_sopr_24h_7d_ema");
let adjusted_sopr_7d_ema = adjusted_sopr_24h_7d_ema.as_ref().map(|ema| {
LazyFromHeightLast::from_computed::<Ident>(
&cfg.name("adjusted_sopr_7d_ema"), cfg.version + v1,
ema.height.read_only_boxed_clone(), ema,
)
});
let adjusted_sopr_24h_30d_ema = import_rolling_opt!(compute_adjusted, "adjusted_sopr_24h_30d_ema");
let adjusted_sopr_30d_ema = adjusted_sopr_24h_30d_ema.as_ref().map(|ema| {
LazyFromHeightLast::from_computed::<Ident>(
&cfg.name("adjusted_sopr_30d_ema"), cfg.version + v1,
ema.height.read_only_boxed_clone(), ema,
)
});
let sell_side_risk_ratio_24h_7d_ema = import_rolling!("sell_side_risk_ratio_24h_7d_ema");
let sell_side_risk_ratio_7d_ema = LazyFromHeightLast::from_computed::<Ident>(
&cfg.name("sell_side_risk_ratio_7d_ema"), cfg.version + v1,
@@ -628,44 +458,24 @@ impl RealizedMetrics {
);
Ok(Self {
// === Realized Cap ===
realized_cap_cents,
realized_cap,
realized_price,
realized_price_extra,
realized_cap_rel_to_own_market_cap: extended
.then(|| {
ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("realized_cap_rel_to_own_market_cap"),
cfg.version,
cfg.indexes,
)
})
.transpose()?,
realized_cap_30d_delta: ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("realized_cap_30d_delta"),
cfg.version,
cfg.indexes,
)?,
// === Investor Price ===
investor_price_cents,
investor_price,
investor_price_extra,
// === Floor/Ceiling Price Bands ===
lower_price_band,
upper_price_band,
cap_raw,
investor_cap_raw,
// === MVRV ===
mvrv,
// === Realized Profit/Loss ===
realized_profit,
realized_profit_7d_ema,
realized_loss,
@@ -674,50 +484,18 @@ impl RealizedMetrics {
net_realized_pnl,
net_realized_pnl_7d_ema,
realized_value,
// === Realized vs Realized Cap Ratios (lazy) ===
realized_profit_rel_to_realized_cap,
realized_loss_rel_to_realized_cap,
net_realized_pnl_rel_to_realized_cap,
// === Total Realized PnL ===
total_realized_pnl,
// === Realized Profit/Loss Rolling Sums ===
realized_profit_24h,
realized_profit_7d,
realized_profit_30d,
realized_profit_1y,
realized_loss_24h,
realized_loss_7d,
realized_loss_30d,
realized_loss_1y,
// === Realized Profit to Loss Ratio (lazy from rolling sums) ===
realized_profit_to_loss_ratio_24h,
realized_profit_to_loss_ratio_7d,
realized_profit_to_loss_ratio_30d,
realized_profit_to_loss_ratio_1y,
// === Value Created/Destroyed Splits (stored) ===
profit_value_created,
profit_value_destroyed,
loss_value_created,
loss_value_destroyed,
// === Value Created/Destroyed Totals (lazy: profit + loss) ===
value_created,
value_destroyed,
// === Capitulation/Profit Flow (lazy aliases) ===
capitulation_flow,
profit_flow,
// === Adjusted Value (lazy: cohort - up_to_1h) ===
adjusted_value_created,
adjusted_value_destroyed,
// === Value Created/Destroyed Rolling Sums ===
value_created_24h,
value_created_7d,
value_created_30d,
@@ -726,8 +504,6 @@ impl RealizedMetrics {
value_destroyed_7d,
value_destroyed_30d,
value_destroyed_1y,
// === SOPR (rolling window ratios) ===
sopr,
sopr_24h,
sopr_7d,
@@ -737,35 +513,10 @@ impl RealizedMetrics {
sopr_7d_ema,
sopr_24h_30d_ema,
sopr_30d_ema,
// === Adjusted Value Created/Destroyed Rolling Sums ===
adjusted_value_created_24h,
adjusted_value_created_7d,
adjusted_value_created_30d,
adjusted_value_created_1y,
adjusted_value_destroyed_24h,
adjusted_value_destroyed_7d,
adjusted_value_destroyed_30d,
adjusted_value_destroyed_1y,
// === Adjusted SOPR (rolling window ratios) ===
adjusted_sopr,
adjusted_sopr_24h,
adjusted_sopr_7d,
adjusted_sopr_30d,
adjusted_sopr_1y,
adjusted_sopr_24h_7d_ema,
adjusted_sopr_7d_ema,
adjusted_sopr_24h_30d_ema,
adjusted_sopr_30d_ema,
// === Sell Side Risk Rolling Sum Intermediates ===
realized_value_24h,
realized_value_7d,
realized_value_30d,
realized_value_1y,
// === Sell Side Risk (rolling window ratios) ===
sell_side_risk_ratio,
sell_side_risk_ratio_24h,
sell_side_risk_ratio_7d,
@@ -775,8 +526,6 @@ impl RealizedMetrics {
sell_side_risk_ratio_7d_ema,
sell_side_risk_ratio_24h_30d_ema,
sell_side_risk_ratio_30d_ema,
// === Net Realized PnL Deltas ===
net_realized_pnl_cumulative_30d_delta: ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("net_realized_pnl_cumulative_30d_delta"),
@@ -797,12 +546,8 @@ impl RealizedMetrics {
cfg.version + v3,
cfg.indexes,
)?,
// === ATH Regret ===
peak_regret,
peak_regret_rel_to_realized_cap,
// === Sent in Profit/Loss ===
sent_in_profit: LazyComputedValueFromHeightCum::forced_import(
cfg.db,
&cfg.name("sent_in_profit"),
@@ -852,7 +597,6 @@ impl RealizedMetrics {
}
/// Push realized state values to height-indexed vectors.
/// State values are CentsUnsigned (deterministic), converted to Dollars for storage.
pub(crate) fn truncate_push(&mut self, height: Height, state: &RealizedState) -> Result<()> {
self.realized_cap_cents
.height
@@ -866,11 +610,9 @@ impl RealizedMetrics {
self.investor_price_cents
.height
.truncate_push(height, state.investor_price())?;
// Push raw values for aggregation
self.cap_raw.truncate_push(height, state.cap_raw())?;
self.investor_cap_raw
.truncate_push(height, state.investor_cap_raw())?;
// Push the 4 splits (totals are derived lazily)
self.profit_value_created
.height
.truncate_push(height, state.profit_value_created().to_dollars())?;
@@ -883,12 +625,9 @@ impl RealizedMetrics {
self.loss_value_destroyed
.height
.truncate_push(height, state.loss_value_destroyed().to_dollars())?;
// ATH regret
self.peak_regret
.height
.truncate_push(height, state.peak_regret().to_dollars())?;
// Volume at profit/loss
self.sent_in_profit
.sats
.height
@@ -901,34 +640,23 @@ impl RealizedMetrics {
Ok(())
}
/// Returns a parallel iterator over all vecs for parallel writing.
pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
/// Returns a Vec of mutable references to all stored vecs for parallel writing.
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.realized_cap_cents.height as &mut dyn AnyStoredVec,
&mut self.realized_profit.height,
&mut self.realized_loss.height,
&mut self.investor_price_cents.height,
// Raw values for aggregation
&mut self.cap_raw as &mut dyn AnyStoredVec,
&mut self.investor_cap_raw as &mut dyn AnyStoredVec,
// The 4 splits (totals are derived lazily)
&mut self.profit_value_created.height,
&mut self.profit_value_destroyed.height,
&mut self.loss_value_created.height,
&mut self.loss_value_destroyed.height,
// ATH regret
&mut self.peak_regret.height,
// Sent in profit/loss
&mut self.sent_in_profit.sats.height,
&mut self.sent_in_loss.sats.height,
]
.into_par_iter()
}
/// Validate computed versions against base version.
pub(crate) fn validate_computed_versions(&mut self, _base_version: Version) -> Result<()> {
// Validation logic for computed vecs
Ok(())
}
/// Compute aggregate values from separate cohorts.
@@ -964,8 +692,6 @@ impl RealizedMetrics {
)?;
// Aggregate raw values for investor_price computation
// (BytesVec doesn't have compute_sum_of_others, so we manually iterate)
// Validate version for investor_price_cents (same pattern as compute_sum_of_others)
let investor_price_dep_version = others
.iter()
.map(|o| o.investor_price_cents.height.version())
@@ -974,13 +700,11 @@ impl RealizedMetrics {
.height
.validate_computed_version_or_reset(investor_price_dep_version)?;
// Start from where the target vecs left off (handles fresh/reset vecs)
let start = self
.cap_raw
.len()
.min(self.investor_cap_raw.len())
.min(self.investor_price_cents.height.len());
// End at the minimum length across all source vecs
let end = others.iter().map(|o| o.cap_raw.len()).min().unwrap_or(0);
for i in start..end {
@@ -998,7 +722,6 @@ impl RealizedMetrics {
self.investor_cap_raw
.truncate_push(height, sum_investor_cap)?;
// Compute investor_price from aggregated raw values
let investor_price = if sum_cap.inner() == 0 {
Cents::ZERO
} else {
@@ -1009,13 +732,11 @@ impl RealizedMetrics {
.truncate_push(height, investor_price)?;
}
// Write to persist computed_version (same pattern as compute_sum_of_others)
{
let _lock = exit.lock();
self.investor_price_cents.height.write()?;
}
// Aggregate the 4 splits (totals are derived lazily)
self.profit_value_created.height.compute_sum_of_others(
starting_indexes.height,
&others
@@ -1048,7 +769,6 @@ impl RealizedMetrics {
.collect::<Vec<_>>(),
exit,
)?;
// ATH regret
self.peak_regret.height.compute_sum_of_others(
starting_indexes.height,
&others
@@ -1057,8 +777,6 @@ impl RealizedMetrics {
.collect::<Vec<_>>(),
exit,
)?;
// Volume at profit/loss
self.sent_in_profit.sats.height.compute_sum_of_others(
starting_indexes.height,
&others
@@ -1085,16 +803,11 @@ impl RealizedMetrics {
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// realized_cap_cents: ComputedFromHeightLast - day1 is lazy, nothing to compute
// investor_price_cents: ComputedFromHeightLast - day1 is lazy, nothing to compute
// realized_profit/loss: ComputedFromHeightCum - compute cumulative from height
self.realized_profit
.compute_cumulative(starting_indexes.height, exit)?;
self.realized_loss
.compute_cumulative(starting_indexes.height, exit)?;
// net_realized_pnl = profit - loss
self.net_realized_pnl
.compute(starting_indexes.height, exit, |vec| {
vec.compute_subtract(
@@ -1106,10 +819,6 @@ impl RealizedMetrics {
Ok(())
})?;
// realized_value = profit + loss
// Note: total_realized_pnl is a lazy alias to realized_value since both
// compute profit + loss with sum aggregation, making them identical.
// ComputedFromHeightLast: day1 is lazy, just compute the height vec directly
self.realized_value.height.compute_add(
starting_indexes.height,
&self.realized_profit.height,
@@ -1117,15 +826,9 @@ impl RealizedMetrics {
exit,
)?;
// Compute derived aggregations for the 4 splits
// (value_created, value_destroyed, capitulation_flow, profit_flow are derived lazily)
// ComputedFromHeightLast: day1 is lazy, nothing to compute
// ATH regret: ComputedFromHeightCum - compute cumulative from height
self.peak_regret
.compute_cumulative(starting_indexes.height, exit)?;
// Volume at profit/loss: LazyComputedValueFromHeightCum - compute cumulative
self.sent_in_profit
.compute_cumulative(starting_indexes.height, exit)?;
self.sent_in_loss
@@ -1134,18 +837,17 @@ impl RealizedMetrics {
Ok(())
}
/// Second phase of computed metrics (realized price from realized cap / supply).
/// Second phase of computed metrics (base-only parts: realized price, rolling sums, EMAs).
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2(
pub(crate) fn compute_rest_part2_base(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
height_to_market_cap: Option<&impl ReadableVec<Height, Dollars>>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
// realized_price = realized_cap / supply
self.realized_price.height.compute_divide(
starting_indexes.height,
&self.realized_cap.height,
@@ -1169,7 +871,6 @@ impl RealizedMetrics {
Some(&self.investor_price.height),
)?;
// realized_cap_30d_delta: height-level rolling change
self.realized_cap_30d_delta.height.compute_rolling_change(
starting_indexes.height,
&blocks.count.height_1m_ago,
@@ -1177,7 +878,7 @@ impl RealizedMetrics {
exit,
)?;
// === Rolling sum intermediates (must be computed before lazy ratios/EMAs that read them) ===
// === Rolling sum intermediates ===
macro_rules! rolling_sum {
($target:expr, $window:expr, $source:expr) => {
$target.height.compute_rolling_sum(
@@ -1186,7 +887,6 @@ impl RealizedMetrics {
};
}
// Value created/destroyed rolling sums (from lazy binary totals)
rolling_sum!(self.value_created_24h, &blocks.count.height_24h_ago, &self.value_created.height);
rolling_sum!(self.value_created_7d, &blocks.count.height_1w_ago, &self.value_created.height);
rolling_sum!(self.value_created_30d, &blocks.count.height_1m_ago, &self.value_created.height);
@@ -1196,71 +896,13 @@ impl RealizedMetrics {
rolling_sum!(self.value_destroyed_30d, &blocks.count.height_1m_ago, &self.value_destroyed.height);
rolling_sum!(self.value_destroyed_1y, &blocks.count.height_1y_ago, &self.value_destroyed.height);
// Adjusted value created/destroyed rolling sums (from lazy adjusted totals)
if let Some(source) = self.adjusted_value_created.as_ref() {
macro_rules! rolling_sum_opt {
($target:expr, $window:expr) => {
if let Some(f) = $target.as_mut() {
f.height.compute_rolling_sum(
starting_indexes.height, $window, &source.height, exit,
)?;
}
};
}
rolling_sum_opt!(self.adjusted_value_created_24h, &blocks.count.height_24h_ago);
rolling_sum_opt!(self.adjusted_value_created_7d, &blocks.count.height_1w_ago);
rolling_sum_opt!(self.adjusted_value_created_30d, &blocks.count.height_1m_ago);
rolling_sum_opt!(self.adjusted_value_created_1y, &blocks.count.height_1y_ago);
}
if let Some(source) = self.adjusted_value_destroyed.as_ref() {
macro_rules! rolling_sum_opt {
($target:expr, $window:expr) => {
if let Some(f) = $target.as_mut() {
f.height.compute_rolling_sum(
starting_indexes.height, $window, &source.height, exit,
)?;
}
};
}
rolling_sum_opt!(self.adjusted_value_destroyed_24h, &blocks.count.height_24h_ago);
rolling_sum_opt!(self.adjusted_value_destroyed_7d, &blocks.count.height_1w_ago);
rolling_sum_opt!(self.adjusted_value_destroyed_30d, &blocks.count.height_1m_ago);
rolling_sum_opt!(self.adjusted_value_destroyed_1y, &blocks.count.height_1y_ago);
}
// Realized value rolling sums (for sell_side_risk_ratio)
// Realized value rolling sums
rolling_sum!(self.realized_value_24h, &blocks.count.height_24h_ago, &self.realized_value.height);
rolling_sum!(self.realized_value_7d, &blocks.count.height_1w_ago, &self.realized_value.height);
rolling_sum!(self.realized_value_30d, &blocks.count.height_1m_ago, &self.realized_value.height);
rolling_sum!(self.realized_value_1y, &blocks.count.height_1y_ago, &self.realized_value.height);
// Realized profit/loss rolling sums (for realized_profit_to_loss_ratio)
if let Some(f) = self.realized_profit_24h.as_mut() {
f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &self.realized_profit.height, exit)?;
}
if let Some(f) = self.realized_profit_7d.as_mut() {
f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &self.realized_profit.height, exit)?;
}
if let Some(f) = self.realized_profit_30d.as_mut() {
f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &self.realized_profit.height, exit)?;
}
if let Some(f) = self.realized_profit_1y.as_mut() {
f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &self.realized_profit.height, exit)?;
}
if let Some(f) = self.realized_loss_24h.as_mut() {
f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &self.realized_loss.height, exit)?;
}
if let Some(f) = self.realized_loss_7d.as_mut() {
f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &self.realized_loss.height, exit)?;
}
if let Some(f) = self.realized_loss_30d.as_mut() {
f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &self.realized_loss.height, exit)?;
}
if let Some(f) = self.realized_loss_1y.as_mut() {
f.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &self.realized_loss.height, exit)?;
}
// 7d rolling average of realized profit (height-level)
// 7d rolling averages
self.realized_profit_7d_ema
.height
.compute_rolling_average(
@@ -1269,16 +911,12 @@ impl RealizedMetrics {
&self.realized_profit.height,
exit,
)?;
// 7d rolling average of realized loss (height-level)
self.realized_loss_7d_ema.height.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1w_ago,
&self.realized_loss.height,
exit,
)?;
// 7d rolling average of net realized PnL (height-level)
self.net_realized_pnl_7d_ema
.height
.compute_rolling_average(
@@ -1288,7 +926,7 @@ impl RealizedMetrics {
exit,
)?;
// 14-day rolling average of sent in profit (sats and dollars)
// 14-day rolling average of sent in profit/loss
self.sent_in_profit_14d_ema.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_2w_ago,
@@ -1296,8 +934,6 @@ impl RealizedMetrics {
&self.sent_in_profit.usd.height,
exit,
)?;
// 14-day rolling average of sent in loss (sats and dollars)
self.sent_in_loss_14d_ema.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_2w_ago,
@@ -1306,14 +942,13 @@ impl RealizedMetrics {
exit,
)?;
// 7d/30d rolling average of SOPR (from 24h rolling ratio)
// SOPR EMAs
self.sopr_24h_7d_ema.height.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1w_ago,
&self.sopr.height,
exit,
)?;
self.sopr_24h_30d_ema.height.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1m_ago,
@@ -1321,28 +956,7 @@ impl RealizedMetrics {
exit,
)?;
// Optional: adjusted SOPR rolling averages (from 24h rolling ratio)
if let Some(adjusted_sopr) = self.adjusted_sopr.as_ref() {
if let Some(ema_7d) = self.adjusted_sopr_24h_7d_ema.as_mut() {
ema_7d.height.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1w_ago,
&adjusted_sopr.height,
exit,
)?;
}
if let Some(ema_30d) = self.adjusted_sopr_24h_30d_ema.as_mut() {
ema_30d.height.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1m_ago,
&adjusted_sopr.height,
exit,
)?;
}
}
// 7d/30d rolling average of sell_side_risk_ratio (from 24h rolling ratio)
// Sell side risk EMAs
self.sell_side_risk_ratio_24h_7d_ema
.height
.compute_rolling_average(
@@ -1351,7 +965,6 @@ impl RealizedMetrics {
&self.sell_side_risk_ratio.height,
exit,
)?;
self.sell_side_risk_ratio_24h_30d_ema
.height
.compute_rolling_average(
@@ -1361,7 +974,7 @@ impl RealizedMetrics {
exit,
)?;
// Net realized PnL cumulative 30d delta (height-level rolling change)
// Net realized PnL cumulative 30d delta
self.net_realized_pnl_cumulative_30d_delta
.height
.compute_rolling_change(
@@ -1371,7 +984,6 @@ impl RealizedMetrics {
exit,
)?;
// Relative to realized cap (height-level)
self.net_realized_pnl_cumulative_30d_delta_rel_to_realized_cap
.height
.compute_percentage(
@@ -1381,27 +993,14 @@ impl RealizedMetrics {
exit,
)?;
// Relative to market cap (height-level)
if let Some(height_to_market_cap) = height_to_market_cap {
self.net_realized_pnl_cumulative_30d_delta_rel_to_market_cap
.height
.compute_percentage(
starting_indexes.height,
&self.net_realized_pnl_cumulative_30d_delta.height,
height_to_market_cap,
exit,
)?;
// Optional: realized_cap_rel_to_own_market_cap
if let Some(rel_vec) = self.realized_cap_rel_to_own_market_cap.as_mut() {
rel_vec.height.compute_percentage(
starting_indexes.height,
&self.realized_cap.height,
height_to_market_cap,
exit,
)?;
}
}
self.net_realized_pnl_cumulative_30d_delta_rel_to_market_cap
.height
.compute_percentage(
starting_indexes.height,
&self.net_realized_pnl_cumulative_30d_delta.height,
height_to_market_cap,
exit,
)?;
Ok(())
}

View File

@@ -0,0 +1,121 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, StoredF32, StoredF64, Version};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::{
ComputeIndexes, blocks,
internal::{
ComputedFromHeightLast, LazyBinaryFromHeightLast, Ratio64,
},
};
use crate::distribution::metrics::ImportConfig;
use super::RealizedBase;
/// Extended realized metrics (only for extended cohorts: all, sth, lth, age_range).
#[derive(Traversable)]
pub struct RealizedExtended<M: StorageMode = Rw> {
pub realized_cap_rel_to_own_market_cap: ComputedFromHeightLast<StoredF32, M>,
// === Realized Profit/Loss Rolling Sums ===
pub realized_profit_24h: ComputedFromHeightLast<Dollars, M>,
pub realized_profit_7d: ComputedFromHeightLast<Dollars, M>,
pub realized_profit_30d: ComputedFromHeightLast<Dollars, M>,
pub realized_profit_1y: ComputedFromHeightLast<Dollars, M>,
pub realized_loss_24h: ComputedFromHeightLast<Dollars, M>,
pub realized_loss_7d: ComputedFromHeightLast<Dollars, M>,
pub realized_loss_30d: ComputedFromHeightLast<Dollars, M>,
pub realized_loss_1y: ComputedFromHeightLast<Dollars, M>,
// === Realized Profit to Loss Ratio (lazy from rolling sums) ===
pub realized_profit_to_loss_ratio_24h: LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>,
pub realized_profit_to_loss_ratio_7d: LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>,
pub realized_profit_to_loss_ratio_30d: LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>,
pub realized_profit_to_loss_ratio_1y: LazyBinaryFromHeightLast<StoredF64, Dollars, Dollars>,
}
impl RealizedExtended {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let v1 = Version::ONE;
macro_rules! import_rolling {
($name:expr) => {
ComputedFromHeightLast::forced_import(cfg.db, &cfg.name($name), cfg.version + v1, cfg.indexes)?
};
}
let realized_profit_24h = import_rolling!("realized_profit_24h");
let realized_profit_7d = import_rolling!("realized_profit_7d");
let realized_profit_30d = import_rolling!("realized_profit_30d");
let realized_profit_1y = import_rolling!("realized_profit_1y");
let realized_loss_24h = import_rolling!("realized_loss_24h");
let realized_loss_7d = import_rolling!("realized_loss_7d");
let realized_loss_30d = import_rolling!("realized_loss_30d");
let realized_loss_1y = import_rolling!("realized_loss_1y");
let realized_profit_to_loss_ratio_24h = LazyBinaryFromHeightLast::from_computed_last::<Ratio64>(
&cfg.name("realized_profit_to_loss_ratio_24h"), cfg.version + v1, &realized_profit_24h, &realized_loss_24h,
);
let realized_profit_to_loss_ratio_7d = LazyBinaryFromHeightLast::from_computed_last::<Ratio64>(
&cfg.name("realized_profit_to_loss_ratio_7d"), cfg.version + v1, &realized_profit_7d, &realized_loss_7d,
);
let realized_profit_to_loss_ratio_30d = LazyBinaryFromHeightLast::from_computed_last::<Ratio64>(
&cfg.name("realized_profit_to_loss_ratio_30d"), cfg.version + v1, &realized_profit_30d, &realized_loss_30d,
);
let realized_profit_to_loss_ratio_1y = LazyBinaryFromHeightLast::from_computed_last::<Ratio64>(
&cfg.name("realized_profit_to_loss_ratio_1y"), cfg.version + v1, &realized_profit_1y, &realized_loss_1y,
);
Ok(RealizedExtended {
realized_cap_rel_to_own_market_cap: ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("realized_cap_rel_to_own_market_cap"),
cfg.version,
cfg.indexes,
)?,
realized_profit_24h,
realized_profit_7d,
realized_profit_30d,
realized_profit_1y,
realized_loss_24h,
realized_loss_7d,
realized_loss_30d,
realized_loss_1y,
realized_profit_to_loss_ratio_24h,
realized_profit_to_loss_ratio_7d,
realized_profit_to_loss_ratio_30d,
realized_profit_to_loss_ratio_1y,
})
}
pub(crate) fn compute_rest_part2_ext(
&mut self,
base: &RealizedBase,
blocks: &blocks::Vecs,
starting_indexes: &ComputeIndexes,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
// Realized profit/loss rolling sums
self.realized_profit_24h.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &base.realized_profit.height, exit)?;
self.realized_profit_7d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &base.realized_profit.height, exit)?;
self.realized_profit_30d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &base.realized_profit.height, exit)?;
self.realized_profit_1y.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &base.realized_profit.height, exit)?;
self.realized_loss_24h.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_24h_ago, &base.realized_loss.height, exit)?;
self.realized_loss_7d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1w_ago, &base.realized_loss.height, exit)?;
self.realized_loss_30d.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1m_ago, &base.realized_loss.height, exit)?;
self.realized_loss_1y.height.compute_rolling_sum(starting_indexes.height, &blocks.count.height_1y_ago, &base.realized_loss.height, exit)?;
// Realized cap relative to own market cap
self.realized_cap_rel_to_own_market_cap.height.compute_percentage(
starting_indexes.height,
&base.realized_cap.height,
height_to_market_cap,
exit,
)?;
Ok(())
}
}

View File

@@ -0,0 +1,15 @@
mod adjusted;
mod base;
mod extended;
mod with_adjusted;
mod with_extended;
mod with_extended_adjusted;
pub use adjusted::*;
pub use base::*;
pub use extended::*;
pub use with_adjusted::*;
pub use with_extended::*;
pub use with_extended_adjusted::*;

View File

@@ -0,0 +1,59 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Dollars, Height};
use derive_more::{Deref, DerefMut};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, prices};
use crate::distribution::metrics::ImportConfig;
use super::{RealizedAdjusted, RealizedBase};
/// Realized metrics with guaranteed adjusted (no Option).
#[derive(Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct RealizedWithAdjusted<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RealizedBase<M>,
#[traversable(flatten)]
pub adjusted: RealizedAdjusted<M>,
}
impl RealizedWithAdjusted {
pub(crate) fn forced_import(cfg: &ImportConfig, up_to_1h: &RealizedBase) -> Result<Self> {
let base = RealizedBase::forced_import(cfg)?;
let adjusted = RealizedAdjusted::forced_import(cfg, &base, up_to_1h)?;
Ok(Self { base, adjusted })
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.base.compute_rest_part2_base(
blocks,
prices,
starting_indexes,
height_to_supply,
height_to_market_cap,
exit,
)?;
self.adjusted.compute_rest_part2_adj(
blocks,
starting_indexes,
exit,
)?;
Ok(())
}
}

View File

@@ -0,0 +1,61 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Dollars, Height};
use derive_more::{Deref, DerefMut};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, prices};
use crate::distribution::metrics::ImportConfig;
use super::{RealizedBase, RealizedExtended};
/// Realized metrics with guaranteed extended (no Option).
#[derive(Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct RealizedWithExtended<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RealizedBase<M>,
#[traversable(flatten)]
pub extended: RealizedExtended<M>,
}
impl RealizedWithExtended {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let base = RealizedBase::forced_import(cfg)?;
let extended = RealizedExtended::forced_import(cfg)?;
Ok(Self { base, extended })
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.base.compute_rest_part2_base(
blocks,
prices,
starting_indexes,
height_to_supply,
height_to_market_cap,
exit,
)?;
self.extended.compute_rest_part2_ext(
&self.base,
blocks,
starting_indexes,
height_to_market_cap,
exit,
)?;
Ok(())
}
}

View File

@@ -0,0 +1,74 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Dollars, Height};
use derive_more::{Deref, DerefMut};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, prices};
use crate::distribution::metrics::ImportConfig;
use super::{RealizedAdjusted, RealizedBase, RealizedExtended};
/// Realized metrics with guaranteed extended AND adjusted (no Options).
#[derive(Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct RealizedWithExtendedAdjusted<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RealizedBase<M>,
#[traversable(flatten)]
pub extended: RealizedExtended<M>,
#[traversable(flatten)]
pub adjusted: RealizedAdjusted<M>,
}
impl RealizedWithExtendedAdjusted {
pub(crate) fn forced_import(cfg: &ImportConfig, up_to_1h: &RealizedBase) -> Result<Self> {
let base = RealizedBase::forced_import(cfg)?;
let extended = RealizedExtended::forced_import(cfg)?;
let adjusted = RealizedAdjusted::forced_import(cfg, &base, up_to_1h)?;
Ok(Self {
base,
extended,
adjusted,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.base.compute_rest_part2_base(
blocks,
prices,
starting_indexes,
height_to_supply,
height_to_market_cap,
exit,
)?;
self.extended.compute_rest_part2_ext(
&self.base,
blocks,
starting_indexes,
height_to_market_cap,
exit,
)?;
self.adjusted.compute_rest_part2_adj(
blocks,
starting_indexes,
exit,
)?;
Ok(())
}
}

View File

@@ -1,328 +0,0 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Sats, StoredF32, StoredF64, Version};
use crate::internal::{
LazyBinaryFromHeightLast, NegPercentageDollarsF32, PercentageDollarsF32, PercentageSatsF64,
};
use super::{ImportConfig, RealizedMetrics, SupplyMetrics, UnrealizedMetrics};
/// Relative metrics comparing cohort values to global values.
/// All `rel_to_` vecs are lazy - computed on-demand from their sources.
#[derive(Clone, Traversable)]
pub struct RelativeMetrics {
// === Supply Relative to Circulating Supply (lazy from global supply) ===
pub supply_rel_to_circulating_supply:
Option<LazyBinaryFromHeightLast<StoredF64, Sats, Sats>>,
// === Supply in Profit/Loss Relative to Own Supply (lazy) ===
pub supply_in_profit_rel_to_own_supply: LazyBinaryFromHeightLast<StoredF64, Sats, Sats>,
pub supply_in_loss_rel_to_own_supply: LazyBinaryFromHeightLast<StoredF64, Sats, Sats>,
// === Supply in Profit/Loss Relative to Circulating Supply (lazy from global supply) ===
pub supply_in_profit_rel_to_circulating_supply:
Option<LazyBinaryFromHeightLast<StoredF64, Sats, Sats>>,
pub supply_in_loss_rel_to_circulating_supply:
Option<LazyBinaryFromHeightLast<StoredF64, Sats, Sats>>,
// === Unrealized vs Market Cap (lazy from global market cap) ===
pub unrealized_profit_rel_to_market_cap:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub unrealized_loss_rel_to_market_cap:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub neg_unrealized_loss_rel_to_market_cap:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub net_unrealized_pnl_rel_to_market_cap:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
// === NUPL (Net Unrealized Profit/Loss) ===
pub nupl: Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
// === Unrealized vs Own Market Cap (lazy) ===
pub unrealized_profit_rel_to_own_market_cap:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub unrealized_loss_rel_to_own_market_cap:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub neg_unrealized_loss_rel_to_own_market_cap:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub net_unrealized_pnl_rel_to_own_market_cap:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
// === Unrealized vs Own Total Unrealized PnL (lazy) ===
pub unrealized_profit_rel_to_own_total_unrealized_pnl:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub unrealized_loss_rel_to_own_total_unrealized_pnl:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
// === Invested Capital in Profit/Loss as % of Realized Cap ===
pub invested_capital_in_profit_pct:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
pub invested_capital_in_loss_pct:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
// === Unrealized Peak Regret Relative to Market Cap (lazy) ===
pub unrealized_peak_regret_rel_to_market_cap:
Option<LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>>,
}
impl RelativeMetrics {
/// Import relative metrics from database.
///
/// All `rel_to_` metrics are lazy - computed on-demand from their sources.
/// `all_supply` provides global sources for `*_rel_to_market_cap` and `*_rel_to_circulating_supply`.
/// `realized` provides realized_cap for invested capital percentage metrics.
pub(crate) fn forced_import(
cfg: &ImportConfig,
unrealized: &UnrealizedMetrics,
supply: &SupplyMetrics,
all_supply: Option<&SupplyMetrics>,
realized: Option<&RealizedMetrics>,
) -> Result<Self> {
let v1 = Version::ONE;
let v2 = Version::new(2);
let extended = cfg.extended();
let compute_rel_to_all = cfg.compute_rel_to_all();
// Global sources from "all" cohort
let global_supply_sats = all_supply.map(|s| &s.total.sats);
let global_market_cap = all_supply.map(|s| &s.total.usd);
// Own market cap source
let own_market_cap = &supply.total.usd;
// For "all" cohort, own_market_cap IS the global market cap
let market_cap = global_market_cap.or_else(|| {
matches!(cfg.filter, Filter::All).then_some(own_market_cap)
});
Ok(Self {
// === Supply Relative to Circulating Supply ===
supply_rel_to_circulating_supply: (compute_rel_to_all
&& global_supply_sats.is_some())
.then(|| {
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_rel_to_circulating_supply"),
cfg.version + v1,
&supply.total.sats,
global_supply_sats.unwrap(),
)
}),
// === Supply in Profit/Loss Relative to Own Supply ===
supply_in_profit_rel_to_own_supply:
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_in_profit_rel_to_own_supply"),
cfg.version + v1,
&unrealized.supply_in_profit.sats,
&supply.total.sats,
),
supply_in_loss_rel_to_own_supply:
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_in_loss_rel_to_own_supply"),
cfg.version + v1,
&unrealized.supply_in_loss.sats,
&supply.total.sats,
),
// === Supply in Profit/Loss Relative to Circulating Supply ===
supply_in_profit_rel_to_circulating_supply: (compute_rel_to_all
&& global_supply_sats.is_some())
.then(|| {
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_in_profit_rel_to_circulating_supply"),
cfg.version + v1,
&unrealized.supply_in_profit.sats,
global_supply_sats.unwrap(),
)
}),
supply_in_loss_rel_to_circulating_supply: (compute_rel_to_all
&& global_supply_sats.is_some())
.then(|| {
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_in_loss_rel_to_circulating_supply"),
cfg.version + v1,
&unrealized.supply_in_loss.sats,
global_supply_sats.unwrap(),
)
}),
// === Unrealized vs Market Cap ===
unrealized_profit_rel_to_market_cap: market_cap.map(|mc| {
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_profit_rel_to_market_cap"),
cfg.version + v2,
&unrealized.unrealized_profit,
mc,
)
}),
unrealized_loss_rel_to_market_cap: market_cap.map(|mc| {
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_loss_rel_to_market_cap"),
cfg.version + v2,
&unrealized.unrealized_loss,
mc,
)
}),
neg_unrealized_loss_rel_to_market_cap: market_cap.map(|mc| {
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
NegPercentageDollarsF32, _, _,
>(
&cfg.name("neg_unrealized_loss_rel_to_market_cap"),
cfg.version + v2,
&unrealized.unrealized_loss,
mc,
)
}),
net_unrealized_pnl_rel_to_market_cap: market_cap.map(|mc| {
LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::<
PercentageDollarsF32, _, _, _, _,
>(
&cfg.name("net_unrealized_pnl_rel_to_market_cap"),
cfg.version + v2,
&unrealized.net_unrealized_pnl,
mc,
)
}),
// NUPL is a proxy for net_unrealized_pnl_rel_to_market_cap
nupl: market_cap.map(|mc| {
LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::<
PercentageDollarsF32, _, _, _, _,
>(
&cfg.name("nupl"),
cfg.version + v2,
&unrealized.net_unrealized_pnl,
mc,
)
}),
// === Unrealized vs Own Market Cap (lazy, optional) ===
unrealized_profit_rel_to_own_market_cap: (extended && compute_rel_to_all)
.then(|| {
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_profit_rel_to_own_market_cap"),
cfg.version + v2,
&unrealized.unrealized_profit,
own_market_cap,
)
}),
unrealized_loss_rel_to_own_market_cap: (extended && compute_rel_to_all)
.then(|| {
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_loss_rel_to_own_market_cap"),
cfg.version + v2,
&unrealized.unrealized_loss,
own_market_cap,
)
}),
neg_unrealized_loss_rel_to_own_market_cap: (extended && compute_rel_to_all)
.then(|| {
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
NegPercentageDollarsF32, _, _,
>(
&cfg.name("neg_unrealized_loss_rel_to_own_market_cap"),
cfg.version + v2,
&unrealized.unrealized_loss,
own_market_cap,
)
}),
net_unrealized_pnl_rel_to_own_market_cap: (extended && compute_rel_to_all)
.then(|| {
LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::<
PercentageDollarsF32, _, _, _, _,
>(
&cfg.name("net_unrealized_pnl_rel_to_own_market_cap"),
cfg.version + v2,
&unrealized.net_unrealized_pnl,
own_market_cap,
)
}),
// === Unrealized vs Own Total Unrealized PnL (lazy, optional) ===
unrealized_profit_rel_to_own_total_unrealized_pnl: extended.then(|| {
LazyBinaryFromHeightLast::from_block_last_and_binary_block::<PercentageDollarsF32, _, _>(
&cfg.name("unrealized_profit_rel_to_own_total_unrealized_pnl"),
cfg.version + v1,
&unrealized.unrealized_profit,
&unrealized.total_unrealized_pnl,
)
}),
unrealized_loss_rel_to_own_total_unrealized_pnl: extended.then(|| {
LazyBinaryFromHeightLast::from_block_last_and_binary_block::<PercentageDollarsF32, _, _>(
&cfg.name("unrealized_loss_rel_to_own_total_unrealized_pnl"),
cfg.version + v1,
&unrealized.unrealized_loss,
&unrealized.total_unrealized_pnl,
)
}),
neg_unrealized_loss_rel_to_own_total_unrealized_pnl: extended.then(|| {
LazyBinaryFromHeightLast::from_block_last_and_binary_block::<NegPercentageDollarsF32, _, _>(
&cfg.name("neg_unrealized_loss_rel_to_own_total_unrealized_pnl"),
cfg.version + v1,
&unrealized.unrealized_loss,
&unrealized.total_unrealized_pnl,
)
}),
net_unrealized_pnl_rel_to_own_total_unrealized_pnl: extended.then(|| {
LazyBinaryFromHeightLast::from_both_binary_block::<PercentageDollarsF32, _, _, _, _>(
&cfg.name("net_unrealized_pnl_rel_to_own_total_unrealized_pnl"),
cfg.version + v2,
&unrealized.net_unrealized_pnl,
&unrealized.total_unrealized_pnl,
)
}),
// === Invested Capital in Profit/Loss as % of Realized Cap ===
invested_capital_in_profit_pct: realized.map(|r| {
LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::<
PercentageDollarsF32, _,
>(
&cfg.name("invested_capital_in_profit_pct"),
cfg.version,
&unrealized.invested_capital_in_profit,
&r.realized_cap,
)
}),
invested_capital_in_loss_pct: realized.map(|r| {
LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::<
PercentageDollarsF32, _,
>(
&cfg.name("invested_capital_in_loss_pct"),
cfg.version,
&unrealized.invested_capital_in_loss,
&r.realized_cap,
)
}),
// === Peak Regret Relative to Market Cap ===
unrealized_peak_regret_rel_to_market_cap: unrealized
.peak_regret
.as_ref()
.zip(market_cap)
.map(|(pr, mc)| {
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_peak_regret_rel_to_market_cap"),
cfg.version,
pr,
mc,
)
}),
})
}
}

View File

@@ -0,0 +1,131 @@
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Sats, StoredF32, StoredF64, Version};
use crate::internal::{
LazyBinaryComputedFromHeightLast, LazyBinaryFromHeightLast, LazyFromHeightLast,
NegPercentageDollarsF32, PercentageDollarsF32, PercentageSatsF64,
};
use crate::distribution::metrics::{ImportConfig, SupplyMetrics, UnrealizedBase};
/// Base relative metrics (always computed when relative is enabled).
/// All fields are non-Optional - market_cap and realized_cap are always
/// available when relative metrics are enabled.
#[derive(Clone, Traversable)]
pub struct RelativeBase {
// === Supply in Profit/Loss Relative to Own Supply ===
pub supply_in_profit_rel_to_own_supply: LazyBinaryFromHeightLast<StoredF64, Sats, Sats>,
pub supply_in_loss_rel_to_own_supply: LazyBinaryFromHeightLast<StoredF64, Sats, Sats>,
// === Unrealized vs Market Cap ===
pub unrealized_profit_rel_to_market_cap: LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub unrealized_loss_rel_to_market_cap: LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub neg_unrealized_loss_rel_to_market_cap:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub net_unrealized_pnl_rel_to_market_cap:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub nupl: LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
// === Invested Capital in Profit/Loss as % of Realized Cap ===
pub invested_capital_in_profit_pct: LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub invested_capital_in_loss_pct: LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
}
impl RelativeBase {
/// Import base relative metrics.
///
/// `market_cap` is either `all_supply.total.usd` (for non-"all" cohorts)
/// or `supply.total.usd` (for the "all" cohort itself).
pub(crate) fn forced_import(
cfg: &ImportConfig,
unrealized: &UnrealizedBase,
supply: &SupplyMetrics,
market_cap: &LazyBinaryComputedFromHeightLast<Dollars, Sats, Dollars>,
realized_cap: &LazyFromHeightLast<Dollars, Cents>,
) -> Self {
let v1 = Version::ONE;
let v2 = Version::new(2);
Self {
supply_in_profit_rel_to_own_supply:
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_in_profit_rel_to_own_supply"),
cfg.version + v1,
&unrealized.supply_in_profit.sats,
&supply.total.sats,
),
supply_in_loss_rel_to_own_supply:
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_in_loss_rel_to_own_supply"),
cfg.version + v1,
&unrealized.supply_in_loss.sats,
&supply.total.sats,
),
unrealized_profit_rel_to_market_cap:
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_profit_rel_to_market_cap"),
cfg.version + v2,
&unrealized.unrealized_profit,
market_cap,
),
unrealized_loss_rel_to_market_cap:
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_loss_rel_to_market_cap"),
cfg.version + v2,
&unrealized.unrealized_loss,
market_cap,
),
neg_unrealized_loss_rel_to_market_cap:
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
NegPercentageDollarsF32, _, _,
>(
&cfg.name("neg_unrealized_loss_rel_to_market_cap"),
cfg.version + v2,
&unrealized.unrealized_loss,
market_cap,
),
net_unrealized_pnl_rel_to_market_cap:
LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::<
PercentageDollarsF32, _, _, _, _,
>(
&cfg.name("net_unrealized_pnl_rel_to_market_cap"),
cfg.version + v2,
&unrealized.net_unrealized_pnl,
market_cap,
),
nupl:
LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::<
PercentageDollarsF32, _, _, _, _,
>(
&cfg.name("nupl"),
cfg.version + v2,
&unrealized.net_unrealized_pnl,
market_cap,
),
invested_capital_in_profit_pct:
LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::<
PercentageDollarsF32, _,
>(
&cfg.name("invested_capital_in_profit_pct"),
cfg.version,
&unrealized.invested_capital_in_profit,
realized_cap,
),
invested_capital_in_loss_pct:
LazyBinaryFromHeightLast::from_block_last_and_lazy_block_last::<
PercentageDollarsF32, _,
>(
&cfg.name("invested_capital_in_loss_pct"),
cfg.version,
&unrealized.invested_capital_in_loss,
realized_cap,
),
}
}
}

View File

@@ -0,0 +1,71 @@
use brk_traversable::Traversable;
use brk_types::{Dollars, Sats, StoredF32, Version};
use crate::internal::{
LazyBinaryComputedFromHeightLast, LazyBinaryFromHeightLast,
NegPercentageDollarsF32, PercentageDollarsF32,
};
use crate::distribution::metrics::{ImportConfig, UnrealizedBase};
/// Extended relative metrics for own market cap (extended && rel_to_all).
#[derive(Clone, Traversable)]
pub struct RelativeExtendedOwnMarketCap {
pub unrealized_profit_rel_to_own_market_cap:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub unrealized_loss_rel_to_own_market_cap:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub neg_unrealized_loss_rel_to_own_market_cap:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub net_unrealized_pnl_rel_to_own_market_cap:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
}
impl RelativeExtendedOwnMarketCap {
pub(crate) fn forced_import(
cfg: &ImportConfig,
unrealized: &UnrealizedBase,
own_market_cap: &LazyBinaryComputedFromHeightLast<Dollars, Sats, Dollars>,
) -> Self {
let v2 = Version::new(2);
Self {
unrealized_profit_rel_to_own_market_cap:
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_profit_rel_to_own_market_cap"),
cfg.version + v2,
&unrealized.unrealized_profit,
own_market_cap,
),
unrealized_loss_rel_to_own_market_cap:
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_loss_rel_to_own_market_cap"),
cfg.version + v2,
&unrealized.unrealized_loss,
own_market_cap,
),
neg_unrealized_loss_rel_to_own_market_cap:
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
NegPercentageDollarsF32, _, _,
>(
&cfg.name("neg_unrealized_loss_rel_to_own_market_cap"),
cfg.version + v2,
&unrealized.unrealized_loss,
own_market_cap,
),
net_unrealized_pnl_rel_to_own_market_cap:
LazyBinaryFromHeightLast::from_binary_block_and_lazy_binary_block_last::<
PercentageDollarsF32, _, _, _, _,
>(
&cfg.name("net_unrealized_pnl_rel_to_own_market_cap"),
cfg.version + v2,
&unrealized.net_unrealized_pnl,
own_market_cap,
),
}
}
}

View File

@@ -0,0 +1,62 @@
use brk_traversable::Traversable;
use brk_types::{Dollars, StoredF32, Version};
use crate::internal::{
LazyBinaryFromHeightLast, NegPercentageDollarsF32, PercentageDollarsF32,
};
use crate::distribution::metrics::{ImportConfig, UnrealizedBase};
/// Extended relative metrics for own total unrealized PnL (extended only).
#[derive(Clone, Traversable)]
pub struct RelativeExtendedOwnPnl {
pub unrealized_profit_rel_to_own_total_unrealized_pnl:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub unrealized_loss_rel_to_own_total_unrealized_pnl:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub neg_unrealized_loss_rel_to_own_total_unrealized_pnl:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
pub net_unrealized_pnl_rel_to_own_total_unrealized_pnl:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
}
impl RelativeExtendedOwnPnl {
pub(crate) fn forced_import(
cfg: &ImportConfig,
unrealized: &UnrealizedBase,
) -> Self {
let v1 = Version::ONE;
let v2 = Version::new(2);
Self {
unrealized_profit_rel_to_own_total_unrealized_pnl:
LazyBinaryFromHeightLast::from_block_last_and_binary_block::<PercentageDollarsF32, _, _>(
&cfg.name("unrealized_profit_rel_to_own_total_unrealized_pnl"),
cfg.version + v1,
&unrealized.unrealized_profit,
&unrealized.total_unrealized_pnl,
),
unrealized_loss_rel_to_own_total_unrealized_pnl:
LazyBinaryFromHeightLast::from_block_last_and_binary_block::<PercentageDollarsF32, _, _>(
&cfg.name("unrealized_loss_rel_to_own_total_unrealized_pnl"),
cfg.version + v1,
&unrealized.unrealized_loss,
&unrealized.total_unrealized_pnl,
),
neg_unrealized_loss_rel_to_own_total_unrealized_pnl:
LazyBinaryFromHeightLast::from_block_last_and_binary_block::<NegPercentageDollarsF32, _, _>(
&cfg.name("neg_unrealized_loss_rel_to_own_total_unrealized_pnl"),
cfg.version + v1,
&unrealized.unrealized_loss,
&unrealized.total_unrealized_pnl,
),
net_unrealized_pnl_rel_to_own_total_unrealized_pnl:
LazyBinaryFromHeightLast::from_both_binary_block::<PercentageDollarsF32, _, _, _, _>(
&cfg.name("net_unrealized_pnl_rel_to_own_total_unrealized_pnl"),
cfg.version + v2,
&unrealized.net_unrealized_pnl,
&unrealized.total_unrealized_pnl,
),
}
}
}

View File

@@ -0,0 +1,43 @@
use brk_types::Dollars;
use brk_traversable::Traversable;
use derive_more::{Deref, DerefMut};
use crate::internal::ComputedFromHeightLast;
use crate::distribution::metrics::{ImportConfig, RealizedBase, SupplyMetrics, UnrealizedBase};
use super::{RelativeBase, RelativeExtendedOwnPnl, RelativePeakRegret};
/// Relative metrics for the "all" cohort (base + own_pnl + peak_regret, NO rel_to_all).
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct RelativeForAll {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RelativeBase,
#[traversable(flatten)]
pub extended_own_pnl: RelativeExtendedOwnPnl,
#[traversable(flatten)]
pub peak_regret: RelativePeakRegret,
}
impl RelativeForAll {
pub(crate) fn forced_import(
cfg: &ImportConfig,
unrealized: &UnrealizedBase,
supply: &SupplyMetrics,
realized_base: &RealizedBase,
peak_regret: &ComputedFromHeightLast<Dollars>,
) -> Self {
// For the "all" cohort, market_cap = own market cap
let market_cap = &supply.total.usd;
Self {
base: RelativeBase::forced_import(
cfg, unrealized, supply, market_cap, &realized_base.realized_cap,
),
extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg, unrealized),
peak_regret: RelativePeakRegret::forced_import(cfg, peak_regret, market_cap),
}
}
}

View File

@@ -0,0 +1,21 @@
mod base;
mod extended_own_market_cap;
mod extended_own_pnl;
mod for_all;
mod peak_regret;
mod to_all;
mod with_extended;
mod with_peak_regret;
mod with_rel_to_all;
pub use base::*;
pub use extended_own_market_cap::*;
pub use extended_own_pnl::*;
pub use for_all::*;
pub use peak_regret::*;
pub use to_all::*;
pub use with_extended::*;
pub use with_peak_regret::*;
pub use with_rel_to_all::*;

View File

@@ -0,0 +1,36 @@
use brk_traversable::Traversable;
use brk_types::{Dollars, Sats, StoredF32};
use crate::internal::{
ComputedFromHeightLast, LazyBinaryComputedFromHeightLast, LazyBinaryFromHeightLast,
PercentageDollarsF32,
};
use crate::distribution::metrics::ImportConfig;
/// Peak regret relative metric.
#[derive(Clone, Traversable)]
pub struct RelativePeakRegret {
pub unrealized_peak_regret_rel_to_market_cap:
LazyBinaryFromHeightLast<StoredF32, Dollars, Dollars>,
}
impl RelativePeakRegret {
pub(crate) fn forced_import(
cfg: &ImportConfig,
peak_regret: &ComputedFromHeightLast<Dollars>,
market_cap: &LazyBinaryComputedFromHeightLast<Dollars, Sats, Dollars>,
) -> Self {
Self {
unrealized_peak_regret_rel_to_market_cap:
LazyBinaryFromHeightLast::from_block_last_and_lazy_binary_computed_block_last::<
PercentageDollarsF32, _, _,
>(
&cfg.name("unrealized_peak_regret_rel_to_market_cap"),
cfg.version,
peak_regret,
market_cap,
),
}
}
}

View File

@@ -0,0 +1,53 @@
use brk_traversable::Traversable;
use brk_types::{Sats, StoredF64, Version};
use crate::internal::{LazyBinaryFromHeightLast, PercentageSatsF64};
use crate::distribution::metrics::{ImportConfig, SupplyMetrics, UnrealizedBase};
/// Relative-to-all metrics (not present for the "all" cohort itself).
#[derive(Clone, Traversable)]
pub struct RelativeToAll {
pub supply_rel_to_circulating_supply:
LazyBinaryFromHeightLast<StoredF64, Sats, Sats>,
pub supply_in_profit_rel_to_circulating_supply:
LazyBinaryFromHeightLast<StoredF64, Sats, Sats>,
pub supply_in_loss_rel_to_circulating_supply:
LazyBinaryFromHeightLast<StoredF64, Sats, Sats>,
}
impl RelativeToAll {
pub(crate) fn forced_import(
cfg: &ImportConfig,
unrealized: &UnrealizedBase,
supply: &SupplyMetrics,
all_supply: &SupplyMetrics,
) -> Self {
let v1 = Version::ONE;
let gs = &all_supply.total.sats;
Self {
supply_rel_to_circulating_supply:
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_rel_to_circulating_supply"),
cfg.version + v1,
&supply.total.sats,
gs,
),
supply_in_profit_rel_to_circulating_supply:
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_in_profit_rel_to_circulating_supply"),
cfg.version + v1,
&unrealized.supply_in_profit.sats,
gs,
),
supply_in_loss_rel_to_circulating_supply:
LazyBinaryFromHeightLast::from_computed_last::<PercentageSatsF64>(
&cfg.name("supply_in_loss_rel_to_circulating_supply"),
cfg.version + v1,
&unrealized.supply_in_loss.sats,
gs,
),
}
}
}

View File

@@ -0,0 +1,56 @@
use brk_types::Dollars;
use brk_traversable::Traversable;
use derive_more::{Deref, DerefMut};
use crate::internal::ComputedFromHeightLast;
use crate::distribution::metrics::{ImportConfig, RealizedBase, SupplyMetrics, UnrealizedBase};
use super::{
RelativeBase, RelativeExtendedOwnMarketCap, RelativeExtendedOwnPnl,
RelativePeakRegret, RelativeToAll,
};
/// Full extended relative metrics (base + rel_to_all + own_market_cap + own_pnl + peak_regret).
/// Used by: sth, lth, age_range cohorts.
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct RelativeWithExtended {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RelativeBase,
#[traversable(flatten)]
pub rel_to_all: RelativeToAll,
#[traversable(flatten)]
pub extended_own_market_cap: RelativeExtendedOwnMarketCap,
#[traversable(flatten)]
pub extended_own_pnl: RelativeExtendedOwnPnl,
#[traversable(flatten)]
pub peak_regret: RelativePeakRegret,
}
impl RelativeWithExtended {
pub(crate) fn forced_import(
cfg: &ImportConfig,
unrealized: &UnrealizedBase,
supply: &SupplyMetrics,
all_supply: &SupplyMetrics,
realized_base: &RealizedBase,
peak_regret: &ComputedFromHeightLast<Dollars>,
) -> Self {
let market_cap = &all_supply.total.usd;
let own_market_cap = &supply.total.usd;
Self {
base: RelativeBase::forced_import(
cfg, unrealized, supply, market_cap, &realized_base.realized_cap,
),
rel_to_all: RelativeToAll::forced_import(cfg, unrealized, supply, all_supply),
extended_own_market_cap: RelativeExtendedOwnMarketCap::forced_import(
cfg, unrealized, own_market_cap,
),
extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg, unrealized),
peak_regret: RelativePeakRegret::forced_import(cfg, peak_regret, market_cap),
}
}
}

View File

@@ -0,0 +1,44 @@
use brk_types::Dollars;
use brk_traversable::Traversable;
use derive_more::{Deref, DerefMut};
use crate::internal::ComputedFromHeightLast;
use crate::distribution::metrics::{ImportConfig, RealizedBase, SupplyMetrics, UnrealizedBase};
use super::{RelativeBase, RelativePeakRegret, RelativeToAll};
/// Relative metrics with rel_to_all + peak_regret (no extended).
/// Used by: max_age, min_age cohorts.
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct RelativeWithPeakRegret {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RelativeBase,
#[traversable(flatten)]
pub rel_to_all: RelativeToAll,
#[traversable(flatten)]
pub peak_regret: RelativePeakRegret,
}
impl RelativeWithPeakRegret {
pub(crate) fn forced_import(
cfg: &ImportConfig,
unrealized: &UnrealizedBase,
supply: &SupplyMetrics,
all_supply: &SupplyMetrics,
realized_base: &RealizedBase,
peak_regret: &ComputedFromHeightLast<Dollars>,
) -> Self {
let market_cap = &all_supply.total.usd;
Self {
base: RelativeBase::forced_import(
cfg, unrealized, supply, market_cap, &realized_base.realized_cap,
),
rel_to_all: RelativeToAll::forced_import(cfg, unrealized, supply, all_supply),
peak_regret: RelativePeakRegret::forced_import(cfg, peak_regret, market_cap),
}
}
}

View File

@@ -0,0 +1,37 @@
use brk_traversable::Traversable;
use derive_more::{Deref, DerefMut};
use crate::distribution::metrics::{ImportConfig, RealizedBase, SupplyMetrics, UnrealizedBase};
use super::{RelativeBase, RelativeToAll};
/// Relative metrics with rel_to_all (no extended, no peak_regret).
/// Used by: epoch, year, type, amount, address cohorts.
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct RelativeWithRelToAll {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RelativeBase,
#[traversable(flatten)]
pub rel_to_all: RelativeToAll,
}
impl RelativeWithRelToAll {
pub(crate) fn forced_import(
cfg: &ImportConfig,
unrealized: &UnrealizedBase,
supply: &SupplyMetrics,
all_supply: &SupplyMetrics,
realized_base: &RealizedBase,
) -> Self {
let market_cap = &all_supply.total.usd;
Self {
base: RelativeBase::forced_import(
cfg, unrealized, supply, market_cap, &realized_base.realized_cap,
),
rel_to_all: RelativeToAll::forced_import(cfg, unrealized, supply, all_supply),
}
}
}

View File

@@ -1,27 +1,26 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, CentsSats, CentsSquaredSats, Dollars, Height, Version};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, AnyVec, BytesVec, Exit, WritableVec, ImportableVec, ReadableCloneableVec,
ReadableVec, Negate, Rw, StorageMode,
AnyStoredVec, AnyVec, BytesVec, Exit, ImportableVec, Negate, ReadableCloneableVec, ReadableVec,
Rw, StorageMode, WritableVec,
};
use crate::{
ComputeIndexes,
distribution::state::UnrealizedState,
internal::{
ComputedFromHeightLast, DollarsMinus, DollarsPlus,
LazyBinaryFromHeightLast, LazyFromHeightLast, ValueFromHeightLast,
ComputedFromHeightLast, DollarsMinus, DollarsPlus, LazyBinaryFromHeightLast,
LazyFromHeightLast, ValueFromHeightLast,
},
prices,
};
use super::ImportConfig;
use crate::distribution::metrics::ImportConfig;
/// Unrealized profit/loss metrics.
/// Base unrealized profit/loss metrics (always computed).
#[derive(Traversable)]
pub struct UnrealizedMetrics<M: StorageMode = Rw> {
pub struct UnrealizedBase<M: StorageMode = Rw> {
// === Supply in Profit/Loss ===
pub supply_in_profit: ValueFromHeightLast<M>,
pub supply_in_loss: ValueFromHeightLast<M>,
@@ -35,21 +34,14 @@ pub struct UnrealizedMetrics<M: StorageMode = Rw> {
pub invested_capital_in_loss: ComputedFromHeightLast<Dollars, M>,
// === Raw values for precise aggregation (used to compute pain/greed indices) ===
/// Σ(price × sats) for UTXOs in profit (raw u128, no indexes)
pub invested_capital_in_profit_raw: M::Stored<BytesVec<Height, CentsSats>>,
/// Σ(price × sats) for UTXOs in loss (raw u128, no indexes)
pub invested_capital_in_loss_raw: M::Stored<BytesVec<Height, CentsSats>>,
/// Σ(price² × sats) for UTXOs in profit (raw u128, no indexes)
pub investor_cap_in_profit_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
/// Σ(price² × sats) for UTXOs in loss (raw u128, no indexes)
pub investor_cap_in_loss_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
// === Pain/Greed Indices (computed in compute_rest from raw values + spot price) ===
/// investor_price_of_losers - spot (average distance underwater, weighted by $)
// === Pain/Greed Indices ===
pub pain_index: ComputedFromHeightLast<Dollars, M>,
/// spot - investor_price_of_winners (average distance in profit, weighted by $)
pub greed_index: ComputedFromHeightLast<Dollars, M>,
/// greed_index - pain_index (positive = greedy market, negative = painful market)
pub net_sentiment: ComputedFromHeightLast<Dollars, M>,
// === Negated ===
@@ -58,18 +50,10 @@ pub struct UnrealizedMetrics<M: StorageMode = Rw> {
// === Net and Total ===
pub net_unrealized_pnl: LazyBinaryFromHeightLast<Dollars>,
pub total_unrealized_pnl: LazyBinaryFromHeightLast<Dollars>,
// === Peak Regret (age_range cohorts only) ===
/// Unrealized peak regret: sum of (peak_price - reference_price) × supply
/// where reference_price = max(spot, cost_basis) and peak = max price during holding period.
/// Only computed for age_range cohorts, then aggregated for overlapping cohorts.
pub peak_regret: Option<ComputedFromHeightLast<Dollars, M>>,
}
impl UnrealizedMetrics {
/// Import unrealized metrics from database.
impl UnrealizedBase {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
// === Supply in Profit/Loss ===
let supply_in_profit = ValueFromHeightLast::forced_import(
cfg.db,
&cfg.name("supply_in_profit"),
@@ -85,7 +69,6 @@ impl UnrealizedMetrics {
cfg.prices,
)?;
// === Unrealized Profit/Loss ===
let unrealized_profit = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("unrealized_profit"),
@@ -99,7 +82,6 @@ impl UnrealizedMetrics {
cfg.indexes,
)?;
// === Invested Capital in Profit/Loss ===
let invested_capital_in_profit = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("invested_capital_in_profit"),
@@ -113,7 +95,6 @@ impl UnrealizedMetrics {
cfg.indexes,
)?;
// === Raw values for precise aggregation ===
let invested_capital_in_profit_raw = BytesVec::forced_import(
cfg.db,
&cfg.name("invested_capital_in_profit_raw"),
@@ -124,12 +105,17 @@ impl UnrealizedMetrics {
&cfg.name("invested_capital_in_loss_raw"),
cfg.version,
)?;
let investor_cap_in_profit_raw =
BytesVec::forced_import(cfg.db, &cfg.name("investor_cap_in_profit_raw"), cfg.version)?;
let investor_cap_in_loss_raw =
BytesVec::forced_import(cfg.db, &cfg.name("investor_cap_in_loss_raw"), cfg.version)?;
let investor_cap_in_profit_raw = BytesVec::forced_import(
cfg.db,
&cfg.name("investor_cap_in_profit_raw"),
cfg.version,
)?;
let investor_cap_in_loss_raw = BytesVec::forced_import(
cfg.db,
&cfg.name("investor_cap_in_loss_raw"),
cfg.version,
)?;
// === Pain/Greed Indices ===
let pain_index = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("pain_index"),
@@ -145,11 +131,10 @@ impl UnrealizedMetrics {
let net_sentiment = ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("net_sentiment"),
cfg.version + Version::ONE, // v1: weighted average for aggregate cohorts
cfg.version + Version::ONE,
cfg.indexes,
)?;
// === Negated ===
let neg_unrealized_loss = LazyFromHeightLast::from_computed::<Negate>(
&cfg.name("neg_unrealized_loss"),
cfg.version,
@@ -157,7 +142,6 @@ impl UnrealizedMetrics {
&unrealized_loss,
);
// === Net and Total ===
let net_unrealized_pnl = LazyBinaryFromHeightLast::from_computed_last::<DollarsMinus>(
&cfg.name("net_unrealized_pnl"),
cfg.version,
@@ -171,19 +155,6 @@ impl UnrealizedMetrics {
&unrealized_loss,
);
// Peak regret: only for age-based UTXO cohorts
let peak_regret = cfg
.compute_peak_regret()
.then(|| {
ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("unrealized_peak_regret"),
cfg.version,
cfg.indexes,
)
})
.transpose()?;
Ok(Self {
supply_in_profit,
supply_in_loss,
@@ -201,11 +172,9 @@ impl UnrealizedMetrics {
neg_unrealized_loss,
net_unrealized_pnl,
total_unrealized_pnl,
peak_regret,
})
}
/// Get minimum length across height-indexed vectors written in block loop.
pub(crate) fn min_stateful_height_len(&self) -> usize {
self.supply_in_profit
.sats
@@ -222,8 +191,11 @@ impl UnrealizedMetrics {
.min(self.investor_cap_in_loss_raw.len())
}
/// Push unrealized state values to height-indexed vectors.
pub(crate) fn truncate_push(&mut self, height: Height, height_state: &UnrealizedState) -> Result<()> {
pub(crate) fn truncate_push(
&mut self,
height: Height,
height_state: &UnrealizedState,
) -> Result<()> {
self.supply_in_profit
.sats
.height
@@ -245,7 +217,6 @@ impl UnrealizedMetrics {
.height
.truncate_push(height, height_state.invested_capital_in_loss.to_dollars())?;
// Raw values for aggregation
self.invested_capital_in_profit_raw.truncate_push(
height,
CentsSats::new(height_state.invested_capital_in_profit_raw),
@@ -266,57 +237,59 @@ impl UnrealizedMetrics {
Ok(())
}
/// Returns a parallel iterator over all vecs for parallel writing.
pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = vec![
&mut self.supply_in_profit.sats.height,
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.supply_in_profit.sats.height as &mut dyn AnyStoredVec,
&mut self.supply_in_loss.sats.height,
&mut self.unrealized_profit.height,
&mut self.unrealized_loss.height,
&mut self.invested_capital_in_profit.height,
&mut self.invested_capital_in_loss.height,
&mut self.invested_capital_in_profit_raw,
&mut self.invested_capital_in_loss_raw,
&mut self.investor_cap_in_profit_raw,
&mut self.investor_cap_in_loss_raw,
];
if let Some(pr) = &mut self.peak_regret {
vecs.push(&mut pr.height);
}
vecs.into_par_iter()
&mut self.invested_capital_in_profit_raw as &mut dyn AnyStoredVec,
&mut self.invested_capital_in_loss_raw as &mut dyn AnyStoredVec,
&mut self.investor_cap_in_profit_raw as &mut dyn AnyStoredVec,
&mut self.investor_cap_in_loss_raw as &mut dyn AnyStoredVec,
]
}
/// Compute aggregate values from separate cohorts.
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &ComputeIndexes,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.supply_in_profit.sats.height.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.supply_in_profit.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
self.supply_in_loss.sats.height.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.supply_in_loss.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
self.unrealized_profit.height.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.unrealized_profit.height)
.collect::<Vec<_>>(),
exit,
)?;
self.supply_in_profit
.sats
.height
.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.supply_in_profit.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
self.supply_in_loss
.sats
.height
.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.supply_in_loss.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
self.unrealized_profit
.height
.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.unrealized_profit.height)
.collect::<Vec<_>>(),
exit,
)?;
self.unrealized_loss.height.compute_sum_of_others(
starting_indexes.height,
&others
@@ -335,24 +308,24 @@ impl UnrealizedMetrics {
.collect::<Vec<_>>(),
exit,
)?;
self.invested_capital_in_loss.height.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.invested_capital_in_loss.height)
.collect::<Vec<_>>(),
exit,
)?;
self.invested_capital_in_loss
.height
.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.invested_capital_in_loss.height)
.collect::<Vec<_>>(),
exit,
)?;
// Raw values for aggregation - manually sum since BytesVec doesn't have compute_sum_of_others
// Start from where the target vecs left off (handles fresh/reset vecs)
// Raw values for aggregation
let start = self
.invested_capital_in_profit_raw
.len()
.min(self.invested_capital_in_loss_raw.len())
.min(self.investor_cap_in_profit_raw.len())
.min(self.investor_cap_in_loss_raw.len());
// End at the minimum length across all source vecs
let end = others
.iter()
.map(|o| o.invested_capital_in_profit_raw.len())
@@ -368,10 +341,22 @@ impl UnrealizedMetrics {
let mut sum_investor_loss = CentsSquaredSats::ZERO;
for o in others.iter() {
sum_invested_profit += o.invested_capital_in_profit_raw.collect_one_at(i).unwrap();
sum_invested_loss += o.invested_capital_in_loss_raw.collect_one_at(i).unwrap();
sum_investor_profit += o.investor_cap_in_profit_raw.collect_one_at(i).unwrap();
sum_investor_loss += o.investor_cap_in_loss_raw.collect_one_at(i).unwrap();
sum_invested_profit += o
.invested_capital_in_profit_raw
.collect_one_at(i)
.unwrap();
sum_invested_loss += o
.invested_capital_in_loss_raw
.collect_one_at(i)
.unwrap();
sum_investor_profit += o
.investor_cap_in_profit_raw
.collect_one_at(i)
.unwrap();
sum_investor_loss += o
.investor_cap_in_loss_raw
.collect_one_at(i)
.unwrap();
}
self.invested_capital_in_profit_raw
@@ -384,21 +369,6 @@ impl UnrealizedMetrics {
.truncate_push(height, sum_investor_loss)?;
}
// Peak regret aggregation (only if this cohort has peak_regret)
if let Some(pr) = &mut self.peak_regret {
let other_prs: Vec<_> = others
.iter()
.filter_map(|v| v.peak_regret.as_ref())
.collect();
if !other_prs.is_empty() {
pr.height.compute_sum_of_others(
starting_indexes.height,
&other_prs.iter().map(|v| &v.height).collect::<Vec<_>>(),
exit,
)?;
}
}
Ok(())
}
@@ -409,8 +379,6 @@ impl UnrealizedMetrics {
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
// Height-based types now have lazy day1, no compute_rest needed.
// Pain index: investor_price_of_losers - spot
self.pain_index.height.compute_transform3(
starting_indexes.height,
@@ -451,15 +419,10 @@ impl UnrealizedMetrics {
exit,
)?;
// Net sentiment height (greed - pain) computed separately for separate cohorts only
// Aggregate cohorts compute it via weighted average in compute_from_stateful
// Dateindex derivation for ALL cohorts happens in compute_net_sentiment_rest
Ok(())
}
/// Compute net_sentiment.height for separate cohorts (greed - pain).
/// Aggregate cohorts skip this - their height is computed via weighted average in compute_from_stateful.
pub(crate) fn compute_net_sentiment_height(
&mut self,
starting_indexes: &ComputeIndexes,

View File

@@ -0,0 +1,9 @@
mod base;
mod peak_regret;
mod with_peak_regret;
pub use base::*;
pub use peak_regret::*;
pub use with_peak_regret::*;

View File

@@ -0,0 +1,49 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Dollars;
use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
use crate::{ComputeIndexes, internal::ComputedFromHeightLast};
use crate::distribution::metrics::ImportConfig;
/// Unrealized peak regret extension (only for age-based UTXO cohorts).
#[derive(Traversable)]
pub struct UnrealizedPeakRegret<M: StorageMode = Rw> {
/// Unrealized peak regret: sum of (peak_price - reference_price) x supply
pub peak_regret: ComputedFromHeightLast<Dollars, M>,
}
impl UnrealizedPeakRegret {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
peak_regret: ComputedFromHeightLast::forced_import(
cfg.db,
&cfg.name("unrealized_peak_regret"),
cfg.version,
cfg.indexes,
)?,
})
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![&mut self.peak_regret.height]
}
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &ComputeIndexes,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.peak_regret.height.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.peak_regret.height)
.collect::<Vec<_>>(),
exit,
)?;
Ok(())
}
}

View File

@@ -0,0 +1,30 @@
use brk_error::Result;
use brk_traversable::Traversable;
use derive_more::{Deref, DerefMut};
use vecdb::{Rw, StorageMode};
use crate::distribution::metrics::ImportConfig;
use super::{UnrealizedBase, UnrealizedPeakRegret};
/// Unrealized metrics with guaranteed peak regret (no Option).
#[derive(Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct UnrealizedWithPeakRegret<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: UnrealizedBase<M>,
#[traversable(flatten)]
pub peak_regret_ext: UnrealizedPeakRegret<M>,
}
impl UnrealizedWithPeakRegret {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
base: UnrealizedBase::forced_import(cfg)?,
peak_regret_ext: UnrealizedPeakRegret::forced_import(cfg)?,
})
}
}

View File

@@ -33,10 +33,6 @@ impl AddressCohortState {
self.inner.realized = RealizedState::default();
}
pub(crate) fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
self.inner.reset_cost_basis_data_if_needed()
}
pub(crate) fn send(
&mut self,
addressdata: &mut FundedAddressData,

View File

@@ -90,7 +90,7 @@ impl Vecs {
indexes,
prices,
&states_path,
Some(&utxo_cohorts.all.metrics.supply),
&utxo_cohorts.all.metrics.supply,
)?;
// Create address data BytesVecs first so we can also use them for identity mappings
@@ -374,7 +374,7 @@ impl Vecs {
blocks,
prices,
starting_indexes,
Some(&height_to_market_cap),
&height_to_market_cap,
exit,
)?;

View File

@@ -2,7 +2,7 @@ use brk_error::Result;
use brk_traversable::{Traversable, TreeNode};
use brk_types::{Dollars, Height, StoredF32, Version};
use vecdb::{
AnyExportableVec, AnyVec, Database, ReadOnlyClone, Ro, Rw, StorageMode, WritableVec,
AnyExportableVec, Database, ReadOnlyClone, Ro, Rw, StorageMode, WritableVec,
};
use crate::indexes;
@@ -90,16 +90,6 @@ impl PercentilesVecs {
Ok(Self { vecs })
}
/// Get minimum length across height-indexed vectors written in block loop.
pub(crate) fn min_stateful_height_len(&self) -> usize {
self.vecs
.iter()
.filter_map(|v| v.as_ref())
.map(|v| v.height.len())
.min()
.unwrap_or(usize::MAX)
}
/// Push percentile prices at this height.
pub(crate) fn truncate_push(
&mut self,

View File

@@ -4,8 +4,8 @@
use brk_traversable::Traversable;
use brk_types::{
Day1, Day3, DifficultyEpoch, HalvingEpoch, Hour1, Hour12, Hour4, Minute1, Minute10, Minute30,
Minute5, Month1, Month3, Month6, Version, Week1, Year1, Year10,
Day1, Day3, DifficultyEpoch, HalvingEpoch, Hour1, Hour4, Hour12, Minute1, Minute5, Minute10,
Minute30, Month1, Month3, Month6, Version, Week1, Year1, Year10,
};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
@@ -19,29 +19,30 @@ use crate::{
},
};
pub type LazyHeightDerivedLastInner<T, S1T> = Indexes<
LazyTransformLast<Minute1, T, S1T>,
LazyTransformLast<Minute5, T, S1T>,
LazyTransformLast<Minute10, T, S1T>,
LazyTransformLast<Minute30, T, S1T>,
LazyTransformLast<Hour1, T, S1T>,
LazyTransformLast<Hour4, T, S1T>,
LazyTransformLast<Hour12, T, S1T>,
LazyTransformLast<Day1, T, S1T>,
LazyTransformLast<Day3, T, S1T>,
LazyTransformLast<Week1, T, S1T>,
LazyTransformLast<Month1, T, S1T>,
LazyTransformLast<Month3, T, S1T>,
LazyTransformLast<Month6, T, S1T>,
LazyTransformLast<Year1, T, S1T>,
LazyTransformLast<Year10, T, S1T>,
LazyTransformLast<HalvingEpoch, T, S1T>,
LazyTransformLast<DifficultyEpoch, T, S1T>,
>;
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct LazyHeightDerivedLast<T, S1T = T>(pub LazyHeightDerivedLastInner<T, S1T>)
pub struct LazyHeightDerivedLast<T, S1T = T>(
#[allow(clippy::type_complexity)]
pub Indexes<
LazyTransformLast<Minute1, T, S1T>,
LazyTransformLast<Minute5, T, S1T>,
LazyTransformLast<Minute10, T, S1T>,
LazyTransformLast<Minute30, T, S1T>,
LazyTransformLast<Hour1, T, S1T>,
LazyTransformLast<Hour4, T, S1T>,
LazyTransformLast<Hour12, T, S1T>,
LazyTransformLast<Day1, T, S1T>,
LazyTransformLast<Day3, T, S1T>,
LazyTransformLast<Week1, T, S1T>,
LazyTransformLast<Month1, T, S1T>,
LazyTransformLast<Month3, T, S1T>,
LazyTransformLast<Month6, T, S1T>,
LazyTransformLast<Year1, T, S1T>,
LazyTransformLast<Year10, T, S1T>,
LazyTransformLast<HalvingEpoch, T, S1T>,
LazyTransformLast<DifficultyEpoch, T, S1T>,
>,
)
where
T: ComputedVecValue + PartialOrd + JsonSchema,
S1T: ComputedVecValue;

View File

@@ -200,6 +200,5 @@ pub use max::*;
pub use min::*;
pub use percentile::*;
pub use percentiles::*;
pub use sparse_last::*;
pub use sum::*;
pub use sum_cum::*;