computer: snapshot

This commit is contained in:
nym21
2026-02-26 23:01:51 +01:00
parent cccaf6b206
commit 78fc5ffcf7
69 changed files with 1578 additions and 2205 deletions

View File

@@ -0,0 +1,72 @@
//! 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)]
#[traversable(merge)]
pub struct ComputedFromHeightFull<T, M: StorageMode = Rw>
where
T: NumericValue + JsonSchema,
{
#[traversable(flatten)]
pub height: Full<Height, T, M>,
#[traversable(flatten)]
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 { 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.height)?;
self.rolling.compute(
max_from,
windows,
self.height.sum_cumulative.sum.inner(),
exit,
)?;
Ok(())
}
}

View File

@@ -70,11 +70,10 @@ where
S2T: VecValue,
F: BinaryTransform<S1T, S2T, T>,
{
self.height.compute_transform2(
self.height.compute_binary::<S1T, S2T, F>(
max_from,
source1,
source2,
|(h, s1, s2, ..)| (h, F::apply(s1, s2)),
exit,
)?;
Ok(())

View File

@@ -4,13 +4,11 @@ use brk_traversable::Traversable;
use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{ReadableBoxedVec, ReadableCloneableVec, LazyVecFrom1, UnaryTransform};
use vecdb::{LazyVecFrom1, ReadableBoxedVec, ReadableCloneableVec, UnaryTransform};
use crate::{
indexes,
internal::{
ComputedFromHeightLast, ComputedVecValue, LazyHeightDerivedLast, NumericValue,
},
internal::{ComputedFromHeightLast, ComputedVecValue, LazyHeightDerivedLast, NumericValue},
};
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
@@ -61,7 +59,12 @@ where
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)),
rest: Box::new(LazyHeightDerivedLast::from_height_source::<F>(
name,
v,
height_source,
indexes,
)),
}
}
@@ -78,8 +81,11 @@ where
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)),
rest: Box::new(LazyHeightDerivedLast::from_lazy::<F, S2T>(
name,
v,
&source.rest,
)),
}
}
}

View File

@@ -3,6 +3,7 @@ mod cumulative;
mod cumulative_rolling_full;
mod cumulative_rolling_sum;
mod distribution;
mod full;
mod last;
mod lazy_computed_full;
mod lazy_last;
@@ -15,6 +16,7 @@ mod value_change;
mod value_ema;
mod value_full;
mod value_last;
mod value_last_rolling;
mod value_lazy_computed_cumulative;
mod value_lazy_last;
mod value_sum_cumulative;
@@ -24,6 +26,7 @@ pub use cumulative::*;
pub use cumulative_rolling_full::*;
pub use cumulative_rolling_sum::*;
pub use distribution::*;
pub use full::*;
pub use last::*;
pub use lazy_computed_full::*;
pub use lazy_last::*;
@@ -36,6 +39,7 @@ pub use value_change::*;
pub use value_ema::*;
pub use value_full::*;
pub use value_last::*;
pub use value_last_rolling::*;
pub use value_lazy_computed_cumulative::*;
pub use value_lazy_last::*;
pub use value_sum_cumulative::*;

View File

@@ -68,7 +68,7 @@ pub(crate) fn compute_spot_percentile_rank(
}
pub struct PercentilesVecs<M: StorageMode = Rw> {
pub vecs: [Option<Price<ComputedFromHeightLast<Dollars, M>>>; PERCENTILES_LEN],
pub vecs: [Price<ComputedFromHeightLast<Dollars, M>>; PERCENTILES_LEN],
}
const VERSION: Version = Version::ONE;
@@ -79,15 +79,17 @@ impl PercentilesVecs {
prefix: &str,
version: Version,
indexes: &indexes::Vecs,
compute: bool,
) -> Result<Self> {
let vecs = PERCENTILES.map(|p| {
compute.then(|| {
let vecs = PERCENTILES
.into_iter()
.map(|p| {
let metric_name = format!("{prefix}_pct{p:02}");
Price::forced_import(db, &metric_name, version + VERSION, indexes)
.unwrap()
})
});
.collect::<Result<Vec<_>>>()?
.try_into()
.ok()
.expect("PERCENTILES length mismatch");
Ok(Self { vecs })
}
@@ -98,17 +100,15 @@ impl PercentilesVecs {
height: Height,
percentile_prices: &[Dollars; PERCENTILES_LEN],
) -> Result<()> {
for (i, vec) in self.vecs.iter_mut().enumerate() {
if let Some(v) = vec {
v.usd.height.truncate_push(height, percentile_prices[i])?;
}
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().flatten() {
for vec in self.vecs.iter_mut() {
vec.usd.height.validate_computed_version_or_reset(version)?;
}
Ok(())
@@ -123,7 +123,7 @@ impl ReadOnlyClone for PercentilesVecs {
vecs: self
.vecs
.each_ref()
.map(|v| v.as_ref().map(|p| p.read_only_clone())),
.map(|v| v.read_only_clone()),
}
}
}
@@ -137,7 +137,7 @@ where
PERCENTILES
.iter()
.zip(self.vecs.iter())
.filter_map(|(p, v)| v.as_ref().map(|v| (format!("pct{p:02}"), v.to_tree_node())))
.map(|(p, v)| (format!("pct{p:02}"), v.to_tree_node()))
.collect(),
)
}
@@ -145,7 +145,6 @@ where
fn iter_any_exportable(&self) -> impl Iterator<Item = &dyn AnyExportableVec> {
self.vecs
.iter()
.flatten()
.flat_map(|p| p.iter_any_exportable())
}
}

View File

@@ -1,454 +0,0 @@
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::{ComputedFromHeightStdDev, Price, StandardDeviationVecsOptions},
prices,
utils::get_percentile,
};
use super::ComputedFromHeightLast;
#[derive(Traversable)]
pub struct ComputedFromHeightRatio<M: StorageMode = Rw> {
pub price: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub ratio: ComputedFromHeightLast<StoredF32, M>,
pub ratio_1w_sma: Option<ComputedFromHeightLast<StoredF32, M>>,
pub ratio_1m_sma: Option<ComputedFromHeightLast<StoredF32, M>>,
pub ratio_pct99: Option<ComputedFromHeightLast<StoredF32, M>>,
pub ratio_pct98: Option<ComputedFromHeightLast<StoredF32, M>>,
pub ratio_pct95: Option<ComputedFromHeightLast<StoredF32, M>>,
pub ratio_pct5: Option<ComputedFromHeightLast<StoredF32, M>>,
pub ratio_pct2: Option<ComputedFromHeightLast<StoredF32, M>>,
pub ratio_pct1: Option<ComputedFromHeightLast<StoredF32, M>>,
pub ratio_pct99_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub ratio_pct98_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub ratio_pct95_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub ratio_pct5_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub ratio_pct2_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub ratio_pct1_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub ratio_sd: Option<ComputedFromHeightStdDev<M>>,
pub ratio_4y_sd: Option<ComputedFromHeightStdDev<M>>,
pub ratio_2y_sd: Option<ComputedFromHeightStdDev<M>>,
pub ratio_1y_sd: Option<ComputedFromHeightStdDev<M>>,
}
const VERSION: Version = Version::TWO;
impl ComputedFromHeightRatio {
#[allow(clippy::too_many_arguments)]
pub(crate) fn forced_import(
db: &Database,
name: &str,
metric_price: Option<&ComputedFromHeightLast<Dollars>>,
version: Version,
indexes: &indexes::Vecs,
extended: bool,
) -> Result<Self> {
let v = version + VERSION;
macro_rules! import {
($suffix:expr) => {
ComputedFromHeightLast::forced_import(
db,
&format!("{name}_{}", $suffix),
v,
indexes,
)
.unwrap()
};
}
// Only compute price internally when metric_price is None
let price = metric_price
.is_none()
.then(|| Price::forced_import(db, name, v, indexes).unwrap());
macro_rules! import_sd {
($suffix:expr, $days:expr) => {
ComputedFromHeightStdDev::forced_import(
db,
&format!("{name}_{}", $suffix),
$days,
v,
indexes,
StandardDeviationVecsOptions::default().add_all(),
)
.unwrap()
};
}
let ratio_pct99 = extended.then(|| import!("ratio_pct99"));
let ratio_pct98 = extended.then(|| import!("ratio_pct98"));
let ratio_pct95 = extended.then(|| import!("ratio_pct95"));
let ratio_pct5 = extended.then(|| import!("ratio_pct5"));
let ratio_pct2 = extended.then(|| import!("ratio_pct2"));
let ratio_pct1 = extended.then(|| import!("ratio_pct1"));
macro_rules! lazy_usd {
($ratio:expr, $suffix:expr) => {
if !extended {
None
} else {
$ratio.as_ref().map(|_| {
Price::forced_import(
db,
&format!("{name}_{}", $suffix),
v,
indexes,
)
.unwrap()
})
}
};
}
Ok(Self {
ratio: import!("ratio"),
ratio_1w_sma: extended.then(|| import!("ratio_1w_sma")),
ratio_1m_sma: extended.then(|| import!("ratio_1m_sma")),
ratio_sd: extended.then(|| import_sd!("ratio", usize::MAX)),
ratio_1y_sd: extended.then(|| import_sd!("ratio_1y", 365)),
ratio_2y_sd: extended.then(|| import_sd!("ratio_2y", 2 * 365)),
ratio_4y_sd: extended.then(|| import_sd!("ratio_4y", 4 * 365)),
ratio_pct99_usd: lazy_usd!(&ratio_pct99, "ratio_pct99_usd"),
ratio_pct98_usd: lazy_usd!(&ratio_pct98, "ratio_pct98_usd"),
ratio_pct95_usd: lazy_usd!(&ratio_pct95, "ratio_pct95_usd"),
ratio_pct5_usd: lazy_usd!(&ratio_pct5, "ratio_pct5_usd"),
ratio_pct2_usd: lazy_usd!(&ratio_pct2, "ratio_pct2_usd"),
ratio_pct1_usd: lazy_usd!(&ratio_pct1, "ratio_pct1_usd"),
price,
ratio_pct99,
ratio_pct98,
ratio_pct95,
ratio_pct5,
ratio_pct2,
ratio_pct1,
})
}
pub(crate) fn forced_import_from_lazy(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
extended: bool,
) -> Result<Self> {
let v = version + VERSION;
macro_rules! import {
($suffix:expr) => {
ComputedFromHeightLast::forced_import(
db,
&format!("{name}_{}", $suffix),
v,
indexes,
)
.unwrap()
};
}
macro_rules! import_sd {
($suffix:expr, $days:expr) => {
ComputedFromHeightStdDev::forced_import_from_lazy(
db,
&format!("{name}_{}", $suffix),
$days,
v,
indexes,
StandardDeviationVecsOptions::default().add_all(),
)
.unwrap()
};
}
let ratio_pct99 = extended.then(|| import!("ratio_pct99"));
let ratio_pct98 = extended.then(|| import!("ratio_pct98"));
let ratio_pct95 = extended.then(|| import!("ratio_pct95"));
let ratio_pct5 = extended.then(|| import!("ratio_pct5"));
let ratio_pct2 = extended.then(|| import!("ratio_pct2"));
let ratio_pct1 = extended.then(|| import!("ratio_pct1"));
macro_rules! lazy_usd {
($ratio:expr, $suffix:expr) => {
$ratio.as_ref().map(|_| {
Price::forced_import(db, &format!("{name}_{}", $suffix), v, indexes)
.unwrap()
})
};
}
Ok(Self {
ratio: import!("ratio"),
ratio_1w_sma: extended.then(|| import!("ratio_1w_sma")),
ratio_1m_sma: extended.then(|| import!("ratio_1m_sma")),
ratio_sd: extended.then(|| import_sd!("ratio", usize::MAX)),
ratio_1y_sd: extended.then(|| import_sd!("ratio_1y", 365)),
ratio_2y_sd: extended.then(|| import_sd!("ratio_2y", 2 * 365)),
ratio_4y_sd: extended.then(|| import_sd!("ratio_4y", 4 * 365)),
ratio_pct99_usd: lazy_usd!(&ratio_pct99, "ratio_pct99_usd"),
ratio_pct98_usd: lazy_usd!(&ratio_pct98, "ratio_pct98_usd"),
ratio_pct95_usd: lazy_usd!(&ratio_pct95, "ratio_pct95_usd"),
ratio_pct5_usd: lazy_usd!(&ratio_pct5, "ratio_pct5_usd"),
ratio_pct2_usd: lazy_usd!(&ratio_pct2, "ratio_pct2_usd"),
ratio_pct1_usd: lazy_usd!(&ratio_pct1, "ratio_pct1_usd"),
price: None,
ratio_pct99,
ratio_pct98,
ratio_pct95,
ratio_pct5,
ratio_pct2,
ratio_pct1,
})
}
/// Compute all: computes price at height level, then ratio + rest.
pub(crate) fn compute_all<F>(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
mut compute: F,
) -> Result<()>
where
F: FnMut(&mut EagerVec<PcoVec<Height, Dollars>>) -> Result<()>,
{
compute(&mut self.price.as_mut().unwrap().usd.height)?;
let price_opt: Option<&EagerVec<PcoVec<Height, Dollars>>> = None;
self.compute_rest(blocks, prices, starting_indexes, exit, price_opt)
}
/// Compute ratio and derived metrics from an externally-provided or internal price.
pub(crate) fn compute_rest(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
price_opt: Option<&impl ReadableVec<Height, Dollars>>,
) -> Result<()> {
let close_price = &prices.usd.price;
let price = price_opt.unwrap_or_else(|| unsafe {
std::mem::transmute(&self.price.as_ref().unwrap().usd.height)
});
// Compute ratio = close_price / metric_price at height level
self.ratio.height.compute_transform2(
starting_indexes.height,
close_price,
price,
|(i, close, price, ..)| {
if price == Dollars::ZERO {
(i, StoredF32::from(1.0))
} else {
(i, StoredF32::from(close / price))
}
},
exit,
)?;
if self.ratio_1w_sma.is_none() {
return Ok(());
}
// SMA using lookback vecs
self.ratio_1w_sma
.as_mut()
.unwrap()
.height
.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1w_ago,
&self.ratio.height,
exit,
)?;
self.ratio_1m_sma
.as_mut()
.unwrap()
.height
.compute_rolling_average(
starting_indexes.height,
&blocks.count.height_1m_ago,
&self.ratio.height,
exit,
)?;
// Percentiles: insert into sorted array on day boundaries
let ratio_version = self.ratio.height.version();
self.mut_ratio_vecs()
.iter_mut()
.try_for_each(|v| -> Result<()> {
v.validate_computed_version_or_reset(ratio_version)?;
Ok(())
})?;
let starting_height = self
.mut_ratio_vecs()
.iter()
.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 = self.ratio.height.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 {
let pos = sorted.binary_search(&ratio).unwrap_or_else(|p| p);
sorted.insert(pos, ratio);
last_day_start = cur_day_start;
}
}
sorted
};
let pct1_vec = &mut self.ratio_pct1.as_mut().unwrap().height;
let pct2_vec = &mut self.ratio_pct2.as_mut().unwrap().height;
let pct5_vec = &mut self.ratio_pct5.as_mut().unwrap().height;
let pct95_vec = &mut self.ratio_pct95.as_mut().unwrap().height;
let pct98_vec = &mut self.ratio_pct98.as_mut().unwrap().height;
let pct99_vec = &mut self.ratio_pct99.as_mut().unwrap().height;
let ratio_len = self.ratio.height.len();
let ratio_data = self.ratio.height.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;
// Insert into sorted history on day boundaries
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()
.into_iter()
.try_for_each(|v| v.flush())?;
}
// Compute stddev at height level
macro_rules! compute_sd {
($($field:ident),*) => {
$(self.$field.as_mut().unwrap().compute_all(
blocks, starting_indexes, exit, &self.ratio.height,
)?;)*
};
}
compute_sd!(ratio_sd, ratio_4y_sd, ratio_2y_sd, ratio_1y_sd);
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_field:ident) => {
if let Some(usd) = self.$usd_field.as_mut() {
if let Some(band) = self.$band_field.as_ref() {
usd.usd
.compute_binary::<Dollars, StoredF32, PriceTimesRatio>(
starting_indexes.height,
metric_price,
&band.height,
exit,
)?;
}
}
};
}
compute_band!(ratio_pct99_usd, ratio_pct99);
compute_band!(ratio_pct98_usd, ratio_pct98);
compute_band!(ratio_pct95_usd, ratio_pct95);
compute_band!(ratio_pct5_usd, ratio_pct5);
compute_band!(ratio_pct2_usd, ratio_pct2);
compute_band!(ratio_pct1_usd, ratio_pct1);
// Stddev USD bands
macro_rules! compute_sd_usd {
($($field:ident),*) => {
$(if let Some(sd) = self.$field.as_mut() {
sd.compute_usd_bands(starting_indexes, metric_price, exit)?;
})*
};
}
compute_sd_usd!(ratio_sd, ratio_4y_sd, ratio_2y_sd, ratio_1y_sd);
Ok(())
}
fn mut_ratio_vecs(&mut self) -> Vec<&mut EagerVec<PcoVec<Height, StoredF32>>> {
macro_rules! collect_vecs {
($($field:ident),*) => {{
let mut vecs = Vec::with_capacity(6);
$(if let Some(v) = self.$field.as_mut() { vecs.push(&mut v.height); })*
vecs
}};
}
collect_vecs!(
ratio_pct1,
ratio_pct2,
ratio_pct5,
ratio_pct95,
ratio_pct98,
ratio_pct99
)
}
}

View File

@@ -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, Exit, ReadableVec, Rw, StorageMode};
use crate::{ComputeIndexes, blocks, indexes, prices};
use super::{ComputedFromHeightRatio, ComputedFromHeightRatioExtension};
#[derive(Deref, DerefMut, Traversable)]
#[traversable(merge)]
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.usd.price;
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(())
}
}

View File

@@ -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},
utils::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()
}
}

View File

@@ -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(())
}
}

View File

@@ -0,0 +1,58 @@
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)]
#[traversable(merge)]
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(())
}
}

View File

@@ -1,527 +0,0 @@
use std::mem;
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};
#[derive(Default)]
pub struct StandardDeviationVecsOptions {
zscore: bool,
bands: bool,
price_bands: bool,
}
impl StandardDeviationVecsOptions {
pub(crate) fn add_all(mut self) -> Self {
self.zscore = true;
self.bands = true;
self.price_bands = true;
self
}
pub(crate) fn zscore(&self) -> bool {
self.zscore
}
pub(crate) fn bands(&self) -> bool {
self.bands
}
pub(crate) fn price_bands(&self) -> bool {
self.price_bands
}
}
#[derive(Traversable)]
pub struct ComputedFromHeightStdDev<M: StorageMode = Rw> {
days: usize,
pub sma: Option<ComputedFromHeightLast<StoredF32, M>>,
pub sd: ComputedFromHeightLast<StoredF32, M>,
pub zscore: Option<ComputedFromHeightLast<StoredF32, M>>,
pub p0_5sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub p1sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub p1_5sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub p2sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub p2_5sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub p3sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub m0_5sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub m1sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub m1_5sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub m2sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub m2_5sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub m3sd: Option<ComputedFromHeightLast<StoredF32, M>>,
pub _0sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub p0_5sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub p1sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub p1_5sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub p2sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub p2_5sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub p3sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub m0_5sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub m1sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub m1_5sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub m2sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub m2_5sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
pub m3sd_usd: Option<Price<ComputedFromHeightLast<Dollars, M>>>,
}
impl ComputedFromHeightStdDev {
#[allow(clippy::too_many_arguments)]
pub(crate) fn forced_import(
db: &Database,
name: &str,
days: usize,
parent_version: Version,
indexes: &indexes::Vecs,
options: StandardDeviationVecsOptions,
) -> Result<Self> {
let version = parent_version + Version::TWO;
macro_rules! import {
($suffix:expr) => {
ComputedFromHeightLast::forced_import(
db,
&format!("{name}_{}", $suffix),
version,
indexes,
)
.unwrap()
};
}
let sma_vec = Some(import!("sma"));
let p0_5sd = options.bands().then(|| import!("p0_5sd"));
let p1sd = options.bands().then(|| import!("p1sd"));
let p1_5sd = options.bands().then(|| import!("p1_5sd"));
let p2sd = options.bands().then(|| import!("p2sd"));
let p2_5sd = options.bands().then(|| import!("p2_5sd"));
let p3sd = options.bands().then(|| import!("p3sd"));
let m0_5sd = options.bands().then(|| import!("m0_5sd"));
let m1sd = options.bands().then(|| import!("m1sd"));
let m1_5sd = options.bands().then(|| import!("m1_5sd"));
let m2sd = options.bands().then(|| import!("m2sd"));
let m2_5sd = options.bands().then(|| import!("m2_5sd"));
let m3sd = options.bands().then(|| import!("m3sd"));
// Import USD price band vecs (computed eagerly at compute time)
macro_rules! lazy_usd {
($band:expr, $suffix:expr) => {
if !options.price_bands() {
None
} else {
$band.as_ref().map(|_| {
Price::forced_import(
db,
&format!("{name}_{}", $suffix),
version,
indexes,
)
.unwrap()
})
}
};
}
Ok(Self {
days,
sd: import!("sd"),
zscore: options.zscore().then(|| import!("zscore")),
// Lazy USD vecs
_0sd_usd: lazy_usd!(&sma_vec, "0sd_usd"),
p0_5sd_usd: lazy_usd!(&p0_5sd, "p0_5sd_usd"),
p1sd_usd: lazy_usd!(&p1sd, "p1sd_usd"),
p1_5sd_usd: lazy_usd!(&p1_5sd, "p1_5sd_usd"),
p2sd_usd: lazy_usd!(&p2sd, "p2sd_usd"),
p2_5sd_usd: lazy_usd!(&p2_5sd, "p2_5sd_usd"),
p3sd_usd: lazy_usd!(&p3sd, "p3sd_usd"),
m0_5sd_usd: lazy_usd!(&m0_5sd, "m0_5sd_usd"),
m1sd_usd: lazy_usd!(&m1sd, "m1sd_usd"),
m1_5sd_usd: lazy_usd!(&m1_5sd, "m1_5sd_usd"),
m2sd_usd: lazy_usd!(&m2sd, "m2sd_usd"),
m2_5sd_usd: lazy_usd!(&m2_5sd, "m2_5sd_usd"),
m3sd_usd: lazy_usd!(&m3sd, "m3sd_usd"),
// Stored band sources
sma: sma_vec,
p0_5sd,
p1sd,
p1_5sd,
p2sd,
p2_5sd,
p3sd,
m0_5sd,
m1sd,
m1_5sd,
m2sd,
m2_5sd,
m3sd,
})
}
pub(crate) fn forced_import_from_lazy(
db: &Database,
name: &str,
days: usize,
parent_version: Version,
indexes: &indexes::Vecs,
options: StandardDeviationVecsOptions,
) -> Result<Self> {
let version = parent_version + Version::TWO;
macro_rules! import {
($suffix:expr) => {
ComputedFromHeightLast::forced_import(
db,
&format!("{name}_{}", $suffix),
version,
indexes,
)
.unwrap()
};
}
let sma_vec = Some(import!("sma"));
let p0_5sd = options.bands().then(|| import!("p0_5sd"));
let p1sd = options.bands().then(|| import!("p1sd"));
let p1_5sd = options.bands().then(|| import!("p1_5sd"));
let p2sd = options.bands().then(|| import!("p2sd"));
let p2_5sd = options.bands().then(|| import!("p2_5sd"));
let p3sd = options.bands().then(|| import!("p3sd"));
let m0_5sd = options.bands().then(|| import!("m0_5sd"));
let m1sd = options.bands().then(|| import!("m1sd"));
let m1_5sd = options.bands().then(|| import!("m1_5sd"));
let m2sd = options.bands().then(|| import!("m2sd"));
let m2_5sd = options.bands().then(|| import!("m2_5sd"));
let m3sd = options.bands().then(|| import!("m3sd"));
// For lazy metric price, use from_lazy_block_last_and_block_last.
macro_rules! lazy_usd {
($band:expr, $suffix:expr) => {
if !options.price_bands() {
None
} else {
$band.as_ref().map(|_| {
Price::forced_import(
db,
&format!("{name}_{}", $suffix),
version,
indexes,
)
.unwrap()
})
}
};
}
Ok(Self {
days,
sd: import!("sd"),
zscore: options.zscore().then(|| import!("zscore")),
_0sd_usd: lazy_usd!(&sma_vec, "0sd_usd"),
p0_5sd_usd: lazy_usd!(&p0_5sd, "p0_5sd_usd"),
p1sd_usd: lazy_usd!(&p1sd, "p1sd_usd"),
p1_5sd_usd: lazy_usd!(&p1_5sd, "p1_5sd_usd"),
p2sd_usd: lazy_usd!(&p2sd, "p2sd_usd"),
p2_5sd_usd: lazy_usd!(&p2_5sd, "p2_5sd_usd"),
p3sd_usd: lazy_usd!(&p3sd, "p3sd_usd"),
m0_5sd_usd: lazy_usd!(&m0_5sd, "m0_5sd_usd"),
m1sd_usd: lazy_usd!(&m1sd, "m1sd_usd"),
m1_5sd_usd: lazy_usd!(&m1_5sd, "m1_5sd_usd"),
m2sd_usd: lazy_usd!(&m2sd, "m2sd_usd"),
m2_5sd_usd: lazy_usd!(&m2_5sd, "m2_5sd_usd"),
m3sd_usd: lazy_usd!(&m3sd, "m3sd_usd"),
sma: sma_vec,
p0_5sd,
p1sd,
p1_5sd,
p2sd,
p2_5sd,
p3sd,
m0_5sd,
m1sd,
m1_5sd,
m2sd,
m2_5sd,
m3sd,
})
}
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.as_mut().unwrap().height.compute_rolling_average(
starting_indexes.height,
window_starts,
source,
exit,
)?;
} else {
// Full history SMA (days == usize::MAX)
self.sma.as_mut().unwrap().height.compute_sma_(
starting_indexes.height,
source,
self.days,
exit,
None,
)?;
}
let sma_opt: Option<&EagerVec<PcoVec<Height, StoredF32>>> = None;
self.compute_rest(blocks, starting_indexes, exit, sma_opt, source)
}
pub(crate) fn compute_rest(
&mut self,
blocks: &blocks::Vecs,
starting_indexes: &ComputeIndexes,
exit: &Exit,
sma_opt: Option<&impl ReadableVec<Height, StoredF32>>,
source: &impl ReadableVec<Height, StoredF32>,
) -> Result<()> {
let sma = sma_opt
.unwrap_or_else(|| unsafe { mem::transmute(&self.sma.as_ref().unwrap().height) });
let source_version = source.version();
self.mut_stateful_height_vecs()
.try_for_each(|v| -> Result<()> {
v.validate_computed_version_or_reset(source_version)?;
Ok(())
})?;
let starting_height = self
.mut_stateful_height_vecs()
.map(|v| Height::from(v.len()))
.min()
.unwrap()
.min(starting_indexes.height);
// Reconstruct running statistics up to starting point.
// We accumulate one data point per day boundary, tracking sum and sum_sq
// for O(1) per-height SD computation (instead of O(n) sorted-array scan).
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;
}
}
}
macro_rules! band_ref {
($field:ident) => {
self.$field.as_mut().map(|c| &mut c.height)
};
}
let mut p0_5sd = band_ref!(p0_5sd);
let mut p1sd = band_ref!(p1sd);
let mut p1_5sd = band_ref!(p1_5sd);
let mut p2sd = band_ref!(p2sd);
let mut p2_5sd = band_ref!(p2_5sd);
let mut p3sd = band_ref!(p3sd);
let mut m0_5sd = band_ref!(m0_5sd);
let mut m1sd = band_ref!(m1sd);
let mut m1_5sd = band_ref!(m1_5sd);
let mut m2sd = band_ref!(m2sd);
let mut m2_5sd = band_ref!(m2_5sd);
let mut m3sd = band_ref!(m3sd);
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;
// Update running statistics on day boundaries
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;
// SD = sqrt((sum_sq/n - 2*avg*sum/n + avg^2))
// This is the population SD of all daily values relative to the current SMA
let sd = 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)
};
self.sd.height.truncate_push_at(index, sd)?;
if let Some(v) = p0_5sd.as_mut() {
v.truncate_push_at(index, average + StoredF32::from(0.5 * *sd))?
}
if let Some(v) = p1sd.as_mut() {
v.truncate_push_at(index, average + sd)?
}
if let Some(v) = p1_5sd.as_mut() {
v.truncate_push_at(index, average + StoredF32::from(1.5 * *sd))?
}
if let Some(v) = p2sd.as_mut() {
v.truncate_push_at(index, average + 2 * sd)?
}
if let Some(v) = p2_5sd.as_mut() {
v.truncate_push_at(index, average + StoredF32::from(2.5 * *sd))?
}
if let Some(v) = p3sd.as_mut() {
v.truncate_push_at(index, average + 3 * sd)?
}
if let Some(v) = m0_5sd.as_mut() {
v.truncate_push_at(index, average - StoredF32::from(0.5 * *sd))?
}
if let Some(v) = m1sd.as_mut() {
v.truncate_push_at(index, average - sd)?
}
if let Some(v) = m1_5sd.as_mut() {
v.truncate_push_at(index, average - StoredF32::from(1.5 * *sd))?
}
if let Some(v) = m2sd.as_mut() {
v.truncate_push_at(index, average - 2 * sd)?
}
if let Some(v) = m2_5sd.as_mut() {
v.truncate_push_at(index, average - StoredF32::from(2.5 * *sd))?
}
if let Some(v) = m3sd.as_mut() {
v.truncate_push_at(index, average - 3 * sd)?
}
}
{
let _lock = exit.lock();
self.mut_stateful_height_vecs()
.try_for_each(|v| v.flush())?;
}
if let Some(zscore) = self.zscore.as_mut() {
zscore.height.compute_zscore(
starting_indexes.height,
source,
sma,
&self.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_field:ident) => {
if let Some(usd) = self.$usd_field.as_mut() {
if let Some(band) = self.$band_field.as_ref() {
usd.usd
.compute_binary::<Dollars, StoredF32, PriceTimesRatio>(
starting_indexes.height,
metric_price,
&band.height,
exit,
)?;
}
}
};
}
compute_band!(_0sd_usd, sma);
compute_band!(p0_5sd_usd, p0_5sd);
compute_band!(p1sd_usd, p1sd);
compute_band!(p1_5sd_usd, p1_5sd);
compute_band!(p2sd_usd, p2sd);
compute_band!(p2_5sd_usd, p2_5sd);
compute_band!(p3sd_usd, p3sd);
compute_band!(m0_5sd_usd, m0_5sd);
compute_band!(m1sd_usd, m1sd);
compute_band!(m1_5sd_usd, m1_5sd);
compute_band!(m2sd_usd, m2sd);
compute_band!(m2_5sd_usd, m2_5sd);
compute_band!(m3sd_usd, m3sd);
Ok(())
}
fn mut_stateful_computed(
&mut self,
) -> impl Iterator<Item = &mut ComputedFromHeightLast<StoredF32>> {
[
Some(&mut self.sd),
self.p0_5sd.as_mut(),
self.p1sd.as_mut(),
self.p1_5sd.as_mut(),
self.p2sd.as_mut(),
self.p2_5sd.as_mut(),
self.p3sd.as_mut(),
self.m0_5sd.as_mut(),
self.m1sd.as_mut(),
self.m1_5sd.as_mut(),
self.m2sd.as_mut(),
self.m2_5sd.as_mut(),
self.m3sd.as_mut(),
]
.into_iter()
.flatten()
}
fn mut_stateful_height_vecs(
&mut self,
) -> impl Iterator<Item = &mut EagerVec<PcoVec<Height, StoredF32>>> {
self.mut_stateful_computed().map(|c| &mut c.height)
}
}

View File

@@ -0,0 +1,262 @@
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)]
#[traversable(merge)]
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()
}
}

View File

@@ -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(())
}

View File

@@ -10,7 +10,10 @@ use vecdb::{Database, EagerVec, Exit, PcoVec, ReadableCloneableVec, Rw, StorageM
use crate::{
indexes,
internal::{ComputedFromHeightCumulativeFull, LazyFromHeightLast, SatsToBitcoin, WindowStarts},
internal::{
ComputedFromHeightCumulativeFull, LazyFromHeightLast, SatsToBitcoin, SatsToDollars,
WindowStarts,
},
prices,
};
@@ -58,14 +61,10 @@ impl ValueFromHeightFull {
self.sats.compute(max_from, windows, exit, compute_sats)?;
self.usd.compute(max_from, windows, exit, |vec| {
Ok(vec.compute_transform2(
Ok(vec.compute_binary::<Sats, Dollars, SatsToDollars>(
max_from,
&self.sats.height,
&prices.usd.price,
|(h, sats, price, ..)| {
let btc = *sats as f64 / 100_000_000.0;
(h, Dollars::from(*price * btc))
},
exit,
)?)
})

View File

@@ -10,7 +10,7 @@ use vecdb::{Database, Exit, ReadableCloneableVec, Rw, StorageMode};
use crate::{
indexes, prices,
internal::{ComputedFromHeightLast, LazyFromHeightLast, SatsToBitcoin},
internal::{ComputedFromHeightLast, LazyFromHeightLast, SatsToBitcoin, SatsToDollars},
};
#[derive(Traversable)]
@@ -56,14 +56,10 @@ impl ValueFromHeightLast {
max_from: Height,
exit: &Exit,
) -> Result<()> {
self.usd.height.compute_transform2(
self.usd.compute_binary::<Sats, Dollars, SatsToDollars>(
max_from,
&self.sats.height,
&prices.usd.price,
|(h, sats, price, ..)| {
let btc = *sats as f64 / 100_000_000.0;
(h, Dollars::from(*price * btc))
},
exit,
)?;
Ok(())

View File

@@ -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)]
#[traversable(merge)]
pub struct ValueFromHeightLastRolling<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub value: ValueFromHeight<M>,
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(())
}
}

View File

@@ -11,7 +11,10 @@ use vecdb::{Database, Exit, ReadableCloneableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{ComputedFromHeightCumulative, ComputedFromHeightLast, LazyFromHeightLast, SatsToBitcoin},
internal::{
ComputedFromHeightCumulative, ComputedFromHeightLast, LazyFromHeightLast, SatsToBitcoin,
SatsToDollars,
},
prices,
};
@@ -57,14 +60,10 @@ impl LazyComputedValueFromHeightCumulative {
) -> Result<()> {
self.sats.compute_rest(max_from, exit)?;
self.usd.height.compute_transform2(
self.usd.compute_binary::<Sats, Dollars, SatsToDollars>(
max_from,
&prices.usd.price,
&self.sats.height,
|(h, price, sats, ..)| {
let btc = *sats as f64 / 100_000_000.0;
(h, Dollars::from(*price * btc))
},
&prices.usd.price,
exit,
)?;
Ok(())

View File

@@ -10,7 +10,10 @@ use vecdb::{Database, EagerVec, Exit, PcoVec, ReadableCloneableVec, Rw, StorageM
use crate::{
indexes, prices,
internal::{ComputedFromHeightCumulativeSum, LazyFromHeightLast, SatsToBitcoin, WindowStarts},
internal::{
ComputedFromHeightCumulativeSum, LazyFromHeightLast, SatsToBitcoin, SatsToDollars,
WindowStarts,
},
};
#[derive(Traversable)]
@@ -57,14 +60,10 @@ impl ValueFromHeightSumCumulative {
self.sats.compute(max_from, windows, exit, compute_sats)?;
self.usd.compute(max_from, windows, exit, |vec| {
Ok(vec.compute_transform2(
Ok(vec.compute_binary::<Sats, Dollars, SatsToDollars>(
max_from,
&self.sats.height,
&prices.usd.price,
|(h, sats, price, ..)| {
let btc = *sats as f64 / 100_000_000.0;
(h, Dollars::from(*price * btc))
},
exit,
)?)
})