mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-12 19:48:15 -07:00
global: snapshot
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
//! RollingDelta - raw change + growth rate (%) across 4 time windows.
|
||||
//! RollingDelta - raw change + growth rate (%) across time windows.
|
||||
//!
|
||||
//! Three tiers:
|
||||
//! - `RollingDelta1m` — 1m window only (2 stored vecs: change + rate). Default for all cohorts.
|
||||
//! - `RollingDeltaExcept1m` — 24h + 1w + 1y windows (6 stored vecs). Extended tier only.
|
||||
//! - `RollingDelta` — all 4 windows (8 stored vecs). Used for standalone global metrics.
|
||||
//!
|
||||
//! For a monotonic source (e.g., cumulative address count):
|
||||
//! - `change._24h` = count_now - count_24h_ago
|
||||
@@ -6,15 +11,76 @@
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints32, Height, Version};
|
||||
use brk_types::{BasisPoints32, BasisPointsSigned32, Height, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, VecIndex};
|
||||
use vecdb::{AnyVec, Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode, VecIndex};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{NumericValue, PercentRollingWindows, RollingWindows, WindowStarts},
|
||||
internal::{
|
||||
ComputedFromHeight, NumericValue, PercentFromHeight, PercentRollingWindows,
|
||||
RollingWindows, WindowStarts,
|
||||
},
|
||||
};
|
||||
|
||||
/// Pre-collect source data from the earliest needed offset.
|
||||
/// Returns (source_data, offset) for use in compute_delta_window.
|
||||
fn collect_source<S: NumericValue>(
|
||||
source: &impl ReadableVec<Height, S>,
|
||||
skip: usize,
|
||||
earliest_starts: &impl ReadableVec<Height, Height>,
|
||||
) -> (Vec<S>, usize) {
|
||||
let source_len = source.len();
|
||||
let offset = if skip > 0 && skip < earliest_starts.len() {
|
||||
earliest_starts.collect_one_at(skip).unwrap().to_usize()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
(source.collect_range_at(offset, source_len), offset)
|
||||
}
|
||||
|
||||
/// Shared computation: change = current - ago, rate = change / ago.
|
||||
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,
|
||||
starts: &impl ReadableVec<Height, Height>,
|
||||
source_data: &[S],
|
||||
offset: usize,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: NumericValue,
|
||||
C: NumericValue,
|
||||
B: NumericValue,
|
||||
{
|
||||
change_h.compute_transform(
|
||||
max_from,
|
||||
starts,
|
||||
|(h, ago_h, ..)| {
|
||||
let current: f64 = source_data[h.to_usize() - offset].into();
|
||||
let ago: f64 = source_data[ago_h.to_usize() - offset].into();
|
||||
(h, C::from(current - ago))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
rate_bps_h.compute_transform(
|
||||
max_from,
|
||||
&*change_h,
|
||||
|(h, change, ..)| {
|
||||
let current_f: f64 = source_data[h.to_usize() - offset].into();
|
||||
let change_f: f64 = change.into();
|
||||
let ago = current_f - change_f;
|
||||
let rate = if ago == 0.0 { 0.0 } else { change_f / ago };
|
||||
(h, B::from(rate))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct RollingDelta<S, C = S, M: StorageMode = Rw>
|
||||
where
|
||||
@@ -60,58 +126,193 @@ where
|
||||
windows: &WindowStarts<'_>,
|
||||
source: &impl ReadableVec<Height, S>,
|
||||
exit: &Exit,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: Default,
|
||||
{
|
||||
// Step 1: change = current - ago
|
||||
for (change_w, starts) in self.change.0.as_mut_array().into_iter().zip(windows.as_array())
|
||||
{
|
||||
// Pre-collect source from earliest ago_h to end for fast array indexing
|
||||
let skip = change_w.height.len();
|
||||
let source_len = source.len();
|
||||
let offset = if skip > 0 && skip < starts.len() {
|
||||
starts.collect_one_at(skip).unwrap().to_usize()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let source_data = source.collect_range_at(offset, source_len);
|
||||
) -> Result<()> {
|
||||
// Pre-collect once using the widest window (1y has earliest ago heights)
|
||||
let skip = self.change.0._24h.height.len();
|
||||
let (source_data, offset) = collect_source(source, skip, windows._1y);
|
||||
|
||||
change_w.height.compute_transform(
|
||||
max_from,
|
||||
*starts,
|
||||
|(h, ago_h, ..)| {
|
||||
let current: f64 = source_data[h.to_usize() - offset].into();
|
||||
let ago: f64 = source_data[ago_h.to_usize() - offset].into();
|
||||
(h, C::from(current - ago))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Step 2: rate = change / ago = change / (current - change)
|
||||
for (growth_w, change_w) in self
|
||||
.rate
|
||||
for ((change_w, rate_w), starts) in self
|
||||
.change
|
||||
.0
|
||||
.as_mut_array()
|
||||
.into_iter()
|
||||
.zip(self.change.0.as_array())
|
||||
.zip(self.rate.0.as_mut_array())
|
||||
.zip(windows.as_array())
|
||||
{
|
||||
growth_w.bps.height.compute_transform2(
|
||||
compute_delta_window(
|
||||
&mut change_w.height,
|
||||
&mut rate_w.bps.height,
|
||||
max_from,
|
||||
source,
|
||||
&change_w.height,
|
||||
|(h, current, change, ..)| {
|
||||
let current_f: f64 = current.into();
|
||||
let change_f: f64 = change.into();
|
||||
let ago = current_f - change_f;
|
||||
let rate = if ago == 0.0 { 0.0 } else { change_f / ago };
|
||||
(h, BasisPoints32::from(rate))
|
||||
},
|
||||
*starts,
|
||||
&source_data,
|
||||
offset,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 1m-only delta: change + growth rate for the 1-month window.
|
||||
/// Default tier for all cohorts (2 stored vecs).
|
||||
#[derive(Traversable)]
|
||||
pub struct RollingDelta1m<S, C = S, M: StorageMode = Rw>
|
||||
where
|
||||
S: NumericValue + JsonSchema,
|
||||
C: NumericValue + JsonSchema,
|
||||
{
|
||||
pub change_1m: ComputedFromHeight<C, M>,
|
||||
pub rate_1m: PercentFromHeight<BasisPointsSigned32, M>,
|
||||
_phantom: std::marker::PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S, C> RollingDelta1m<S, C>
|
||||
where
|
||||
S: NumericValue + JsonSchema,
|
||||
C: NumericValue + JsonSchema,
|
||||
{
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
change_1m: ComputedFromHeight::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.height.len();
|
||||
let (source_data, offset) = collect_source(source, skip, height_1m_ago);
|
||||
|
||||
compute_delta_window(
|
||||
&mut self.change_1m.height,
|
||||
&mut self.rate_1m.bps.height,
|
||||
max_from,
|
||||
height_1m_ago,
|
||||
&source_data,
|
||||
offset,
|
||||
exit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extended delta: 24h + 1w + 1y windows (6 stored vecs).
|
||||
/// Only for All/LTH/STH cohorts (Extended tier).
|
||||
#[derive(Traversable)]
|
||||
pub struct RollingDeltaExcept1m<S, C = S, M: StorageMode = Rw>
|
||||
where
|
||||
S: NumericValue + JsonSchema,
|
||||
C: NumericValue + JsonSchema,
|
||||
{
|
||||
#[traversable(rename = "24h")]
|
||||
pub change_24h: ComputedFromHeight<C, M>,
|
||||
pub change_1w: ComputedFromHeight<C, M>,
|
||||
pub change_1y: ComputedFromHeight<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> RollingDeltaExcept1m<S, C>
|
||||
where
|
||||
S: NumericValue + JsonSchema,
|
||||
C: NumericValue + JsonSchema,
|
||||
{
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
change_24h: ComputedFromHeight::forced_import(
|
||||
db,
|
||||
&format!("{name}_change_24h"),
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
change_1w: ComputedFromHeight::forced_import(
|
||||
db,
|
||||
&format!("{name}_change_1w"),
|
||||
version,
|
||||
indexes,
|
||||
)?,
|
||||
change_1y: ComputedFromHeight::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<()> {
|
||||
// Pre-collect once using the widest window (1y has earliest ago heights)
|
||||
let skip = self.change_24h.height.len();
|
||||
let (source_data, offset) = collect_source(source, skip, windows._1y);
|
||||
|
||||
let changes = [&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.height,
|
||||
&mut rate_w.bps.height,
|
||||
max_from,
|
||||
starts,
|
||||
&source_data,
|
||||
offset,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
mod aggregated;
|
||||
mod cumulative;
|
||||
mod cumulative_sum;
|
||||
mod full;
|
||||
mod delta;
|
||||
mod full;
|
||||
mod rolling_average;
|
||||
mod sum;
|
||||
|
||||
pub use aggregated::*;
|
||||
pub use cumulative::*;
|
||||
pub use cumulative_sum::*;
|
||||
pub use full::*;
|
||||
pub use delta::*;
|
||||
pub use full::*;
|
||||
pub use rolling_average::*;
|
||||
pub use sum::*;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! ComputedFromHeightSum - stored height + RollingWindows (sum only).
|
||||
//!
|
||||
//! Like ComputedFromHeightCumulativeSum but without the cumulative vec.
|
||||
|
||||
use std::ops::SubAssign;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Version};
|
||||
use schemars::JsonSchema;
|
||||
use vecdb::{Database, EagerVec, Exit, ImportableVec, PcoVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{NumericValue, RollingWindows, WindowStarts},
|
||||
};
|
||||
|
||||
#[derive(Traversable)]
|
||||
pub struct ComputedFromHeightSum<T, M: StorageMode = Rw>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
{
|
||||
pub height: M::Stored<EagerVec<PcoVec<Height, T>>>,
|
||||
pub sum: RollingWindows<T, M>,
|
||||
}
|
||||
|
||||
impl<T> ComputedFromHeightSum<T>
|
||||
where
|
||||
T: NumericValue + JsonSchema,
|
||||
{
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let height: EagerVec<PcoVec<Height, T>> = EagerVec::forced_import(db, name, version)?;
|
||||
let sum = RollingWindows::forced_import(db, &format!("{name}_sum"), version, indexes)?;
|
||||
|
||||
Ok(Self { height, sum })
|
||||
}
|
||||
|
||||
/// Compute height data via closure, then rolling sum.
|
||||
pub(crate) fn compute(
|
||||
&mut self,
|
||||
max_from: Height,
|
||||
windows: &WindowStarts<'_>,
|
||||
exit: &Exit,
|
||||
compute_height: impl FnOnce(&mut EagerVec<PcoVec<Height, T>>) -> Result<()>,
|
||||
) -> Result<()>
|
||||
where
|
||||
T: Default + SubAssign,
|
||||
{
|
||||
compute_height(&mut self.height)?;
|
||||
self.sum
|
||||
.compute_rolling_sum(max_from, windows, &self.height, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
//! Change values from Height - stores signed sats and dollars (changes can be negative).
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Bitcoin, Cents, CentsSigned, Dollars, Height, Sats, SatsSigned, Version};
|
||||
use vecdb::{Database, Exit, ReadableCloneableVec, ReadableVec, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{CentsSignedToDollars, ComputedFromHeight, LazyFromHeight, SatsSignedToBitcoin},
|
||||
};
|
||||
|
||||
/// Change values indexed by height - sats (stored), btc (lazy), cents (stored), usd (lazy).
|
||||
#[derive(Traversable)]
|
||||
pub struct ValueFromHeightChange<M: StorageMode = Rw> {
|
||||
pub sats: ComputedFromHeight<SatsSigned, M>,
|
||||
pub btc: LazyFromHeight<Bitcoin, SatsSigned>,
|
||||
pub cents: ComputedFromHeight<CentsSigned, M>,
|
||||
pub usd: LazyFromHeight<Dollars, CentsSigned>,
|
||||
}
|
||||
|
||||
impl ValueFromHeightChange {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let sats = ComputedFromHeight::forced_import(db, name, version, indexes)?;
|
||||
|
||||
let btc = LazyFromHeight::from_computed::<SatsSignedToBitcoin>(
|
||||
&format!("{name}_btc"),
|
||||
version,
|
||||
sats.height.read_only_boxed_clone(),
|
||||
&sats,
|
||||
);
|
||||
|
||||
let cents =
|
||||
ComputedFromHeight::forced_import(db, &format!("{name}_cents"), version, indexes)?;
|
||||
|
||||
let usd = LazyFromHeight::from_computed::<CentsSignedToDollars>(
|
||||
&format!("{name}_usd"),
|
||||
version,
|
||||
cents.height.read_only_boxed_clone(),
|
||||
¢s,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
sats,
|
||||
btc,
|
||||
cents,
|
||||
usd,
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute rolling change for both sats and cents in one call.
|
||||
pub(crate) fn compute_rolling(
|
||||
&mut self,
|
||||
starting_height: Height,
|
||||
window_starts: &impl ReadableVec<Height, Height>,
|
||||
sats_source: &impl ReadableVec<Height, Sats>,
|
||||
cents_source: &(impl ReadableVec<Height, Cents> + Sync),
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.sats.height.compute_rolling_change(
|
||||
starting_height,
|
||||
window_starts,
|
||||
sats_source,
|
||||
exit,
|
||||
)?;
|
||||
self.cents.height.compute_rolling_change(
|
||||
starting_height,
|
||||
window_starts,
|
||||
cents_source,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
mod base;
|
||||
mod change;
|
||||
mod cumulative;
|
||||
mod cumulative_sum;
|
||||
mod full;
|
||||
@@ -7,7 +6,6 @@ mod lazy;
|
||||
mod rolling;
|
||||
|
||||
pub use base::*;
|
||||
pub use change::*;
|
||||
pub use cumulative::*;
|
||||
pub use cumulative_sum::*;
|
||||
pub use full::*;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use brk_types::{Bitcoin, Cents, CentsSigned, Dollars, Sats, SatsFract, SatsSigned};
|
||||
use brk_types::{Bitcoin, Cents, CentsSigned, Dollars, Sats, SatsFract};
|
||||
use vecdb::{BinaryTransform, UnaryTransform};
|
||||
|
||||
pub struct SatsToBitcoin;
|
||||
@@ -10,15 +10,6 @@ impl UnaryTransform<Sats, Bitcoin> for SatsToBitcoin {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SatsSignedToBitcoin;
|
||||
|
||||
impl UnaryTransform<SatsSigned, Bitcoin> for SatsSignedToBitcoin {
|
||||
#[inline(always)]
|
||||
fn apply(sats: SatsSigned) -> Bitcoin {
|
||||
Bitcoin::from(sats)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SatsToCents;
|
||||
|
||||
impl BinaryTransform<Sats, Cents, Cents> for SatsToCents {
|
||||
|
||||
@@ -16,7 +16,7 @@ pub use bps::{
|
||||
pub use currency::{
|
||||
CentsSignedToDollars, CentsSubtractToCentsSigned, CentsTimesTenths,
|
||||
CentsUnsignedToDollars, CentsUnsignedToSats, DollarsToSatsFract, NegCentsUnsignedToDollars,
|
||||
SatsSignedToBitcoin, SatsToBitcoin, SatsToCents,
|
||||
SatsToBitcoin, SatsToCents,
|
||||
};
|
||||
pub use derived::{
|
||||
Days7, Days30, Days365, DaysToYears, PerSec, PriceTimesRatioBp32Cents, PriceTimesRatioCents,
|
||||
|
||||
Reference in New Issue
Block a user