mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-13 03:58:12 -07:00
computer: snapshot
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user