Skip to main content

sandbox_quant/strategy/
volatility_compression.rs

1use std::collections::VecDeque;
2
3use crate::model::signal::Signal;
4use crate::model::tick::Tick;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7enum PositionState {
8    Flat,
9    Long,
10}
11
12#[derive(Debug)]
13pub struct VolatilityCompressionStrategy {
14    period: usize,
15    compression_threshold: f64,
16    prices: VecDeque<f64>,
17    position: PositionState,
18    min_ticks_between_signals: u64,
19    last_signal_tick: u64,
20    tick_count: u64,
21    mean: Option<f64>,
22}
23
24impl VolatilityCompressionStrategy {
25    pub fn new(period: usize, threshold_bps: usize, min_ticks_between_signals: u64) -> Self {
26        Self {
27            period: period.max(2),
28            compression_threshold: (threshold_bps.clamp(10, 5000) as f64) / 10_000.0,
29            prices: VecDeque::new(),
30            position: PositionState::Flat,
31            min_ticks_between_signals: min_ticks_between_signals.max(1),
32            last_signal_tick: 0,
33            tick_count: 0,
34            mean: None,
35        }
36    }
37
38    pub fn on_tick(&mut self, tick: &Tick) -> Signal {
39        self.tick_count += 1;
40        self.prices.push_back(tick.price);
41        while self.prices.len() > self.period {
42            let _ = self.prices.pop_front();
43        }
44        if self.prices.len() < self.period {
45            return Signal::Hold;
46        }
47
48        let n = self.prices.len() as f64;
49        let mean = self.prices.iter().sum::<f64>() / n;
50        self.mean = Some(mean);
51        let variance = self
52            .prices
53            .iter()
54            .map(|p| {
55                let d = *p - mean;
56                d * d
57            })
58            .sum::<f64>()
59            / n.max(1.0);
60        let std_dev = variance.sqrt();
61        let bandwidth = (2.0 * std_dev) / mean.abs().max(f64::EPSILON);
62        let upper_trigger = mean + std_dev;
63        let cooldown_ok = self.tick_count.saturating_sub(self.last_signal_tick)
64            >= self.min_ticks_between_signals;
65
66        if self.position == PositionState::Flat
67            && bandwidth <= self.compression_threshold
68            && tick.price > upper_trigger
69            && cooldown_ok
70        {
71            self.position = PositionState::Long;
72            self.last_signal_tick = self.tick_count;
73            Signal::Buy
74        } else if self.position == PositionState::Long && tick.price < mean && cooldown_ok {
75            self.position = PositionState::Flat;
76            self.last_signal_tick = self.tick_count;
77            Signal::Sell
78        } else {
79            Signal::Hold
80        }
81    }
82
83    pub fn mean_value(&self) -> Option<f64> {
84        self.mean
85    }
86}