computer: snapshot

This commit is contained in:
nym21
2026-02-27 10:54:36 +01:00
parent 72c17096ea
commit c75421f46e
44 changed files with 1079 additions and 722 deletions
+3 -3
View File
@@ -11,7 +11,7 @@ use vecdb::{
VecValue,
};
use crate::utils::get_percentile;
use brk_types::get_percentile;
use super::ComputedVecValue;
@@ -358,6 +358,7 @@ where
let window_starts_batch: Vec<I> = window_starts.collect_range_at(start, fi_len);
let zero = T::from(0_usize);
let mut values: Vec<T> = Vec::new();
first_indexes_batch
.iter()
@@ -389,8 +390,7 @@ where
vec.truncate_push_at(idx, zero)?;
}
} else {
let mut values: Vec<T> =
source.collect_range_at(range_start_usize, range_end_usize);
source.collect_range_into_at(range_start_usize, range_end_usize, &mut values);
// Compute sum before sorting
let len = values.len();
@@ -15,3 +15,31 @@ pub struct DistributionStats<A, B = A, C = A, D = A, E = A, F = A, G = A, H = A>
pub p75: G,
pub p90: H,
}
impl<A> DistributionStats<A> {
/// Apply a fallible operation to each of the 8 fields.
pub fn try_for_each_mut(&mut self, mut f: impl FnMut(&mut A) -> brk_error::Result<()>) -> brk_error::Result<()> {
f(&mut self.average)?;
f(&mut self.min)?;
f(&mut self.max)?;
f(&mut self.p10)?;
f(&mut self.p25)?;
f(&mut self.median)?;
f(&mut self.p75)?;
f(&mut self.p90)?;
Ok(())
}
/// Get minimum value by applying a function to each field.
pub fn min_by(&self, mut f: impl FnMut(&A) -> usize) -> usize {
f(&self.average)
.min(f(&self.min))
.min(f(&self.max))
.min(f(&self.p10))
.min(f(&self.p25))
.min(f(&self.median))
.min(f(&self.p75))
.min(f(&self.p90))
}
}
+1
View File
@@ -6,6 +6,7 @@ mod indexes;
mod lazy_eager_indexes;
mod multi;
mod single;
pub(crate) mod sliding_window;
mod traits;
mod windows;
@@ -24,7 +24,7 @@ where
{
pub height: M::Stored<EagerVec<PcoVec<Height, T>>>,
pub cumulative: ComputedFromHeightLast<T, M>,
pub rolling: RollingWindows<T, M>,
pub sum: RollingWindows<T, M>,
}
const VERSION: Version = Version::ZERO;
@@ -49,7 +49,7 @@ where
Ok(Self {
height,
cumulative,
rolling,
sum: rolling,
})
}
@@ -68,7 +68,7 @@ where
self.cumulative
.height
.compute_cumulative(max_from, &self.height, exit)?;
self.rolling
self.sum
.compute_rolling_sum(max_from, windows, &self.height, exit)?;
Ok(())
}
@@ -6,8 +6,8 @@ use vecdb::{AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, ReadableVec,
use crate::{
ComputeIndexes, blocks, indexes,
internal::{ComputedFromHeightStdDevExtended, Price},
utils::get_percentile,
};
use brk_types::get_percentile;
use super::super::ComputedFromHeightLast;
@@ -7,7 +7,7 @@ use brk_types::{
};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{LazyAggVec, ReadableBoxedVec, ReadableCloneableVec};
use vecdb::{LazyAggVec, ReadOnlyClone, ReadableBoxedVec, ReadableCloneableVec};
use crate::{
indexes, indexes_from,
@@ -41,6 +41,17 @@ pub struct ComputedHeightDerivedLast<T>(
where
T: ComputedVecValue + PartialOrd + JsonSchema;
/// Already read-only (no StorageMode); cloning is sufficient.
impl<T> ReadOnlyClone for ComputedHeightDerivedLast<T>
where
T: ComputedVecValue + PartialOrd + JsonSchema,
{
type ReadOnly = Self;
fn read_only_clone(&self) -> Self {
self.clone()
}
}
const VERSION: Version = Version::ZERO;
impl<T> ComputedHeightDerivedLast<T>
@@ -9,7 +9,9 @@ use brk_types::{
};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use vecdb::{LazyVecFrom1, ReadableBoxedVec, ReadableCloneableVec, UnaryTransform, VecIndex, VecValue};
use vecdb::{
LazyVecFrom1, ReadableBoxedVec, ReadableCloneableVec, UnaryTransform, VecIndex, VecValue,
};
use crate::{
indexes, indexes_from,
@@ -108,7 +110,8 @@ where
where
S1T: NumericValue,
{
let derived = ComputedHeightDerivedLast::forced_import(name, height_source, version, indexes);
let derived =
ComputedHeightDerivedLast::forced_import(name, height_source, version, indexes);
Self::from_derived_computed::<F>(name, version, &derived)
}
@@ -66,62 +66,33 @@ where
T: Copy + Ord + From<f64> + Default,
f64: From<T>,
{
// Single pass per window: all 8 stats extracted from one sorted vec
compute_rolling_distribution_from_starts(
max_from,
windows._24h,
source,
&mut self.0.average._24h.height,
&mut self.0.min._24h.height,
&mut self.0.max._24h.height,
&mut self.0.p10._24h.height,
&mut self.0.p25._24h.height,
&mut self.0.median._24h.height,
&mut self.0.p75._24h.height,
&mut self.0.p90._24h.height,
exit,
max_from, windows._24h, source,
&mut self.0.average._24h.height, &mut self.0.min._24h.height,
&mut self.0.max._24h.height, &mut self.0.p10._24h.height,
&mut self.0.p25._24h.height, &mut self.0.median._24h.height,
&mut self.0.p75._24h.height, &mut self.0.p90._24h.height, exit,
)?;
compute_rolling_distribution_from_starts(
max_from,
windows._7d,
source,
&mut self.0.average._7d.height,
&mut self.0.min._7d.height,
&mut self.0.max._7d.height,
&mut self.0.p10._7d.height,
&mut self.0.p25._7d.height,
&mut self.0.median._7d.height,
&mut self.0.p75._7d.height,
&mut self.0.p90._7d.height,
exit,
max_from, windows._7d, source,
&mut self.0.average._7d.height, &mut self.0.min._7d.height,
&mut self.0.max._7d.height, &mut self.0.p10._7d.height,
&mut self.0.p25._7d.height, &mut self.0.median._7d.height,
&mut self.0.p75._7d.height, &mut self.0.p90._7d.height, exit,
)?;
compute_rolling_distribution_from_starts(
max_from,
windows._30d,
source,
&mut self.0.average._30d.height,
&mut self.0.min._30d.height,
&mut self.0.max._30d.height,
&mut self.0.p10._30d.height,
&mut self.0.p25._30d.height,
&mut self.0.median._30d.height,
&mut self.0.p75._30d.height,
&mut self.0.p90._30d.height,
exit,
max_from, windows._30d, source,
&mut self.0.average._30d.height, &mut self.0.min._30d.height,
&mut self.0.max._30d.height, &mut self.0.p10._30d.height,
&mut self.0.p25._30d.height, &mut self.0.median._30d.height,
&mut self.0.p75._30d.height, &mut self.0.p90._30d.height, exit,
)?;
compute_rolling_distribution_from_starts(
max_from,
windows._1y,
source,
&mut self.0.average._1y.height,
&mut self.0.min._1y.height,
&mut self.0.max._1y.height,
&mut self.0.p10._1y.height,
&mut self.0.p25._1y.height,
&mut self.0.median._1y.height,
&mut self.0.p75._1y.height,
&mut self.0.p90._1y.height,
exit,
max_from, windows._1y, source,
&mut self.0.average._1y.height, &mut self.0.min._1y.height,
&mut self.0.max._1y.height, &mut self.0.p10._1y.height,
&mut self.0.p25._1y.height, &mut self.0.median._1y.height,
&mut self.0.p75._1y.height, &mut self.0.p90._1y.height, exit,
)?;
Ok(())
@@ -72,18 +72,9 @@ impl StoredValueRollingWindows {
usd_source: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.0
._24h
.compute_rolling_sum(max_from, windows._24h, sats_source, usd_source, exit)?;
self.0
._7d
.compute_rolling_sum(max_from, windows._7d, sats_source, usd_source, exit)?;
self.0
._30d
.compute_rolling_sum(max_from, windows._30d, sats_source, usd_source, exit)?;
self.0
._1y
.compute_rolling_sum(max_from, windows._1y, sats_source, usd_source, exit)?;
for (w, starts) in self.0.as_mut_array().into_iter().zip(windows.as_array()) {
w.compute_rolling_sum(max_from, starts, sats_source, usd_source, exit)?;
}
Ok(())
}
}
@@ -26,6 +26,12 @@ pub struct WindowStarts<'a> {
pub _1y: &'a EagerVec<PcoVec<Height, Height>>,
}
impl<'a> WindowStarts<'a> {
pub fn as_array(&self) -> [&'a EagerVec<PcoVec<Height, Height>>; 4] {
[self._24h, self._7d, self._30d, self._1y]
}
}
/// 4 rolling window vecs (24h, 7d, 30d, 1y), each with height data + all 17 index views.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
@@ -64,22 +70,9 @@ where
where
T: Default + SubAssign,
{
self.0
._24h
.height
.compute_rolling_sum(max_from, windows._24h, source, exit)?;
self.0
._7d
.height
.compute_rolling_sum(max_from, windows._7d, source, exit)?;
self.0
._30d
.height
.compute_rolling_sum(max_from, windows._30d, source, exit)?;
self.0
._1y
.height
.compute_rolling_sum(max_from, windows._1y, source, exit)?;
for (w, starts) in self.0.as_mut_array().into_iter().zip(windows.as_array()) {
w.height.compute_rolling_sum(max_from, starts, source, exit)?;
}
Ok(())
}
}
@@ -1,6 +1,8 @@
mod block_count_target;
mod cents_to_dollars;
mod cents_to_sats;
mod ohlc_cents_to_dollars;
mod ohlc_cents_to_sats;
mod dollar_halve;
mod dollar_identity;
@@ -35,6 +37,8 @@ mod volatility_sqrt7;
pub use block_count_target::*;
pub use cents_to_dollars::*;
pub use cents_to_sats::*;
pub use ohlc_cents_to_dollars::*;
pub use ohlc_cents_to_sats::*;
pub use dollar_halve::*;
pub use dollar_identity::*;
@@ -0,0 +1,11 @@
use brk_types::{OHLCCents, OHLCDollars};
use vecdb::UnaryTransform;
pub struct OhlcCentsToDollars;
impl UnaryTransform<OHLCCents, OHLCDollars> for OhlcCentsToDollars {
#[inline(always)]
fn apply(cents: OHLCCents) -> OHLCDollars {
OHLCDollars::from(cents)
}
}
@@ -0,0 +1,19 @@
use brk_types::{Close, High, Low, OHLCCents, OHLCSats, Open};
use vecdb::UnaryTransform;
use super::CentsUnsignedToSats;
/// OHLCCents -> OHLCSats with high/low swapped (inverse price relationship).
pub struct OhlcCentsToSats;
impl UnaryTransform<OHLCCents, OHLCSats> for OhlcCentsToSats {
#[inline(always)]
fn apply(cents: OHLCCents) -> OHLCSats {
OHLCSats {
open: Open::new(CentsUnsignedToSats::apply(*cents.open)),
high: High::new(CentsUnsignedToSats::apply(*cents.low)),
low: Low::new(CentsUnsignedToSats::apply(*cents.high)),
close: Close::new(CentsUnsignedToSats::apply(*cents.close)),
}
}
}
@@ -0,0 +1,188 @@
/// Sqrt-decomposed sorted structure for O(sqrt(n)) insert/remove/kth.
///
/// Maintains `blocks` sorted sub-arrays where each block is sorted and
/// the blocks are ordered (max of block[i] <= min of block[i+1]).
/// Total element count is tracked via `total_len`.
struct SortedBlocks {
blocks: Vec<Vec<f64>>,
total_len: usize,
block_size: usize,
}
impl SortedBlocks {
fn new(capacity: usize) -> Self {
let block_size = ((capacity as f64).sqrt() as usize).max(64);
Self {
blocks: Vec::new(),
total_len: 0,
block_size,
}
}
fn len(&self) -> usize {
self.total_len
}
fn is_empty(&self) -> bool {
self.total_len == 0
}
/// Insert a value in sorted order. O(sqrt(n)).
fn insert(&mut self, value: f64) {
self.total_len += 1;
if self.blocks.is_empty() {
self.blocks.push(vec![value]);
return;
}
// Find the block where value belongs: first block whose max >= value
let block_idx = self.blocks.iter().position(|b| {
*b.last().unwrap() >= value
}).unwrap_or(self.blocks.len() - 1);
let block = &mut self.blocks[block_idx];
let pos = block.partition_point(|a| *a < value);
block.insert(pos, value);
// Split if block too large
if block.len() > 2 * self.block_size {
let mid = block.len() / 2;
let right = block[mid..].to_vec();
block.truncate(mid);
self.blocks.insert(block_idx + 1, right);
}
}
/// 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)
return false;
}
false
}
/// Get the k-th smallest element (0-indexed). O(sqrt(n)).
fn kth(&self, mut k: usize) -> f64 {
for block in &self.blocks {
if k < block.len() {
return block[k];
}
k -= block.len();
}
unreachable!("kth out of bounds")
}
fn first(&self) -> f64 {
self.blocks.first().unwrap().first().copied().unwrap()
}
fn last(&self) -> f64 {
self.blocks.last().unwrap().last().copied().unwrap()
}
}
/// Sorted sliding window for rolling distribution/median computations.
///
/// Uses sqrt-decomposition for O(sqrt(n)) insert/remove/kth instead of
/// O(n) memmoves with a flat sorted Vec.
pub(crate) struct SlidingWindowSorted {
sorted: SortedBlocks,
running_sum: f64,
prev_start: usize,
}
impl SlidingWindowSorted {
pub fn with_capacity(cap: usize) -> Self {
Self {
sorted: SortedBlocks::new(cap),
running_sum: 0.0,
prev_start: 0,
}
}
/// Reconstruct state from historical data (the elements in [range_start..skip]).
pub fn reconstruct(&mut self, partial_values: &[f64], range_start: usize, skip: usize) {
self.prev_start = range_start;
for idx in range_start..skip {
let v = partial_values[idx - range_start];
self.running_sum += v;
self.sorted.insert(v);
}
}
/// Add a new value and remove all expired values up to `new_start`.
pub fn advance(&mut self, value: f64, new_start: usize, partial_values: &[f64], range_start: usize) {
self.running_sum += value;
self.sorted.insert(value);
while self.prev_start < new_start {
let old = partial_values[self.prev_start - range_start];
self.running_sum -= old;
self.sorted.remove(old);
self.prev_start += 1;
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.sorted.is_empty()
}
#[inline]
pub fn average(&self) -> f64 {
if self.sorted.is_empty() {
0.0
} else {
self.running_sum / self.sorted.len() as f64
}
}
#[inline]
pub fn min(&self) -> f64 {
if self.sorted.is_empty() { 0.0 } else { self.sorted.first() }
}
#[inline]
pub fn max(&self) -> f64 {
if self.sorted.is_empty() { 0.0 } else { self.sorted.last() }
}
/// Extract a percentile (0.0-1.0) using linear interpolation.
#[inline]
pub fn percentile(&self, p: f64) -> f64 {
let len = self.sorted.len();
if len == 0 {
return 0.0;
}
if len == 1 {
return self.sorted.kth(0);
}
let rank = p * (len - 1) as f64;
let lo = rank.floor() as usize;
let hi = rank.ceil() as usize;
if lo == hi {
self.sorted.kth(lo)
} else {
let frac = rank - lo as f64;
self.sorted.kth(lo) * (1.0 - frac) + self.sorted.kth(hi) * frac
}
}
}
@@ -15,3 +15,9 @@ pub struct Windows<A, B = A, C = A, D = A> {
#[traversable(rename = "1y")]
pub _1y: D,
}
impl<A> Windows<A> {
pub fn as_mut_array(&mut self) -> [&mut A; 4] {
[&mut self._24h, &mut self._7d, &mut self._30d, &mut self._1y]
}
}