sandbox_quant/strategy/
volatility_compression.rs1use 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}