use brk_types::{Cents, Dollars};
mod config;
mod filter;
mod scale;
mod seed;
mod stencil;
mod window;
pub use config::{Config, START_HEIGHT_FAST, START_HEIGHT_SLOW};
pub use filter::PaymentFilter;
pub use scale::{
bin_to_cents, cents_to_bin, sats_to_bin, HistogramEma, HistogramEmaCompact, HistogramRaw,
BINS_PER_DECADE, NUM_BINS,
};
pub use seed::{pre_oracle_price_cents, pre_oracle_prices_from, seed_bin, seed_price_cents};
use stencil::Stencil;
use window::EmaWindow;
pub const VERSION: u32 = 4;
#[derive(Clone)]
pub struct Oracle {
window: EmaWindow,
ref_bin: f64,
config: Config,
warmup: bool,
stencil: Stencil,
}
impl Oracle {
pub fn new(start_bin: f64, config: Config) -> Self {
Self {
window: EmaWindow::new(config.window_size, config.alpha),
ref_bin: start_bin,
warmup: false,
stencil: Stencil::new(config.shape_weight),
config,
}
}
pub fn from_seed() -> Self {
Self::new(seed_bin(), Config::slow())
}
pub fn from_checkpoint(ref_bin: f64, config: Config, fill: impl FnOnce(&mut Self)) -> Self {
let mut oracle = Self::new(ref_bin, config);
oracle.warmup = true;
fill(&mut oracle);
oracle.warmup = false;
oracle.window.recompute();
oracle
}
pub fn process_histogram(&mut self, hist: &HistogramRaw) -> f64 {
self.window.push(hist);
if !self.warmup {
self.window.recompute();
self.ref_bin = self.stencil.pick(
self.window.ema(),
self.ref_bin,
self.config.search_below,
self.config.search_above,
);
}
self.ref_bin
}
pub fn reconfigure(&mut self, config: Config) {
let kept = self.window.recent(config.window_size);
*self = Self::from_checkpoint(self.ref_bin, config, |o| {
kept.iter().for_each(|h| {
o.process_histogram(h);
});
});
}
pub fn ref_bin(&self) -> f64 {
self.ref_bin
}
pub fn ema(&self) -> &HistogramEma {
self.window.ema()
}
pub fn price_cents(&self) -> Cents {
bin_to_cents(self.ref_bin).into()
}
pub fn price_dollars(&self) -> Dollars {
self.price_cents().into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn oracle_basic() {
let oracle = Oracle::new(1600.0, Config::default());
assert_eq!(oracle.ref_bin(), 1600.0);
assert_eq!(oracle.price_cents(), bin_to_cents(1600.0).into());
}
#[test]
fn from_seed_matches_manual_seed() {
let mut seeded = Oracle::from_seed();
let mut manual = Oracle::new(seed_bin(), Config::slow());
let mut hist = HistogramRaw::zeros();
hist.increment(1200);
assert_eq!(seeded.ref_bin(), manual.ref_bin());
assert_eq!(
seeded.process_histogram(&hist),
manual.process_histogram(&hist)
);
assert!(seeded.ema().iter().eq(manual.ema().iter()));
}
#[test]
fn reconfigure_matches_fresh_warmup() {
let hists: Vec<HistogramRaw> = (0..60)
.map(|i| {
let mut h = HistogramRaw::zeros();
h.increment(1200 + i % 7);
h.increment(1600 + i % 5);
h
})
.collect();
let fast = Config::default();
let mut switched = Oracle::new(1600.0, Config::slow());
hists.iter().for_each(|h| {
switched.process_histogram(h);
});
switched.reconfigure(fast);
let keep = fast.window_size;
let fresh = Oracle::from_checkpoint(switched.ref_bin(), fast, |o| {
hists[hists.len() - keep..].iter().for_each(|h| {
o.process_histogram(h);
});
});
assert!(switched.ema().iter().eq(fresh.ema().iter()));
}
#[test]
fn sequential_ema_matches_fresh_warmups() {
let hists: Vec<HistogramRaw> = (0..80)
.map(|i| {
let mut h = HistogramRaw::zeros();
h.increment(1000 + i % 11);
h.increment(1300 + i % 13);
h.increment(1700 + i % 17);
h
})
.collect();
for config in [Config::slow(), Config::default()] {
let query_start = config.window_size + 5;
let query_end = query_start + 20;
let seed = 1600.0;
let mut sequential = Oracle::from_checkpoint(seed, config, |o| {
hists[query_start + 1 - config.window_size..query_start + 1]
.iter()
.for_each(|h| {
o.process_histogram(h);
});
});
for height in query_start..query_end {
if height != query_start {
sequential.process_histogram(&hists[height]);
}
let fresh = Oracle::from_checkpoint(seed, config, |o| {
hists[height + 1 - config.window_size..height + 1]
.iter()
.for_each(|h| {
o.process_histogram(h);
});
});
assert!(sequential.ema().iter().eq(fresh.ema().iter()));
}
}
}
}