global: snapshot

This commit is contained in:
nym21
2026-03-02 23:57:22 +01:00
parent ccb2db2309
commit 0628f08e6b
21 changed files with 245 additions and 351 deletions
@@ -160,35 +160,15 @@ impl ComputedFromHeightStdDevExtended {
.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)?;
const MULTIPLIERS: [f32; 12] = [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, -0.5, -1.0, -1.5, -2.0, -2.5, -3.0];
let band_vecs: Vec<_> = self.mut_band_height_vecs().collect();
for (vec, mult) in band_vecs.into_iter().zip(MULTIPLIERS) {
for (offset, _) in source_data.iter().enumerate() {
let index = start + offset;
let average = sma_data[offset];
let sd = sd_data[offset];
vec.truncate_push_at(index, average + StoredF32::from(mult * *sd))?;
}
}
{
@@ -56,26 +56,28 @@ impl SortedBlocks {
/// Remove one occurrence of value. O(sqrt(n)).
fn remove(&mut self, value: f64) -> bool {
for (bi, block) in self.blocks.iter_mut().enumerate() {
if block.is_empty() {
continue;
}
// If value > block max, it's not in this block
if *block.last().unwrap() < value {
continue;
}
let pos = block.partition_point(|a| *a < value);
if pos < block.len() && block[pos] == value {
block.remove(pos);
self.total_len -= 1;
if block.is_empty() {
self.blocks.remove(bi);
}
return true;
}
// Value not found (would be in this block range but isn't)
if self.blocks.is_empty() {
return false;
}
// Binary search for first block whose max >= value
let bi = self
.blocks
.partition_point(|b| b.last().is_some_and(|&last| last < value));
if bi >= self.blocks.len() {
return false;
}
let block = &mut self.blocks[bi];
let pos = block.partition_point(|a| *a < value);
if pos < block.len() && block[pos] == value {
block.remove(pos);
self.total_len -= 1;
if block.is_empty() {
self.blocks.remove(bi);
}
return true;
}
false
}
+91 -69
View File
@@ -66,20 +66,25 @@ impl TDigest {
return;
}
// Find nearest centroid by mean
let pos = self
// Single binary search: unclamped position doubles as insert point
let search = self
.centroids
.binary_search_by(|c| c.mean.partial_cmp(&value).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or_else(|i| i.min(self.centroids.len() - 1));
.binary_search_by(|c| c.mean.partial_cmp(&value).unwrap_or(std::cmp::Ordering::Equal));
let insert_pos = match search {
Ok(i) | Err(i) => i,
};
// Check neighbors for the actual nearest
let nearest = if pos > 0
&& (value - self.centroids[pos - 1].mean).abs()
< (value - self.centroids[pos].mean).abs()
// Find nearest centroid from insert_pos
let nearest = if insert_pos >= self.centroids.len() {
self.centroids.len() - 1
} else if insert_pos == 0 {
0
} else if (value - self.centroids[insert_pos - 1].mean).abs()
< (value - self.centroids[insert_pos].mean).abs()
{
pos - 1
insert_pos - 1
} else {
pos
insert_pos
};
// Compute quantile of nearest centroid
@@ -97,15 +102,7 @@ impl TDigest {
c.mean = (c.mean * c.weight + value) / (c.weight + 1.0);
c.weight += 1.0;
} else {
// Insert new centroid at sorted position
let insert_pos = self
.centroids
.binary_search_by(|c| {
c.mean
.partial_cmp(&value)
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or_else(|i| i);
// Insert new centroid at sorted position (reuse insert_pos)
self.centroids.insert(
insert_pos,
Centroid {
@@ -148,65 +145,84 @@ impl TDigest {
self.centroids = merged;
}
pub fn quantile(&self, q: f64) -> f64 {
/// Batch quantile query in a single pass. `qs` must be sorted ascending.
pub fn quantiles(&self, qs: &[f64], out: &mut [f64]) {
if self.centroids.is_empty() {
return 0.0;
}
if q <= 0.0 {
return self.min;
}
if q >= 1.0 {
return self.max;
out.iter_mut().for_each(|o| *o = 0.0);
return;
}
if self.centroids.len() == 1 {
return self.centroids[0].mean;
let mean = self.centroids[0].mean;
for (i, &q) in qs.iter().enumerate() {
out[i] = if q <= 0.0 {
self.min
} else if q >= 1.0 {
self.max
} else {
mean
};
}
return;
}
let total: f64 = self.centroids.iter().map(|c| c.weight).sum();
let target = q * total;
let mut cum = 0.0;
let mut ci = 0;
for i in 0..self.centroids.len() {
let c = &self.centroids[i];
let mid = cum + c.weight / 2.0;
for (qi, &q) in qs.iter().enumerate() {
if q <= 0.0 {
out[qi] = self.min;
continue;
}
if q >= 1.0 {
out[qi] = self.max;
continue;
}
if target < mid {
// Interpolate between previous centroid (or min) and this one
if i == 0 {
// Between min and first centroid center
let first_mid = c.weight / 2.0;
if first_mid == 0.0 {
return self.min;
}
return self.min + (c.mean - self.min) * (target / first_mid);
let target = q * total;
// Advance centroids until the current centroid's midpoint exceeds target
while ci < self.centroids.len() {
let mid = cum + self.centroids[ci].weight / 2.0;
if target < mid {
break;
}
let prev = &self.centroids[i - 1];
cum += self.centroids[ci].weight;
ci += 1;
}
if ci >= self.centroids.len() {
// Past all centroids — interpolate between last centroid and max
let last = self.centroids.last().unwrap();
let last_mid = total - last.weight / 2.0;
let remaining = total - last_mid;
out[qi] = if remaining == 0.0 {
self.max
} else {
last.mean + (self.max - last.mean) * ((target - last_mid) / remaining)
};
} else if ci == 0 {
// Before first centroid — interpolate between min and first centroid
let c = &self.centroids[0];
let first_mid = c.weight / 2.0;
out[qi] = if first_mid == 0.0 {
self.min
} else {
self.min + (c.mean - self.min) * (target / first_mid)
};
} else {
// Between centroid ci-1 and ci
let c = &self.centroids[ci];
let prev = &self.centroids[ci - 1];
let mid = cum + c.weight / 2.0;
let prev_center = cum - prev.weight / 2.0;
let frac = if mid == prev_center {
0.5
} else {
(target - prev_center) / (mid - prev_center)
};
return prev.mean + (c.mean - prev.mean) * frac;
out[qi] = prev.mean + (c.mean - prev.mean) * frac;
}
cum += c.weight;
}
// Between last centroid center and max
let last = self.centroids.last().unwrap();
let last_mid = total - last.weight / 2.0;
let remaining = total - last_mid;
if remaining == 0.0 {
return self.max;
}
last.mean + (self.max - last.mean) * ((target - last_mid) / remaining)
}
/// Batch quantile query. `qs` must be sorted ascending.
pub fn quantiles(&self, qs: &[f64], out: &mut [f64]) {
for (i, &q) in qs.iter().enumerate() {
out[i] = self.quantile(q);
}
}
}
@@ -215,6 +231,12 @@ impl TDigest {
mod tests {
use super::*;
fn quantile(td: &TDigest, q: f64) -> f64 {
let mut out = [0.0];
td.quantiles(&[q], &mut out);
out[0]
}
#[test]
fn basic_quantiles() {
let mut td = TDigest::default();
@@ -223,13 +245,13 @@ mod tests {
}
assert_eq!(td.count(), 1000);
let median = td.quantile(0.5);
let median = quantile(&td, 0.5);
assert!((median - 500.0).abs() < 10.0, "median was {median}");
let p99 = td.quantile(0.99);
let p99 = quantile(&td, 0.99);
assert!((p99 - 990.0).abs() < 15.0, "p99 was {p99}");
let p01 = td.quantile(0.01);
let p01 = quantile(&td, 0.01);
assert!((p01 - 10.0).abs() < 15.0, "p01 was {p01}");
}
@@ -237,16 +259,16 @@ mod tests {
fn empty_digest() {
let td = TDigest::default();
assert_eq!(td.count(), 0);
assert_eq!(td.quantile(0.5), 0.0);
assert_eq!(quantile(&td, 0.5), 0.0);
}
#[test]
fn single_value() {
let mut td = TDigest::default();
td.add(42.0);
assert_eq!(td.quantile(0.0), 42.0);
assert_eq!(td.quantile(0.5), 42.0);
assert_eq!(td.quantile(1.0), 42.0);
assert_eq!(quantile(&td, 0.0), 42.0);
assert_eq!(quantile(&td, 0.5), 42.0);
assert_eq!(quantile(&td, 1.0), 42.0);
}
#[test]
@@ -258,6 +280,6 @@ mod tests {
assert_eq!(td.count(), 100);
td.reset();
assert_eq!(td.count(), 0);
assert_eq!(td.quantile(0.5), 0.0);
assert_eq!(quantile(&td, 0.5), 0.0);
}
}
@@ -1,12 +0,0 @@
use brk_types::Cents;
use vecdb::UnaryTransform;
/// Cents -> Cents (identity transform for lazy references)
pub struct CentsIdentity;
impl UnaryTransform<Cents, Cents> for CentsIdentity {
#[inline(always)]
fn apply(cents: Cents) -> Cents {
cents
}
}
@@ -1,12 +0,0 @@
use brk_types::Dollars;
use vecdb::UnaryTransform;
/// Dollars -> Dollars (identity transform for lazy references)
pub struct DollarsIdentity;
impl UnaryTransform<Dollars, Dollars> for DollarsIdentity {
#[inline(always)]
fn apply(dollars: Dollars) -> Dollars {
dollars
}
}
@@ -1,12 +0,0 @@
use brk_types::StoredF32;
use vecdb::UnaryTransform;
/// StoredF32 -> StoredF32 (identity transform for lazy references/proxies)
pub struct StoredF32Identity;
impl UnaryTransform<StoredF32, StoredF32> for StoredF32Identity {
#[inline(always)]
fn apply(v: StoredF32) -> StoredF32 {
v
}
}
@@ -0,0 +1,13 @@
use std::marker::PhantomData;
use vecdb::{UnaryTransform, VecValue};
/// T -> T (identity transform for lazy references)
pub struct Identity<T>(PhantomData<T>);
impl<T: VecValue> UnaryTransform<T, T> for Identity<T> {
#[inline(always)]
fn apply(v: T) -> T {
v
}
}
@@ -4,7 +4,7 @@ mod bps16_to_float;
mod bps32_to_float;
mod block_count_target;
mod cents_halve;
mod cents_identity;
mod identity;
mod cents_plus;
mod cents_signed_to_dollars;
mod cents_subtract_to_cents_signed;
@@ -12,9 +12,7 @@ mod cents_times_tenths;
mod cents_to_dollars;
mod cents_to_sats;
mod dollar_halve;
mod dollar_identity;
mod dollars_to_sats_fract;
mod f32_identity;
mod neg_cents_to_dollars;
mod ohlc_cents_to_dollars;
mod ohlc_cents_to_sats;
@@ -30,6 +28,7 @@ mod percentage_u32_f32;
mod price_times_ratio_cents;
mod ratio32;
mod ratio_cents64;
mod per_sec;
mod ratio_u64_f32;
mod return_f32_tenths;
mod return_i8;
@@ -38,13 +37,10 @@ mod sats_to_cents;
mod sat_halve;
mod sat_halve_to_bitcoin;
mod sat_identity;
mod sat_mask;
mod sat_to_bitcoin;
mod days_to_years;
mod volatility_sqrt30;
mod volatility_sqrt365;
mod volatility_sqrt7;
mod volatility;
pub use bp16_to_float::*;
pub use bp32_to_float::*;
@@ -52,7 +48,7 @@ pub use bps16_to_float::*;
pub use bps32_to_float::*;
pub use block_count_target::*;
pub use cents_halve::*;
pub use cents_identity::*;
pub use identity::*;
pub use cents_plus::*;
pub use cents_signed_to_dollars::*;
pub use cents_subtract_to_cents_signed::*;
@@ -67,9 +63,7 @@ pub use percentage_cents_signed_dollars_f32::*;
pub use percentage_cents_signed_f32::*;
pub use dollar_halve::*;
pub use dollar_identity::*;
pub use dollars_to_sats_fract::*;
pub use f32_identity::*;
pub use percentage_diff_close_cents::*;
pub use percentage_diff_close_dollars::*;
pub use percentage_dollars_f32::*;
@@ -79,6 +73,7 @@ pub use percentage_u32_f32::*;
pub use price_times_ratio_cents::*;
pub use ratio32::*;
pub use ratio_cents64::*;
pub use per_sec::*;
pub use ratio_u64_f32::*;
pub use return_f32_tenths::*;
pub use return_i8::*;
@@ -86,10 +81,7 @@ pub use return_u16::*;
pub use sats_to_cents::*;
pub use sat_halve::*;
pub use sat_halve_to_bitcoin::*;
pub use sat_identity::*;
pub use sat_mask::*;
pub use sat_to_bitcoin::*;
pub use days_to_years::*;
pub use volatility_sqrt7::*;
pub use volatility_sqrt30::*;
pub use volatility_sqrt365::*;
pub use volatility::*;
@@ -0,0 +1,17 @@
use brk_types::{StoredF32, StoredU64, Timestamp};
use vecdb::BinaryTransform;
/// (StoredU64, Timestamp) -> StoredF32 rate (count / interval_seconds)
pub struct PerSec;
impl BinaryTransform<StoredU64, Timestamp, StoredF32> for PerSec {
#[inline(always)]
fn apply(count: StoredU64, interval: Timestamp) -> StoredF32 {
let interval_f64 = f64::from(*interval);
if interval_f64 > 0.0 {
StoredF32::from(*count as f64 / interval_f64)
} else {
StoredF32::NAN
}
}
}
@@ -1,12 +0,0 @@
use brk_types::Sats;
use vecdb::UnaryTransform;
/// Sats -> Sats (identity transform for lazy references)
pub struct SatsIdentity;
impl UnaryTransform<Sats, Sats> for SatsIdentity {
#[inline(always)]
fn apply(sats: Sats) -> Sats {
sats
}
}
@@ -0,0 +1,33 @@
use std::marker::PhantomData;
use brk_types::StoredF32;
use vecdb::UnaryTransform;
pub trait SqrtDays {
const FACTOR: f32;
}
pub struct Days7;
impl SqrtDays for Days7 {
const FACTOR: f32 = 2.6457513; // 7.0_f32.sqrt()
}
pub struct Days30;
impl SqrtDays for Days30 {
const FACTOR: f32 = 5.477226; // 30.0_f32.sqrt()
}
pub struct Days365;
impl SqrtDays for Days365 {
const FACTOR: f32 = 19.104973; // 365.0_f32.sqrt()
}
/// StoredF32 × sqrt(D) -> StoredF32 (annualize daily volatility to D-day period)
pub struct TimesSqrt<D: SqrtDays>(PhantomData<D>);
impl<D: SqrtDays> UnaryTransform<StoredF32, StoredF32> for TimesSqrt<D> {
#[inline(always)]
fn apply(v: StoredF32) -> StoredF32 {
(*v * D::FACTOR).into()
}
}
@@ -1,13 +0,0 @@
use brk_types::StoredF32;
use vecdb::UnaryTransform;
/// StoredF32 × sqrt(30) -> StoredF32 (1-month volatility from daily SD)
pub struct StoredF32TimesSqrt30;
impl UnaryTransform<StoredF32, StoredF32> for StoredF32TimesSqrt30 {
#[inline(always)]
fn apply(v: StoredF32) -> StoredF32 {
// 30.0_f32.sqrt() = 5.477226
(*v * 5.477226_f32).into()
}
}
@@ -1,13 +0,0 @@
use brk_types::StoredF32;
use vecdb::UnaryTransform;
/// StoredF32 × sqrt(365) -> StoredF32 (1-year volatility from daily SD)
pub struct StoredF32TimesSqrt365;
impl UnaryTransform<StoredF32, StoredF32> for StoredF32TimesSqrt365 {
#[inline(always)]
fn apply(v: StoredF32) -> StoredF32 {
// 365.0_f32.sqrt() = 19.104973
(*v * 19.104973_f32).into()
}
}
@@ -1,13 +0,0 @@
use brk_types::StoredF32;
use vecdb::UnaryTransform;
/// StoredF32 × sqrt(7) -> StoredF32 (1-week volatility from daily SD)
pub struct StoredF32TimesSqrt7;
impl UnaryTransform<StoredF32, StoredF32> for StoredF32TimesSqrt7 {
#[inline(always)]
fn apply(v: StoredF32) -> StoredF32 {
// 7.0_f32.sqrt() = 2.6457513
(*v * 2.6457513_f32).into()
}
}