global: snapshot

This commit is contained in:
nym21
2026-02-28 23:14:06 +01:00
parent 1750c06369
commit a6664bbb93
64 changed files with 57 additions and 80 deletions
@@ -0,0 +1,46 @@
mod rolling_full;
mod rolling_sum;
mod windows;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Dollars, Sats, Version};
use vecdb::{Database, ReadableCloneableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{ComputedFromHeightLast, LazyFromHeightLast, SatsToBitcoin},
};
pub use rolling_full::*;
pub use rolling_sum::*;
#[derive(Traversable)]
pub struct ByUnit<M: StorageMode = Rw> {
pub sats: ComputedFromHeightLast<Sats, M>,
pub btc: LazyFromHeightLast<Bitcoin, Sats>,
pub usd: ComputedFromHeightLast<Dollars, M>,
}
impl ByUnit {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let sats = ComputedFromHeightLast::forced_import(db, name, version, indexes)?;
let btc = LazyFromHeightLast::from_computed::<SatsToBitcoin>(
&format!("{name}_btc"),
version,
sats.height.read_only_boxed_clone(),
&sats,
);
let usd =
ComputedFromHeightLast::forced_import(db, &format!("{name}_usd"), version, indexes)?;
Ok(Self { sats, btc, usd })
}
}
@@ -0,0 +1,116 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{ByUnit, DistributionStats, WindowStarts, Windows},
traits::compute_rolling_distribution_from_starts,
};
/// One window slot: sum + 8 distribution stats, each a ByUnit.
///
/// Tree: `sum.sats.height`, `average.sats.height`, etc.
#[derive(Traversable)]
pub struct RollingFullSlot<M: StorageMode = Rw> {
pub sum: ByUnit<M>,
#[traversable(flatten)]
pub distribution: DistributionStats<ByUnit<M>>,
}
impl RollingFullSlot {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
sum: ByUnit::forced_import(db, &format!("{name}_sum"), version, indexes)?,
distribution: DistributionStats {
average: ByUnit::forced_import(db, &format!("{name}_average"), version, indexes)?,
min: ByUnit::forced_import(db, &format!("{name}_min"), version, indexes)?,
max: ByUnit::forced_import(db, &format!("{name}_max"), version, indexes)?,
pct10: ByUnit::forced_import(db, &format!("{name}_p10"), version, indexes)?,
pct25: ByUnit::forced_import(db, &format!("{name}_p25"), version, indexes)?,
median: ByUnit::forced_import(db, &format!("{name}_median"), version, indexes)?,
pct75: ByUnit::forced_import(db, &format!("{name}_p75"), version, indexes)?,
pct90: ByUnit::forced_import(db, &format!("{name}_p90"), version, indexes)?,
},
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
starts: &impl ReadableVec<Height, Height>,
sats_source: &impl ReadableVec<Height, Sats>,
usd_source: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.sum.sats.height.compute_rolling_sum(max_from, starts, sats_source, exit)?;
self.sum.usd.height.compute_rolling_sum(max_from, starts, usd_source, exit)?;
let d = &mut self.distribution;
compute_rolling_distribution_from_starts(
max_from, starts, sats_source,
&mut d.average.sats.height, &mut d.min.sats.height,
&mut d.max.sats.height, &mut d.pct10.sats.height,
&mut d.pct25.sats.height, &mut d.median.sats.height,
&mut d.pct75.sats.height, &mut d.pct90.sats.height, exit,
)?;
compute_rolling_distribution_from_starts(
max_from, starts, usd_source,
&mut d.average.usd.height, &mut d.min.usd.height,
&mut d.max.usd.height, &mut d.pct10.usd.height,
&mut d.pct25.usd.height, &mut d.median.usd.height,
&mut d.pct75.usd.height, &mut d.pct90.usd.height, exit,
)?;
Ok(())
}
}
/// Rolling sum + distribution across 4 windows, window-first.
///
/// Tree: `_24h.sum.sats.height`, `_24h.average.sats.height`, etc.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct RollingFullByUnit<M: StorageMode = Rw>(pub Windows<RollingFullSlot<M>>);
const VERSION: Version = Version::ZERO;
impl RollingFullByUnit {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self(Windows {
_24h: RollingFullSlot::forced_import(db, &format!("{name}_24h"), v, indexes)?,
_7d: RollingFullSlot::forced_import(db, &format!("{name}_7d"), v, indexes)?,
_30d: RollingFullSlot::forced_import(db, &format!("{name}_30d"), v, indexes)?,
_1y: RollingFullSlot::forced_import(db, &format!("{name}_1y"), v, indexes)?,
}))
}
pub(crate) fn compute(
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
sats_source: &impl ReadableVec<Height, Sats>,
usd_source: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
for (slot, starts) in self.0.as_mut_array().into_iter().zip(windows.as_array()) {
slot.compute(max_from, starts, sats_source, usd_source, exit)?;
}
Ok(())
}
}
@@ -0,0 +1,46 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{ByUnit, WindowStarts, Windows},
};
/// Rolling sum only, window-first then unit.
///
/// Tree: `_24h.sats.height`, `_24h.btc.height`, etc.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct RollingSumByUnit<M: StorageMode = Rw>(pub Windows<ByUnit<M>>);
const VERSION: Version = Version::ZERO;
impl RollingSumByUnit {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self(Windows::<ByUnit>::forced_import(db, &format!("{name}_sum"), v, indexes)?))
}
pub(crate) fn compute_rolling_sum(
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
sats_source: &impl ReadableVec<Height, Sats>,
usd_source: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
for (w, starts) in self.0.as_mut_array().into_iter().zip(windows.as_array()) {
w.sats.height.compute_rolling_sum(max_from, starts, sats_source, exit)?;
w.usd.height.compute_rolling_sum(max_from, starts, usd_source, exit)?;
}
Ok(())
}
}
@@ -0,0 +1,24 @@
use brk_error::Result;
use brk_types::Version;
use vecdb::Database;
use crate::{
indexes,
internal::{ByUnit, Windows},
};
impl Windows<ByUnit> {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
_24h: ByUnit::forced_import(db, &format!("{name}_24h"), version, indexes)?,
_7d: ByUnit::forced_import(db, &format!("{name}_7d"), version, indexes)?,
_30d: ByUnit::forced_import(db, &format!("{name}_30d"), version, indexes)?,
_1y: ByUnit::forced_import(db, &format!("{name}_1y"), version, indexes)?,
})
}
}
@@ -0,0 +1,98 @@
use brk_traversable::Traversable;
use brk_types::{
Day1, Day3, DifficultyEpoch, HalvingEpoch, Height, Hour1, Hour12, Hour4, Minute1, Minute10,
Minute30, Minute5, Month1, Month3, Month6, Version, Week1, Year1, Year10,
};
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::{Formattable, ReadableCloneableVec, LazyVecFrom1, UnaryTransform, VecValue};
use crate::indexes;
/// Lazy constant vecs for all index levels.
/// Uses const generic transforms to return the same value for every index.
#[derive(Clone, Traversable)]
#[traversable(merge)]
pub struct ConstantVecs<T>
where
T: VecValue + Formattable + Serialize + JsonSchema,
{
pub height: LazyVecFrom1<Height, T, Height, Height>,
pub minute1: LazyVecFrom1<Minute1, T, Minute1, Minute1>,
pub minute5: LazyVecFrom1<Minute5, T, Minute5, Minute5>,
pub minute10: LazyVecFrom1<Minute10, T, Minute10, Minute10>,
pub minute30: LazyVecFrom1<Minute30, T, Minute30, Minute30>,
pub hour1: LazyVecFrom1<Hour1, T, Hour1, Hour1>,
pub hour4: LazyVecFrom1<Hour4, T, Hour4, Hour4>,
pub hour12: LazyVecFrom1<Hour12, T, Hour12, Hour12>,
pub day1: LazyVecFrom1<Day1, T, Day1, Day1>,
pub day3: LazyVecFrom1<Day3, T, Day3, Day3>,
pub week1: LazyVecFrom1<Week1, T, Week1, Week1>,
pub month1: LazyVecFrom1<Month1, T, Month1, Month1>,
pub month3: LazyVecFrom1<Month3, T, Month3, Month3>,
pub month6: LazyVecFrom1<Month6, T, Month6, Month6>,
pub year1: LazyVecFrom1<Year1, T, Year1, Year1>,
pub year10: LazyVecFrom1<Year10, T, Year10, Year10>,
pub halvingepoch: LazyVecFrom1<HalvingEpoch, T, HalvingEpoch, HalvingEpoch>,
pub difficultyepoch: LazyVecFrom1<DifficultyEpoch, T, DifficultyEpoch, DifficultyEpoch>,
}
impl<T: VecValue + Formattable + Serialize + JsonSchema> ConstantVecs<T> {
/// Create constant vecs using a transform that ignores input and returns a constant.
pub(crate) fn new<F>(name: &str, version: Version, indexes: &indexes::Vecs) -> Self
where
F: UnaryTransform<Height, T>
+ UnaryTransform<Minute1, T>
+ UnaryTransform<Minute5, T>
+ UnaryTransform<Minute10, T>
+ UnaryTransform<Minute30, T>
+ UnaryTransform<Hour1, T>
+ UnaryTransform<Hour4, T>
+ UnaryTransform<Hour12, T>
+ UnaryTransform<Day1, T>
+ UnaryTransform<Day3, T>
+ UnaryTransform<Week1, T>
+ UnaryTransform<Month1, T>
+ UnaryTransform<Month3, T>
+ UnaryTransform<Month6, T>
+ UnaryTransform<Year1, T>
+ UnaryTransform<Year10, T>
+ UnaryTransform<HalvingEpoch, T>
+ UnaryTransform<DifficultyEpoch, T>,
{
macro_rules! period {
($idx:ident, $I:ty) => {
LazyVecFrom1::transformed::<F>(
name,
version,
indexes.$idx.identity.read_only_boxed_clone(),
)
};
}
Self {
height: LazyVecFrom1::transformed::<F>(
name,
version,
indexes.height.identity.read_only_boxed_clone(),
),
minute1: period!(minute1, Minute1),
minute5: period!(minute5, Minute5),
minute10: period!(minute10, Minute10),
minute30: period!(minute30, Minute30),
hour1: period!(hour1, Hour1),
hour4: period!(hour4, Hour4),
hour12: period!(hour12, Hour12),
day1: period!(day1, Day1),
day3: period!(day3, Day3),
week1: period!(week1, Week1),
month1: period!(month1, Month1),
month3: period!(month3, Month3),
month6: period!(month6, Month6),
year1: period!(year1, Year1),
year10: period!(year10, Year10),
halvingepoch: period!(halvingepoch, HalvingEpoch),
difficultyepoch: period!(difficultyepoch, DifficultyEpoch),
}
}
}
@@ -0,0 +1,72 @@
//! ComputedFromHeightCumulative - stored height + LazyAggVec + cumulative (from height).
//!
//! Like ComputedFromHeightCumulativeSum but without RollingWindows.
//! Used for distribution metrics where rolling is optional per cohort.
//! Cumulative gets its own ComputedFromHeightLast so it has LazyAggVec index views.
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::{ComputedFromHeightLast, NumericValue},
};
#[derive(Traversable)]
pub struct ComputedFromHeightCumulative<T, M: StorageMode = Rw>
where
T: NumericValue + JsonSchema,
{
pub height: M::Stored<EagerVec<PcoVec<Height, T>>>,
pub cumulative: ComputedFromHeightLast<T, M>,
}
const VERSION: Version = Version::ZERO;
impl<T> ComputedFromHeightCumulative<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
let height: EagerVec<PcoVec<Height, T>> = EagerVec::forced_import(db, name, v)?;
let cumulative =
ComputedFromHeightLast::forced_import(db, &format!("{name}_cumulative"), v, indexes)?;
Ok(Self { height, cumulative })
}
/// Compute height data via closure, then cumulative only (no rolling).
pub(crate) fn compute(
&mut self,
max_from: Height,
exit: &Exit,
compute_height: impl FnOnce(&mut EagerVec<PcoVec<Height, T>>) -> Result<()>,
) -> Result<()>
where
T: Default,
{
compute_height(&mut self.height)?;
self.compute_rest(max_from, exit)
}
/// Compute cumulative from already-filled height vec.
pub(crate) fn compute_rest(&mut self, max_from: Height, exit: &Exit) -> Result<()>
where
T: Default,
{
self.cumulative
.height
.compute_cumulative(max_from, &self.height, exit)?;
Ok(())
}
}
@@ -0,0 +1,76 @@
//! ComputedFromHeightCumulativeFull - stored height + LazyAggVec + cumulative (from height) + RollingFull.
//!
//! For metrics with stored per-block data, cumulative sums, and rolling windows.
//! Cumulative gets its own ComputedFromHeightLast so it has LazyAggVec index views too.
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::{ComputedFromHeightLast, NumericValue, RollingFull, WindowStarts},
};
#[derive(Traversable)]
pub struct ComputedFromHeightCumulativeFull<T, M: StorageMode = Rw>
where
T: NumericValue + JsonSchema,
{
pub height: M::Stored<EagerVec<PcoVec<Height, T>>>,
pub cumulative: ComputedFromHeightLast<T, M>,
#[traversable(flatten)]
pub rolling: RollingFull<T, M>,
}
const VERSION: Version = Version::ZERO;
impl<T> ComputedFromHeightCumulativeFull<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
let height: EagerVec<PcoVec<Height, T>> = EagerVec::forced_import(db, name, v)?;
let cumulative =
ComputedFromHeightLast::forced_import(db, &format!("{name}_cumulative"), v, indexes)?;
let rolling = RollingFull::forced_import(db, name, v, indexes)?;
Ok(Self {
height,
cumulative,
rolling,
})
}
/// Compute height data via closure, then cumulative + rolling.
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: From<f64> + Default + SubAssign + Copy + Ord,
f64: From<T>,
{
compute_height(&mut self.height)?;
self.cumulative
.height
.compute_cumulative(max_from, &self.height, exit)?;
self.rolling
.compute(max_from, windows, &self.height, exit)?;
Ok(())
}
}
@@ -0,0 +1,75 @@
//! ComputedFromHeightCumulativeSum - stored height + LazyAggVec + cumulative (from height) + RollingWindows (sum).
//!
//! Like ComputedFromHeightCumulativeFull but with rolling sum only (no distribution).
//! Used for count metrics where distribution stats aren't meaningful.
//! Cumulative gets its own ComputedFromHeightLast so it has LazyAggVec index views too.
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::{ComputedFromHeightLast, NumericValue, RollingWindows, WindowStarts},
};
#[derive(Traversable)]
pub struct ComputedFromHeightCumulativeSum<T, M: StorageMode = Rw>
where
T: NumericValue + JsonSchema,
{
pub height: M::Stored<EagerVec<PcoVec<Height, T>>>,
pub cumulative: ComputedFromHeightLast<T, M>,
pub sum: RollingWindows<T, M>,
}
const VERSION: Version = Version::ZERO;
impl<T> ComputedFromHeightCumulativeSum<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
let height: EagerVec<PcoVec<Height, T>> = EagerVec::forced_import(db, name, v)?;
let cumulative =
ComputedFromHeightLast::forced_import(db, &format!("{name}_cumulative"), v, indexes)?;
let rolling = RollingWindows::forced_import(db, name, v, indexes)?;
Ok(Self {
height,
cumulative,
sum: rolling,
})
}
/// Compute height data via closure, then cumulative + 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.cumulative
.height
.compute_cumulative(max_from, &self.height, exit)?;
self.sum
.compute_rolling_sum(max_from, windows, &self.height, exit)?;
Ok(())
}
}
@@ -0,0 +1,65 @@
//! ComputedFromHeight using Distribution aggregation (no sum/cumulative).
//!
//! Stored height data + LazyAggVec index views + rolling distribution windows.
//! Use for block-based metrics where sum/cumulative would be misleading
//! (e.g., activity counts that can't be deduplicated across blocks).
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;
use crate::internal::{ComputedVecValue, NumericValue, RollingDistribution, WindowStarts};
#[derive(Traversable)]
pub struct ComputedFromHeightDistribution<T, M: StorageMode = Rw>
where
T: ComputedVecValue + PartialOrd + JsonSchema,
{
pub height: M::Stored<EagerVec<PcoVec<Height, T>>>,
#[traversable(flatten)]
pub rolling: RollingDistribution<T, M>,
}
const VERSION: Version = Version::ZERO;
impl<T> ComputedFromHeightDistribution<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
let height: EagerVec<PcoVec<Height, T>> = EagerVec::forced_import(db, name, v)?;
let rolling = RollingDistribution::forced_import(db, name, v, indexes)?;
Ok(Self { height, rolling })
}
/// Compute height data via closure, then rolling distribution.
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: Copy + Ord + From<f64> + Default,
f64: From<T>,
{
compute_height(&mut self.height)?;
self.rolling
.compute_distribution(max_from, windows, &self.height, exit)?;
Ok(())
}
}
@@ -0,0 +1,73 @@
//! ComputedFromHeightFull - Full (distribution + sum + cumulative) + RollingFull.
//!
//! For metrics aggregated per-block from finer-grained sources (e.g., per-tx data),
//! where we want full per-block stats plus rolling window stats.
use std::ops::SubAssign;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use schemars::JsonSchema;
use vecdb::{Database, Exit, Rw, StorageMode};
use crate::{
indexes,
internal::{Full, NumericValue, RollingFull, WindowStarts},
};
#[derive(Traversable)]
pub struct ComputedFromHeightFull<T, M: StorageMode = Rw>
where
T: NumericValue + JsonSchema,
{
#[traversable(flatten)]
pub full: Full<Height, T, M>,
pub rolling: RollingFull<T, M>,
}
const VERSION: Version = Version::ZERO;
impl<T> ComputedFromHeightFull<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
let height = Full::forced_import(db, name, v)?;
let rolling = RollingFull::forced_import(db, name, v, indexes)?;
Ok(Self {
full: height,
rolling,
})
}
/// Compute Full stats via closure, then rolling windows from the per-block sum.
pub(crate) fn compute(
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
exit: &Exit,
compute_full: impl FnOnce(&mut Full<Height, T>) -> Result<()>,
) -> Result<()>
where
T: From<f64> + Default + SubAssign + Copy + Ord,
f64: From<T>,
{
compute_full(&mut self.full)?;
self.rolling.compute(
max_from,
windows,
self.full.sum_cumulative.sum.inner(),
exit,
)?;
Ok(())
}
}
@@ -0,0 +1,81 @@
//! ComputedFromHeight using only Last aggregation.
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{
BinaryTransform, Database, EagerVec, Exit, ImportableVec, PcoVec, ReadableCloneableVec,
ReadableVec, Rw, StorageMode, VecValue,
};
use crate::indexes;
use crate::internal::{ComputedHeightDerivedLast, ComputedVecValue, NumericValue};
#[derive(Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct ComputedFromHeightLast<T, M: StorageMode = Rw>
where
T: ComputedVecValue + PartialOrd + JsonSchema,
{
pub height: M::Stored<EagerVec<PcoVec<Height, T>>>,
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub rest: Box<ComputedHeightDerivedLast<T>>,
}
const VERSION: Version = Version::ZERO;
impl<T> ComputedFromHeightLast<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
let height: EagerVec<PcoVec<Height, T>> = EagerVec::forced_import(db, name, v)?;
let rest = ComputedHeightDerivedLast::forced_import(
name,
height.read_only_boxed_clone(),
v,
indexes,
);
Ok(Self {
height,
rest: Box::new(rest),
})
}
/// Eagerly compute this vec as a binary transform of two sources.
pub(crate) fn compute_binary<S1T, S2T, F>(
&mut self,
max_from: Height,
source1: &impl ReadableVec<Height, S1T>,
source2: &impl ReadableVec<Height, S2T>,
exit: &Exit,
) -> Result<()>
where
S1T: VecValue,
S2T: VecValue,
F: BinaryTransform<S1T, S2T, T>,
{
self.height.compute_binary::<S1T, S2T, F>(
max_from,
source1,
source2,
exit,
)?;
Ok(())
}
}
@@ -0,0 +1,69 @@
//! LazyComputedFromHeightCumulativeFull - block full with lazy height transform + cumulative + rolling.
use std::ops::SubAssign;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{Database, Exit, LazyVecFrom1, ReadableCloneableVec, Rw, StorageMode, UnaryTransform};
use crate::{
indexes,
internal::{ComputedHeightDerivedCumulativeFull, ComputedVecValue, NumericValue, WindowStarts},
};
const VERSION: Version = Version::ZERO;
/// Block full aggregation with lazy height transform + cumulative + rolling windows.
#[derive(Deref, DerefMut, Traversable)]
pub struct LazyComputedFromHeightFull<T, S = T, M: StorageMode = Rw>
where
T: NumericValue + JsonSchema,
S: ComputedVecValue,
{
#[traversable(rename = "base")]
pub height: LazyVecFrom1<Height, T, Height, S>,
#[deref]
#[deref_mut]
pub rest: Box<ComputedHeightDerivedCumulativeFull<T, M>>,
}
impl<T, S> LazyComputedFromHeightFull<T, S>
where
T: NumericValue + JsonSchema,
S: ComputedVecValue + JsonSchema,
{
pub(crate) fn forced_import<F: UnaryTransform<S, T>>(
db: &Database,
name: &str,
version: Version,
source: &impl ReadableCloneableVec<Height, S>,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
let height = LazyVecFrom1::transformed::<F>(name, v, source.read_only_boxed_clone());
let rest = ComputedHeightDerivedCumulativeFull::forced_import(db, name, v, indexes)?;
Ok(Self {
height,
rest: Box::new(rest),
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
exit: &Exit,
) -> Result<()>
where
T: From<f64> + Default + SubAssign + Copy + Ord,
f64: From<T>,
{
self.rest.compute(max_from, windows, &self.height, exit)
}
}
@@ -0,0 +1,62 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats, Version};
use vecdb::{Database, Exit, Rw, StorageMode};
use crate::{
indexes,
internal::{ByUnit, SatsToDollars},
prices,
};
#[derive(Traversable)]
pub struct LazyComputedValueFromHeightCumulative<M: StorageMode = Rw> {
pub base: ByUnit<M>,
pub cumulative: ByUnit<M>,
}
const VERSION: Version = Version::ONE;
impl LazyComputedValueFromHeightCumulative {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self {
base: ByUnit::forced_import(db, name, v, indexes)?,
cumulative: ByUnit::forced_import(db, &format!("{name}_cumulative"), v, indexes)?,
})
}
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
max_from: Height,
exit: &Exit,
) -> Result<()> {
self.cumulative
.sats
.height
.compute_cumulative(max_from, &self.base.sats.height, exit)?;
self.base
.usd
.compute_binary::<Sats, Dollars, SatsToDollars>(
max_from,
&self.base.sats.height,
&prices.price.usd.height,
exit,
)?;
self.cumulative
.usd
.height
.compute_cumulative(max_from, &self.base.usd.height, exit)?;
Ok(())
}
}
@@ -0,0 +1,91 @@
//! Lazy unary transform from height with Last aggregation.
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{LazyVecFrom1, ReadableBoxedVec, ReadableCloneableVec, UnaryTransform};
use crate::{
indexes,
internal::{ComputedFromHeightLast, ComputedVecValue, LazyHeightDerivedLast, NumericValue},
};
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct LazyFromHeightLast<T, S1T = T>
where
T: ComputedVecValue + PartialOrd + JsonSchema,
S1T: ComputedVecValue,
{
pub height: LazyVecFrom1<Height, T, Height, S1T>,
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub rest: Box<LazyHeightDerivedLast<T, S1T>>,
}
const VERSION: Version = Version::ZERO;
impl<T, S1T> LazyFromHeightLast<T, S1T>
where
T: ComputedVecValue + JsonSchema + 'static,
S1T: ComputedVecValue + JsonSchema,
{
pub(crate) fn from_computed<F: UnaryTransform<S1T, T>>(
name: &str,
version: Version,
height_source: ReadableBoxedVec<Height, S1T>,
source: &ComputedFromHeightLast<S1T>,
) -> Self
where
S1T: NumericValue,
{
let v = version + VERSION;
Self {
height: LazyVecFrom1::transformed::<F>(name, v, height_source),
rest: Box::new(LazyHeightDerivedLast::from_computed::<F>(name, v, source)),
}
}
pub(crate) fn from_height_source<F: UnaryTransform<S1T, T>>(
name: &str,
version: Version,
height_source: ReadableBoxedVec<Height, S1T>,
indexes: &indexes::Vecs,
) -> Self
where
S1T: NumericValue,
{
let v = version + VERSION;
Self {
height: LazyVecFrom1::transformed::<F>(name, v, height_source.clone()),
rest: Box::new(LazyHeightDerivedLast::from_height_source::<F>(
name,
v,
height_source,
indexes,
)),
}
}
/// Create by unary-transforming a LazyFromHeightLast source (chaining lazy vecs).
pub(crate) fn from_lazy<F, S2T>(
name: &str,
version: Version,
source: &LazyFromHeightLast<S1T, S2T>,
) -> Self
where
F: UnaryTransform<S1T, T>,
S2T: ComputedVecValue + JsonSchema,
{
let v = version + VERSION;
Self {
height: LazyVecFrom1::transformed::<F>(name, v, source.height.read_only_boxed_clone()),
rest: Box::new(LazyHeightDerivedLast::from_lazy::<F, S2T>(
name,
v,
&source.rest,
)),
}
}
}
@@ -0,0 +1,47 @@
//! Lazy value wrapper for ValueFromHeightLast - all transforms are lazy.
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Dollars, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::UnaryTransform;
use crate::internal::{LazyValueFromHeight, LazyValueHeightDerivedLast, ValueFromHeightLast};
const VERSION: Version = Version::ZERO;
/// Lazy value wrapper with height + all derived last transforms from ValueFromHeightLast.
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct LazyValueFromHeightLast {
#[traversable(flatten)]
pub height: LazyValueFromHeight,
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub rest: Box<LazyValueHeightDerivedLast>,
}
impl LazyValueFromHeightLast {
pub(crate) fn from_block_source<SatsTransform, BitcoinTransform, DollarsTransform>(
name: &str,
source: &ValueFromHeightLast,
version: Version,
) -> Self
where
SatsTransform: UnaryTransform<Sats, Sats>,
BitcoinTransform: UnaryTransform<Sats, Bitcoin>,
DollarsTransform: UnaryTransform<Dollars, Dollars>,
{
let v = version + VERSION;
let height =
LazyValueFromHeight::from_block_source::<SatsTransform, BitcoinTransform, DollarsTransform>(name, source, v);
let rest =
LazyValueHeightDerivedLast::from_block_source::<SatsTransform, BitcoinTransform, DollarsTransform>(
name, source, v,
);
Self { height, rest: Box::new(rest) }
}
}
@@ -0,0 +1,47 @@
mod by_unit;
mod constant;
mod cumulative;
mod cumulative_full;
mod cumulative_sum;
mod distribution;
mod full;
mod last;
mod lazy_computed_full;
mod lazy_computed_value_cumulative;
mod lazy_last;
mod lazy_value_last;
mod percentiles;
mod price;
mod ratio;
mod stddev;
mod stored_value_last;
mod value_change;
mod value_ema;
mod value_full;
mod value_last;
mod value_last_rolling;
mod value_sum_cumulative;
pub use by_unit::*;
pub use constant::*;
pub use cumulative::*;
pub use cumulative_full::*;
pub use cumulative_sum::*;
pub use distribution::*;
pub use full::*;
pub use last::*;
pub use lazy_computed_full::*;
pub use lazy_computed_value_cumulative::*;
pub use lazy_last::*;
pub use lazy_value_last::*;
pub use percentiles::*;
pub use price::*;
pub use ratio::*;
pub use stddev::*;
pub use stored_value_last::*;
pub use value_change::*;
pub use value_ema::*;
pub use value_full::*;
pub use value_last::*;
pub use value_last_rolling::*;
pub use value_sum_cumulative::*;
@@ -0,0 +1,150 @@
use brk_error::Result;
use brk_traversable::{Traversable, TreeNode};
use brk_types::{Dollars, Height, StoredF32, Version};
use vecdb::{AnyExportableVec, Database, ReadOnlyClone, Ro, Rw, StorageMode, WritableVec};
use crate::indexes;
use crate::internal::{ComputedFromHeightLast, Price};
pub const PERCENTILES: [u8; 19] = [
5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95,
];
pub const PERCENTILES_LEN: usize = PERCENTILES.len();
/// Compute spot percentile rank by interpolating within percentile bands.
/// Returns a value between 0 and 100 indicating where spot sits in the distribution.
pub(crate) fn compute_spot_percentile_rank(
percentile_prices: &[Dollars; PERCENTILES_LEN],
spot: Dollars,
) -> StoredF32 {
if spot.is_nan() || percentile_prices[0].is_nan() {
return StoredF32::NAN;
}
let spot_f64 = f64::from(spot);
// Below lowest percentile (p5) - extrapolate towards 0
let p5 = f64::from(percentile_prices[0]);
if spot_f64 <= p5 {
if p5 == 0.0 {
return StoredF32::from(0.0);
}
// Linear extrapolation: rank = 5 * (spot / p5)
return StoredF32::from((5.0 * spot_f64 / p5).max(0.0));
}
// Above highest percentile (p95) - extrapolate towards 100
let p95 = f64::from(percentile_prices[PERCENTILES_LEN - 1]);
let p90 = f64::from(percentile_prices[PERCENTILES_LEN - 2]);
if spot_f64 >= p95 {
if p95 == p90 {
return StoredF32::from(100.0);
}
// Linear extrapolation using p90-p95 slope
let slope = 5.0 / (p95 - p90);
return StoredF32::from((95.0 + (spot_f64 - p95) * slope).min(100.0));
}
// Find the band containing spot and interpolate
for i in 0..PERCENTILES_LEN - 1 {
let lower = f64::from(percentile_prices[i]);
let upper = f64::from(percentile_prices[i + 1]);
if spot_f64 >= lower && spot_f64 <= upper {
let lower_pct = f64::from(PERCENTILES[i]);
let upper_pct = f64::from(PERCENTILES[i + 1]);
if upper == lower {
return StoredF32::from(lower_pct);
}
// Linear interpolation
let ratio = (spot_f64 - lower) / (upper - lower);
return StoredF32::from(lower_pct + ratio * (upper_pct - lower_pct));
}
}
StoredF32::NAN
}
pub struct PercentilesVecs<M: StorageMode = Rw> {
pub vecs: [Price<ComputedFromHeightLast<Dollars, M>>; PERCENTILES_LEN],
}
const VERSION: Version = Version::ONE;
impl PercentilesVecs {
pub(crate) fn forced_import(
db: &Database,
prefix: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let vecs = PERCENTILES
.into_iter()
.map(|p| {
let metric_name = format!("{prefix}_pct{p:02}");
Price::forced_import(db, &metric_name, version + VERSION, indexes)
})
.collect::<Result<Vec<_>>>()?
.try_into()
.ok()
.expect("PERCENTILES length mismatch");
Ok(Self { vecs })
}
/// Push percentile prices at this height.
pub(crate) fn truncate_push(
&mut self,
height: Height,
percentile_prices: &[Dollars; PERCENTILES_LEN],
) -> Result<()> {
for (i, v) in self.vecs.iter_mut().enumerate() {
v.usd.height.truncate_push(height, percentile_prices[i])?;
}
Ok(())
}
/// Validate computed versions or reset if mismatched.
pub(crate) fn validate_computed_version_or_reset(&mut self, version: Version) -> Result<()> {
for vec in self.vecs.iter_mut() {
vec.usd.height.validate_computed_version_or_reset(version)?;
}
Ok(())
}
}
impl ReadOnlyClone for PercentilesVecs {
type ReadOnly = PercentilesVecs<Ro>;
fn read_only_clone(&self) -> Self::ReadOnly {
PercentilesVecs {
vecs: self
.vecs
.each_ref()
.map(|v| v.read_only_clone()),
}
}
}
impl<M: StorageMode> Traversable for PercentilesVecs<M>
where
Price<ComputedFromHeightLast<Dollars, M>>: Traversable,
{
fn to_tree_node(&self) -> TreeNode {
TreeNode::Branch(
PERCENTILES
.iter()
.zip(self.vecs.iter())
.map(|(p, v)| (format!("pct{p:02}"), v.to_tree_node()))
.collect(),
)
}
fn iter_any_exportable(&self) -> impl Iterator<Item = &dyn AnyExportableVec> {
self.vecs
.iter()
.flat_map(|p| p.iter_any_exportable())
}
}
@@ -0,0 +1,65 @@
//! Generic price wrapper with both USD and sats representations.
//!
//! All prices use this single struct with different USD types.
//! Sats is always lazily derived from USD via DollarsToSatsFract.
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, SatsFract, Version};
use schemars::JsonSchema;
use vecdb::{Database, ReadableCloneableVec, UnaryTransform};
use super::{ComputedFromHeightLast, LazyFromHeightLast};
use crate::{
indexes,
internal::{ComputedVecValue, DollarsToSatsFract, NumericValue},
};
/// Generic price metric with both USD and sats representations.
#[derive(Clone, Traversable)]
pub struct Price<U> {
pub usd: U,
pub sats: LazyFromHeightLast<SatsFract, Dollars>,
}
impl Price<ComputedFromHeightLast<Dollars>> {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let usd = ComputedFromHeightLast::forced_import(db, name, version, indexes)?;
let sats = LazyFromHeightLast::from_computed::<DollarsToSatsFract>(
&format!("{name}_sats"),
version,
usd.height.read_only_boxed_clone(),
&usd,
);
Ok(Self { usd, sats })
}
}
impl<ST> Price<LazyFromHeightLast<Dollars, ST>>
where
ST: ComputedVecValue + NumericValue + JsonSchema + 'static,
{
pub(crate) fn from_computed<F: UnaryTransform<ST, Dollars>>(
name: &str,
version: Version,
source: &ComputedFromHeightLast<ST>,
) -> Self {
let usd = LazyFromHeightLast::from_computed::<F>(
name,
version,
source.height.read_only_boxed_clone(),
source,
);
let sats = LazyFromHeightLast::from_lazy::<DollarsToSatsFract, ST>(
&format!("{name}_sats"),
version,
&usd,
);
Self { usd, sats }
}
}
@@ -0,0 +1,52 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, indexes, prices};
use super::{ComputedFromHeightRatio, ComputedFromHeightRatioExtension};
#[derive(Deref, DerefMut, Traversable)]
pub struct ComputedFromHeightRatioExtended<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub base: ComputedFromHeightRatio<M>,
#[traversable(flatten)]
pub extended: ComputedFromHeightRatioExtension<M>,
}
impl ComputedFromHeightRatioExtended {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
base: ComputedFromHeightRatio::forced_import(db, name, version, indexes)?,
extended: ComputedFromHeightRatioExtension::forced_import(db, name, version, indexes)?,
})
}
/// Compute ratio and all extended metrics from an externally-provided metric price.
pub(crate) fn compute_rest(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
metric_price: &impl ReadableVec<Height, Dollars>,
) -> Result<()> {
let close_price = &prices.price.usd.height;
self.base
.compute_ratio(starting_indexes, close_price, metric_price, exit)?;
self.extended
.compute_rest(blocks, starting_indexes, exit, &self.base.ratio.height)?;
self.extended
.compute_usd_bands(starting_indexes, metric_price, exit)?;
Ok(())
}
}
@@ -0,0 +1,277 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, StoredF32, Version};
use vecdb::{AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode, VecIndex, WritableVec};
use crate::{
ComputeIndexes, blocks, indexes,
internal::{ComputedFromHeightStdDevExtended, Price},
};
use brk_types::get_percentile;
use super::super::ComputedFromHeightLast;
#[derive(Traversable)]
pub struct ComputedFromHeightRatioExtension<M: StorageMode = Rw> {
pub ratio_1w_sma: ComputedFromHeightLast<StoredF32, M>,
pub ratio_1m_sma: ComputedFromHeightLast<StoredF32, M>,
pub ratio_pct99: ComputedFromHeightLast<StoredF32, M>,
pub ratio_pct98: ComputedFromHeightLast<StoredF32, M>,
pub ratio_pct95: ComputedFromHeightLast<StoredF32, M>,
pub ratio_pct5: ComputedFromHeightLast<StoredF32, M>,
pub ratio_pct2: ComputedFromHeightLast<StoredF32, M>,
pub ratio_pct1: ComputedFromHeightLast<StoredF32, M>,
pub ratio_pct99_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub ratio_pct98_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub ratio_pct95_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub ratio_pct5_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub ratio_pct2_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub ratio_pct1_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub ratio_sd: ComputedFromHeightStdDevExtended<M>,
pub ratio_4y_sd: ComputedFromHeightStdDevExtended<M>,
pub ratio_2y_sd: ComputedFromHeightStdDevExtended<M>,
pub ratio_1y_sd: ComputedFromHeightStdDevExtended<M>,
}
const VERSION: Version = Version::TWO;
impl ComputedFromHeightRatioExtension {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
macro_rules! import {
($suffix:expr) => {
ComputedFromHeightLast::forced_import(
db,
&format!("{name}_{}", $suffix),
v,
indexes,
)?
};
}
macro_rules! import_sd {
($suffix:expr, $days:expr) => {
ComputedFromHeightStdDevExtended::forced_import(
db,
&format!("{name}_{}", $suffix),
$days,
v,
indexes,
)?
};
}
macro_rules! import_usd {
($suffix:expr) => {
Price::forced_import(db, &format!("{name}_{}", $suffix), v, indexes)?
};
}
Ok(Self {
ratio_1w_sma: import!("ratio_1w_sma"),
ratio_1m_sma: import!("ratio_1m_sma"),
ratio_sd: import_sd!("ratio", usize::MAX),
ratio_1y_sd: import_sd!("ratio_1y", 365),
ratio_2y_sd: import_sd!("ratio_2y", 2 * 365),
ratio_4y_sd: import_sd!("ratio_4y", 4 * 365),
ratio_pct99: import!("ratio_pct99"),
ratio_pct98: import!("ratio_pct98"),
ratio_pct95: import!("ratio_pct95"),
ratio_pct5: import!("ratio_pct5"),
ratio_pct2: import!("ratio_pct2"),
ratio_pct1: import!("ratio_pct1"),
ratio_pct99_usd: import_usd!("ratio_pct99_usd"),
ratio_pct98_usd: import_usd!("ratio_pct98_usd"),
ratio_pct95_usd: import_usd!("ratio_pct95_usd"),
ratio_pct5_usd: import_usd!("ratio_pct5_usd"),
ratio_pct2_usd: import_usd!("ratio_pct2_usd"),
ratio_pct1_usd: import_usd!("ratio_pct1_usd"),
})
}
/// Compute extended ratio metrics from an externally-provided ratio source.
pub(crate) fn compute_rest(
&mut self,
blocks: &blocks::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
ratio_source: &impl ReadableVec<Height, StoredF32>,
) -> Result<()> {
// SMA using lookback vecs
self.ratio_1w_sma.height.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1w_ago,
ratio_source,
exit,
)?;
self.ratio_1m_sma.height.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1m_ago,
ratio_source,
exit,
)?;
// Percentiles: insert into sorted array on day boundaries
let ratio_version = ratio_source.version();
self.mut_ratio_vecs()
.try_for_each(|v| -> Result<()> {
v.validate_computed_version_or_reset(ratio_version)?;
Ok(())
})?;
let starting_height = self
.mut_ratio_vecs()
.map(|v| Height::from(v.len()))
.min()
.unwrap()
.min(starting_indexes.height);
let start = starting_height.to_usize();
let day_start = &blocks.count.height_24h_ago;
// Collect sorted history up to starting point (one per day boundary)
let mut sorted = {
let ratio_data = ratio_source.collect_range_at(0, start);
let day_start_hist = day_start.collect_range_at(0, start);
let mut sorted: Vec<StoredF32> = Vec::new();
let mut last_day_start = Height::from(0_usize);
for (h, ratio) in ratio_data.into_iter().enumerate() {
let cur_day_start = day_start_hist[h];
if h == 0 || cur_day_start != last_day_start {
sorted.push(ratio);
last_day_start = cur_day_start;
}
}
sorted.sort_unstable();
sorted
};
let pct1_vec = &mut self.ratio_pct1.height;
let pct2_vec = &mut self.ratio_pct2.height;
let pct5_vec = &mut self.ratio_pct5.height;
let pct95_vec = &mut self.ratio_pct95.height;
let pct98_vec = &mut self.ratio_pct98.height;
let pct99_vec = &mut self.ratio_pct99.height;
let ratio_len = ratio_source.len();
let ratio_data = ratio_source.collect_range_at(start, ratio_len);
let mut last_day_start = if start > 0 {
day_start
.collect_one_at(start - 1)
.unwrap_or(Height::from(0_usize))
} else {
Height::from(0_usize)
};
let day_start_data = day_start.collect_range_at(start, ratio_len);
for (offset, ratio) in ratio_data.into_iter().enumerate() {
let index = start + offset;
let cur_day_start = day_start_data[offset];
if index == 0 || cur_day_start != last_day_start {
let pos = sorted.binary_search(&ratio).unwrap_or_else(|p| p);
sorted.insert(pos, ratio);
last_day_start = cur_day_start;
}
if sorted.is_empty() {
pct1_vec.truncate_push_at(index, StoredF32::NAN)?;
pct2_vec.truncate_push_at(index, StoredF32::NAN)?;
pct5_vec.truncate_push_at(index, StoredF32::NAN)?;
pct95_vec.truncate_push_at(index, StoredF32::NAN)?;
pct98_vec.truncate_push_at(index, StoredF32::NAN)?;
pct99_vec.truncate_push_at(index, StoredF32::NAN)?;
} else {
pct1_vec.truncate_push_at(index, get_percentile(&sorted, 0.01))?;
pct2_vec.truncate_push_at(index, get_percentile(&sorted, 0.02))?;
pct5_vec.truncate_push_at(index, get_percentile(&sorted, 0.05))?;
pct95_vec.truncate_push_at(index, get_percentile(&sorted, 0.95))?;
pct98_vec.truncate_push_at(index, get_percentile(&sorted, 0.98))?;
pct99_vec.truncate_push_at(index, get_percentile(&sorted, 0.99))?;
}
}
{
let _lock = exit.lock();
self.mut_ratio_vecs()
.try_for_each(|v| v.flush())?;
}
// Compute stddev at height level
self.ratio_sd
.compute_all(blocks, starting_indexes, exit, ratio_source)?;
self.ratio_4y_sd
.compute_all(blocks, starting_indexes, exit, ratio_source)?;
self.ratio_2y_sd
.compute_all(blocks, starting_indexes, exit, ratio_source)?;
self.ratio_1y_sd
.compute_all(blocks, starting_indexes, exit, ratio_source)?;
Ok(())
}
/// Compute USD ratio bands: usd_band = metric_price * ratio_percentile
pub(crate) fn compute_usd_bands(
&mut self,
starting_indexes: &ComputeIndexes,
metric_price: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
use crate::internal::PriceTimesRatio;
macro_rules! compute_band {
($usd_field:ident, $band_source:expr) => {
self.$usd_field
.usd
.compute_binary::<Dollars, StoredF32, PriceTimesRatio>(
starting_indexes.height,
metric_price,
$band_source,
exit,
)?;
};
}
compute_band!(ratio_pct99_usd, &self.ratio_pct99.height);
compute_band!(ratio_pct98_usd, &self.ratio_pct98.height);
compute_band!(ratio_pct95_usd, &self.ratio_pct95.height);
compute_band!(ratio_pct5_usd, &self.ratio_pct5.height);
compute_band!(ratio_pct2_usd, &self.ratio_pct2.height);
compute_band!(ratio_pct1_usd, &self.ratio_pct1.height);
// Stddev USD bands
self.ratio_sd
.compute_usd_bands(starting_indexes, metric_price, exit)?;
self.ratio_4y_sd
.compute_usd_bands(starting_indexes, metric_price, exit)?;
self.ratio_2y_sd
.compute_usd_bands(starting_indexes, metric_price, exit)?;
self.ratio_1y_sd
.compute_usd_bands(starting_indexes, metric_price, exit)?;
Ok(())
}
fn mut_ratio_vecs(
&mut self,
) -> impl Iterator<Item = &mut EagerVec<PcoVec<Height, StoredF32>>> {
[
&mut self.ratio_pct1.height,
&mut self.ratio_pct2.height,
&mut self.ratio_pct5.height,
&mut self.ratio_pct95.height,
&mut self.ratio_pct98.height,
&mut self.ratio_pct99.height,
]
.into_iter()
}
}
@@ -0,0 +1,62 @@
mod extended;
mod extension;
mod price_extended;
pub use extended::*;
pub use extension::*;
pub use price_extended::*;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, StoredF32, Version};
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, indexes};
use super::ComputedFromHeightLast;
#[derive(Traversable)]
pub struct ComputedFromHeightRatio<M: StorageMode = Rw> {
pub ratio: ComputedFromHeightLast<StoredF32, M>,
}
const VERSION: Version = Version::TWO;
impl ComputedFromHeightRatio {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self {
ratio: ComputedFromHeightLast::forced_import(db, &format!("{name}_ratio"), v, indexes)?,
})
}
/// Compute ratio = close_price / metric_price at height level
pub(crate) fn compute_ratio(
&mut self,
starting_indexes: &ComputeIndexes,
close_price: &impl ReadableVec<Height, Dollars>,
metric_price: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.ratio.height.compute_transform2(
starting_indexes.height,
close_price,
metric_price,
|(i, close, price, ..)| {
if price == Dollars::ZERO {
(i, StoredF32::from(1.0))
} else {
(i, StoredF32::from(close / price))
}
},
exit,
)?;
Ok(())
}
}
@@ -0,0 +1,57 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode};
use crate::internal::{ComputedFromHeightLast, Price};
use crate::{ComputeIndexes, blocks, indexes, prices};
use super::ComputedFromHeightRatioExtended;
#[derive(Deref, DerefMut, Traversable)]
pub struct ComputedFromHeightPriceWithRatioExtended<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub inner: ComputedFromHeightRatioExtended<M>,
pub price: Price<ComputedFromHeightLast<Dollars, M>>,
}
impl ComputedFromHeightPriceWithRatioExtended {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + Version::TWO;
Ok(Self {
inner: ComputedFromHeightRatioExtended::forced_import(db, name, version, indexes)?,
price: Price::forced_import(db, name, v, indexes)?,
})
}
/// Compute price via closure, then compute ratio + extended metrics.
pub(crate) fn compute_all<F>(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
mut compute_price: F,
) -> Result<()>
where
F: FnMut(&mut EagerVec<PcoVec<Height, Dollars>>) -> Result<()>,
{
compute_price(&mut self.price.usd.height)?;
self.inner.compute_rest(
blocks,
prices,
starting_indexes,
exit,
&self.price.usd.height,
)?;
Ok(())
}
}
@@ -0,0 +1,278 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, StoredF32, Version};
use vecdb::{
AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode, VecIndex,
WritableVec,
};
use crate::{ComputeIndexes, blocks, indexes};
use crate::internal::{ComputedFromHeightLast, Price};
use super::ComputedFromHeightStdDev;
#[derive(Traversable)]
pub struct ComputedFromHeightStdDevExtended<M: StorageMode = Rw> {
#[traversable(flatten)]
pub base: ComputedFromHeightStdDev<M>,
pub zscore: ComputedFromHeightLast<StoredF32, M>,
pub p0_5sd: ComputedFromHeightLast<StoredF32, M>,
pub p1sd: ComputedFromHeightLast<StoredF32, M>,
pub p1_5sd: ComputedFromHeightLast<StoredF32, M>,
pub p2sd: ComputedFromHeightLast<StoredF32, M>,
pub p2_5sd: ComputedFromHeightLast<StoredF32, M>,
pub p3sd: ComputedFromHeightLast<StoredF32, M>,
pub m0_5sd: ComputedFromHeightLast<StoredF32, M>,
pub m1sd: ComputedFromHeightLast<StoredF32, M>,
pub m1_5sd: ComputedFromHeightLast<StoredF32, M>,
pub m2sd: ComputedFromHeightLast<StoredF32, M>,
pub m2_5sd: ComputedFromHeightLast<StoredF32, M>,
pub m3sd: ComputedFromHeightLast<StoredF32, M>,
pub _0sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub p0_5sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub p1sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub p1_5sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub p2sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub p2_5sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub p3sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub m0_5sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub m1sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub m1_5sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub m2sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub m2_5sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
pub m3sd_usd: Price<ComputedFromHeightLast<Dollars, M>>,
}
impl ComputedFromHeightStdDevExtended {
pub(crate) fn forced_import(
db: &Database,
name: &str,
days: usize,
parent_version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let version = parent_version + Version::TWO;
macro_rules! import {
($suffix:expr) => {
ComputedFromHeightLast::forced_import(
db,
&format!("{name}_{}", $suffix),
version,
indexes,
)?
};
}
macro_rules! import_usd {
($suffix:expr) => {
Price::forced_import(db, &format!("{name}_{}", $suffix), version, indexes)?
};
}
Ok(Self {
base: ComputedFromHeightStdDev::forced_import(db, name, days, parent_version, indexes)?,
zscore: import!("zscore"),
p0_5sd: import!("p0_5sd"),
p1sd: import!("p1sd"),
p1_5sd: import!("p1_5sd"),
p2sd: import!("p2sd"),
p2_5sd: import!("p2_5sd"),
p3sd: import!("p3sd"),
m0_5sd: import!("m0_5sd"),
m1sd: import!("m1sd"),
m1_5sd: import!("m1_5sd"),
m2sd: import!("m2sd"),
m2_5sd: import!("m2_5sd"),
m3sd: import!("m3sd"),
_0sd_usd: import_usd!("0sd_usd"),
p0_5sd_usd: import_usd!("p0_5sd_usd"),
p1sd_usd: import_usd!("p1sd_usd"),
p1_5sd_usd: import_usd!("p1_5sd_usd"),
p2sd_usd: import_usd!("p2sd_usd"),
p2_5sd_usd: import_usd!("p2_5sd_usd"),
p3sd_usd: import_usd!("p3sd_usd"),
m0_5sd_usd: import_usd!("m0_5sd_usd"),
m1sd_usd: import_usd!("m1sd_usd"),
m1_5sd_usd: import_usd!("m1_5sd_usd"),
m2sd_usd: import_usd!("m2sd_usd"),
m2_5sd_usd: import_usd!("m2_5sd_usd"),
m3sd_usd: import_usd!("m3sd_usd"),
})
}
pub(crate) fn compute_all(
&mut self,
blocks: &blocks::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
source: &impl ReadableVec<Height, StoredF32>,
) -> Result<()> {
self.base
.compute_all(blocks, starting_indexes, exit, source)?;
let sma_opt: Option<&EagerVec<PcoVec<Height, StoredF32>>> = None;
self.compute_bands(starting_indexes, exit, sma_opt, source)
}
pub(crate) fn compute_bands(
&mut self,
starting_indexes: &ComputeIndexes,
exit: &Exit,
sma_opt: Option<&impl ReadableVec<Height, StoredF32>>,
source: &impl ReadableVec<Height, StoredF32>,
) -> Result<()> {
let source_version = source.version();
self.mut_band_height_vecs()
.try_for_each(|v| -> Result<()> {
v.validate_computed_version_or_reset(source_version)?;
Ok(())
})?;
let starting_height = self
.mut_band_height_vecs()
.map(|v| Height::from(v.len()))
.min()
.unwrap()
.min(starting_indexes.height);
let start = starting_height.to_usize();
let source_len = source.len();
let source_data = source.collect_range_at(start, source_len);
let sma_len = sma_opt
.map(|s| s.len())
.unwrap_or(self.base.sma.height.len());
let sma_data: Vec<StoredF32> = if let Some(sma) = sma_opt {
sma.collect_range_at(start, sma_len)
} else {
self.base.sma.height.collect_range_at(start, sma_len)
};
let sd_data = self
.base
.sd
.height
.collect_range_at(start, self.base.sd.height.len());
for (offset, _ratio) in source_data.into_iter().enumerate() {
let index = start + offset;
let average = sma_data[offset];
let sd = sd_data[offset];
self.p0_5sd
.height
.truncate_push_at(index, average + StoredF32::from(0.5 * *sd))?;
self.p1sd.height.truncate_push_at(index, average + sd)?;
self.p1_5sd
.height
.truncate_push_at(index, average + StoredF32::from(1.5 * *sd))?;
self.p2sd.height.truncate_push_at(index, average + 2 * sd)?;
self.p2_5sd
.height
.truncate_push_at(index, average + StoredF32::from(2.5 * *sd))?;
self.p3sd.height.truncate_push_at(index, average + 3 * sd)?;
self.m0_5sd
.height
.truncate_push_at(index, average - StoredF32::from(0.5 * *sd))?;
self.m1sd.height.truncate_push_at(index, average - sd)?;
self.m1_5sd
.height
.truncate_push_at(index, average - StoredF32::from(1.5 * *sd))?;
self.m2sd.height.truncate_push_at(index, average - 2 * sd)?;
self.m2_5sd
.height
.truncate_push_at(index, average - StoredF32::from(2.5 * *sd))?;
self.m3sd.height.truncate_push_at(index, average - 3 * sd)?;
}
{
let _lock = exit.lock();
self.mut_band_height_vecs().try_for_each(|v| v.flush())?;
}
if let Some(sma) = sma_opt {
self.zscore.height.compute_zscore(
starting_indexes.height,
source,
sma,
&self.base.sd.height,
exit,
)?;
} else {
self.zscore.height.compute_zscore(
starting_indexes.height,
source,
&self.base.sma.height,
&self.base.sd.height,
exit,
)?;
}
Ok(())
}
/// Compute USD price bands: usd_band = metric_price * band_ratio
pub(crate) fn compute_usd_bands(
&mut self,
starting_indexes: &ComputeIndexes,
metric_price: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
use crate::internal::PriceTimesRatio;
macro_rules! compute_band {
($usd_field:ident, $band_source:expr) => {
self.$usd_field
.usd
.compute_binary::<Dollars, StoredF32, PriceTimesRatio>(
starting_indexes.height,
metric_price,
$band_source,
exit,
)?;
};
}
compute_band!(_0sd_usd, &self.base.sma.height);
compute_band!(p0_5sd_usd, &self.p0_5sd.height);
compute_band!(p1sd_usd, &self.p1sd.height);
compute_band!(p1_5sd_usd, &self.p1_5sd.height);
compute_band!(p2sd_usd, &self.p2sd.height);
compute_band!(p2_5sd_usd, &self.p2_5sd.height);
compute_band!(p3sd_usd, &self.p3sd.height);
compute_band!(m0_5sd_usd, &self.m0_5sd.height);
compute_band!(m1sd_usd, &self.m1sd.height);
compute_band!(m1_5sd_usd, &self.m1_5sd.height);
compute_band!(m2sd_usd, &self.m2sd.height);
compute_band!(m2_5sd_usd, &self.m2_5sd.height);
compute_band!(m3sd_usd, &self.m3sd.height);
Ok(())
}
fn mut_band_height_vecs(
&mut self,
) -> impl Iterator<Item = &mut EagerVec<PcoVec<Height, StoredF32>>> {
[
&mut self.p0_5sd.height,
&mut self.p1sd.height,
&mut self.p1_5sd.height,
&mut self.p2sd.height,
&mut self.p2_5sd.height,
&mut self.p3sd.height,
&mut self.m0_5sd.height,
&mut self.m1sd.height,
&mut self.m1_5sd.height,
&mut self.m2sd.height,
&mut self.m2_5sd.height,
&mut self.m3sd.height,
]
.into_iter()
}
}
@@ -0,0 +1,168 @@
mod extended;
pub use extended::*;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, StoredF32, Version};
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, VecIndex, WritableVec};
use crate::{ComputeIndexes, blocks, indexes};
use crate::internal::ComputedFromHeightLast;
#[derive(Traversable)]
pub struct ComputedFromHeightStdDev<M: StorageMode = Rw> {
days: usize,
pub sma: ComputedFromHeightLast<StoredF32, M>,
pub sd: ComputedFromHeightLast<StoredF32, M>,
}
impl ComputedFromHeightStdDev {
pub(crate) fn forced_import(
db: &Database,
name: &str,
days: usize,
parent_version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let version = parent_version + Version::TWO;
let sma = ComputedFromHeightLast::forced_import(
db,
&format!("{name}_sma"),
version,
indexes,
)?;
let sd = ComputedFromHeightLast::forced_import(
db,
&format!("{name}_sd"),
version,
indexes,
)?;
Ok(Self { days, sma, sd })
}
pub(crate) fn compute_all(
&mut self,
blocks: &blocks::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
source: &impl ReadableVec<Height, StoredF32>,
) -> Result<()> {
// 1. Compute SMA using the appropriate lookback vec (or full-history SMA)
if self.days != usize::MAX {
let window_starts = blocks.count.start_vec(self.days);
self.sma.height.compute_rolling_average(
starting_indexes.height,
window_starts,
source,
exit,
)?;
} else {
// Full history SMA (days == usize::MAX)
self.sma.height.compute_sma_(
starting_indexes.height,
source,
self.days,
exit,
None,
)?;
}
// Split borrows: sd is mutated, sma is read
compute_sd(
&mut self.sd,
blocks,
starting_indexes,
exit,
&self.sma.height,
source,
)
}
}
fn compute_sd(
sd: &mut ComputedFromHeightLast<StoredF32>,
blocks: &blocks::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
sma: &impl ReadableVec<Height, StoredF32>,
source: &impl ReadableVec<Height, StoredF32>,
) -> Result<()> {
let source_version = source.version();
sd.height
.validate_computed_version_or_reset(source_version)?;
let starting_height = Height::from(sd.height.len()).min(starting_indexes.height);
let day_start = &blocks.count.height_24h_ago;
let start = starting_height.to_usize();
let mut n: usize = 0;
let mut welford_sum: f64 = 0.0;
let mut welford_sum_sq: f64 = 0.0;
if start > 0 {
let day_start_hist = day_start.collect_range_at(0, start);
let source_hist = source.collect_range_at(0, start);
let mut last_ds = Height::from(0_usize);
for h in 0..start {
let cur_ds = day_start_hist[h];
if h == 0 || cur_ds != last_ds {
let val = *source_hist[h] as f64;
n += 1;
welford_sum += val;
welford_sum_sq += val * val;
last_ds = cur_ds;
}
}
}
let source_len = source.len();
let source_data = source.collect_range_at(start, source_len);
let sma_data = sma.collect_range_at(start, sma.len());
let mut last_day_start = if start > 0 {
day_start
.collect_one_at(start - 1)
.unwrap_or(Height::from(0_usize))
} else {
Height::from(0_usize)
};
let day_start_data = day_start.collect_range_at(start, source_len);
for (offset, ratio) in source_data.into_iter().enumerate() {
let index = start + offset;
let cur_day_start = day_start_data[offset];
if index == 0 || cur_day_start != last_day_start {
let val = *ratio as f64;
n += 1;
welford_sum += val;
welford_sum_sq += val * val;
last_day_start = cur_day_start;
}
let average = sma_data[offset];
let avg_f64 = *average as f64;
let sd_val = if n > 0 {
let nf = n as f64;
let variance =
welford_sum_sq / nf - 2.0 * avg_f64 * welford_sum / nf + avg_f64 * avg_f64;
StoredF32::from(variance.max(0.0).sqrt() as f32)
} else {
StoredF32::from(0.0_f32)
};
sd.height.truncate_push_at(index, sd_val)?;
}
{
let _lock = exit.lock();
sd.height.flush()?;
}
Ok(())
}
@@ -0,0 +1,53 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{
indexes,
internal::ByUnit,
};
const VERSION: Version = Version::ZERO;
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct StoredValueFromHeightLast<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
pub base: ByUnit<M>,
}
impl StoredValueFromHeightLast {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self {
base: ByUnit::forced_import(db, name, v, indexes)?,
})
}
pub(crate) fn compute_rolling_sum(
&mut self,
max_from: Height,
window_starts: &impl ReadableVec<Height, Height>,
sats_source: &impl ReadableVec<Height, Sats>,
usd_source: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.base
.sats
.height
.compute_rolling_sum(max_from, window_starts, sats_source, exit)?;
self.base
.usd
.height
.compute_rolling_sum(max_from, window_starts, usd_source, exit)?;
Ok(())
}
}
@@ -0,0 +1,68 @@
//! 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, Dollars, Height, Sats, SatsSigned, Version};
use vecdb::{Database, Exit, ReadableCloneableVec, ReadableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{ComputedFromHeightLast, LazyFromHeightLast, SatsSignedToBitcoin},
};
const VERSION: Version = Version::ZERO;
/// Change values indexed by height - sats (stored), btc (lazy), usd (stored).
#[derive(Traversable)]
pub struct ValueChangeFromHeight<M: StorageMode = Rw> {
pub sats: ComputedFromHeightLast<SatsSigned, M>,
pub btc: LazyFromHeightLast<Bitcoin, SatsSigned>,
pub usd: ComputedFromHeightLast<Dollars, M>,
}
impl ValueChangeFromHeight {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
let sats = ComputedFromHeightLast::forced_import(db, name, v, indexes)?;
let btc = LazyFromHeightLast::from_computed::<SatsSignedToBitcoin>(
&format!("{name}_btc"),
v,
sats.height.read_only_boxed_clone(),
&sats,
);
let usd = ComputedFromHeightLast::forced_import(
db,
&format!("{name}_usd"),
v,
indexes,
)?;
Ok(Self { sats, btc, usd })
}
/// Compute rolling change for both sats and dollars 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>,
dollars_source: &(impl ReadableVec<Height, Dollars> + Sync),
exit: &Exit,
) -> Result<()> {
self.sats
.height
.compute_rolling_change(starting_height, window_starts, sats_source, exit)?;
self.usd
.height
.compute_rolling_change(starting_height, window_starts, dollars_source, exit)?;
Ok(())
}
}
@@ -0,0 +1,53 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{
indexes,
internal::ByUnit,
};
const VERSION: Version = Version::ZERO;
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct ValueEmaFromHeight<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
pub base: ByUnit<M>,
}
impl ValueEmaFromHeight {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self {
base: ByUnit::forced_import(db, name, v, indexes)?,
})
}
pub(crate) fn compute_rolling_average(
&mut self,
starting_height: Height,
window_starts: &impl ReadableVec<Height, Height>,
sats_source: &impl ReadableVec<Height, Sats>,
dollars_source: &(impl ReadableVec<Height, Dollars> + Sync),
exit: &Exit,
) -> Result<()> {
self.base
.sats
.height
.compute_rolling_average(starting_height, window_starts, sats_source, exit)?;
self.base
.usd
.height
.compute_rolling_average(starting_height, window_starts, dollars_source, exit)?;
Ok(())
}
}
@@ -0,0 +1,78 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats, Version};
use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode};
use crate::{
indexes,
internal::{ByUnit, RollingFullByUnit, SatsToDollars, WindowStarts},
prices,
};
#[derive(Traversable)]
pub struct ValueFromHeightFull<M: StorageMode = Rw> {
pub base: ByUnit<M>,
pub cumulative: ByUnit<M>,
#[traversable(flatten)]
pub rolling: RollingFullByUnit<M>,
}
const VERSION: Version = Version::TWO;
impl ValueFromHeightFull {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self {
base: ByUnit::forced_import(db, name, v, indexes)?,
cumulative: ByUnit::forced_import(db, &format!("{name}_cumulative"), v, indexes)?,
rolling: RollingFullByUnit::forced_import(db, name, v, indexes)?,
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
prices: &prices::Vecs,
exit: &Exit,
compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>,
) -> Result<()> {
compute_sats(&mut self.base.sats.height)?;
self.cumulative
.sats
.height
.compute_cumulative(max_from, &self.base.sats.height, exit)?;
self.base
.usd
.height
.compute_binary::<Sats, Dollars, SatsToDollars>(
max_from,
&self.base.sats.height,
&prices.price.usd.height,
exit,
)?;
self.cumulative
.usd
.height
.compute_cumulative(max_from, &self.base.usd.height, exit)?;
self.rolling.compute(
max_from,
windows,
&self.base.sats.height,
&self.base.usd.height,
exit,
)?;
Ok(())
}
}
@@ -0,0 +1,49 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, Rw, StorageMode};
use crate::{
indexes, prices,
internal::{ByUnit, SatsToDollars},
};
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct ValueFromHeightLast<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
pub base: ByUnit<M>,
}
const VERSION: Version = Version::TWO;
impl ValueFromHeightLast {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self {
base: ByUnit::forced_import(db, name, v, indexes)?,
})
}
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
max_from: Height,
exit: &Exit,
) -> Result<()> {
self.base.usd.compute_binary::<Sats, Dollars, SatsToDollars>(
max_from,
&self.base.sats.height,
&prices.price.usd.height,
exit,
)?;
Ok(())
}
}
@@ -0,0 +1,64 @@
//! Value type for Height + Rolling pattern.
//!
//! Combines ValueFromHeight (sats/btc/usd per height, no period views) with
//! StoredValueRollingWindows (rolling sums across 4 windows).
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode};
use crate::{
indexes,
internal::{StoredValueRollingWindows, ValueFromHeight, WindowStarts},
prices,
};
#[derive(Deref, DerefMut, Traversable)]
pub struct ValueFromHeightLastRolling<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub value: ValueFromHeight<M>,
#[traversable(flatten)]
pub rolling: StoredValueRollingWindows<M>,
}
const VERSION: Version = Version::ZERO;
impl ValueFromHeightLastRolling {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self {
value: ValueFromHeight::forced_import(db, name, v)?,
rolling: StoredValueRollingWindows::forced_import(db, name, v, indexes)?,
})
}
/// Compute sats height via closure, then USD from price, then rolling windows.
pub(crate) fn compute(
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
prices: &prices::Vecs,
exit: &Exit,
compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>,
) -> Result<()> {
compute_sats(&mut self.value.sats)?;
self.value.compute_usd(prices, max_from, exit)?;
self.rolling.compute_rolling_sum(
max_from,
windows,
&self.value.sats,
&self.value.usd,
exit,
)?;
Ok(())
}
}
@@ -0,0 +1,77 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats, Version};
use vecdb::{Database, EagerVec, Exit, PcoVec, Rw, StorageMode};
use crate::{
indexes,
internal::{ByUnit, RollingSumByUnit, SatsToDollars, WindowStarts},
prices,
};
#[derive(Traversable)]
pub struct ValueFromHeightSumCumulative<M: StorageMode = Rw> {
pub base: ByUnit<M>,
pub cumulative: ByUnit<M>,
pub sum: RollingSumByUnit<M>,
}
const VERSION: Version = Version::TWO;
impl ValueFromHeightSumCumulative {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let v = version + VERSION;
Ok(Self {
base: ByUnit::forced_import(db, name, v, indexes)?,
cumulative: ByUnit::forced_import(db, &format!("{name}_cumulative"), v, indexes)?,
sum: RollingSumByUnit::forced_import(db, name, v, indexes)?,
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
windows: &WindowStarts<'_>,
prices: &prices::Vecs,
exit: &Exit,
compute_sats: impl FnOnce(&mut EagerVec<PcoVec<Height, Sats>>) -> Result<()>,
) -> Result<()> {
compute_sats(&mut self.base.sats.height)?;
self.cumulative
.sats
.height
.compute_cumulative(max_from, &self.base.sats.height, exit)?;
self.base
.usd
.height
.compute_binary::<Sats, Dollars, SatsToDollars>(
max_from,
&self.base.sats.height,
&prices.price.usd.height,
exit,
)?;
self.cumulative
.usd
.height
.compute_cumulative(max_from, &self.base.usd.height, exit)?;
self.sum.compute_rolling_sum(
max_from,
windows,
&self.base.sats.height,
&self.base.usd.height,
exit,
)?;
Ok(())
}
}