global: snapshot part 3

This commit is contained in:
nym21
2026-03-20 12:54:26 +01:00
parent b8e57f4788
commit 1d671ea41f
32 changed files with 1561 additions and 1249 deletions

View File

@@ -6,7 +6,7 @@ use brk_cohort::{
};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Indexes, Sats, Version};
use brk_types::{Cents, CentsSquaredSats, Dollars, Height, Indexes, Sats, Version};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode, WritableVec,
@@ -738,15 +738,9 @@ impl UTXOCohorts<Rw> {
.min()
.unwrap_or_default()
.min(Height::from(self.profitability.min_stateful_len()))
.min(Height::from(
self.all.metrics.realized.min_stateful_len(),
))
.min(Height::from(
self.sth.metrics.realized.min_stateful_len(),
))
.min(Height::from(
self.lth.metrics.realized.min_stateful_len(),
))
.min(Height::from(self.all.min_stateful_len()))
.min(Height::from(self.sth.min_stateful_len()))
.min(Height::from(self.lth.min_stateful_len()))
}
/// Import state for all separate cohorts at or before given height.
@@ -790,7 +784,7 @@ impl UTXOCohorts<Rw> {
/// Aggregate RealizedFull fields from age_range states and push to all/sth/lth.
/// Called during the block loop after separate cohorts' push_state but before reset.
pub(crate) fn push_overlapping_realized_full(&mut self) {
pub(crate) fn push_overlapping(&mut self, height_price: Cents) {
let Self {
all,
sth,
@@ -805,14 +799,26 @@ impl UTXOCohorts<Rw> {
let mut sth_acc = RealizedFullAccum::default();
let mut lth_acc = RealizedFullAccum::default();
for ar in age_range.iter() {
if let Some(state) = ar.state.as_ref() {
let r = &state.realized;
all_acc.add(r);
let mut all_icap = (0u128, 0u128);
let mut sth_icap = (0u128, 0u128);
let mut lth_icap = (0u128, 0u128);
for ar in age_range.iter_mut() {
if let Some(state) = ar.state.as_mut() {
all_acc.add(&state.realized);
let u = state.compute_unrealized_state(height_price);
all_icap.0 += u.investor_cap_in_profit_raw;
all_icap.1 += u.investor_cap_in_loss_raw;
if sth_filter.includes(&ar.metrics.filter) {
sth_acc.add(r);
sth_acc.add(&state.realized);
sth_icap.0 += u.investor_cap_in_profit_raw;
sth_icap.1 += u.investor_cap_in_loss_raw;
} else {
lth_acc.add(r);
lth_acc.add(&state.realized);
lth_icap.0 += u.investor_cap_in_profit_raw;
lth_icap.1 += u.investor_cap_in_loss_raw;
}
}
}
@@ -820,6 +826,13 @@ impl UTXOCohorts<Rw> {
all.metrics.realized.push_accum(&all_acc);
sth.metrics.realized.push_accum(&sth_acc);
lth.metrics.realized.push_accum(&lth_acc);
all.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(all_icap.0));
all.metrics.unrealized.investor_cap_in_loss_raw.push(CentsSquaredSats::new(all_icap.1));
sth.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(sth_icap.0));
sth.metrics.unrealized.investor_cap_in_loss_raw.push(CentsSquaredSats::new(sth_icap.1));
lth.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(lth_icap.0));
lth.metrics.unrealized.investor_cap_in_loss_raw.push(CentsSquaredSats::new(lth_icap.1));
}
}

View File

@@ -551,8 +551,8 @@ fn push_cohort_states(
},
);
// Phase 2: aggregate age_range realized states → push to overlapping cohorts' RealizedFull
utxo_cohorts.push_overlapping_realized_full();
// Phase 2: aggregate age_range states → push to overlapping cohorts
utxo_cohorts.push_overlapping(height_price);
// Phase 3: reset per-block values
utxo_cohorts

View File

@@ -50,11 +50,10 @@ impl CohortMetricsBase for AllCohortMetrics {
}
fn min_stateful_len(&self) -> usize {
self.supply
.min_len()
.min(self.outputs.min_len())
.min(self.activity.min_len())
.min(self.realized.min_stateful_len())
// Only check per-block pushed vecs, not aggregated ones (supply, outputs,
// activity, realized core, unrealized core are summed from age_range).
self.realized
.min_stateful_len()
.min(self.unrealized.min_stateful_len())
.min(self.cost_basis.min_stateful_len())
}
@@ -138,6 +137,7 @@ impl AllCohortMetrics {
self.cost_basis.compute_prices(
starting_indexes,
&prices.spot.cents.height,
&self.unrealized.invested_capital.in_profit.cents.height,
&self.unrealized.invested_capital.in_loss.cents.height,
&self.supply.in_profit.sats.height,

View File

@@ -47,11 +47,10 @@ impl CohortMetricsBase for ExtendedCohortMetrics {
}
fn min_stateful_len(&self) -> usize {
self.supply
.min_len()
.min(self.outputs.min_len())
.min(self.activity.min_len())
.min(self.realized.min_stateful_len())
// Only check per-block pushed vecs, not aggregated ones (supply, outputs,
// activity, realized core, unrealized core are summed from age_range).
self.realized
.min_stateful_len()
.min(self.unrealized.min_stateful_len())
.min(self.cost_basis.min_stateful_len())
}
@@ -116,6 +115,7 @@ impl ExtendedCohortMetrics {
self.cost_basis.compute_prices(
starting_indexes,
&prices.spot.cents.height,
&self.unrealized.invested_capital.in_profit.cents.height,
&self.unrealized.invested_capital.in_loss.cents.height,
&self.supply.in_profit.sats.height,

View File

@@ -1,17 +1,19 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPoints16, Cents, Height, Indexes, Sats, Version};
use brk_types::CentsSquaredSats;
use brk_types::{BasisPoints16, Cents, Height, Indexes, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::internal::{FiatPerBlock, PerBlock, PercentPerBlock, PercentilesVecs, Price, PERCENTILES_LEN};
use crate::internal::{
PERCENTILES_LEN, PerBlock, PercentPerBlock, PercentilesVecs, Price,
};
use super::ImportConfig;
#[derive(Traversable)]
pub struct CostBasisSide<M: StorageMode = Rw> {
pub per_coin: FiatPerBlock<Cents, M>,
pub per_dollar: FiatPerBlock<Cents, M>,
pub per_coin: Price<PerBlock<Cents, M>>,
pub per_dollar: Price<PerBlock<Cents, M>>,
}
/// Cost basis metrics: min/max + profit/loss splits + percentiles + supply density.
@@ -31,12 +33,12 @@ impl CostBasis {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
in_profit: CostBasisSide {
per_coin: cfg.import("cost_basis_in_profit_per_coin", Version::ZERO)?,
per_dollar: cfg.import("cost_basis_in_profit_per_dollar", Version::ZERO)?,
per_coin: Price::forced_import(cfg.db, &cfg.name("cost_basis_in_profit_per_coin"), cfg.version + Version::ONE, cfg.indexes)?,
per_dollar: Price::forced_import(cfg.db, &cfg.name("cost_basis_in_profit_per_dollar"), cfg.version + Version::ONE, cfg.indexes)?,
},
in_loss: CostBasisSide {
per_coin: cfg.import("cost_basis_in_loss_per_coin", Version::ZERO)?,
per_dollar: cfg.import("cost_basis_in_loss_per_dollar", Version::ZERO)?,
per_coin: Price::forced_import(cfg.db, &cfg.name("cost_basis_in_loss_per_coin"), cfg.version + Version::ONE, cfg.indexes)?,
per_dollar: Price::forced_import(cfg.db, &cfg.name("cost_basis_in_loss_per_dollar"), cfg.version + Version::ONE, cfg.indexes)?,
},
min: cfg.import("cost_basis_min", Version::ZERO)?,
max: cfg.import("cost_basis_max", Version::ZERO)?,
@@ -124,9 +126,11 @@ impl CostBasis {
vecs
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_prices(
&mut self,
starting_indexes: &Indexes,
spot: &impl ReadableVec<Height, Cents>,
invested_cap_in_profit: &impl ReadableVec<Height, Cents>,
invested_cap_in_loss: &impl ReadableVec<Height, Cents>,
supply_in_profit_sats: &impl ReadableVec<Height, Sats>,
@@ -135,46 +139,64 @@ impl CostBasis {
investor_cap_in_loss_raw: &impl ReadableVec<Height, CentsSquaredSats>,
exit: &Exit,
) -> Result<()> {
self.in_profit.per_coin.cents.height.compute_transform2(
self.in_profit.per_coin.cents.height.compute_transform3(
starting_indexes.height,
invested_cap_in_profit,
supply_in_profit_sats,
|(h, invested_cents, supply_sats, ..)| {
spot,
|(h, invested_cents, supply_sats, spot, ..)| {
let supply = supply_sats.as_u128();
if supply == 0 { return (h, Cents::ZERO); }
(h, Cents::new((invested_cents.as_u128() * Sats::ONE_BTC_U128 / supply) as u64))
if supply == 0 {
return (h, spot);
}
(
h,
Cents::new((invested_cents.as_u128() * Sats::ONE_BTC_U128 / supply) as u64),
)
},
exit,
)?;
self.in_loss.per_coin.cents.height.compute_transform2(
self.in_loss.per_coin.cents.height.compute_transform3(
starting_indexes.height,
invested_cap_in_loss,
supply_in_loss_sats,
|(h, invested_cents, supply_sats, ..)| {
spot,
|(h, invested_cents, supply_sats, spot, ..)| {
let supply = supply_sats.as_u128();
if supply == 0 { return (h, Cents::ZERO); }
(h, Cents::new((invested_cents.as_u128() * Sats::ONE_BTC_U128 / supply) as u64))
if supply == 0 {
return (h, spot);
}
(
h,
Cents::new((invested_cents.as_u128() * Sats::ONE_BTC_U128 / supply) as u64),
)
},
exit,
)?;
self.in_profit.per_dollar.cents.height.compute_transform2(
self.in_profit.per_dollar.cents.height.compute_transform3(
starting_indexes.height,
investor_cap_in_profit_raw,
invested_cap_in_profit,
|(h, investor_cap, invested_cents, ..)| {
spot,
|(h, investor_cap, invested_cents, spot, ..)| {
let invested_raw = invested_cents.as_u128() * Sats::ONE_BTC_U128;
if invested_raw == 0 { return (h, Cents::ZERO); }
if invested_raw == 0 {
return (h, spot);
}
(h, Cents::new((investor_cap.inner() / invested_raw) as u64))
},
exit,
)?;
self.in_loss.per_dollar.cents.height.compute_transform2(
self.in_loss.per_dollar.cents.height.compute_transform3(
starting_indexes.height,
investor_cap_in_loss_raw,
invested_cap_in_loss,
|(h, investor_cap, invested_cents, ..)| {
spot,
|(h, investor_cap, invested_cents, spot, ..)| {
let invested_raw = invested_cents.as_u128() * Sats::ONE_BTC_U128;
if invested_raw == 0 { return (h, Cents::ZERO); }
if invested_raw == 0 {
return (h, spot);
}
(h, Cents::new((investor_cap.inner() / invested_raw) as u64))
},
exit,

View File

@@ -127,7 +127,7 @@ impl RealizedFull {
// Peak regret
let peak_regret = RealizedPeakRegret {
value: cfg.import("realized_peak_regret", Version::new(2))?,
value: cfg.import("realized_peak_regret", Version::new(3))?,
to_rcap: cfg.import("realized_peak_regret_to_rcap", Version::new(2))?,
};

View File

@@ -33,9 +33,7 @@ pub struct UnrealizedFull<M: StorageMode = Rw> {
pub gross_pnl: FiatPerBlock<Cents, M>,
pub invested_capital: UnrealizedInvestedCapital<M>,
#[traversable(hidden)]
pub investor_cap_in_profit_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
#[traversable(hidden)]
pub investor_cap_in_loss_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
pub sentiment: UnrealizedSentiment<M>,
@@ -48,18 +46,20 @@ impl UnrealizedFull {
let gross_pnl = cfg.import("unrealized_gross_pnl", v0)?;
let v1 = Version::ONE;
let invested_capital = UnrealizedInvestedCapital {
in_profit: cfg.import("invested_capital_in_profit", v0)?,
in_loss: cfg.import("invested_capital_in_loss", v0)?,
in_profit: cfg.import("invested_capital_in_profit", v1)?,
in_loss: cfg.import("invested_capital_in_loss", v1)?,
};
let investor_cap_in_profit_raw = cfg.import("investor_cap_in_profit_raw", v0)?;
let investor_cap_in_loss_raw = cfg.import("investor_cap_in_loss_raw", v0)?;
let sentiment = UnrealizedSentiment {
pain_index: cfg.import("pain_index", v0)?,
greed_index: cfg.import("greed_index", v0)?,
net: cfg.import("net_sentiment", Version::ONE)?,
pain_index: cfg.import("pain_index", v1)?,
greed_index: cfg.import("greed_index", v1)?,
net: cfg.import("net_sentiment", Version::new(2))?,
};
Ok(Self {
@@ -73,9 +73,10 @@ impl UnrealizedFull {
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.inner
.min_stateful_len()
.min(self.investor_cap_in_profit_raw.len())
// Only check per-block pushed vecs (investor_cap_raw).
// Core-level vecs (profit/loss) are aggregated from age_range, not stateful.
self.investor_cap_in_profit_raw
.len()
.min(self.investor_cap_in_loss_raw.len())
}
@@ -90,14 +91,8 @@ impl UnrealizedFull {
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = self.inner.collect_vecs_mut();
vecs.push(&mut self.gross_pnl.cents.height as &mut dyn AnyStoredVec);
vecs.push(&mut self.invested_capital.in_profit.cents.height as &mut dyn AnyStoredVec);
vecs.push(&mut self.invested_capital.in_loss.cents.height as &mut dyn AnyStoredVec);
vecs.push(&mut self.investor_cap_in_profit_raw as &mut dyn AnyStoredVec);
vecs.push(&mut self.investor_cap_in_loss_raw as &mut dyn AnyStoredVec);
vecs.push(&mut self.sentiment.pain_index.cents.height as &mut dyn AnyStoredVec);
vecs.push(&mut self.sentiment.greed_index.cents.height as &mut dyn AnyStoredVec);
vecs.push(&mut self.sentiment.net.cents.height as &mut dyn AnyStoredVec);
vecs
}
@@ -120,30 +115,41 @@ impl UnrealizedFull {
)?;
// invested_capital_in_profit = supply_profit_sats × spot / ONE_BTC - unrealized_profit
self.invested_capital.in_profit.cents.height.compute_transform3(
starting_indexes.height,
supply_in_profit_sats,
&prices.spot.cents.height,
&self.inner.basic.profit.cents.height,
|(h, supply_sats, spot, profit, ..): (_, Sats, Cents, Cents, _)| {
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
(h, Cents::new(market_value.saturating_sub(profit.as_u128()) as u64))
},
exit,
)?;
self.invested_capital
.in_profit
.cents
.height
.compute_transform3(
starting_indexes.height,
supply_in_profit_sats,
&prices.spot.cents.height,
&self.inner.basic.profit.cents.height,
|(h, supply_sats, spot, profit, ..): (_, Sats, Cents, Cents, _)| {
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
(
h,
Cents::new(market_value.saturating_sub(profit.as_u128()) as u64),
)
},
exit,
)?;
// invested_capital_in_loss = supply_loss_sats × spot / ONE_BTC + unrealized_loss
self.invested_capital.in_loss.cents.height.compute_transform3(
starting_indexes.height,
supply_in_loss_sats,
&prices.spot.cents.height,
&self.inner.basic.loss.cents.height,
|(h, supply_sats, spot, loss, ..): (_, Sats, Cents, Cents, _)| {
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
(h, Cents::new((market_value + loss.as_u128()) as u64))
},
exit,
)?;
self.invested_capital
.in_loss
.cents
.height
.compute_transform3(
starting_indexes.height,
supply_in_loss_sats,
&prices.spot.cents.height,
&self.inner.basic.loss.cents.height,
|(h, supply_sats, spot, loss, ..): (_, Sats, Cents, Cents, _)| {
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
(h, Cents::new((market_value + loss.as_u128()) as u64))
},
exit,
)?;
Ok(())
}
@@ -171,7 +177,10 @@ impl UnrealizedFull {
}
let investor_price = investor_cap.inner() / invested_cap_raw;
let spot_u128 = spot.as_u128();
(h, Cents::new(spot_u128.saturating_sub(investor_price) as u64))
(
h,
Cents::new(spot_u128.saturating_sub(investor_price) as u64),
)
},
exit,
)?;
@@ -189,7 +198,10 @@ impl UnrealizedFull {
}
let investor_price = investor_cap.inner() / invested_cap_raw;
let spot_u128 = spot.as_u128();
(h, Cents::new(investor_price.saturating_sub(spot_u128) as u64))
(
h,
Cents::new(investor_price.saturating_sub(spot_u128) as u64),
)
},
exit,
)?;

View File

@@ -63,7 +63,7 @@ impl UnrealizedLike for UnrealizedFull {
&mut self.inner
}
fn min_stateful_len(&self) -> usize {
self.inner.min_stateful_len()
UnrealizedFull::min_stateful_len(self)
}
#[inline(always)]
fn push_state(&mut self, state: &UnrealizedState) {

View File

@@ -5,7 +5,10 @@ use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode};
use crate::{
indexes,
internal::{AmountPerBlock, CachedWindowStarts, LazyRollingSumsAmountFromHeight, SatsToCents},
internal::{
AmountPerBlock, CachedWindowStarts, LazyRollingAvgsAmountFromHeight,
LazyRollingSumsAmountFromHeight, SatsToCents,
},
prices,
};
@@ -14,6 +17,7 @@ pub struct AmountPerBlockCumulativeWithSums<M: StorageMode = Rw> {
pub base: AmountPerBlock<M>,
pub cumulative: AmountPerBlock<M>,
pub sum: LazyRollingSumsAmountFromHeight,
pub average: LazyRollingAvgsAmountFromHeight,
}
const VERSION: Version = Version::TWO;
@@ -39,11 +43,20 @@ impl AmountPerBlockCumulativeWithSums {
cached_starts,
indexes,
);
let average = LazyRollingAvgsAmountFromHeight::new(
&format!("{name}_average"),
v,
&cumulative.sats.height,
&cumulative.cents.height,
cached_starts,
indexes,
);
Ok(Self {
base,
cumulative,
sum,
average,
})
}

View File

@@ -6,8 +6,9 @@ use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode};
use crate::{
indexes,
internal::{
AmountPerBlock, CachedWindowStarts, LazyRollingSumsAmountFromHeight,
RollingDistributionAmountPerBlock, SatsToCents, WindowStarts,
AmountPerBlock, CachedWindowStarts, LazyRollingAvgsAmountFromHeight,
LazyRollingSumsAmountFromHeight, RollingDistributionAmountPerBlock, SatsToCents,
WindowStarts,
},
prices,
};
@@ -17,8 +18,9 @@ pub struct AmountPerBlockFull<M: StorageMode = Rw> {
pub base: AmountPerBlock<M>,
pub cumulative: AmountPerBlock<M>,
pub sum: LazyRollingSumsAmountFromHeight,
pub average: LazyRollingAvgsAmountFromHeight,
#[traversable(flatten)]
pub rolling: RollingDistributionAmountPerBlock<M>,
pub distribution: RollingDistributionAmountPerBlock<M>,
}
const VERSION: Version = Version::TWO;
@@ -34,12 +36,8 @@ impl AmountPerBlockFull {
let v = version + VERSION;
let base = AmountPerBlock::forced_import(db, name, v, indexes)?;
let cumulative = AmountPerBlock::forced_import(
db,
&format!("{name}_cumulative"),
v,
indexes,
)?;
let cumulative =
AmountPerBlock::forced_import(db, &format!("{name}_cumulative"), v, indexes)?;
let sum = LazyRollingSumsAmountFromHeight::new(
&format!("{name}_sum"),
v,
@@ -48,14 +46,22 @@ impl AmountPerBlockFull {
cached_starts,
indexes,
);
let rolling =
RollingDistributionAmountPerBlock::forced_import(db, name, v, indexes)?;
let average = LazyRollingAvgsAmountFromHeight::new(
&format!("{name}_average"),
v,
&cumulative.sats.height,
&cumulative.cents.height,
cached_starts,
indexes,
);
let rolling = RollingDistributionAmountPerBlock::forced_import(db, name, v, indexes)?;
Ok(Self {
base,
cumulative,
sum,
rolling,
average,
distribution: rolling,
})
}
@@ -89,7 +95,7 @@ impl AmountPerBlockFull {
.height
.compute_cumulative(max_from, &self.base.cents.height, exit)?;
self.rolling.compute(
self.distribution.compute(
max_from,
windows,
&self.base.sats.height,

View File

@@ -0,0 +1,124 @@
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{DeltaAvg, LazyDeltaVec, LazyVecFrom1, ReadableCloneableVec};
use crate::{
indexes,
internal::{
CachedWindowStarts, CentsUnsignedToDollars, DerivedResolutions, LazyPerBlock,
LazyRollingAvgFromHeight, Resolutions, SatsToBitcoin, Windows,
},
};
/// Single window slot: lazy rolling average for Amount (sats + btc + cents + usd).
#[derive(Clone, Traversable)]
pub struct LazyRollingAvgAmountFromHeight {
pub btc: LazyPerBlock<Bitcoin, Sats>,
pub sats: LazyRollingAvgFromHeight<Sats>,
pub usd: LazyPerBlock<Dollars, Cents>,
pub cents: LazyRollingAvgFromHeight<Cents>,
}
/// Lazy rolling averages for all 4 windows, for Amount (sats + btc + cents + usd).
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct LazyRollingAvgsAmountFromHeight(pub Windows<LazyRollingAvgAmountFromHeight>);
impl LazyRollingAvgsAmountFromHeight {
pub fn new(
name: &str,
version: Version,
cumulative_sats: &(impl ReadableCloneableVec<Height, Sats> + 'static),
cumulative_cents: &(impl ReadableCloneableVec<Height, Cents> + 'static),
cached_starts: &CachedWindowStarts,
indexes: &indexes::Vecs,
) -> Self {
let cum_sats = cumulative_sats.read_only_boxed_clone();
let cum_cents = cumulative_cents.read_only_boxed_clone();
let make_slot = |suffix: &str, cached_start: &vecdb::CachedVec<Height, Height>| {
let full_name = format!("{name}_{suffix}");
let cached = cached_start.clone();
let starts_version = cached.version();
// Sats lazy rolling avg
let sats_avg = LazyDeltaVec::<Height, Sats, Sats, DeltaAvg>::new(
&format!("{full_name}_sats"),
version,
cum_sats.clone(),
starts_version,
{
let cached = cached.clone();
move || cached.get()
},
);
let sats_resolutions = Resolutions::forced_import(
&format!("{full_name}_sats"),
sats_avg.read_only_boxed_clone(),
version,
indexes,
);
let sats = LazyRollingAvgFromHeight {
height: sats_avg,
resolutions: Box::new(sats_resolutions),
};
// Btc lazy from sats
let btc = LazyPerBlock {
height: LazyVecFrom1::transformed::<SatsToBitcoin>(
&full_name,
version,
sats.height.read_only_boxed_clone(),
),
resolutions: Box::new(DerivedResolutions::from_derived_computed::<SatsToBitcoin>(
&full_name,
version,
&sats.resolutions,
)),
};
// Cents rolling avg
let cents_avg = LazyDeltaVec::<Height, Cents, Cents, DeltaAvg>::new(
&format!("{full_name}_cents"),
version,
cum_cents.clone(),
starts_version,
move || cached.get(),
);
let cents_resolutions = Resolutions::forced_import(
&format!("{full_name}_cents"),
cents_avg.read_only_boxed_clone(),
version,
indexes,
);
let cents = LazyRollingAvgFromHeight {
height: cents_avg,
resolutions: Box::new(cents_resolutions),
};
// Usd lazy from cents
let usd = LazyPerBlock {
height: LazyVecFrom1::transformed::<CentsUnsignedToDollars>(
&format!("{full_name}_usd"),
version,
cents.height.read_only_boxed_clone(),
),
resolutions: Box::new(DerivedResolutions::from_derived_computed::<
CentsUnsignedToDollars,
>(
&format!("{full_name}_usd"), version, &cents.resolutions
)),
};
LazyRollingAvgAmountFromHeight {
btc,
sats,
usd,
cents,
}
};
Self(cached_starts.0.map_with_suffix(make_slot))
}
}

View File

@@ -4,6 +4,7 @@ mod cumulative_sum;
mod full;
mod lazy;
mod lazy_derived_resolutions;
mod lazy_rolling_avg;
mod lazy_rolling_sum;
mod rolling_distribution;
mod with_deltas;
@@ -14,6 +15,7 @@ pub use cumulative_sum::*;
pub use full::*;
pub use lazy::*;
pub use lazy_derived_resolutions::*;
pub use lazy_rolling_avg::*;
pub use lazy_rolling_sum::*;
pub use rolling_distribution::*;
pub use with_deltas::*;

View File

@@ -9,9 +9,7 @@ use vecdb::{
use crate::{
indexes,
internal::{
CachedWindowStarts, NumericValue, PerBlock, RollingComplete, WindowStarts,
},
internal::{CachedWindowStarts, NumericValue, PerBlock, RollingComplete, WindowStarts},
};
#[derive(Traversable)]
@@ -70,8 +68,7 @@ where
f64: From<T>,
A: VecIndex + VecValue + brk_types::CheckedSub<A>,
{
let combined_version =
source.version() + first_indexes.version() + count_indexes.version();
let combined_version = source.version() + first_indexes.version() + count_indexes.version();
let mut index = max_from;
index = {
@@ -121,7 +118,7 @@ where
);
self.sum.height.push(sum_val);
cumulative_val = cumulative_val + sum_val;
cumulative_val += sum_val;
self.cumulative.height.push(cumulative_val);
Ok(())

View File

@@ -17,7 +17,10 @@ use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode};
use crate::{
indexes,
internal::{CachedWindowStarts, PerBlock, LazyRollingSumsFromHeight, NumericValue},
internal::{
CachedWindowStarts, LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight, NumericValue,
PerBlock,
},
};
#[derive(Traversable)]
@@ -29,6 +32,7 @@ where
pub base: PerBlock<T, M>,
pub cumulative: PerBlock<C, M>,
pub sum: LazyRollingSumsFromHeight<C>,
pub average: LazyRollingAvgsFromHeight<C>,
}
impl<T, C> PerBlockCumulativeWithSums<T, C>
@@ -53,11 +57,19 @@ where
cached_starts,
indexes,
);
let average = LazyRollingAvgsFromHeight::new(
&format!("{name}_average"),
version,
&cumulative.height,
cached_starts,
indexes,
);
Ok(Self {
base,
cumulative,
sum,
average,
})
}

View File

@@ -21,7 +21,7 @@ where
{
pub base: M::Stored<EagerVec<PcoVec<Height, T>>>,
#[traversable(hidden)]
pub cumulative: M::Stored<EagerVec<PcoVec<Height, f64>>>,
pub cumulative: M::Stored<EagerVec<PcoVec<Height, T>>>,
#[traversable(flatten)]
pub average: LazyRollingAvgsFromHeight<T>,
}
@@ -38,8 +38,8 @@ where
cached_starts: &CachedWindowStarts,
) -> Result<Self> {
let base: EagerVec<PcoVec<Height, T>> = EagerVec::forced_import(db, name, version)?;
let cumulative: EagerVec<PcoVec<Height, f64>> =
EagerVec::forced_import(db, &format!("{name}_cumulative"), version)?;
let cumulative: EagerVec<PcoVec<Height, T>> =
EagerVec::forced_import(db, &format!("{name}_cumulative"), version + Version::ONE)?;
let average = LazyRollingAvgsFromHeight::new(
&format!("{name}_average"),
version + Version::ONE,

View File

@@ -12,7 +12,7 @@ pub struct LazyRollingAvgFromHeight<T>
where
T: NumericValue + JsonSchema,
{
pub height: LazyDeltaVec<Height, f64, T, DeltaAvg>,
pub height: LazyDeltaVec<Height, T, T, DeltaAvg>,
#[traversable(flatten)]
pub resolutions: Box<Resolutions<T>>,
}

View File

@@ -12,10 +12,11 @@ use crate::{
use super::LazyRollingAvgFromHeight;
/// Lazy rolling averages for all 4 window durations (24h, 1w, 1m, 1y),
/// derived from an f64 cumulative vec + cached window starts.
/// derived from a cumulative vec + cached window starts.
///
/// Nothing is stored on disk — all values are computed on-the-fly via
/// `LazyDeltaVec<Height, f64, T, DeltaAvg>`: `(cum[h] - cum[start-1]) / (h - start + 1)`.
/// `LazyDeltaVec<Height, T, T, DeltaAvg>`: `(cum[h] - cum[start-1]) / (h - start + 1)`.
/// T is converted to f64 internally for division, then back to T.
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct LazyRollingAvgsFromHeight<T>(pub Windows<LazyRollingAvgFromHeight<T>>)
@@ -29,7 +30,7 @@ where
pub fn new(
name: &str,
version: Version,
cumulative: &(impl ReadableCloneableVec<Height, f64> + 'static),
cumulative: &(impl ReadableCloneableVec<Height, T> + 'static),
cached_starts: &CachedWindowStarts,
indexes: &indexes::Vecs,
) -> Self {
@@ -39,7 +40,7 @@ where
let full_name = format!("{name}_{suffix}");
let cached = cached_start.clone();
let starts_version = cached.version();
let avg = LazyDeltaVec::<Height, f64, T, DeltaAvg>::new(
let avg = LazyDeltaVec::<Height, T, T, DeltaAvg>::new(
&full_name,
version,
cum_source.clone(),

View File

@@ -10,18 +10,19 @@ use vecdb::{Database, Exit, ReadableCloneableVec, ReadableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{
CachedWindowStarts, NumericValue, LazyRollingSumsFromHeight, RollingDistribution,
WindowStarts,
CachedWindowStarts, NumericValue, LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight,
RollingDistribution, WindowStarts,
},
};
/// Lazy rolling sums + stored rolling distribution (8 stats × 4 windows).
/// Lazy rolling sums + lazy rolling averages + stored rolling distribution (7 stats × 4 windows).
#[derive(Traversable)]
pub struct RollingComplete<T, M: StorageMode = Rw>
where
T: NumericValue + JsonSchema,
{
pub sum: LazyRollingSumsFromHeight<T>,
pub average: LazyRollingAvgsFromHeight<T>,
#[traversable(flatten)]
pub distribution: RollingDistribution<T, M>,
}
@@ -45,9 +46,20 @@ where
cached_starts,
indexes,
);
let average = LazyRollingAvgsFromHeight::new(
&format!("{name}_average"),
version,
cumulative,
cached_starts,
indexes,
);
let distribution = RollingDistribution::forced_import(db, name, version, indexes)?;
Ok(Self { sum, distribution })
Ok(Self {
sum,
average,
distribution,
})
}
/// Compute rolling distribution stats across all 4 windows.

View File

@@ -6,12 +6,12 @@ use vecdb::{ReadableCloneableVec, UnaryTransform};
use crate::{
indexes,
internal::{
CachedWindowStarts, ComputedVecValue, LazyRollingDistribution, LazyRollingSumsFromHeight,
NumericValue, RollingComplete,
CachedWindowStarts, ComputedVecValue, LazyRollingAvgsFromHeight,
LazyRollingDistribution, LazyRollingSumsFromHeight, NumericValue, RollingComplete,
},
};
/// Lazy analog of `RollingComplete<T>`: lazy rolling sums + lazy rolling distribution.
/// Lazy analog of `RollingComplete<T>`: lazy rolling sums + lazy rolling averages + lazy rolling distribution.
/// Zero stored vecs.
#[derive(Clone, Traversable)]
pub struct LazyRollingComplete<T, S1T>
@@ -20,6 +20,7 @@ where
S1T: ComputedVecValue + JsonSchema,
{
pub sum: LazyRollingSumsFromHeight<T>,
pub average: LazyRollingAvgsFromHeight<T>,
#[traversable(flatten)]
pub distribution: LazyRollingDistribution<T, S1T>,
}
@@ -44,11 +45,22 @@ where
cached_starts,
indexes,
);
let average = LazyRollingAvgsFromHeight::new(
&format!("{name}_average"),
version,
cumulative,
cached_starts,
indexes,
);
let distribution = LazyRollingDistribution::from_rolling_distribution::<F>(
name,
version,
&source.distribution,
);
Self { sum, distribution }
Self {
sum,
average,
distribution,
}
}
}

View File

@@ -12,6 +12,7 @@ impl<T: VecValue> UnaryTransform<T, T> for Identity<T> {
}
}
pub struct HalveSats;
impl UnaryTransform<Sats, Sats> for HalveSats {

View File

@@ -95,7 +95,7 @@ impl Vecs {
},
exit,
)?;
self.subsidy.compute(prices, starting_indexes.height, exit)?;
self.subsidy.compute_rest(starting_indexes.height, prices, exit)?;
self.unclaimed.base.sats.height.compute_transform(
starting_indexes.height,

View File

@@ -36,7 +36,9 @@ impl Vecs {
coinbase: AmountPerBlockCumulativeWithSums::forced_import(
db, "coinbase", version, indexes, cached_starts,
)?,
subsidy: AmountPerBlockCumulative::forced_import(db, "subsidy", version, indexes)?,
subsidy: AmountPerBlockCumulativeWithSums::forced_import(
db, "subsidy", version, indexes, cached_starts,
)?,
fees: AmountPerBlockFull::forced_import(db, "fees", version, indexes, cached_starts)?,
unclaimed: AmountPerBlockCumulative::forced_import(
db,

View File

@@ -11,7 +11,7 @@ use crate::internal::{
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
pub coinbase: AmountPerBlockCumulativeWithSums<M>,
pub subsidy: AmountPerBlockCumulative<M>,
pub subsidy: AmountPerBlockCumulativeWithSums<M>,
pub fees: AmountPerBlockFull<M>,
pub unclaimed: AmountPerBlockCumulative<M>,
#[traversable(wrap = "fees", rename = "dominance")]