global: snapshot

This commit is contained in:
nym21
2026-03-02 19:44:45 +01:00
parent 4e7cd9ab6f
commit ccb2db2309
37 changed files with 337 additions and 1412 deletions

View File

@@ -1,7 +1,7 @@
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Height, StoredU32, Timestamp};
use vecdb::{EagerVec, Exit, PcoVec, ReadableVec, VecIndex};
use vecdb::{AnyVec, Cursor, EagerVec, Exit, PcoVec, ReadableVec, VecIndex};
use crate::ComputeIndexes;
@@ -28,135 +28,134 @@ impl Vecs {
exit,
)?;
// Compute rolling window starts (collect monotonic data once for all windows)
let monotonic_data: Vec<Timestamp> = time.timestamp_monotonic.collect();
self.compute_rolling_start_hours(&monotonic_data, time, starting_indexes, exit, 1, |s| {
// Compute rolling window starts
self.compute_rolling_start_hours(time, starting_indexes, exit, 1, |s| {
&mut s.height_1h_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 1, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 1, |s| {
&mut s.height_24h_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 3, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 3, |s| {
&mut s.height_3d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 7, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 7, |s| {
&mut s.height_1w_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 8, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 8, |s| {
&mut s.height_8d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 9, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 9, |s| {
&mut s.height_9d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 12, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 12, |s| {
&mut s.height_12d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 13, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 13, |s| {
&mut s.height_13d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 14, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 14, |s| {
&mut s.height_2w_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 21, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 21, |s| {
&mut s.height_21d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 26, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 26, |s| {
&mut s.height_26d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 30, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 30, |s| {
&mut s.height_1m_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 34, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 34, |s| {
&mut s.height_34d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 55, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 55, |s| {
&mut s.height_55d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 60, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 60, |s| {
&mut s.height_2m_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 63, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 63, |s| {
&mut s.height_9w_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 84, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 84, |s| {
&mut s.height_12w_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 89, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 89, |s| {
&mut s.height_89d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 90, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 90, |s| {
&mut s.height_3m_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 98, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 98, |s| {
&mut s.height_14w_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 111, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 111, |s| {
&mut s.height_111d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 144, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 144, |s| {
&mut s.height_144d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 180, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 180, |s| {
&mut s.height_6m_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 182, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 182, |s| {
&mut s.height_26w_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 200, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 200, |s| {
&mut s.height_200d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 270, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 270, |s| {
&mut s.height_9m_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 350, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 350, |s| {
&mut s.height_350d_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 360, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 360, |s| {
&mut s.height_12m_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 365, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 365, |s| {
&mut s.height_1y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 420, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 420, |s| {
&mut s.height_14m_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 730, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 730, |s| {
&mut s.height_2y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 780, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 780, |s| {
&mut s.height_26m_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 1095, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 1095, |s| {
&mut s.height_3y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 1400, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 1400, |s| {
&mut s.height_200w_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 1460, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 1460, |s| {
&mut s.height_4y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 1825, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 1825, |s| {
&mut s.height_5y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 2190, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 2190, |s| {
&mut s.height_6y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 2920, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 2920, |s| {
&mut s.height_8y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 3285, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 3285, |s| {
&mut s.height_9y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 3650, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 3650, |s| {
&mut s.height_10y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 4380, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 4380, |s| {
&mut s.height_12y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 5110, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 5110, |s| {
&mut s.height_14y_ago
})?;
self.compute_rolling_start(&monotonic_data, time, starting_indexes, exit, 9490, |s| {
self.compute_rolling_start(time, starting_indexes, exit, 9490, |s| {
&mut s.height_26y_ago
})?;
@@ -185,7 +184,6 @@ impl Vecs {
fn compute_rolling_start<F>(
&mut self,
monotonic_data: &[Timestamp],
time: &time::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
@@ -195,13 +193,23 @@ impl Vecs {
where
F: FnOnce(&mut Self) -> &mut EagerVec<PcoVec<Height, Height>>,
{
let mut prev = Height::ZERO;
Ok(get_field(self).compute_transform(
let field = get_field(self);
let resume_from = field.len().min(starting_indexes.height.to_usize());
let mut prev = if resume_from > 0 {
field.collect_one_at(resume_from - 1).unwrap()
} else {
Height::ZERO
};
let mut cursor = Cursor::new(&time.timestamp_monotonic);
cursor.advance(prev.to_usize());
let mut prev_ts = cursor.next().unwrap();
Ok(field.compute_transform(
starting_indexes.height,
&time.timestamp_monotonic,
|(h, t, ..)| {
while t.difference_in_days_between(monotonic_data[prev.to_usize()]) >= days {
while t.difference_in_days_between(prev_ts) >= days {
prev.increment();
prev_ts = cursor.next().unwrap();
if prev > h {
unreachable!()
}
@@ -214,7 +222,6 @@ impl Vecs {
fn compute_rolling_start_hours<F>(
&mut self,
monotonic_data: &[Timestamp],
time: &time::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
@@ -224,13 +231,23 @@ impl Vecs {
where
F: FnOnce(&mut Self) -> &mut EagerVec<PcoVec<Height, Height>>,
{
let mut prev = Height::ZERO;
Ok(get_field(self).compute_transform(
let field = get_field(self);
let resume_from = field.len().min(starting_indexes.height.to_usize());
let mut prev = if resume_from > 0 {
field.collect_one_at(resume_from - 1).unwrap()
} else {
Height::ZERO
};
let mut cursor = Cursor::new(&time.timestamp_monotonic);
cursor.advance(prev.to_usize());
let mut prev_ts = cursor.next().unwrap();
Ok(field.compute_transform(
starting_indexes.height,
&time.timestamp_monotonic,
|(h, t, ..)| {
while t.difference_in_hours_between(monotonic_data[prev.to_usize()]) >= hours {
while t.difference_in_hours_between(prev_ts) >= hours {
prev.increment();
prev_ts = cursor.next().unwrap();
if prev > h {
unreachable!()
}

View File

@@ -22,7 +22,7 @@ pub struct Vecs<M: StorageMode = Rw> {
/// Time-based periods (minute10year10) are lazy: `idx.to_timestamp()` is a pure
/// function of the index, so no storage or decompression is needed.
/// Epoch-based periods (halvingepoch, difficultyepoch) are eager: their timestamps
/// come from block data via `compute_indirect`.
/// come from block data via `compute_indirect_sequential`.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct TimestampIndexes<M: StorageMode = Rw>(
@@ -56,13 +56,13 @@ impl TimestampIndexes {
starting_indexes: &ComputeIndexes,
exit: &Exit,
) -> Result<()> {
self.halvingepoch.compute_indirect(
self.halvingepoch.compute_indirect_sequential(
starting_indexes.halvingepoch,
&indexes.halvingepoch.first_height,
&indexer.vecs.blocks.timestamp,
exit,
)?;
self.difficultyepoch.compute_indirect(
self.difficultyepoch.compute_indirect_sequential(
starting_indexes.difficultyepoch,
&indexes.difficultyepoch.first_height,
&indexer.vecs.blocks.timestamp,

View File

@@ -1,21 +1,21 @@
use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path};
use brk_cohort::{
AGE_BOUNDARIES, ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount,
ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount,
ByMaxAge, ByMinAge, BySpendableType, ByYear, CohortContext, Filter, Filtered, TERM_NAMES, Term,
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{
Cents, CentsCompact, CostBasisDistribution, Date, Day1, Dollars, Height, ONE_HOUR_IN_SEC, Sats,
StoredF32, Timestamp, Version,
Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Height, Sats,
StoredF32, Version,
};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode, VecIndex, WritableVec};
use crate::{
ComputeIndexes, blocks,
distribution::{DynCohortVecs, compute::PriceRangeMax, state::BlockState},
distribution::DynCohortVecs,
indexes,
internal::{PERCENTILES, PERCENTILES_LEN, compute_spot_percentile_rank},
prices,
@@ -23,7 +23,7 @@ use crate::{
use crate::distribution::metrics::{
AdjustedCohortMetrics, AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase,
ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig, PeakRegretCohortMetrics,
ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
SupplyMetrics,
};
@@ -39,13 +39,13 @@ const COST_BASIS_PRICE_DIGITS: i32 = 5;
/// All UTXO cohorts organized by filter type.
///
/// Each group uses a concrete metrics type matching its required features:
/// - age_range: extended realized + extended cost basis + peak regret
/// - age_range: extended realized + extended cost basis
/// - epoch/year/amount/type: basic metrics with relative
/// - all: extended + adjusted + peak regret (no rel_to_all)
/// - sth: extended + adjusted + peak regret
/// - lth: extended + peak regret
/// - max_age: adjusted + peak regret
/// - min_age: peak regret
/// - all: extended + adjusted (no rel_to_all)
/// - sth: extended + adjusted
/// - lth: extended
/// - max_age: adjusted
/// - min_age: basic
#[derive(Traversable)]
pub struct UTXOCohorts<M: StorageMode = Rw> {
pub all: UTXOCohortVecs<AllCohortMetrics<M>>,
@@ -53,7 +53,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
pub lth: UTXOCohortVecs<ExtendedCohortMetrics<M>>,
pub age_range: ByAgeRange<UTXOCohortVecs<ExtendedCohortMetrics<M>>>,
pub max_age: ByMaxAge<UTXOCohortVecs<AdjustedCohortMetrics<M>>>,
pub min_age: ByMinAge<UTXOCohortVecs<PeakRegretCohortMetrics<M>>>,
pub min_age: ByMinAge<UTXOCohortVecs<BasicCohortMetrics<M>>>,
pub ge_amount: ByGreatEqualAmount<UTXOCohortVecs<BasicCohortMetrics<M>>>,
pub amount_range: ByAmountRange<UTXOCohortVecs<BasicCohortMetrics<M>>>,
pub lt_amount: ByLowerThanAmount<UTXOCohortVecs<BasicCohortMetrics<M>>>,
@@ -189,7 +189,7 @@ impl UTXOCohorts<Rw> {
})?
};
// min_age: PeakRegretCohortMetrics
// min_age: BasicCohortMetrics
let min_age = {
ByMinAge::try_new(&|f: Filter, name: &'static str| -> Result<_> {
let full_name = CohortContext::Utxo.full_name(&f, name);
@@ -202,7 +202,7 @@ impl UTXOCohorts<Rw> {
};
Ok(UTXOCohortVecs::new(
None,
PeakRegretCohortMetrics::forced_import(&cfg)?,
BasicCohortMetrics::forced_import(&cfg)?,
))
})?
};
@@ -307,7 +307,7 @@ impl UTXOCohorts<Rw> {
let age_range = &self.age_range;
let amount_range = &self.amount_range;
// all: aggregate of all age_range (base + peak_regret)
// all: aggregate of all age_range
// Note: realized.extended rolling sums are computed from base in compute_rest_part2.
// Note: cost_basis.extended percentiles are computed in truncate_push_aggregate_percentiles.
{
@@ -318,19 +318,9 @@ impl UTXOCohorts<Rw> {
self.all
.metrics
.compute_base_from_others(starting_indexes, &sources_dyn, exit)?;
let pr_sources: Vec<_> = age_range
.iter()
.map(|v| &v.metrics.unrealized.peak_regret_ext)
.collect();
self.all
.metrics
.unrealized
.peak_regret_ext
.compute_from_stateful(starting_indexes, &pr_sources, exit)?;
}
// sth: aggregate of matching age_range (base + peak_regret)
// sth: aggregate of matching age_range
{
let sth_filter = self.sth.metrics.filter().clone();
let matching: Vec<_> = age_range
@@ -345,19 +335,9 @@ impl UTXOCohorts<Rw> {
self.sth
.metrics
.compute_base_from_others(starting_indexes, &sources_dyn, exit)?;
let pr_sources: Vec<_> = matching
.iter()
.map(|v| &v.metrics.unrealized.peak_regret_ext)
.collect();
self.sth
.metrics
.unrealized
.peak_regret_ext
.compute_from_stateful(starting_indexes, &pr_sources, exit)?;
}
// lth: aggregate of matching age_range (base + peak_regret)
// lth: aggregate of matching age_range
{
let lth_filter = self.lth.metrics.filter().clone();
let matching: Vec<_> = age_range
@@ -372,19 +352,9 @@ impl UTXOCohorts<Rw> {
self.lth
.metrics
.compute_base_from_others(starting_indexes, &sources_dyn, exit)?;
let pr_sources: Vec<_> = matching
.iter()
.map(|v| &v.metrics.unrealized.peak_regret_ext)
.collect();
self.lth
.metrics
.unrealized
.peak_regret_ext
.compute_from_stateful(starting_indexes, &pr_sources, exit)?;
}
// min_age: base + peak_regret from matching age_range
// min_age: base from matching age_range
self.min_age
.iter_mut()
.collect::<Vec<_>>()
@@ -403,15 +373,6 @@ impl UTXOCohorts<Rw> {
vecs.metrics
.compute_base_from_others(starting_indexes, &sources_dyn, exit)?;
let pr_sources: Vec<_> = matching
.iter()
.map(|v| &v.metrics.unrealized.peak_regret_ext)
.collect();
vecs.metrics
.unrealized
.peak_regret_ext
.compute_from_stateful(starting_indexes, &pr_sources, exit)?;
Ok(())
})?;
@@ -434,15 +395,6 @@ impl UTXOCohorts<Rw> {
vecs.metrics
.compute_base_from_others(starting_indexes, &sources_dyn, exit)?;
let pr_sources: Vec<_> = matching
.iter()
.map(|v| &v.metrics.unrealized.peak_regret_ext)
.collect();
vecs.metrics
.unrealized
.peak_regret_ext
.compute_from_stateful(starting_indexes, &pr_sources, exit)?;
Ok(())
})?;
@@ -829,7 +781,7 @@ impl UTXOCohorts<Rw> {
&mut self,
height: Height,
spot: Cents,
day1_opt: Option<Day1>,
date_opt: Option<Date>,
states_path: &Path,
) -> Result<()> {
// Collect (filter, entries, total_sats, total_usd) from age_range cohorts.
@@ -940,7 +892,7 @@ impl UTXOCohorts<Rw> {
let mut sats_at_price: u64 = 0;
let mut usd_at_price: u128 = 0;
let collect_merged = day1_opt.is_some();
let collect_merged = date_opt.is_some();
let max_unique_prices = if collect_merged {
relevant.iter().map(|e| e.len()).max().unwrap_or(0)
} else {
@@ -1027,10 +979,9 @@ impl UTXOCohorts<Rw> {
.truncate_push(height, rank)?;
// Write daily cost basis snapshot
if let Some(day1) = day1_opt
if let Some(date) = date_opt
&& let Some(cohort_name) = target.cohort_name
{
let date = Date::from(day1);
let dir = states_path.join(format!("utxo_{cohort_name}_cost_basis/by_date"));
fs::create_dir_all(&dir)?;
let path = dir.join(date.to_string());
@@ -1070,100 +1021,4 @@ impl UTXOCohorts<Rw> {
Ok(())
}
/// Compute and push peak regret for all age_range cohorts.
pub(crate) fn compute_and_push_peak_regret(
&mut self,
chain_state: &[BlockState],
current_height: Height,
current_timestamp: Timestamp,
spot: Cents,
price_range_max: &PriceRangeMax,
) -> Result<()> {
const FIRST_PRICE_HEIGHT: usize = 68_195;
let start_height = FIRST_PRICE_HEIGHT;
let end_height = current_height.to_usize() + 1;
if end_height <= start_height {
for cohort in self.age_range.iter_mut() {
cohort
.metrics
.unrealized
.peak_regret_ext
.peak_regret
.cents
.height
.truncate_push(current_height, Cents::ZERO)?;
}
return Ok(());
}
let spot_u128 = spot.as_u128();
let current_ts = *current_timestamp;
let splits: [usize; 20] = std::array::from_fn(|k| {
let boundary_seconds = (AGE_BOUNDARIES[k] as u32) * ONE_HOUR_IN_SEC;
let threshold_ts = current_ts.saturating_sub(boundary_seconds);
chain_state[..end_height].partition_point(|b| *b.timestamp <= threshold_ts)
});
let ranges: [(usize, usize); 21] = std::array::from_fn(|i| {
if i == 0 {
(splits[0], end_height)
} else if i < 20 {
(splits[i], splits[i - 1])
} else {
(start_height, splits[19])
}
});
let regrets: [Cents; 21] = ranges
.into_par_iter()
.map(|(range_start, range_end)| {
let effective_start = range_start.max(start_height);
if effective_start >= range_end {
return Cents::ZERO;
}
let mut regret: u128 = 0;
for (i, block) in chain_state[effective_start..range_end].iter().enumerate() {
let supply = block.supply.value;
if supply.is_zero() {
continue;
}
let cost_basis = block.price;
let receive_height = Height::from(effective_start + i);
let peak = price_range_max.max_between(receive_height, current_height);
let peak_u128 = peak.as_u128();
let cost_u128 = cost_basis.as_u128();
let supply_u128 = supply.as_u128();
regret += if spot_u128 >= cost_u128 {
(peak_u128 - spot_u128) * supply_u128
} else {
(peak_u128 - cost_u128) * supply_u128
};
}
Cents::new((regret / Sats::ONE_BTC_U128) as u64)
})
.collect::<Vec<_>>()
.try_into()
.unwrap();
for (cohort, regret) in self.age_range.iter_mut().zip(regrets) {
cohort
.metrics
.unrealized
.peak_regret_ext
.peak_regret
.cents
.height
.truncate_push(current_height, regret)?;
}
Ok(())
}
}

View File

@@ -17,14 +17,15 @@ impl UTXOCohorts<Rw> {
///
/// `price_range_max` is used to compute the peak price during each UTXO's holding period
/// for accurate peak regret calculation.
/// Returns the minimum receive_height that was modified, if any.
pub(crate) fn send(
&mut self,
height_to_sent: FxHashMap<Height, Transacted>,
chain_state: &mut [BlockState],
price_range_max: &PriceRangeMax,
) {
) -> Option<Height> {
if chain_state.is_empty() {
return;
return None;
}
let last_block = chain_state.last().unwrap();
@@ -32,8 +33,12 @@ impl UTXOCohorts<Rw> {
let current_price = last_block.price;
let chain_len = chain_state.len();
let send_height = Height::from(chain_len - 1);
let mut min_receive_height: Option<Height> = None;
for (receive_height, sent) in height_to_sent {
min_receive_height = Some(
min_receive_height.map_or(receive_height, |cur| cur.min(receive_height)),
);
// Update chain_state to reflect spent supply
chain_state[receive_height.to_usize()].supply -= &sent.spendable_supply;
@@ -108,5 +113,7 @@ impl UTXOCohorts<Rw> {
);
});
}
min_receive_height
}
}

View File

@@ -3,16 +3,13 @@ use std::thread;
use brk_cohort::ByAddressType;
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{
Cents, Date, Day1, Height, OutputType, Sats, StoredU64, Timestamp, TxIndex, TypeIndex,
};
use brk_types::{Cents, Date, Height, OutputType, Sats, Timestamp, TxIndex, TypeIndex, ONE_DAY_IN_SEC};
use rayon::prelude::*;
use rustc_hash::FxHashSet;
use tracing::{debug, info};
use vecdb::{AnyVec, Exit, ReadableVec, VecIndex};
use crate::{
blocks,
distribution::{
address::{AddressTypeToActivityCounts, AddressTypeToAddressCount},
block::{
@@ -45,7 +42,6 @@ pub(crate) fn process_blocks(
inputs: &inputs::Vecs,
outputs: &outputs::Vecs,
transactions: &transactions::Vecs,
blocks: &blocks::Vecs,
starting_height: Height,
last_height: Height,
chain_state: &mut Vec<BlockState>,
@@ -73,9 +69,6 @@ pub(crate) fn process_blocks(
let height_to_tx_count = &transactions.count.tx_count.height;
let height_to_output_count = &outputs.count.total_count.full.sum_cumulative.sum.0;
let height_to_input_count = &inputs.count.full.sum_cumulative.sum.0;
let height_to_date = &blocks.time.date;
let day1_to_first_height = &indexes.day1.first_height;
let day1_to_height_count = &indexes.day1.height_count;
let txindex_to_output_count = &indexes.txindex.output_count;
let txindex_to_input_count = &indexes.txindex.input_count;
@@ -201,21 +194,16 @@ pub(crate) fn process_blocks(
// Track activity counts - reset each block
let mut activity_counts = AddressTypeToActivityCounts::default();
// Pre-collect lazy vecs that don't support iterators
let height_to_date_vec: Vec<Date> = height_to_date.collect_range_at(start_usize, end_usize);
debug!("creating AddressCache");
let mut cache = AddressCache::new();
debug!("AddressCache created, entering main loop");
// Cache for day1 lookups - same day1 repeats ~140 times per day
let mut cached_day1: Option<Day1> = None;
let mut cached_date_first_height = Height::ZERO;
let mut cached_date_height_count = StoredU64::default();
// Reusable hashsets for received addresses (avoid per-block allocation)
let mut received_addresses = ByAddressType::<FxHashSet<TypeIndex>>::default();
// Track earliest chain_state modification from sends (for incremental supply_state writes)
let mut min_supply_modified: Option<Height> = None;
// Main block iteration
for height in starting_height.to_usize()..=last_height.to_usize() {
let height = Height::from(height);
@@ -413,8 +401,13 @@ pub(crate) fn process_blocks(
// Main thread: Update UTXO cohorts
vecs.utxo_cohorts
.receive(transacted, height, timestamp, block_price);
vecs.utxo_cohorts
.send(height_to_sent, chain_state, ctx.price_range_max);
if let Some(min_h) = vecs.utxo_cohorts
.send(height_to_sent, chain_state, ctx.price_range_max)
{
min_supply_modified = Some(
min_supply_modified.map_or(min_h, |cur| cur.min(min_h)),
);
}
});
// Push to height-indexed vectors
@@ -428,26 +421,12 @@ pub(crate) fn process_blocks(
vecs.address_activity
.truncate_push_height(height, &activity_counts)?;
// Get date info for unrealized state computation (cold path - once per day)
// Cache day1 lookups: same day1 repeats ~140 times per day,
// avoiding redundant PcoVec page decompressions.
let date = height_to_date_vec[offset];
let day1 = Day1::try_from(date).unwrap();
let (date_first_height, date_height_count) = if cached_day1 == Some(day1) {
(cached_date_first_height, cached_date_height_count)
} else {
let fh: Height = day1_to_first_height.collect_one(day1).unwrap();
let hc = day1_to_height_count.collect_one(day1).unwrap();
cached_day1 = Some(day1);
cached_date_first_height = fh;
cached_date_height_count = hc;
(fh, hc)
};
let is_date_last_height =
date_first_height + Height::from(date_height_count).decremented().unwrap() == height;
let day1_opt = is_date_last_height.then_some(day1);
let h = height.to_usize();
let is_last_of_day = height == last_height
|| *cached_timestamps[h] / ONE_DAY_IN_SEC
!= *cached_timestamps[h + 1] / ONE_DAY_IN_SEC;
let date_opt = is_last_of_day.then(|| Date::from(timestamp));
// Push cohort states and compute unrealized
push_cohort_states(
&mut vecs.utxo_cohorts,
&mut vecs.address_cohorts,
@@ -455,25 +434,13 @@ pub(crate) fn process_blocks(
block_price,
)?;
// Compute and push percentiles for aggregate cohorts (all, sth, lth)
vecs.utxo_cohorts.truncate_push_aggregate_percentiles(
height,
block_price,
day1_opt,
date_opt,
&vecs.states_path,
)?;
// Compute unrealized peak regret by age range (once per day)
if day1_opt.is_some() {
vecs.utxo_cohorts.compute_and_push_peak_regret(
chain_state,
height,
timestamp,
block_price,
ctx.price_range_max,
)?;
}
// Periodic checkpoint flush
if height != last_height
&& height != Height::ZERO
@@ -495,7 +462,8 @@ pub(crate) fn process_blocks(
let _lock = exit.lock();
// Write to disk (pure I/O) - no changes saved for periodic flushes
write(vecs, height, chain_state, false)?;
write(vecs, height, chain_state, min_supply_modified, false)?;
min_supply_modified = None;
vecs.flush()?;
// Recreate readers
@@ -519,7 +487,7 @@ pub(crate) fn process_blocks(
)?;
// Write to disk (pure I/O) - save changes for rollback
write(vecs, last_height, chain_state, true)?;
write(vecs, last_height, chain_state, min_supply_modified, true)?;
Ok(())
}

View File

@@ -4,7 +4,7 @@ use brk_error::Result;
use brk_types::{EmptyAddressData, FundedAddressData, Height};
use rayon::prelude::*;
use tracing::info;
use vecdb::{AnyStoredVec, WritableVec, Stamp};
use vecdb::{AnyStoredVec, AnyVec, VecIndex, WritableVec, Stamp};
use crate::distribution::{
Vecs,
@@ -54,6 +54,7 @@ pub(crate) fn write(
vecs: &mut Vecs,
height: Height,
chain_state: &[BlockState],
min_supply_modified: Option<Height>,
with_changes: bool,
) -> Result<()> {
info!("Writing to disk...");
@@ -62,9 +63,13 @@ pub(crate) fn write(
let stamp = Stamp::from(height);
// Prepare chain_state before parallel write
vecs.supply_state.truncate_if_needed(Height::ZERO)?;
for block_state in chain_state {
// Incremental supply_state write: only rewrite from the earliest modified height
let supply_state_len = vecs.supply_state.len();
let truncate_to = min_supply_modified
.map_or(supply_state_len, |h| h.to_usize().min(supply_state_len));
vecs.supply_state
.truncate_if_needed(Height::from(truncate_to))?;
for block_state in &chain_state[truncate_to..] {
vecs.supply_state.push(block_state.supply.clone());
}

View File

@@ -9,11 +9,10 @@ use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, ImportConfig, OutputsMetrics, RealizedBase,
RealizedWithAdjusted, RelativeWithPeakRegret, SupplyMetrics, UnrealizedBase,
UnrealizedWithPeakRegret,
RealizedWithAdjusted, RelativeWithRelToAll, SupplyMetrics, UnrealizedBase,
};
/// Cohort metrics with adjusted realized + peak regret (no extended).
/// Cohort metrics with adjusted realized (no extended).
/// Used by: max_age cohorts.
#[derive(Traversable)]
pub struct AdjustedCohortMetrics<M: StorageMode = Rw> {
@@ -24,8 +23,8 @@ pub struct AdjustedCohortMetrics<M: StorageMode = Rw> {
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<M>>,
pub unrealized: Box<UnrealizedBase<M>>,
pub relative: Box<RelativeWithRelToAll<M>>,
}
impl CohortMetricsBase for AdjustedCohortMetrics {
@@ -53,8 +52,8 @@ impl CohortMetricsBase for AdjustedCohortMetrics {
) -> 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 unrealized_state = state.compute_unrealized_state(height_price);
self.unrealized.truncate_push(height, &unrealized_state)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
@@ -64,8 +63,7 @@ impl CohortMetricsBase for AdjustedCohortMetrics {
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.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
@@ -75,10 +73,10 @@ impl AdjustedCohortMetrics {
cfg: &ImportConfig,
) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?;
let unrealized = UnrealizedBase::forced_import(cfg)?;
let realized = RealizedWithAdjusted::forced_import(cfg)?;
let relative = RelativeWithPeakRegret::forced_import(cfg)?;
let relative = RelativeWithRelToAll::forced_import(cfg)?;
Ok(Self {
filter: cfg.filter.clone(),
@@ -117,12 +115,11 @@ impl AdjustedCohortMetrics {
self.relative.compute(
starting_indexes.height,
&self.unrealized.base,
&self.unrealized,
&self.realized.base,
&self.supply.total.sats.height,
height_to_market_cap,
all_supply_sats,
&self.unrealized.peak_regret_ext.peak_regret.usd.height,
exit,
)?;

View File

@@ -10,11 +10,11 @@ use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig,
OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeForAll, SupplyMetrics,
UnrealizedBase, UnrealizedWithPeakRegret,
UnrealizedBase,
};
/// All-cohort metrics: extended + adjusted realized, extended cost basis,
/// peak regret, relative for-all (no rel_to_all).
/// relative for-all (no rel_to_all).
/// Used by: the "all" cohort.
#[derive(Traversable)]
pub struct AllCohortMetrics<M: StorageMode = Rw> {
@@ -25,7 +25,7 @@ pub struct AllCohortMetrics<M: StorageMode = Rw> {
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithExtendedAdjusted<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub unrealized: Box<UnrealizedWithPeakRegret<M>>,
pub unrealized: Box<UnrealizedBase<M>>,
pub relative: Box<RelativeForAll<M>>,
}
@@ -83,10 +83,9 @@ impl CohortMetricsBase for AllCohortMetrics {
) -> Result<()> {
state.apply_pending();
self.cost_basis.truncate_push_minmax(height, state)?;
let (height_unrealized_state, _) = state.compute_unrealized_states(height_price, None);
let unrealized_state = state.compute_unrealized_state(height_price);
self.unrealized
.base
.truncate_push(height, &height_unrealized_state)?;
.truncate_push(height, &unrealized_state)?;
self.cost_basis
.extended
.truncate_push_percentiles(height, state, height_price)?;
@@ -100,8 +99,7 @@ impl CohortMetricsBase for AllCohortMetrics {
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.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
@@ -115,7 +113,7 @@ impl AllCohortMetrics {
cfg: &ImportConfig,
supply: SupplyMetrics,
) -> Result<Self> {
let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?;
let unrealized = UnrealizedBase::forced_import(cfg)?;
let realized = RealizedWithExtendedAdjusted::forced_import(cfg)?;
let relative = RelativeForAll::forced_import(cfg)?;
@@ -156,11 +154,10 @@ impl AllCohortMetrics {
self.relative.compute(
starting_indexes.height,
&self.unrealized.base,
&self.unrealized,
&self.realized.base,
&self.supply.total.sats.height,
height_to_market_cap,
&self.unrealized.peak_regret_ext.peak_regret.usd.height,
exit,
)?;

View File

@@ -51,8 +51,8 @@ impl CohortMetricsBase for BasicCohortMetrics {
) -> 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)?;
let unrealized_state = state.compute_unrealized_state(height_price);
self.unrealized.truncate_push(height, &unrealized_state)?;
Ok(())
}
fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {

View File

@@ -10,10 +10,10 @@ use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig,
OutputsMetrics, RealizedBase, RealizedWithExtended, RelativeWithExtended, SupplyMetrics,
UnrealizedBase, UnrealizedWithPeakRegret,
UnrealizedBase,
};
/// Cohort metrics with extended realized + extended cost basis + peak regret (no adjusted).
/// Cohort metrics with extended realized + extended cost basis (no adjusted).
/// Used by: lth, age_range cohorts.
#[derive(Traversable)]
pub struct ExtendedCohortMetrics<M: StorageMode = Rw> {
@@ -24,7 +24,7 @@ pub struct ExtendedCohortMetrics<M: StorageMode = Rw> {
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithExtended<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub unrealized: Box<UnrealizedWithPeakRegret<M>>,
pub unrealized: Box<UnrealizedBase<M>>,
pub relative: Box<RelativeWithExtended<M>>,
}
@@ -54,8 +54,8 @@ impl CohortMetricsBase for ExtendedCohortMetrics {
) -> 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 unrealized_state = state.compute_unrealized_state(height_price);
self.unrealized.truncate_push(height, &unrealized_state)?;
self.cost_basis.extended.truncate_push_percentiles(height, state, height_price)?;
Ok(())
}
@@ -67,8 +67,7 @@ impl CohortMetricsBase for ExtendedCohortMetrics {
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.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
@@ -78,7 +77,7 @@ impl ExtendedCohortMetrics {
cfg: &ImportConfig,
) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?;
let unrealized = UnrealizedBase::forced_import(cfg)?;
let realized = RealizedWithExtended::forced_import(cfg)?;
let relative = RelativeWithExtended::forced_import(cfg)?;
@@ -115,13 +114,12 @@ impl ExtendedCohortMetrics {
self.relative.compute(
starting_indexes.height,
&self.unrealized.base,
&self.unrealized,
&self.realized.base,
&self.supply.total.sats.height,
height_to_market_cap,
all_supply_sats,
&self.supply.total.usd.height,
&self.unrealized.peak_regret_ext.peak_regret.usd.height,
exit,
)?;

View File

@@ -10,10 +10,10 @@ use crate::{ComputeIndexes, blocks, distribution::state::CohortState, prices};
use crate::distribution::metrics::{
ActivityMetrics, CohortMetricsBase, CostBasisBase, CostBasisWithExtended, ImportConfig,
OutputsMetrics, RealizedBase, RealizedWithExtendedAdjusted, RelativeWithExtended,
SupplyMetrics, UnrealizedBase, UnrealizedWithPeakRegret,
SupplyMetrics, UnrealizedBase,
};
/// Cohort metrics with extended + adjusted realized, extended cost basis, peak regret.
/// Cohort metrics with extended + adjusted realized, extended cost basis.
/// Used by: sth cohort.
#[derive(Traversable)]
pub struct ExtendedAdjustedCohortMetrics<M: StorageMode = Rw> {
@@ -24,7 +24,7 @@ pub struct ExtendedAdjustedCohortMetrics<M: StorageMode = Rw> {
pub activity: Box<ActivityMetrics<M>>,
pub realized: Box<RealizedWithExtendedAdjusted<M>>,
pub cost_basis: Box<CostBasisWithExtended<M>>,
pub unrealized: Box<UnrealizedWithPeakRegret<M>>,
pub unrealized: Box<UnrealizedBase<M>>,
pub relative: Box<RelativeWithExtended<M>>,
}
@@ -53,8 +53,8 @@ impl CohortMetricsBase for ExtendedAdjustedCohortMetrics {
) -> 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 unrealized_state = state.compute_unrealized_state(height_price);
self.unrealized.truncate_push(height, &unrealized_state)?;
self.cost_basis.extended.truncate_push_percentiles(height, state, height_price)?;
Ok(())
}
@@ -66,8 +66,7 @@ impl CohortMetricsBase for ExtendedAdjustedCohortMetrics {
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.extend(self.unrealized.collect_vecs_mut());
vecs
}
}
@@ -77,7 +76,7 @@ impl ExtendedAdjustedCohortMetrics {
cfg: &ImportConfig,
) -> Result<Self> {
let supply = SupplyMetrics::forced_import(cfg)?;
let unrealized = UnrealizedWithPeakRegret::forced_import(cfg)?;
let unrealized = UnrealizedBase::forced_import(cfg)?;
let realized = RealizedWithExtendedAdjusted::forced_import(cfg)?;
let relative = RelativeWithExtended::forced_import(cfg)?;
@@ -119,13 +118,12 @@ impl ExtendedAdjustedCohortMetrics {
self.relative.compute(
starting_indexes.height,
&self.unrealized.base,
&self.unrealized,
&self.realized.base,
&self.supply.total.sats.height,
height_to_market_cap,
all_supply_sats,
&self.supply.total.usd.height,
&self.unrealized.peak_regret_ext.peak_regret.usd.height,
exit,
)?;

View File

@@ -4,12 +4,8 @@ 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

@@ -1,126 +0,0 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Sats, 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<M>>,
}
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,
) -> 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)?;
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>,
all_supply_sats: &impl ReadableVec<Height, Sats>,
exit: &Exit,
) -> Result<()> {
self.realized.compute_rest_part2_base(
blocks,
prices,
starting_indexes,
&self.supply.total.btc.height,
height_to_market_cap,
exit,
)?;
self.relative.compute(
starting_indexes.height,
&self.unrealized.base,
&self.realized,
&self.supply.total.sats.height,
height_to_market_cap,
all_supply_sats,
&self.unrealized.peak_regret_ext.peak_regret.usd.height,
exit,
)?;
Ok(())
}
}

View File

@@ -6,9 +6,9 @@ use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::distribution::metrics::{ImportConfig, RealizedBase, UnrealizedBase};
use super::{RelativeBase, RelativeExtendedOwnPnl, RelativePeakRegret};
use super::{RelativeBase, RelativeExtendedOwnPnl};
/// Relative metrics for the "all" cohort (base + own_pnl + peak_regret, NO rel_to_all).
/// Relative metrics for the "all" cohort (base + own_pnl, NO rel_to_all).
#[derive(Deref, DerefMut, Traversable)]
pub struct RelativeForAll<M: StorageMode = Rw> {
#[deref]
@@ -17,8 +17,6 @@ pub struct RelativeForAll<M: StorageMode = Rw> {
pub base: RelativeBase<M>,
#[traversable(flatten)]
pub extended_own_pnl: RelativeExtendedOwnPnl<M>,
#[traversable(flatten)]
pub peak_regret: RelativePeakRegret<M>,
}
impl RelativeForAll {
@@ -26,7 +24,6 @@ impl RelativeForAll {
Ok(Self {
base: RelativeBase::forced_import(cfg)?,
extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg)?,
peak_regret: RelativePeakRegret::forced_import(cfg)?,
})
}
@@ -38,7 +35,6 @@ impl RelativeForAll {
realized: &RealizedBase,
supply_total_sats: &impl ReadableVec<Height, Sats>,
market_cap: &impl ReadableVec<Height, Dollars>,
peak_regret_val: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.base.compute(
@@ -50,8 +46,6 @@ impl RelativeForAll {
exit,
)?;
self.extended_own_pnl.compute(max_from, unrealized, exit)?;
self.peak_regret
.compute(max_from, peak_regret_val, market_cap, exit)?;
Ok(())
}
}

View File

@@ -3,10 +3,8 @@ 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::*;
@@ -14,8 +12,6 @@ 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

@@ -1,47 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, StoredF32};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::internal::{
ComputedFromHeight, PercentageDollarsF32,
};
use crate::distribution::metrics::ImportConfig;
/// Peak regret relative metric.
#[derive(Traversable)]
pub struct RelativePeakRegret<M: StorageMode = Rw> {
pub unrealized_peak_regret_rel_to_market_cap:
ComputedFromHeight<StoredF32, M>,
}
impl RelativePeakRegret {
pub(crate) fn forced_import(
cfg: &ImportConfig,
) -> Result<Self> {
Ok(Self {
unrealized_peak_regret_rel_to_market_cap:
ComputedFromHeight::forced_import(
cfg.db,
&cfg.name("unrealized_peak_regret_rel_to_market_cap"),
cfg.version,
cfg.indexes,
)?,
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
peak_regret: &impl ReadableVec<Height, Dollars>,
market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.unrealized_peak_regret_rel_to_market_cap
.compute_binary::<Dollars, Dollars, PercentageDollarsF32>(
max_from, peak_regret, market_cap, exit,
)
}
}

View File

@@ -7,11 +7,11 @@ use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::distribution::metrics::{ImportConfig, RealizedBase, UnrealizedBase};
use super::{
RelativeBase, RelativeExtendedOwnMarketCap, RelativeExtendedOwnPnl, RelativePeakRegret,
RelativeBase, RelativeExtendedOwnMarketCap, RelativeExtendedOwnPnl,
RelativeToAll,
};
/// Full extended relative metrics (base + rel_to_all + own_market_cap + own_pnl + peak_regret).
/// Full extended relative metrics (base + rel_to_all + own_market_cap + own_pnl).
/// Used by: sth, lth, age_range cohorts.
#[derive(Deref, DerefMut, Traversable)]
pub struct RelativeWithExtended<M: StorageMode = Rw> {
@@ -25,8 +25,6 @@ pub struct RelativeWithExtended<M: StorageMode = Rw> {
pub extended_own_market_cap: RelativeExtendedOwnMarketCap<M>,
#[traversable(flatten)]
pub extended_own_pnl: RelativeExtendedOwnPnl<M>,
#[traversable(flatten)]
pub peak_regret: RelativePeakRegret<M>,
}
impl RelativeWithExtended {
@@ -36,7 +34,6 @@ impl RelativeWithExtended {
rel_to_all: RelativeToAll::forced_import(cfg)?,
extended_own_market_cap: RelativeExtendedOwnMarketCap::forced_import(cfg)?,
extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg)?,
peak_regret: RelativePeakRegret::forced_import(cfg)?,
})
}
@@ -50,7 +47,6 @@ impl RelativeWithExtended {
market_cap: &impl ReadableVec<Height, Dollars>,
all_supply_sats: &impl ReadableVec<Height, Sats>,
own_market_cap: &impl ReadableVec<Height, Dollars>,
peak_regret_val: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.base.compute(
@@ -71,8 +67,6 @@ impl RelativeWithExtended {
self.extended_own_market_cap
.compute(max_from, unrealized, own_market_cap, exit)?;
self.extended_own_pnl.compute(max_from, unrealized, exit)?;
self.peak_regret
.compute(max_from, peak_regret_val, market_cap, exit)?;
Ok(())
}
}

View File

@@ -1,65 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats};
use derive_more::{Deref, DerefMut};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::distribution::metrics::{ImportConfig, RealizedBase, UnrealizedBase};
use super::{RelativeBase, RelativePeakRegret, RelativeToAll};
/// Relative metrics with rel_to_all + peak_regret (no extended).
/// Used by: max_age, min_age cohorts.
#[derive(Deref, DerefMut, Traversable)]
pub struct RelativeWithPeakRegret<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: RelativeBase<M>,
#[traversable(flatten)]
pub rel_to_all: RelativeToAll<M>,
#[traversable(flatten)]
pub peak_regret: RelativePeakRegret<M>,
}
impl RelativeWithPeakRegret {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
base: RelativeBase::forced_import(cfg)?,
rel_to_all: RelativeToAll::forced_import(cfg)?,
peak_regret: RelativePeakRegret::forced_import(cfg)?,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute(
&mut self,
max_from: Height,
unrealized: &UnrealizedBase,
realized: &RealizedBase,
supply_total_sats: &impl ReadableVec<Height, Sats>,
market_cap: &impl ReadableVec<Height, Dollars>,
all_supply_sats: &impl ReadableVec<Height, Sats>,
peak_regret_val: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.base.compute(
max_from,
unrealized,
realized,
supply_total_sats,
market_cap,
exit,
)?;
self.rel_to_all.compute(
max_from,
unrealized,
supply_total_sats,
all_supply_sats,
exit,
)?;
self.peak_regret
.compute(max_from, peak_regret_val, market_cap, exit)?;
Ok(())
}
}

View File

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

View File

@@ -1,49 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Cents;
use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
use crate::{ComputeIndexes, internal::FiatFromHeight};
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: FiatFromHeight<Cents, M>,
}
impl UnrealizedPeakRegret {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
peak_regret: FiatFromHeight::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.cents.height]
}
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &ComputeIndexes,
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.peak_regret.cents.height.compute_sum_of_others(
starting_indexes.height,
&others
.iter()
.map(|v| &v.peak_regret.cents.height)
.collect::<Vec<_>>(),
exit,
)?;
Ok(())
}
}

View File

@@ -1,28 +0,0 @@
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)]
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

@@ -255,13 +255,9 @@ impl CohortState {
self.cost_basis_data.compute_percentiles()
}
pub(crate) fn compute_unrealized_states(
&mut self,
height_price: Cents,
date_price: Option<Cents>,
) -> (UnrealizedState, Option<UnrealizedState>) {
pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState {
self.cost_basis_data
.compute_unrealized_states(height_price, date_price)
.compute_unrealized_state(height_price)
}
pub(crate) fn write(&mut self, height: Height, cleanup: bool) -> Result<()> {

View File

@@ -257,33 +257,21 @@ impl CostBasisData {
self.cached_percentiles
}
pub(crate) fn compute_unrealized_states(
&mut self,
height_price: Cents,
date_price: Option<Cents>,
) -> (UnrealizedState, Option<UnrealizedState>) {
pub(crate) fn compute_unrealized_state(&mut self, height_price: Cents) -> UnrealizedState {
if self.is_empty() {
return (
UnrealizedState::ZERO,
date_price.map(|_| UnrealizedState::ZERO),
);
return UnrealizedState::ZERO;
}
let map = &self.state.as_ref().unwrap().base.map;
let date_state =
date_price.map(|p| CachedUnrealizedState::compute_full_standalone(p.into(), map));
let height_state = if let Some(cache) = self.cache.as_mut() {
if let Some(cache) = self.cache.as_mut() {
cache.get_at_price(height_price, map)
} else {
let cache = CachedUnrealizedState::compute_fresh(height_price, map);
let state = cache.current_state();
self.cache = Some(cache);
state
};
(height_state, date_state)
}
}
pub(crate) fn clean(&mut self) -> Result<()> {

View File

@@ -280,12 +280,4 @@ impl CachedUnrealizedState {
state
}
/// Compute final UnrealizedState directly (not cached).
/// Used for date_state which doesn't use the cache.
pub(crate) fn compute_full_standalone(
current_price: CentsCompact,
map: &CostBasisMap,
) -> UnrealizedState {
Self::compute_raw(current_price, map).to_output()
}
}

View File

@@ -379,7 +379,6 @@ impl Vecs {
inputs,
outputs,
transactions,
blocks,
starting_height,
last_height,
&mut chain_state,

View File

@@ -75,7 +75,7 @@ where
) -> Result<()> {
macro_rules! period {
($field:ident) => {
self.0.$field.compute_indirect(
self.0.$field.compute_indirect_sequential(
starting_indexes.$field,
&indexes.$field.first_height,
height_source,

View File

@@ -32,6 +32,9 @@ impl Vecs {
starting_indexes.height,
h2d,
|(h, di, _)| {
if last_di.is_none() && h.to_usize() > 0 {
last_di = Some(h2d.collect_one_at(h.to_usize() - 1).unwrap());
}
let same_day = last_di.is_some_and(|prev| prev == di);
last_di = Some(di);
if same_day {

View File

@@ -1,7 +1,7 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, PoolSlug, StoredF32, StoredU16, StoredU32};
use vecdb::{BinaryTransform, Database, Exit, ReadableVec, Rw, StorageMode, Version};
use vecdb::{AnyVec, BinaryTransform, Database, Exit, ReadableVec, Rw, StorageMode, VecIndex, Version};
use crate::{
blocks,
@@ -277,7 +277,19 @@ impl Vecs {
)?;
{
let mut prev = StoredU32::ZERO;
let resume_from = self
.blocks_since_block
.height
.len()
.min(starting_indexes.height.to_usize());
let mut prev = if resume_from > 0 {
self.blocks_since_block
.height
.collect_one_at(resume_from - 1)
.unwrap()
} else {
StoredU32::ZERO
};
self.blocks_since_block.height.compute_transform(
starting_indexes.height,
&self.blocks_mined.height,