Skip to main content

sandbox_quant/strategy/
macd_crossover.rs

1use crate::indicator::ema::Ema;
2use crate::model::signal::Signal;
3use crate::model::tick::Tick;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
6enum PositionState {
7    Flat,
8    Long,
9}
10
11#[derive(Debug)]
12pub struct MacdCrossoverStrategy {
13    fast_ema: Ema,
14    slow_ema: Ema,
15    signal_ema: Ema,
16    prev_macd: Option<f64>,
17    prev_signal: Option<f64>,
18    position: PositionState,
19    min_ticks_between_signals: u64,
20    last_signal_tick: u64,
21    tick_count: u64,
22}
23
24impl MacdCrossoverStrategy {
25    pub fn new(fast_period: usize, slow_period: usize, min_ticks_between_signals: u64) -> Self {
26        let fast = fast_period.max(2);
27        let slow = slow_period.max(fast + 1);
28        let signal_period = (slow / 2).clamp(2, 9);
29        Self {
30            fast_ema: Ema::new(fast),
31            slow_ema: Ema::new(slow),
32            signal_ema: Ema::new(signal_period),
33            prev_macd: None,
34            prev_signal: None,
35            position: PositionState::Flat,
36            min_ticks_between_signals: min_ticks_between_signals.max(1),
37            last_signal_tick: 0,
38            tick_count: 0,
39        }
40    }
41
42    pub fn on_tick(&mut self, tick: &Tick) -> Signal {
43        self.tick_count += 1;
44        let Some(fast) = self.fast_ema.push(tick.price) else {
45            return Signal::Hold;
46        };
47        let Some(slow) = self.slow_ema.push(tick.price) else {
48            return Signal::Hold;
49        };
50
51        let macd = fast - slow;
52        let Some(signal) = self.signal_ema.push(macd) else {
53            self.prev_macd = Some(macd);
54            return Signal::Hold;
55        };
56
57        let cooldown_ok = self.tick_count.saturating_sub(self.last_signal_tick)
58            >= self.min_ticks_between_signals;
59
60        let out = match (self.prev_macd, self.prev_signal) {
61            (Some(pm), Some(ps)) if pm <= ps && macd > signal => {
62                if self.position == PositionState::Flat && cooldown_ok {
63                    self.position = PositionState::Long;
64                    self.last_signal_tick = self.tick_count;
65                    Signal::Buy
66                } else {
67                    Signal::Hold
68                }
69            }
70            (Some(pm), Some(ps)) if pm >= ps && macd < signal => {
71                if self.position == PositionState::Long && cooldown_ok {
72                    self.position = PositionState::Flat;
73                    self.last_signal_tick = self.tick_count;
74                    Signal::Sell
75                } else {
76                    Signal::Hold
77                }
78            }
79            _ if macd > signal && self.position == PositionState::Flat && cooldown_ok => {
80                self.position = PositionState::Long;
81                self.last_signal_tick = self.tick_count;
82                Signal::Buy
83            }
84            _ if macd < signal && self.position == PositionState::Long && cooldown_ok => {
85                self.position = PositionState::Flat;
86                self.last_signal_tick = self.tick_count;
87                Signal::Sell
88            }
89            _ => Signal::Hold,
90        };
91
92        self.prev_macd = Some(macd);
93        self.prev_signal = Some(signal);
94        out
95    }
96}