use std::fmt::Debug;
use serde::{Deserialize, Serialize};
use crate::{
data::{
domain::{AggregatedPrice, Price, Volume},
event::{Ohlcv, TradeEvent},
},
indicator::{
config::{AtrConfig, AtrSmoothingType, EmaWindow, SmaWindow},
streaming::{
StreamingIndicator,
moving_averages::{StreamingEma, StreamingEwm, StreamingSma},
},
},
math::accumulators::KahanSum,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
enum AtrSmoother {
Wilders(StreamingEwm),
Sma(StreamingSma),
Ema(StreamingEma),
}
impl StreamingIndicator for AtrSmoother {
type Input = f64;
type Output<'a> = Option<f64>;
fn update(&mut self, value: Self::Input) -> Self::Output<'_> {
match self {
Self::Wilders(ewm) => ewm.update(value),
Self::Sma(sma) => sma.update(value),
Self::Ema(ema) => ema.update(value),
}
}
fn reset(&mut self) {
match self {
Self::Wilders(ewm) => ewm.reset(),
Self::Sma(sma) => sma.reset(),
Self::Ema(ema) => ema.reset(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamingAtr {
window_size: u16,
smoother: AtrSmoother,
prev_close: Option<Price>,
}
impl Default for StreamingAtr {
fn default() -> Self {
let window_size = 14;
let alpha = 1.0 / f64::from(window_size);
let smoother = AtrSmoother::Wilders(StreamingEwm::new(alpha, window_size as usize));
Self {
window_size,
smoother,
prev_close: None,
}
}
}
impl StreamingAtr {
#[must_use]
pub fn new(cfg: AtrConfig) -> Self {
let AtrConfig { window, smoothing } = cfg;
let smoother = match smoothing {
AtrSmoothingType::Wilders => {
let alpha = 1.0 / f64::from(window);
AtrSmoother::Wilders(StreamingEwm::new(alpha, window as usize))
}
AtrSmoothingType::Sma => AtrSmoother::Sma(StreamingSma::new(SmaWindow(window))),
AtrSmoothingType::Ema => AtrSmoother::Ema(StreamingEma::new(EmaWindow(window))),
};
Self {
window_size: window,
smoother,
..Default::default()
}
}
}
impl StreamingAtr {
fn calculate_true_range(&self, ohlcv: Ohlcv) -> f64 {
let hl_range = ohlcv.high - ohlcv.low;
self.prev_close.map_or(hl_range.0, |prev_c| {
let high_close_range = (ohlcv.high - prev_c).abs();
let low_close_range = (ohlcv.low - prev_c).abs();
hl_range.max(high_close_range).max(low_close_range).0
})
}
}
impl StreamingIndicator for StreamingAtr {
type Input = Ohlcv;
type Output<'a> = Option<f64>;
fn update(&mut self, current: Self::Input) -> Self::Output<'_> {
let true_range = self.calculate_true_range(current);
self.prev_close = Some(current.close);
self.smoother.update(true_range)
}
fn reset(&mut self) {
self.smoother.reset();
self.prev_close = None;
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
struct KahanAccumulator {
sum_price_x_volume: KahanSum,
sum_volume: KahanSum,
}
impl KahanAccumulator {
fn add(self, price: Price, volume: Volume) -> Self {
let v = volume.0;
if !v.is_finite() || v <= 0.0 {
return self;
}
Self {
sum_price_x_volume: self.sum_price_x_volume.add(price.0 * v),
sum_volume: self.sum_volume.add(v),
}
}
fn value(self) -> Option<f64> {
let total_v = self.sum_volume.value();
(total_v > 0.0).then(|| self.sum_price_x_volume.value() / total_v)
}
fn reset(&mut self) {
*self = Self::default();
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct StreamingOhlcvVwap {
source: AggregatedPrice,
acc: KahanAccumulator,
}
impl StreamingOhlcvVwap {
#[must_use]
pub fn new(source: AggregatedPrice) -> Self {
Self {
source,
acc: KahanAccumulator::default(),
}
}
#[must_use]
pub fn value(&self) -> Option<f64> {
self.acc.value()
}
fn weighting_price(&self, ohlcv: Ohlcv) -> Price {
match self.source {
AggregatedPrice::Hlc3 => Price((ohlcv.high + ohlcv.low + ohlcv.close).0 / 3.0),
AggregatedPrice::Hl2 => Price((ohlcv.high + ohlcv.low).0 / 2.0),
AggregatedPrice::Ohlc4 => {
Price((ohlcv.open + ohlcv.high + ohlcv.low + ohlcv.close).0 / 4.0)
}
AggregatedPrice::Close => ohlcv.close,
}
}
}
impl Default for StreamingOhlcvVwap {
fn default() -> Self {
Self::new(AggregatedPrice::default())
}
}
impl StreamingIndicator for StreamingOhlcvVwap {
type Input = Ohlcv;
type Output<'a> = Option<f64>;
fn update(&mut self, current: Self::Input) -> Self::Output<'_> {
let price = self.weighting_price(current);
self.acc = self.acc.add(price, current.volume);
self.acc.value()
}
fn reset(&mut self) {
self.acc.reset();
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct StreamingTradesVwap {
acc: KahanAccumulator,
}
impl StreamingTradesVwap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn value(&self) -> Option<f64> {
self.acc.value()
}
}
impl StreamingIndicator for StreamingTradesVwap {
type Input = TradeEvent;
type Output<'a> = Option<f64>;
fn update(&mut self, current: Self::Input) -> Self::Output<'_> {
self.acc = self.acc.add(current.price, current.quantity);
self.acc.value()
}
fn reset(&mut self) {
self.acc = KahanAccumulator::default();
}
}
#[cfg(test)]
mod tests {
use chrono::{DateTime, Utc};
use super::*;
use crate::data::domain::Quantity;
fn mock_candle(open: f64, high: f64, low: f64, close: f64, vol: f64) -> Ohlcv {
Ohlcv {
open_timestamp: DateTime::<Utc>::MIN_UTC,
close_timestamp: DateTime::<Utc>::MIN_UTC,
open: Price(open),
high: Price(high),
low: Price(low),
close: Price(close),
volume: Quantity(vol),
quote_asset_volume: None,
number_of_trades: None,
taker_buy_base_asset_volume: None,
taker_buy_quote_asset_volume: None,
}
}
fn mock_trade(price: f64, qty: f64) -> TradeEvent {
TradeEvent {
timestamp: DateTime::<Utc>::MIN_UTC,
price: Price(price),
quantity: Quantity(qty),
trade_id: None,
quote_asset_volume: None,
is_buyer_maker: None,
is_best_match: None,
}
}
#[test]
fn atr_calculates_true_range_correctly_across_edge_cases() {
let mut atr = StreamingAtr::new(AtrConfig {
window: 1,
smoothing: AtrSmoothingType::Sma,
});
let candle1 = mock_candle(10., 15., 5., 12., 100.);
assert_eq!(atr.update(candle1), Some(10.0));
let candle2 = mock_candle(12., 14., 10., 13., 100.);
assert_eq!(atr.update(candle2), Some(4.0));
let candle3 = mock_candle(20., 25., 20., 24., 100.);
assert_eq!(atr.update(candle3), Some(12.0));
let candle4 = mock_candle(10., 10., 5., 8., 100.);
assert_eq!(atr.update(candle4), Some(19.0));
}
#[test]
fn atr_constant_true_range_is_stable_for_all_smoothers() {
for smoothing in [
AtrSmoothingType::Wilders,
AtrSmoothingType::Sma,
AtrSmoothingType::Ema,
] {
let mut atr = StreamingAtr::new(AtrConfig {
window: 3,
smoothing,
});
let mut last = None;
for _ in 0..6 {
last = atr.update(mock_candle(10., 11., 9., 10., 100.));
}
assert_eq!(
last,
Some(2.0),
"smoother {smoothing:?} should settle at TR"
);
}
}
#[test]
fn atr_reset_clears_previous_close() {
let mut atr = StreamingAtr::new(AtrConfig {
window: 1,
smoothing: AtrSmoothingType::Sma,
});
atr.update(mock_candle(10., 15., 5., 12., 100.));
atr.reset();
let after = atr.update(mock_candle(20., 25., 20., 24., 100.));
assert_eq!(after, Some(5.0)); }
#[test]
fn atr_default_is_wilders_window_14() {
let atr = StreamingAtr::default();
assert_eq!(atr.window_size, 14);
assert!(matches!(atr.smoother, AtrSmoother::Wilders(_)));
}
#[test]
fn ohlcv_vwap_accumulates_and_ignores_bad_volume() {
let mut vwap = StreamingOhlcvVwap::new(AggregatedPrice::Hlc3);
assert_eq!(vwap.value(), None);
let c1 = mock_candle(0., 10., 8., 9., 100.);
assert_eq!(vwap.update(c1), Some(9.0));
let c2 = mock_candle(0., 20., 10., 15., 200.);
assert_eq!(vwap.update(c2), Some(13.0));
let c3 = mock_candle(0., 50., 40., 45., 0.);
assert_eq!(vwap.update(c3), Some(13.0));
let c4 = mock_candle(0., 50., 40., 45., -50.);
assert_eq!(vwap.update(c4), Some(13.0));
}
#[test]
fn ohlcv_vwap_respects_price_sources() {
let candle = mock_candle(10., 20., 10., 18., 100.);
let mut vwap_hlc3 = StreamingOhlcvVwap::new(AggregatedPrice::Hlc3);
assert_eq!(vwap_hlc3.update(candle), Some((20. + 10. + 18.) / 3.0));
let mut vwap_hl2 = StreamingOhlcvVwap::new(AggregatedPrice::Hl2);
assert_eq!(vwap_hl2.update(candle), Some(f64::midpoint(20., 10.)));
let mut vwap_ohlc4 = StreamingOhlcvVwap::new(AggregatedPrice::Ohlc4);
assert_eq!(
vwap_ohlc4.update(candle),
Some((10. + 20. + 10. + 18.) / 4.0)
);
let mut vwap_close = StreamingOhlcvVwap::new(AggregatedPrice::Close);
assert_eq!(vwap_close.update(candle), Some(18.0)); }
#[test]
fn ohlcv_vwap_is_none_until_positive_volume() {
let mut vwap = StreamingOhlcvVwap::new(AggregatedPrice::Hlc3);
assert_eq!(vwap.update(mock_candle(0., 10., 8., 9., 0.)), None);
assert_eq!(vwap.update(mock_candle(0., 10., 8., 9., -5.)), None);
assert_eq!(vwap.update(mock_candle(0., 10., 8., 9., 100.)), Some(9.0));
}
#[test]
fn ohlcv_vwap_reset_re_anchors() {
let mut vwap = StreamingOhlcvVwap::new(AggregatedPrice::Hlc3);
vwap.update(mock_candle(0., 10., 8., 9., 100.));
vwap.update(mock_candle(0., 20., 10., 15., 200.));
assert_eq!(vwap.value(), Some(13.0));
vwap.reset();
assert_eq!(vwap.value(), None);
assert_eq!(vwap.update(mock_candle(0., 20., 10., 15., 50.)), Some(15.0));
}
#[test]
fn trades_vwap_accumulates_correctly() {
let mut vwap = StreamingTradesVwap::new();
assert_eq!(vwap.update(mock_trade(100.0, 2.0)), Some(100.0));
assert_eq!(vwap.update(mock_trade(110.0, 8.0)), Some(108.0));
vwap.reset();
assert_eq!(vwap.value(), None);
assert_eq!(vwap.update(mock_trade(50.0, 10.0)), Some(50.0));
}
#[test]
fn trades_vwap_ignores_non_positive_quantity() {
let mut vwap = StreamingTradesVwap::new();
assert_eq!(vwap.update(mock_trade(100.0, 0.0)), None);
assert_eq!(vwap.update(mock_trade(100.0, -3.0)), None);
assert_eq!(vwap.update(mock_trade(100.0, 2.0)), Some(100.0));
assert_eq!(vwap.update(mock_trade(999.0, 0.0)), Some(100.0));
}
#[test]
fn kahan_sum_recovers_precision_lost_by_naive_summation() {
use crate::math::accumulators::KahanSum;
let mut values = vec![1.0];
values.extend(std::iter::repeat_n(1e-16, 100));
let truth = 100.0_f64.mul_add(1e-16, 1.0);
let kahan = values
.iter()
.fold(KahanSum::default(), |acc, &v| acc.add(v))
.value();
let naive: f64 = values.iter().sum();
assert_f64_eq!(kahan, truth); assert_f64_eq!(naive, 1.0); assert!((kahan - truth).abs() < (naive - truth).abs());
}
}