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