global: snapshot

This commit is contained in:
nym21
2026-03-08 01:30:30 +01:00
parent cf6c755e51
commit 6bb5c63db7
23 changed files with 2024 additions and 198 deletions

View File

@@ -15,7 +15,7 @@ use crate::{blocks, distribution::DynCohortVecs, indexes, prices};
use crate::distribution::metrics::{
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics,
ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig, MinimalCohortMetrics,
SupplyMetrics,
ProfitabilityMetrics, SupplyMetrics,
};
use super::{percentiles::PercentileCache, vecs::UTXOCohortVecs};
@@ -39,6 +39,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
pub amount_range: ByAmountRange<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub lt_amount: ByLowerThanAmount<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub type_: BySpendableType<UTXOCohortVecs<MinimalCohortMetrics<M>>>,
pub profitability: ProfitabilityMetrics<M>,
#[traversable(skip)]
pub(super) percentile_cache: PercentileCache,
/// Cached partition_point positions for tick_tock boundary searches.
@@ -141,6 +142,9 @@ impl UTXOCohorts<Rw> {
AllCohortMetrics::forced_import_with_supply(&all_cfg, all_supply)?,
);
// Phase 3b: Import profitability metrics (derived from "all" during k-way merge).
let profitability = ProfitabilityMetrics::forced_import(db, v, indexes)?;
// Phase 4: Import aggregate cohorts.
// sth: ExtendedAdjustedCohortMetrics
@@ -227,6 +231,7 @@ impl UTXOCohorts<Rw> {
amount_range,
lt_amount,
ge_amount,
profitability,
percentile_cache: PercentileCache::default(),
tick_tock_cached_positions: [0; 20],
})
@@ -590,6 +595,7 @@ impl UTXOCohorts<Rw> {
for v in self.type_.iter_mut() {
vecs.extend(v.metrics.collect_all_vecs_mut());
}
vecs.extend(self.profitability.collect_all_vecs_mut());
vecs.into_par_iter()
}
@@ -599,12 +605,13 @@ impl UTXOCohorts<Rw> {
.try_for_each(|v| v.write_state(height, cleanup))
}
/// Get minimum height from all separate cohorts' height-indexed vectors.
/// Get minimum height from all separate cohorts' + profitability height-indexed vectors.
pub(crate) fn min_separate_stateful_height_len(&self) -> Height {
self.iter_separate()
.map(|v| Height::from(v.min_stateful_height_len()))
.min()
.unwrap_or_default()
.min(Height::from(self.profitability.min_stateful_height_len()))
}
/// Import state for all separate cohorts at or before given height.
@@ -650,7 +657,6 @@ impl UTXOCohorts<Rw> {
for v in self.lt_amount.iter_mut() {
v.metrics.validate_computed_versions(base_version)?;
}
Ok(())
}
}

View File

@@ -1,12 +1,15 @@
use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path};
use brk_cohort::{Filtered, TERM_NAMES};
use brk_cohort::{
compute_profitability_boundaries, Filtered, PROFITABILITY_BOUNDARY_COUNT,
PROFITABILITY_RANGE_COUNT, PROFIT_COUNT, TERM_NAMES,
};
use brk_error::Result;
use brk_types::{Cents, CentsCompact, CostBasisDistribution, Date, Height, Sats};
use brk_types::{Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Height, Sats};
use crate::internal::{PERCENTILES, PERCENTILES_LEN};
use crate::distribution::metrics::{CohortMetricsBase, CostBasis};
use crate::distribution::metrics::{CostBasis, ProfitabilityMetrics};
use super::groups::UTXOCohorts;
@@ -27,45 +30,47 @@ impl CachedPercentiles {
}
}
/// Cached percentile results for all/sth/lth.
/// Cached percentile + profitability results for all/sth/lth.
/// Avoids re-merging 21 BTreeMaps on every block.
#[derive(Clone, Default)]
pub(super) struct PercentileCache {
all: CachedPercentiles,
sth: CachedPercentiles,
lth: CachedPercentiles,
profitability: [(u64, u128); PROFITABILITY_RANGE_COUNT],
initialized: bool,
}
impl UTXOCohorts {
/// Compute and push percentiles for aggregate cohorts (all, sth, lth).
/// Compute and push percentiles + profitability for aggregate cohorts.
///
/// Full K-way merge only runs at day boundaries or when the cache is empty.
/// For intermediate blocks, pushes cached percentile arrays.
/// For intermediate blocks, pushes cached values.
pub(crate) fn truncate_push_aggregate_percentiles(
&mut self,
height: Height,
spot_price: Cents,
date_opt: Option<Date>,
states_path: &Path,
) -> Result<()> {
if date_opt.is_some() || !self.percentile_cache.initialized {
self.merge_and_push_percentiles(height, date_opt, states_path)
} else {
self.push_cached_percentiles(height)
self.recompute_cache(spot_price, date_opt, states_path)?;
}
self.push_cached(height)
}
/// Full K-way merge: compute percentiles from scratch, update cache, push.
fn merge_and_push_percentiles(
/// Full K-way merge: recompute percentiles + profitability from scratch, update cache.
fn recompute_cache(
&mut self,
height: Height,
spot_price: Cents,
date_opt: Option<Date>,
states_path: &Path,
) -> Result<()> {
let collect_merged = date_opt.is_some();
let boundaries = compute_profitability_boundaries(spot_price);
let targets = {
let sth_filter = self.sth.metrics.filter().clone();
let sth_filter = self.sth.metrics.filter.clone();
let mut totals = AllSthLth::<(u64, u128)>::default();
let maps: Vec<_> = self
@@ -101,31 +106,26 @@ impl UTXOCohorts {
};
let all_has_data = totals.all.0 > 0;
let mut targets = totals.map(|(sats, usd)| PercTarget::new(sats, usd, cap));
self.percentile_cache.profitability = Default::default();
if all_has_data {
merge_k_way(&maps, &mut targets, collect_merged);
merge_k_way(
&maps,
&mut targets,
&boundaries,
&mut self.percentile_cache.profitability,
collect_merged,
);
}
targets
};
// Update cache + push
self.percentile_cache.all = targets.all.to_cached();
self.percentile_cache.sth = targets.sth.to_cached();
self.percentile_cache.lth = targets.lth.to_cached();
self.percentile_cache.initialized = true;
self.percentile_cache
.all
.push(height, &mut self.all.metrics.cost_basis)?;
self.percentile_cache
.sth
.push(height, &mut self.sth.metrics.cost_basis)?;
self.percentile_cache
.lth
.push(height, &mut self.lth.metrics.cost_basis)?;
// Serialize full distribution at day boundaries
if let Some(date) = date_opt {
write_distribution(states_path, "all", date, targets.all.merged)?;
write_distribution(states_path, TERM_NAMES.short.id, date, targets.sth.merged)?;
@@ -135,8 +135,8 @@ impl UTXOCohorts {
Ok(())
}
/// Fast path: push cached percentile arrays.
fn push_cached_percentiles(&mut self, height: Height) -> Result<()> {
/// Push cached percentile + profitability values.
fn push_cached(&mut self, height: Height) -> Result<()> {
self.percentile_cache
.all
.push(height, &mut self.all.metrics.cost_basis)?;
@@ -146,10 +146,60 @@ impl UTXOCohorts {
self.percentile_cache
.lth
.push(height, &mut self.lth.metrics.cost_basis)?;
Ok(())
push_profitability(
height,
&self.percentile_cache.profitability,
&mut self.profitability,
)
}
}
/// Convert raw (cents × sats) accumulator to Dollars (÷ 100 for cents→dollars, ÷ 1e8 for sats).
#[inline]
fn raw_usd_to_dollars(raw: u128) -> Dollars {
Dollars::from(raw as f64 / 1e10)
}
/// Push profitability range + profit/loss aggregate values to vecs.
fn push_profitability(
height: Height,
buckets: &[(u64, u128); PROFITABILITY_RANGE_COUNT],
metrics: &mut ProfitabilityMetrics,
) -> Result<()> {
// Push 25 range buckets
for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() {
let (sats, usd_raw) = buckets[i];
bucket.truncate_push(height, Sats::from(sats), raw_usd_to_dollars(usd_raw))?;
}
// ByProfit: forward cumulative sum over ranges[0..15], pushed in reverse.
// profit[0] (breakeven) = sum(0..=14), ..., profit[14] (_1000pct) = ranges[0]
let profit_arr = metrics.profit.as_array_mut();
let mut cum_sats = 0u64;
let mut cum_usd = 0u128;
for i in 0..PROFIT_COUNT {
cum_sats += buckets[i].0;
cum_usd += buckets[i].1;
profit_arr[PROFIT_COUNT - 1 - i]
.truncate_push(height, Sats::from(cum_sats), raw_usd_to_dollars(cum_usd))?;
}
// ByLoss: backward cumulative sum over ranges[15..25], pushed in reverse.
// loss[0] (breakeven) = sum(15..=24), ..., loss[9] (_90pct) = ranges[24]
let loss_arr = metrics.loss.as_array_mut();
let loss_count = loss_arr.len();
cum_sats = 0;
cum_usd = 0;
for i in 0..loss_count {
cum_sats += buckets[PROFITABILITY_RANGE_COUNT - 1 - i].0;
cum_usd += buckets[PROFITABILITY_RANGE_COUNT - 1 - i].1;
loss_arr[loss_count - 1 - i]
.truncate_push(height, Sats::from(cum_sats), raw_usd_to_dollars(cum_usd))?;
}
Ok(())
}
fn write_distribution(
states_path: &Path,
name: &str,
@@ -166,9 +216,12 @@ fn write_distribution(
}
/// K-way merge via BinaryHeap over BTreeMap iterators.
/// Also accumulates profitability buckets for the "all" target using cursor approach.
fn merge_k_way(
maps: &[(&std::collections::BTreeMap<CentsCompact, Sats>, bool)],
targets: &mut AllSthLth<PercTarget>,
boundaries: &[Cents; PROFITABILITY_BOUNDARY_COUNT],
prof: &mut [(u64, u128); PROFITABILITY_RANGE_COUNT],
collect_merged: bool,
) {
let mut iters: Vec<_> = maps
@@ -185,36 +238,40 @@ fn merge_k_way(
}
let mut current_price: Option<CentsCompact> = None;
let mut early_exit = false;
let mut boundary_idx = 0usize;
while let Some(Reverse((price, ci))) = heap.pop() {
let (ref mut iter, is_sth) = iters[ci];
let (_, &sats) = iter.next().unwrap();
let amount = u64::from(sats);
let usd = Cents::from(price).as_u128() * amount as u128;
let price_cents = Cents::from(price);
let usd = price_cents.as_u128() * amount as u128;
if let Some(prev) = current_price
&& prev != price
{
targets.for_each_mut(|t| t.finalize_price(prev.into(), collect_merged));
if !collect_merged && targets.all_match(|t| t.done()) {
early_exit = true;
break;
}
}
current_price = Some(price);
targets.all.accumulate(amount, usd);
targets.term_mut(is_sth).accumulate(amount, usd);
// Profitability: advance cursor past boundaries (prices are ascending)
while boundary_idx < PROFITABILITY_BOUNDARY_COUNT
&& price_cents >= boundaries[boundary_idx]
{
boundary_idx += 1;
}
prof[boundary_idx].0 += amount;
prof[boundary_idx].1 += usd;
if let Some(&(&next_price, _)) = iter.peek() {
heap.push(Reverse((next_price, ci)));
}
}
if !early_exit
&& let Some(price) = current_price
{
if let Some(price) = current_price {
targets.for_each_mut(|t| t.finalize_price(price.into(), collect_merged));
}
}
@@ -254,9 +311,6 @@ impl<T> AllSthLth<T> {
f(&mut self.lth);
}
fn all_match(&self, mut f: impl FnMut(&T) -> bool) -> bool {
f(&self.all) && f(&self.sth) && f(&self.lth)
}
}
struct PercTarget {
@@ -362,8 +416,4 @@ impl PercTarget {
self.price_usd = 0;
}
fn done(&self) -> bool {
(self.total_sats == 0 || self.sat_idx >= PERCENTILES_LEN)
&& (self.total_usd == 0 || self.usd_idx >= PERCENTILES_LEN)
}
}

View File

@@ -438,6 +438,7 @@ pub(crate) fn process_blocks(
vecs.utxo_cohorts.truncate_push_aggregate_percentiles(
height,
block_price,
date_opt,
&vecs.states_path,
)?;

View File

@@ -10,10 +10,10 @@ use crate::{
indexes,
internal::{
CentsType, ComputedFromHeight, ComputedFromHeightCumulative,
ComputedFromHeightCumulativeSum, ComputedFromHeightRatio, FiatFromHeight, NumericValue,
PercentFromHeight, PercentRollingWindows, Price, RollingDelta1m, RollingDeltaExcept1m,
RollingWindow24h, RollingWindows, RollingWindowsFrom1w,
ValueFromHeight, ValueFromHeightCumulative,
ComputedFromHeightCumulativeSum, ComputedFromHeightRatio, FiatFromHeight,
FiatRollingDelta1m, FiatRollingDeltaExcept1m, NumericValue, PercentFromHeight,
PercentRollingWindows, Price, RollingDelta1m, RollingDeltaExcept1m, RollingWindow24h,
RollingWindows, RollingWindowsFrom1w, ValueFromHeight, ValueFromHeightCumulative,
},
};
@@ -96,6 +96,16 @@ impl<S: NumericValue + JsonSchema, C: NumericValue + JsonSchema> ConfigImport
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
}
}
impl<S: NumericValue + JsonSchema, C: CentsType> ConfigImport for FiatRollingDelta1m<S, C> {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
}
}
impl<S: NumericValue + JsonSchema, C: CentsType> ConfigImport for FiatRollingDeltaExcept1m<S, C> {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
}
}
impl<T: BytesVecValue> ConfigImport for BytesVec<Height, T> {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Ok(Self::forced_import(

View File

@@ -36,6 +36,7 @@ mod cohort;
mod config;
mod cost_basis;
mod outputs;
mod profitability;
mod realized;
mod relative;
mod supply;
@@ -48,6 +49,7 @@ pub use cohort::{
};
pub use config::ImportConfig;
pub use cost_basis::CostBasis;
pub use profitability::ProfitabilityMetrics;
pub use outputs::OutputsMetrics;
pub use realized::{
RealizedAdjusted, RealizedBase, RealizedCore, RealizedFull, RealizedLike, RealizedMinimal,

View File

@@ -0,0 +1,124 @@
use brk_cohort::{ByLoss, ByProfit, ByProfitabilityRange};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, Database, Rw, StorageMode, WritableVec};
use crate::{indexes, internal::ComputedFromHeight};
/// Supply + realized cap for a single profitability bucket.
#[derive(Traversable)]
pub struct ProfitabilityBucket<M: StorageMode = Rw> {
pub supply: ComputedFromHeight<Sats, M>,
pub realized_cap: ComputedFromHeight<Dollars, M>,
}
impl<M: StorageMode> ProfitabilityBucket<M> {
fn min_len(&self) -> usize {
self.supply.height.len().min(self.realized_cap.height.len())
}
}
impl ProfitabilityBucket {
fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
supply: ComputedFromHeight::forced_import(
db,
&format!("{name}_supply"),
version,
indexes,
)?,
realized_cap: ComputedFromHeight::forced_import(
db,
&format!("{name}_realized_cap"),
version,
indexes,
)?,
})
}
pub(crate) fn truncate_push(
&mut self,
height: Height,
supply: Sats,
realized_cap: Dollars,
) -> Result<()> {
self.supply.height.truncate_push(height, supply)?;
self.realized_cap
.height
.truncate_push(height, realized_cap)?;
Ok(())
}
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.supply.height as &mut dyn AnyStoredVec,
&mut self.realized_cap.height as &mut dyn AnyStoredVec,
]
}
}
/// All profitability metrics: 25 ranges + 15 profit thresholds + 10 loss thresholds.
#[derive(Traversable)]
pub struct ProfitabilityMetrics<M: StorageMode = Rw> {
pub range: ByProfitabilityRange<ProfitabilityBucket<M>>,
pub profit: ByProfit<ProfitabilityBucket<M>>,
pub loss: ByLoss<ProfitabilityBucket<M>>,
}
impl<M: StorageMode> ProfitabilityMetrics<M> {
pub(crate) fn min_stateful_height_len(&self) -> usize {
self.range.iter()
.chain(self.profit.iter())
.chain(self.loss.iter())
.map(|b| b.min_len())
.min()
.unwrap_or(0)
}
}
impl ProfitabilityMetrics {
pub(crate) fn forced_import(
db: &Database,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let range = ByProfitabilityRange::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, version, indexes)
})?;
let profit = ByProfit::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, version, indexes)
})?;
let loss = ByLoss::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, version, indexes)
})?;
Ok(Self {
range,
profit,
loss,
})
}
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = Vec::new();
for bucket in self.range.iter_mut() {
vecs.extend(bucket.collect_all_vecs_mut());
}
for bucket in self.profit.iter_mut() {
vecs.extend(bucket.collect_all_vecs_mut());
}
for bucket in self.loss.iter_mut() {
vecs.extend(bucket.collect_all_vecs_mut());
}
vecs
}
}

View File

@@ -10,8 +10,8 @@ use crate::{
blocks,
distribution::state::RealizedOps,
internal::{
ComputedFromHeight, LazyFromHeight, NegCentsUnsignedToDollars, RatioCents64,
RollingDelta1m, RollingWindow24h,
ComputedFromHeight, FiatRollingDelta1m, LazyFromHeight, NegCentsUnsignedToDollars,
RatioCents64, RollingWindow24h,
},
prices,
};
@@ -27,7 +27,7 @@ pub struct RealizedCore<M: StorageMode = Rw> {
#[traversable(flatten)]
pub minimal: RealizedMinimal<M>,
pub realized_cap_delta: RollingDelta1m<Cents, CentsSigned, M>,
pub realized_cap_delta: FiatRollingDelta1m<Cents, CentsSigned, M>,
pub neg_realized_loss: LazyFromHeight<Dollars, Cents>,
pub net_realized_pnl: ComputedFromHeight<CentsSigned, M>,

View File

@@ -14,12 +14,12 @@ use crate::{
blocks,
distribution::state::RealizedState,
internal::{
CentsUnsignedToDollars, ComputedFromHeight, ComputedFromHeightCumulative, FiatFromHeight,
CentsUnsignedToDollars, ComputedFromHeight, ComputedFromHeightCumulative,
ComputedFromHeightRatio, ComputedFromHeightRatioPercentiles,
ComputedFromHeightRatioStdDevBands, LazyFromHeight, PercentFromHeight,
PercentRollingWindows, Price, RatioCents64, RatioCentsBp32,
RatioCentsSignedCentsBps32, RatioCentsSignedDollarsBps32, RatioDollarsBp32,
RollingDelta1m, RollingDeltaExcept1m, RollingWindows, RollingWindowsFrom1w,
ComputedFromHeightRatioStdDevBands, FiatFromHeight, FiatRollingDelta1m,
FiatRollingDeltaExcept1m, LazyFromHeight, PercentFromHeight, PercentRollingWindows, Price,
RatioCents64, RatioCentsBp32, RatioCentsSignedCentsBps32, RatioCentsSignedDollarsBps32,
RatioDollarsBp32, RollingWindows, RollingWindowsFrom1w,
},
prices,
};
@@ -59,12 +59,12 @@ pub struct RealizedFull<M: StorageMode = Rw> {
pub net_realized_pnl_cumulative: ComputedFromHeight<CentsSigned, M>,
pub net_realized_pnl_sum_extended: RollingWindowsFrom1w<CentsSigned, M>,
pub net_pnl_delta: RollingDelta1m<CentsSigned, CentsSigned, M>,
pub net_pnl_delta_extended: RollingDeltaExcept1m<CentsSigned, CentsSigned, M>,
pub net_pnl_delta: FiatRollingDelta1m<CentsSigned, CentsSigned, M>,
pub net_pnl_delta_extended: FiatRollingDeltaExcept1m<CentsSigned, CentsSigned, M>,
pub net_pnl_change_1m_rel_to_realized_cap: PercentFromHeight<BasisPointsSigned32, M>,
pub net_pnl_change_1m_rel_to_market_cap: PercentFromHeight<BasisPointsSigned32, M>,
pub realized_cap_delta_extended: RollingDeltaExcept1m<Cents, CentsSigned, M>,
pub realized_cap_delta_extended: FiatRollingDeltaExcept1m<Cents, CentsSigned, M>,
pub investor_price: Price<ComputedFromHeight<Cents, M>>,
pub investor_price_ratio: ComputedFromHeightRatio<M>,
@@ -468,14 +468,14 @@ impl RealizedFull {
self.net_pnl_change_1m_rel_to_realized_cap
.compute_binary::<CentsSigned, Cents, RatioCentsSignedCentsBps32>(
starting_indexes.height,
&self.net_pnl_delta.change_1m.height,
&self.net_pnl_delta.change_1m.cents.height,
&self.base.core.minimal.realized_cap_cents.height,
exit,
)?;
self.net_pnl_change_1m_rel_to_market_cap
.compute_binary::<CentsSigned, Dollars, RatioCentsSignedDollarsBps32>(
starting_indexes.height,
&self.net_pnl_delta.change_1m.height,
&self.net_pnl_delta.change_1m.cents.height,
height_to_market_cap,
exit,
)?;

View File

@@ -25,7 +25,7 @@ use crate::{
/// Pre-collect source data from the earliest needed offset.
/// Returns (source_data, offset) for use in compute_delta_window.
fn collect_source<S: NumericValue>(
pub(super) fn collect_source<S: NumericValue>(
source: &impl ReadableVec<Height, S>,
skip: usize,
earliest_starts: &impl ReadableVec<Height, Height>,
@@ -40,7 +40,7 @@ fn collect_source<S: NumericValue>(
}
/// Shared computation: change = current - ago, rate = change / ago.
fn compute_delta_window<S, C, B>(
pub(super) fn compute_delta_window<S, C, B>(
change_h: &mut EagerVec<PcoVec<Height, C>>,
rate_bps_h: &mut EagerVec<PcoVec<Height, B>>,
max_from: Height,

View File

@@ -0,0 +1,275 @@
//! Fiat delta variants — same as RollingDelta* but change is FiatFromHeight<C>
//! (stored cents + lazy USD) instead of ComputedFromHeight<C> (stored cents only).
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPointsSigned32, Height, Version};
use schemars::JsonSchema;
use vecdb::{AnyVec, Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{
CentsType, FiatFromHeight, NumericValue, PercentFromHeight, PercentRollingWindows,
WindowStarts,
},
};
use super::delta::{collect_source, compute_delta_window};
/// Fiat 1m-only delta: fiat change (cents + usd) + rate for the 1-month window.
#[derive(Traversable)]
pub struct FiatRollingDelta1m<S, C, M: StorageMode = Rw>
where
S: NumericValue + JsonSchema,
C: CentsType,
{
pub change_1m: FiatFromHeight<C, M>,
pub rate_1m: PercentFromHeight<BasisPointsSigned32, M>,
_phantom: std::marker::PhantomData<S>,
}
impl<S, C> FiatRollingDelta1m<S, C>
where
S: NumericValue + JsonSchema,
C: CentsType,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
change_1m: FiatFromHeight::forced_import(
db,
&format!("{name}_change_1m"),
version,
indexes,
)?,
rate_1m: PercentFromHeight::forced_import(
db,
&format!("{name}_rate_1m"),
version,
indexes,
)?,
_phantom: std::marker::PhantomData,
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
height_1m_ago: &impl ReadableVec<Height, Height>,
source: &impl ReadableVec<Height, S>,
exit: &Exit,
) -> Result<()> {
let skip = self.change_1m.cents.height.len();
let (source_data, offset) = collect_source(source, skip, height_1m_ago);
compute_delta_window(
&mut self.change_1m.cents.height,
&mut self.rate_1m.bps.height,
max_from,
height_1m_ago,
&source_data,
offset,
exit,
)
}
}
/// Fiat extended delta: 24h + 1w + 1y windows, fiat change (cents + usd) + rate.
#[derive(Traversable)]
pub struct FiatRollingDeltaExcept1m<S, C, M: StorageMode = Rw>
where
S: NumericValue + JsonSchema,
C: CentsType,
{
#[traversable(rename = "24h")]
pub change_24h: FiatFromHeight<C, M>,
pub change_1w: FiatFromHeight<C, M>,
pub change_1y: FiatFromHeight<C, M>,
#[traversable(rename = "24h")]
pub rate_24h: PercentFromHeight<BasisPointsSigned32, M>,
pub rate_1w: PercentFromHeight<BasisPointsSigned32, M>,
pub rate_1y: PercentFromHeight<BasisPointsSigned32, M>,
_phantom: std::marker::PhantomData<S>,
}
impl<S, C> FiatRollingDeltaExcept1m<S, C>
where
S: NumericValue + JsonSchema,
C: CentsType,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
change_24h: FiatFromHeight::forced_import(
db,
&format!("{name}_change_24h"),
version,
indexes,
)?,
change_1w: FiatFromHeight::forced_import(
db,
&format!("{name}_change_1w"),
version,
indexes,
)?,
change_1y: FiatFromHeight::forced_import(
db,
&format!("{name}_change_1y"),
version,
indexes,
)?,
rate_24h: PercentFromHeight::forced_import(
db,
&format!("{name}_rate_24h"),
version,
indexes,
)?,
rate_1w: PercentFromHeight::forced_import(
db,
&format!("{name}_rate_1w"),
version,
indexes,
)?,
rate_1y: PercentFromHeight::forced_import(
db,
&format!("{name}_rate_1y"),
version,
indexes,
)?,
_phantom: std::marker::PhantomData,
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
source: &impl ReadableVec<Height, S>,
exit: &Exit,
) -> Result<()> {
let skip = self.change_24h.cents.height.len();
let (source_data, offset) = collect_source(source, skip, windows._1y);
let changes: [&mut FiatFromHeight<C>; 3] =
[&mut self.change_24h, &mut self.change_1w, &mut self.change_1y];
let rates = [&mut self.rate_24h, &mut self.rate_1w, &mut self.rate_1y];
let starts = [windows._24h, windows._1w, windows._1y];
for ((change_w, rate_w), starts) in changes.into_iter().zip(rates).zip(starts) {
compute_delta_window(
&mut change_w.cents.height,
&mut rate_w.bps.height,
max_from,
starts,
&source_data,
offset,
exit,
)?;
}
Ok(())
}
}
/// Fiat rolling delta: all 4 windows, fiat change (cents + usd) + rate.
#[derive(Traversable)]
pub struct FiatRollingDelta<S, C, M: StorageMode = Rw>
where
S: NumericValue + JsonSchema,
C: CentsType,
{
pub change_24h: FiatFromHeight<C, M>,
pub change_1w: FiatFromHeight<C, M>,
pub change_1m: FiatFromHeight<C, M>,
pub change_1y: FiatFromHeight<C, M>,
pub rate: PercentRollingWindows<BasisPointsSigned32, M>,
_phantom: std::marker::PhantomData<S>,
}
impl<S, C> FiatRollingDelta<S, C>
where
S: NumericValue + JsonSchema,
C: CentsType,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
change_24h: FiatFromHeight::forced_import(
db,
&format!("{name}_change_24h"),
version,
indexes,
)?,
change_1w: FiatFromHeight::forced_import(
db,
&format!("{name}_change_1w"),
version,
indexes,
)?,
change_1m: FiatFromHeight::forced_import(
db,
&format!("{name}_change_1m"),
version,
indexes,
)?,
change_1y: FiatFromHeight::forced_import(
db,
&format!("{name}_change_1y"),
version,
indexes,
)?,
rate: PercentRollingWindows::forced_import(
db,
&format!("{name}_rate"),
version,
indexes,
)?,
_phantom: std::marker::PhantomData,
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
source: &impl ReadableVec<Height, S>,
exit: &Exit,
) -> Result<()> {
let skip = self.change_24h.cents.height.len();
let (source_data, offset) = collect_source(source, skip, windows._1y);
let changes: [&mut FiatFromHeight<C>; 4] = [
&mut self.change_24h,
&mut self.change_1w,
&mut self.change_1m,
&mut self.change_1y,
];
let rates = self.rate.0.as_mut_array();
let starts = windows.as_array();
for ((change_w, rate_w), starts) in changes.into_iter().zip(rates).zip(starts) {
compute_delta_window(
&mut change_w.cents.height,
&mut rate_w.bps.height,
max_from,
*starts,
&source_data,
offset,
exit,
)?;
}
Ok(())
}
}

View File

@@ -2,6 +2,7 @@ mod aggregated;
mod cumulative;
mod cumulative_sum;
mod delta;
mod fiat_delta;
mod full;
mod rolling_average;
mod sum;
@@ -10,6 +11,7 @@ pub use aggregated::*;
pub use cumulative::*;
pub use cumulative_sum::*;
pub use delta::*;
pub use fiat_delta::*;
pub use full::*;
pub use rolling_average::*;
pub use sum::*;

View File

@@ -0,0 +1,41 @@
use brk_traversable::Traversable;
use brk_types::{Dollars, Version};
use vecdb::ReadableCloneableVec;
use super::{ComputedFromHeight, LazyFromHeight};
use crate::internal::{Identity, NumericValue};
use super::fiat::CentsType;
/// Lazy fiat: both cents and usd are lazy views of a stored source.
/// Zero extra stored vecs.
#[derive(Clone, Traversable)]
pub struct LazyFiatFromHeight<C: CentsType> {
pub cents: LazyFromHeight<C, C>,
pub usd: LazyFromHeight<Dollars, C>,
}
impl<C: CentsType> LazyFiatFromHeight<C> {
pub(crate) fn from_computed(
name: &str,
version: Version,
source: &ComputedFromHeight<C>,
) -> Self
where
C: NumericValue,
{
let cents = LazyFromHeight::from_computed::<Identity<C>>(
&format!("{name}_cents"),
version,
source.height.read_only_boxed_clone(),
source,
);
let usd = LazyFromHeight::from_computed::<C::ToDollars>(
&format!("{name}_usd"),
version,
source.height.read_only_boxed_clone(),
source,
);
Self { cents, usd }
}
}

View File

@@ -4,6 +4,7 @@ mod computed;
mod constant;
mod fiat;
mod lazy;
mod lazy_fiat;
mod percent;
mod percentiles;
mod price;
@@ -17,6 +18,7 @@ pub use computed::*;
pub use constant::*;
pub use fiat::*;
pub use lazy::*;
pub use lazy_fiat::*;
pub use percent::*;
pub use percentiles::*;
pub use price::*;

View File

@@ -44,39 +44,34 @@ impl Vecs {
self.velocity
.compute(blocks, transactions, distribution, starting_indexes, exit)?;
// 4. Compute cap growth rates across 4 windows
// 4. Compute market cap delta (change + rate across 4 windows)
let window_starts = blocks.count.window_starts();
let realized_cap = &distribution
.utxo_cohorts
.all
.metrics
.realized
.realized_cap
.height;
self.market_cap_delta.compute(
starting_indexes.height,
&window_starts,
&self.market_cap.cents.height,
exit,
)?;
let mcgr_arr = self.market_cap_growth_rate.0.as_mut_array();
let rcgr_arr = self.realized_cap_growth_rate.0.as_mut_array();
// 5. market_cap_rate - realized_cap_rate per window
let all_realized = &distribution.utxo_cohorts.all.metrics.realized;
let mcr_arr = self.market_cap_delta.rate.0.as_array();
let diff_arr = self.market_minus_realized_cap_growth_rate.0.as_mut_array();
let starts_arr = window_starts.as_array();
// 24h, 1w, 1y from extended; 1m from core delta
let rcr_rates = [
&all_realized.realized_cap_delta_extended.rate_24h.bps.height,
&all_realized.realized_cap_delta_extended.rate_1w.bps.height,
&all_realized.realized_cap_delta.rate_1m.bps.height,
&all_realized.realized_cap_delta_extended.rate_1y.bps.height,
];
for i in 0..4 {
mcgr_arr[i].bps.height.compute_rolling_ratio_change(
starting_indexes.height,
*starts_arr[i],
&self.market_cap.height,
exit,
)?;
rcgr_arr[i].bps.height.compute_rolling_ratio_change(
starting_indexes.height,
*starts_arr[i],
realized_cap,
exit,
)?;
diff_arr[i].height.compute_subtract(
starting_indexes.height,
&mcgr_arr[i].bps.height,
&rcgr_arr[i].bps.height,
&mcr_arr[i].bps.height,
rcr_rates[i],
exit,
)?;
}

View File

@@ -6,7 +6,7 @@ use brk_types::{Cents, Dollars, Sats, Version};
use crate::{
distribution, indexes,
internal::{
Identity, LazyFromHeight, LazyValueFromHeight, PercentFromHeight, PercentRollingWindows,
FiatRollingDelta, Identity, LazyFiatFromHeight, LazyValueFromHeight, PercentFromHeight,
RollingWindows, SatsToBitcoin, finalize_db, open_db,
},
};
@@ -45,30 +45,22 @@ impl Vecs {
// Velocity
let velocity = super::velocity::Vecs::forced_import(&db, version, indexes)?;
// Market cap - lazy identity from distribution supply in USD
let market_cap = LazyFromHeight::from_lazy::<Identity<Dollars>, Cents>(
"market_cap",
version,
&supply_metrics.total.usd,
);
// Market cap - lazy fiat (cents + usd) from distribution supply
let market_cap =
LazyFiatFromHeight::from_computed("market_cap", version, &supply_metrics.total.cents);
// Growth rates (4 windows: 24h, 1w, 1m, 1y)
let market_cap_growth_rate = PercentRollingWindows::forced_import(
// Market cap delta (change + rate across 4 windows)
let market_cap_delta = FiatRollingDelta::forced_import(
&db,
"market_cap_growth_rate",
version + Version::TWO,
indexes,
)?;
let realized_cap_growth_rate = PercentRollingWindows::forced_import(
&db,
"realized_cap_growth_rate",
version + Version::TWO,
"market_cap_delta",
version + Version::new(3),
indexes,
)?;
let market_minus_realized_cap_growth_rate = RollingWindows::forced_import(
&db,
"market_minus_realized_cap_growth_rate",
version + Version::ONE,
version + Version::TWO,
indexes,
)?;
@@ -79,8 +71,7 @@ impl Vecs {
inflation_rate,
velocity,
market_cap,
market_cap_growth_rate,
realized_cap_growth_rate,
market_cap_delta,
market_minus_realized_cap_growth_rate,
};
finalize_db(&this.db, &this)?;

View File

@@ -1,10 +1,10 @@
use brk_traversable::Traversable;
use brk_types::{BasisPointsSigned32, Dollars};
use brk_types::{BasisPointsSigned32, Cents, CentsSigned};
use vecdb::{Database, Rw, StorageMode};
use super::{burned, velocity};
use crate::internal::{
LazyFromHeight, LazyValueFromHeight, PercentFromHeight, PercentRollingWindows, RollingWindows,
FiatRollingDelta, LazyFiatFromHeight, LazyValueFromHeight, PercentFromHeight, RollingWindows,
};
#[derive(Traversable)]
@@ -16,8 +16,7 @@ pub struct Vecs<M: StorageMode = Rw> {
pub burned: burned::Vecs<M>,
pub inflation_rate: PercentFromHeight<BasisPointsSigned32, M>,
pub velocity: velocity::Vecs<M>,
pub market_cap: LazyFromHeight<Dollars>,
pub market_cap_growth_rate: PercentRollingWindows<BasisPointsSigned32, M>,
pub realized_cap_growth_rate: PercentRollingWindows<BasisPointsSigned32, M>,
pub market_cap: LazyFiatFromHeight<Cents>,
pub market_cap_delta: FiatRollingDelta<Cents, CentsSigned, M>,
pub market_minus_realized_cap_growth_rate: RollingWindows<BasisPointsSigned32, M>,
}