1use std::collections::HashMap;
30
31use crate::indicators::Indicator;
32
33use super::{Signal, Strategy, StrategyContext};
34use crate::backtesting::signal::SignalStrength;
35
36#[derive(Debug, Default)]
50struct IndicatorSlot(Option<*const Vec<Option<f64>>>);
51
52unsafe impl Send for IndicatorSlot {}
55unsafe impl Sync for IndicatorSlot {}
56
57impl Clone for IndicatorSlot {
58 fn clone(&self) -> Self {
60 IndicatorSlot(None)
61 }
62}
63
64impl IndicatorSlot {
65 fn set(&mut self, v: &Vec<Option<f64>>) {
66 self.0 = Some(v as *const _);
67 }
68
69 #[inline]
75 unsafe fn get(&self) -> Option<&Vec<Option<f64>>> {
76 self.0.map(|p| unsafe { &*p })
77 }
78}
79
80#[derive(Debug, Clone)]
87pub struct SmaCrossover {
88 pub fast_period: usize,
90 pub slow_period: usize,
92 fast_key: String,
93 slow_key: String,
94 fast_slot: IndicatorSlot,
95 slow_slot: IndicatorSlot,
96}
97
98impl SmaCrossover {
99 pub fn new(fast_period: usize, slow_period: usize) -> Self {
101 Self {
102 fast_period,
103 slow_period,
104 fast_key: format!("sma_{fast_period}"),
105 slow_key: format!("sma_{slow_period}"),
106 fast_slot: IndicatorSlot::default(),
107 slow_slot: IndicatorSlot::default(),
108 }
109 }
110}
111
112impl Default for SmaCrossover {
113 fn default() -> Self {
114 Self::new(10, 20)
115 }
116}
117
118impl Strategy for SmaCrossover {
119 fn name(&self) -> &str {
120 "SMA Crossover"
121 }
122
123 fn required_indicators(&self) -> Vec<(String, Indicator)> {
124 vec![
125 (self.fast_key.clone(), Indicator::Sma(self.fast_period)),
126 (self.slow_key.clone(), Indicator::Sma(self.slow_period)),
127 ]
128 }
129
130 fn setup(&mut self, indicators: &HashMap<String, Vec<Option<f64>>>) {
131 if let Some(v) = indicators.get(&self.fast_key) {
132 self.fast_slot.set(v);
133 }
134 if let Some(v) = indicators.get(&self.slow_key) {
135 self.slow_slot.set(v);
136 }
137 }
138
139 fn warmup_period(&self) -> usize {
140 self.slow_period.max(self.fast_period) + 1
141 }
142
143 fn on_candle(&self, ctx: &StrategyContext) -> Signal {
144 let candle = ctx.current_candle();
145 let i = ctx.index;
146 if i == 0 {
147 return Signal::hold();
148 }
149
150 let fast_vals =
155 unsafe { self.fast_slot.get() }.or_else(|| ctx.indicators.get(&self.fast_key));
156 let slow_vals =
157 unsafe { self.slow_slot.get() }.or_else(|| ctx.indicators.get(&self.slow_key));
158 let (Some(fast_vals), Some(slow_vals)) = (fast_vals, slow_vals) else {
159 return Signal::hold();
160 };
161
162 let get = |vals: &Vec<Option<f64>>, idx: usize| vals.get(idx).and_then(|&v| v);
163 let (Some(fn_), Some(sn), Some(fp), Some(sp)) = (
164 get(fast_vals, i),
165 get(slow_vals, i),
166 get(fast_vals, i - 1),
167 get(slow_vals, i - 1),
168 ) else {
169 return Signal::hold();
170 };
171
172 if fp <= sp && fn_ > sn {
174 if ctx.is_short() {
175 return Signal::exit(candle.timestamp, candle.close)
176 .with_reason("SMA bullish crossover - close short");
177 }
178 if !ctx.has_position() {
179 return Signal::long(candle.timestamp, candle.close)
180 .with_reason("SMA bullish crossover");
181 }
182 }
183
184 if fp >= sp && fn_ < sn {
186 if ctx.is_long() {
187 return Signal::exit(candle.timestamp, candle.close)
188 .with_reason("SMA bearish crossover - close long");
189 }
190 if !ctx.has_position() {
191 return Signal::short(candle.timestamp, candle.close)
192 .with_reason("SMA bearish crossover");
193 }
194 }
195
196 Signal::hold()
197 }
198}
199
200#[derive(Debug, Clone)]
207pub struct RsiReversal {
208 pub period: usize,
210 pub oversold: f64,
212 pub overbought: f64,
214 rsi_key: String,
215 rsi_slot: IndicatorSlot,
216}
217
218impl RsiReversal {
219 pub fn new(period: usize) -> Self {
221 Self {
222 period,
223 oversold: 30.0,
224 overbought: 70.0,
225 rsi_key: format!("rsi_{period}"),
226 rsi_slot: IndicatorSlot::default(),
227 }
228 }
229
230 pub fn with_thresholds(mut self, oversold: f64, overbought: f64) -> Self {
232 self.oversold = oversold;
233 self.overbought = overbought;
234 self
235 }
236}
237
238impl Default for RsiReversal {
239 fn default() -> Self {
240 Self::new(14)
241 }
242}
243
244impl Strategy for RsiReversal {
245 fn name(&self) -> &str {
246 "RSI Reversal"
247 }
248
249 fn required_indicators(&self) -> Vec<(String, Indicator)> {
250 vec![(self.rsi_key.clone(), Indicator::Rsi(self.period))]
251 }
252
253 fn setup(&mut self, indicators: &HashMap<String, Vec<Option<f64>>>) {
254 if let Some(v) = indicators.get(&self.rsi_key) {
255 self.rsi_slot.set(v);
256 }
257 }
258
259 fn warmup_period(&self) -> usize {
260 self.period + 1
261 }
262
263 fn on_candle(&self, ctx: &StrategyContext) -> Signal {
264 let candle = ctx.current_candle();
265 let i = ctx.index;
266
267 let rsi_vals = unsafe { self.rsi_slot.get() }.or_else(|| ctx.indicators.get(&self.rsi_key));
269 let Some(rsi_vals) = rsi_vals else {
270 return Signal::hold();
271 };
272 let get = |idx: usize| rsi_vals.get(idx).and_then(|&v| v);
273 let Some(rsi_val) = get(i) else {
274 return Signal::hold();
275 };
276 let rsi_prev = if i > 0 { get(i - 1) } else { None };
277
278 if let Some(prev) = rsi_prev
280 && prev <= self.oversold
281 && rsi_val > self.oversold
282 {
283 let strength = if prev < 20.0 {
286 SignalStrength::strong()
287 } else if prev < 25.0 {
288 SignalStrength::medium()
289 } else {
290 SignalStrength::weak()
291 };
292 if ctx.is_short() {
293 return Signal::exit(candle.timestamp, candle.close)
294 .with_strength(strength)
295 .with_reason(format!(
296 "RSI crossed above {:.0} - close short",
297 self.oversold
298 ));
299 }
300 if !ctx.has_position() {
301 return Signal::long(candle.timestamp, candle.close)
302 .with_strength(strength)
303 .with_reason(format!("RSI crossed above {:.0}", self.oversold));
304 }
305 }
306
307 if let Some(prev) = rsi_prev
309 && prev >= self.overbought
310 && rsi_val < self.overbought
311 {
312 let strength = if prev > 80.0 {
313 SignalStrength::strong()
314 } else if prev > 75.0 {
315 SignalStrength::medium()
316 } else {
317 SignalStrength::weak()
318 };
319 if ctx.is_long() {
320 return Signal::exit(candle.timestamp, candle.close)
321 .with_strength(strength)
322 .with_reason(format!(
323 "RSI crossed below {:.0} - close long",
324 self.overbought
325 ));
326 }
327 if !ctx.has_position() {
328 return Signal::short(candle.timestamp, candle.close)
329 .with_strength(strength)
330 .with_reason(format!("RSI crossed below {:.0}", self.overbought));
331 }
332 }
333
334 Signal::hold()
335 }
336}
337
338#[derive(Debug, Clone)]
345pub struct MacdSignal {
346 pub fast: usize,
348 pub slow: usize,
350 pub signal: usize,
352 line_key: String,
353 sig_key: String,
354 line_slot: IndicatorSlot,
355 sig_slot: IndicatorSlot,
356}
357
358impl MacdSignal {
359 pub fn new(fast: usize, slow: usize, signal: usize) -> Self {
361 Self {
362 fast,
363 slow,
364 signal,
365 line_key: format!("macd_line_{fast}_{slow}_{signal}"),
366 sig_key: format!("macd_signal_{fast}_{slow}_{signal}"),
367 line_slot: IndicatorSlot::default(),
368 sig_slot: IndicatorSlot::default(),
369 }
370 }
371}
372
373impl Default for MacdSignal {
374 fn default() -> Self {
375 Self::new(12, 26, 9)
376 }
377}
378
379impl Strategy for MacdSignal {
380 fn name(&self) -> &str {
381 "MACD Signal"
382 }
383
384 fn required_indicators(&self) -> Vec<(String, Indicator)> {
385 vec![(
386 "macd".to_string(),
387 Indicator::Macd {
388 fast: self.fast,
389 slow: self.slow,
390 signal: self.signal,
391 },
392 )]
393 }
394
395 fn setup(&mut self, indicators: &HashMap<String, Vec<Option<f64>>>) {
396 if let Some(v) = indicators.get(&self.line_key) {
397 self.line_slot.set(v);
398 }
399 if let Some(v) = indicators.get(&self.sig_key) {
400 self.sig_slot.set(v);
401 }
402 }
403
404 fn warmup_period(&self) -> usize {
405 self.slow + self.signal
406 }
407
408 fn on_candle(&self, ctx: &StrategyContext) -> Signal {
409 let candle = ctx.current_candle();
410 let i = ctx.index;
411 if i == 0 {
412 return Signal::hold();
413 }
414
415 let line_vals =
417 unsafe { self.line_slot.get() }.or_else(|| ctx.indicators.get(&self.line_key));
418 let sig_vals = unsafe { self.sig_slot.get() }.or_else(|| ctx.indicators.get(&self.sig_key));
419 let (Some(line_vals), Some(sig_vals)) = (line_vals, sig_vals) else {
420 return Signal::hold();
421 };
422
423 let get = |vals: &Vec<Option<f64>>, idx: usize| vals.get(idx).and_then(|&v| v);
424 let (Some(ln), Some(sn), Some(lp), Some(sp)) = (
425 get(line_vals, i),
426 get(sig_vals, i),
427 get(line_vals, i - 1),
428 get(sig_vals, i - 1),
429 ) else {
430 return Signal::hold();
431 };
432
433 if lp <= sp && ln > sn {
436 if ctx.is_short() {
437 return Signal::exit(candle.timestamp, candle.close)
438 .with_reason("MACD bullish crossover - close short");
439 }
440 if !ctx.has_position() {
441 return Signal::long(candle.timestamp, candle.close)
442 .with_reason("MACD bullish crossover");
443 }
444 }
445
446 if lp >= sp && ln < sn {
448 if ctx.is_long() {
449 return Signal::exit(candle.timestamp, candle.close)
450 .with_reason("MACD bearish crossover - close long");
451 }
452 if !ctx.has_position() {
453 return Signal::short(candle.timestamp, candle.close)
454 .with_reason("MACD bearish crossover");
455 }
456 }
457
458 Signal::hold()
459 }
460}
461
462#[derive(Debug, Clone)]
477pub struct BollingerMeanReversion {
478 pub period: usize,
480 pub std_dev: f64,
482 pub exit_at_middle: bool,
484 lower_key: String,
485 middle_key: String,
486 upper_key: String,
487 lower_slot: IndicatorSlot,
488 middle_slot: IndicatorSlot,
489 upper_slot: IndicatorSlot,
490}
491
492impl BollingerMeanReversion {
493 pub fn new(period: usize, std_dev: f64) -> Self {
495 Self {
496 period,
497 std_dev,
498 exit_at_middle: true,
499 lower_key: format!("bollinger_lower_{period}_{std_dev}"),
500 middle_key: format!("bollinger_middle_{period}_{std_dev}"),
501 upper_key: format!("bollinger_upper_{period}_{std_dev}"),
502 lower_slot: IndicatorSlot::default(),
503 middle_slot: IndicatorSlot::default(),
504 upper_slot: IndicatorSlot::default(),
505 }
506 }
507
508 pub fn exit_at_middle(mut self, at_middle: bool) -> Self {
510 self.exit_at_middle = at_middle;
511 self
512 }
513}
514
515impl Default for BollingerMeanReversion {
516 fn default() -> Self {
517 Self::new(20, 2.0)
518 }
519}
520
521impl Strategy for BollingerMeanReversion {
522 fn name(&self) -> &str {
523 "Bollinger Mean Reversion"
524 }
525
526 fn required_indicators(&self) -> Vec<(String, Indicator)> {
527 vec![(
528 "bollinger".to_string(),
529 Indicator::Bollinger {
530 period: self.period,
531 std_dev: self.std_dev,
532 },
533 )]
534 }
535
536 fn setup(&mut self, indicators: &HashMap<String, Vec<Option<f64>>>) {
537 if let Some(v) = indicators.get(&self.lower_key) {
538 self.lower_slot.set(v);
539 }
540 if let Some(v) = indicators.get(&self.middle_key) {
541 self.middle_slot.set(v);
542 }
543 if let Some(v) = indicators.get(&self.upper_key) {
544 self.upper_slot.set(v);
545 }
546 }
547
548 fn warmup_period(&self) -> usize {
549 self.period
550 }
551
552 fn on_candle(&self, ctx: &StrategyContext) -> Signal {
553 let candle = ctx.current_candle();
554 let close = candle.close;
555 let i = ctx.index;
556
557 let lower_vals =
559 unsafe { self.lower_slot.get() }.or_else(|| ctx.indicators.get(&self.lower_key));
560 let middle_vals =
561 unsafe { self.middle_slot.get() }.or_else(|| ctx.indicators.get(&self.middle_key));
562 let upper_vals =
563 unsafe { self.upper_slot.get() }.or_else(|| ctx.indicators.get(&self.upper_key));
564 let (Some(lower_vals), Some(middle_vals), Some(upper_vals)) =
565 (lower_vals, middle_vals, upper_vals)
566 else {
567 return Signal::hold();
568 };
569
570 let get = |vals: &Vec<Option<f64>>, idx: usize| vals.get(idx).and_then(|&v| v);
571 let (Some(lower_val), Some(middle_val), Some(upper_val)) =
572 (get(lower_vals, i), get(middle_vals, i), get(upper_vals, i))
573 else {
574 return Signal::hold();
575 };
576
577 if close <= lower_val && !ctx.has_position() {
579 return Signal::long(candle.timestamp, close)
580 .with_reason("Price at lower Bollinger Band");
581 }
582
583 if ctx.is_long() {
585 let exit_level = if self.exit_at_middle {
586 middle_val
587 } else {
588 upper_val
589 };
590 if close >= exit_level {
591 return Signal::exit(candle.timestamp, close).with_reason(format!(
592 "Price reached {} Bollinger Band",
593 if self.exit_at_middle {
594 "middle"
595 } else {
596 "upper"
597 }
598 ));
599 }
600 }
601
602 if close >= upper_val && !ctx.has_position() {
604 return Signal::short(candle.timestamp, close)
605 .with_reason("Price at upper Bollinger Band");
606 }
607
608 if ctx.is_short() {
610 let exit_level = if self.exit_at_middle {
611 middle_val
612 } else {
613 lower_val
614 };
615 if close <= exit_level {
616 return Signal::exit(candle.timestamp, close).with_reason(format!(
617 "Price reached {} Bollinger Band",
618 if self.exit_at_middle {
619 "middle"
620 } else {
621 "lower"
622 }
623 ));
624 }
625 }
626
627 Signal::hold()
628 }
629}
630
631#[derive(Debug, Clone)]
637pub struct SuperTrendFollow {
638 pub period: usize,
640 pub multiplier: f64,
642 uptrend_key: String,
643 uptrend_slot: IndicatorSlot,
644}
645
646impl SuperTrendFollow {
647 pub fn new(period: usize, multiplier: f64) -> Self {
649 Self {
650 period,
651 multiplier,
652 uptrend_key: format!("supertrend_uptrend_{period}_{multiplier}"),
653 uptrend_slot: IndicatorSlot::default(),
654 }
655 }
656}
657
658impl Default for SuperTrendFollow {
659 fn default() -> Self {
660 Self::new(10, 3.0)
661 }
662}
663
664impl Strategy for SuperTrendFollow {
665 fn name(&self) -> &str {
666 "SuperTrend Follow"
667 }
668
669 fn required_indicators(&self) -> Vec<(String, Indicator)> {
670 vec![(
671 "supertrend".to_string(),
672 Indicator::Supertrend {
673 period: self.period,
674 multiplier: self.multiplier,
675 },
676 )]
677 }
678
679 fn setup(&mut self, indicators: &HashMap<String, Vec<Option<f64>>>) {
680 if let Some(v) = indicators.get(&self.uptrend_key) {
681 self.uptrend_slot.set(v);
682 }
683 }
684
685 fn warmup_period(&self) -> usize {
686 self.period + 1
687 }
688
689 fn on_candle(&self, ctx: &StrategyContext) -> Signal {
690 let candle = ctx.current_candle();
691 let i = ctx.index;
692
693 let vals =
695 unsafe { self.uptrend_slot.get() }.or_else(|| ctx.indicators.get(&self.uptrend_key));
696 let Some(vals) = vals else {
697 return Signal::hold();
698 };
699 let get = |idx: usize| vals.get(idx).and_then(|&v| v);
700 let (Some(now), Some(prev)) = (get(i), if i > 0 { get(i - 1) } else { None }) else {
701 return Signal::hold();
702 };
703
704 let is_uptrend = now > 0.5;
705 let was_uptrend = prev > 0.5;
706
707 if is_uptrend && !was_uptrend {
709 if ctx.is_short() {
710 return Signal::exit(candle.timestamp, candle.close)
711 .with_reason("SuperTrend turned bullish - close short");
712 }
713 if !ctx.has_position() {
714 return Signal::long(candle.timestamp, candle.close)
715 .with_reason("SuperTrend turned bullish");
716 }
717 }
718
719 if !is_uptrend && was_uptrend {
721 if ctx.is_long() {
722 return Signal::exit(candle.timestamp, candle.close)
723 .with_reason("SuperTrend turned bearish - close long");
724 }
725 if !ctx.has_position() {
726 return Signal::short(candle.timestamp, candle.close)
727 .with_reason("SuperTrend turned bearish");
728 }
729 }
730
731 Signal::hold()
732 }
733}
734
735#[derive(Debug, Clone)]
742pub struct DonchianBreakout {
743 pub period: usize,
745 pub exit_at_middle: bool,
747 upper_key: String,
748 middle_key: String,
749 lower_key: String,
750 upper_slot: IndicatorSlot,
751 middle_slot: IndicatorSlot,
752 lower_slot: IndicatorSlot,
753}
754
755impl DonchianBreakout {
756 pub fn new(period: usize) -> Self {
758 Self {
759 period,
760 exit_at_middle: true,
761 upper_key: format!("donchian_upper_{period}"),
762 middle_key: format!("donchian_middle_{period}"),
763 lower_key: format!("donchian_lower_{period}"),
764 upper_slot: IndicatorSlot::default(),
765 middle_slot: IndicatorSlot::default(),
766 lower_slot: IndicatorSlot::default(),
767 }
768 }
769
770 pub fn exit_at_middle(mut self, at_middle: bool) -> Self {
772 self.exit_at_middle = at_middle;
773 self
774 }
775}
776
777impl Default for DonchianBreakout {
778 fn default() -> Self {
779 Self::new(20)
780 }
781}
782
783impl Strategy for DonchianBreakout {
784 fn name(&self) -> &str {
785 "Donchian Breakout"
786 }
787
788 fn required_indicators(&self) -> Vec<(String, Indicator)> {
789 vec![(
790 "donchian".to_string(),
791 Indicator::DonchianChannels(self.period),
792 )]
793 }
794
795 fn setup(&mut self, indicators: &HashMap<String, Vec<Option<f64>>>) {
796 if let Some(v) = indicators.get(&self.upper_key) {
797 self.upper_slot.set(v);
798 }
799 if let Some(v) = indicators.get(&self.middle_key) {
800 self.middle_slot.set(v);
801 }
802 if let Some(v) = indicators.get(&self.lower_key) {
803 self.lower_slot.set(v);
804 }
805 }
806
807 fn warmup_period(&self) -> usize {
808 self.period
809 }
810
811 fn on_candle(&self, ctx: &StrategyContext) -> Signal {
812 let candle = ctx.current_candle();
813 let close = candle.close;
814 let i = ctx.index;
815
816 let upper_vals =
818 unsafe { self.upper_slot.get() }.or_else(|| ctx.indicators.get(&self.upper_key));
819 let middle_vals =
820 unsafe { self.middle_slot.get() }.or_else(|| ctx.indicators.get(&self.middle_key));
821 let lower_vals =
822 unsafe { self.lower_slot.get() }.or_else(|| ctx.indicators.get(&self.lower_key));
823 let (Some(upper_vals), Some(middle_vals), Some(lower_vals)) =
824 (upper_vals, middle_vals, lower_vals)
825 else {
826 return Signal::hold();
827 };
828 let get = |vals: &Vec<Option<f64>>, idx: usize| vals.get(idx).and_then(|&v| v);
829 let (Some(_upper_val), Some(middle_val), Some(_lower_val)) =
830 (get(upper_vals, i), get(middle_vals, i), get(lower_vals, i))
831 else {
832 return Signal::hold();
833 };
834 let prev_upper = if i > 0 { get(upper_vals, i - 1) } else { None };
835 let prev_lower = if i > 0 { get(lower_vals, i - 1) } else { None };
836
837 if let Some(prev_up) = prev_upper
845 && close > prev_up
846 {
847 if ctx.is_short() {
848 return Signal::exit(candle.timestamp, close)
849 .with_reason("Donchian upper channel breakout - close short");
850 }
851 if !ctx.has_position() {
852 return Signal::long(candle.timestamp, close)
853 .with_reason("Donchian upper channel breakout");
854 }
855 }
856
857 if let Some(prev_low) = prev_lower
860 && close < prev_low
861 {
862 if ctx.is_long() {
863 return Signal::exit(candle.timestamp, close)
864 .with_reason("Donchian lower channel breakdown - close long");
865 }
866 if !ctx.has_position() {
867 return Signal::short(candle.timestamp, close)
868 .with_reason("Donchian lower channel breakdown");
869 }
870 }
871
872 if ctx.is_long() && self.exit_at_middle && close <= middle_val {
874 return Signal::exit(candle.timestamp, close)
875 .with_reason("Price reached Donchian middle channel");
876 }
877
878 if ctx.is_short() && self.exit_at_middle && close >= middle_val {
880 return Signal::exit(candle.timestamp, close)
881 .with_reason("Price reached Donchian middle channel");
882 }
883
884 Signal::hold()
885 }
886}
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891 use crate::backtesting::{Position, PositionSide};
892 use crate::models::chart::Candle;
893
894 fn make_candle(ts: i64, close: f64) -> Candle {
895 Candle {
896 timestamp: ts,
897 open: close,
898 high: close,
899 low: close,
900 close,
901 volume: 1000,
902 adj_close: None,
903 provider_id: None,
904 }
905 }
906
907 #[test]
908 fn test_sma_crossover_default() {
909 let s = SmaCrossover::default();
910 assert_eq!(s.fast_period, 10);
911 assert_eq!(s.slow_period, 20);
912 }
913
914 #[test]
915 fn test_sma_crossover_custom() {
916 let s = SmaCrossover::new(5, 15);
917 assert_eq!(s.fast_period, 5);
918 assert_eq!(s.slow_period, 15);
919 }
920
921 #[test]
922 fn test_rsi_default() {
923 let s = RsiReversal::default();
924 assert_eq!(s.period, 14);
925 assert!((s.oversold - 30.0).abs() < 0.01);
926 assert!((s.overbought - 70.0).abs() < 0.01);
927 }
928
929 #[test]
930 fn test_rsi_with_thresholds() {
931 let s = RsiReversal::new(10).with_thresholds(25.0, 75.0);
932 assert_eq!(s.period, 10);
933 assert!((s.oversold - 25.0).abs() < 0.01);
934 assert!((s.overbought - 75.0).abs() < 0.01);
935 }
936
937 #[test]
938 fn test_macd_default() {
939 let s = MacdSignal::default();
940 assert_eq!(s.fast, 12);
941 assert_eq!(s.slow, 26);
942 assert_eq!(s.signal, 9);
943 }
944
945 #[test]
946 fn test_bollinger_default() {
947 let s = BollingerMeanReversion::default();
948 assert_eq!(s.period, 20);
949 assert!((s.std_dev - 2.0).abs() < 0.01);
950 }
951
952 #[test]
953 fn test_supertrend_default() {
954 let s = SuperTrendFollow::default();
955 assert_eq!(s.period, 10);
956 assert!((s.multiplier - 3.0).abs() < 0.01);
957 }
958
959 #[test]
960 fn test_donchian_default() {
961 let s = DonchianBreakout::default();
962 assert_eq!(s.period, 20);
963 assert!(s.exit_at_middle);
964 }
965
966 #[test]
967 fn test_strategy_names() {
968 assert_eq!(SmaCrossover::default().name(), "SMA Crossover");
969 assert_eq!(RsiReversal::default().name(), "RSI Reversal");
970 assert_eq!(MacdSignal::default().name(), "MACD Signal");
971 assert_eq!(
972 BollingerMeanReversion::default().name(),
973 "Bollinger Mean Reversion"
974 );
975 assert_eq!(SuperTrendFollow::default().name(), "SuperTrend Follow");
976 assert_eq!(DonchianBreakout::default().name(), "Donchian Breakout");
977 }
978
979 #[test]
980 fn test_required_indicators() {
981 let sma = SmaCrossover::new(5, 10);
982 let indicators = sma.required_indicators();
983 assert_eq!(indicators.len(), 2);
984 assert_eq!(indicators[0].0, "sma_5");
985 assert_eq!(indicators[1].0, "sma_10");
986
987 let rsi = RsiReversal::new(14);
988 let indicators = rsi.required_indicators();
989 assert_eq!(indicators.len(), 1);
990 assert_eq!(indicators[0].0, "rsi_14");
991 }
992
993 #[test]
994 fn test_sma_crossover_fires_on_touch() {
995 use crate::backtesting::signal::SignalDirection;
996
997 let strategy = SmaCrossover::new(5, 10);
998 let candles = vec![make_candle(1, 100.0), make_candle(2, 101.0)];
999 let mut indicators = HashMap::new();
1000 indicators.insert("sma_5".to_string(), vec![Some(10.0), Some(11.0)]);
1001 indicators.insert("sma_10".to_string(), vec![Some(10.0), Some(10.0)]);
1002
1003 let ctx = StrategyContext {
1004 candles: &candles,
1005 index: 1,
1006 position: None,
1007 equity: 10_000.0,
1008 indicators: &indicators,
1009 extremes: None,
1010 indicator_index: None,
1011 };
1012
1013 let signal = strategy.on_candle(&ctx);
1015 assert_eq!(signal.direction, SignalDirection::Long);
1016 }
1017
1018 #[test]
1019 fn test_donchian_short_exits_on_upper_breakout_when_exit_at_middle_false() {
1020 let strategy = DonchianBreakout::new(3).exit_at_middle(false);
1021
1022 let candles = vec![make_candle(1, 100.0), make_candle(2, 115.0)];
1023 let mut indicators = HashMap::new();
1024 indicators.insert(
1025 "donchian_upper_3".to_string(),
1026 vec![Some(110.0), Some(112.0)],
1027 );
1028 indicators.insert(
1029 "donchian_middle_3".to_string(),
1030 vec![Some(100.0), Some(101.0)],
1031 );
1032 indicators.insert("donchian_lower_3".to_string(), vec![Some(90.0), Some(91.0)]);
1033
1034 let position = Position::new(
1035 PositionSide::Short,
1036 1,
1037 105.0,
1038 10.0,
1039 0.0,
1040 Signal::short(1, 105.0),
1041 );
1042
1043 let ctx = StrategyContext {
1044 candles: &candles,
1045 index: 1,
1046 position: Some(&position),
1047 equity: 10_000.0,
1048 indicators: &indicators,
1049 extremes: None,
1050 indicator_index: None,
1051 };
1052
1053 let signal = strategy.on_candle(&ctx);
1054 assert!(signal.is_exit());
1055 }
1056
1057 #[test]
1058 fn test_rsi_strength_grades_on_precross_extreme() {
1059 let strategy = RsiReversal::new(14);
1060
1061 let candles = vec![make_candle(1, 100.0), make_candle(2, 101.0)];
1062
1063 let mut deep_indicators = HashMap::new();
1064 deep_indicators.insert("rsi_14".to_string(), vec![Some(15.0), Some(31.0)]);
1065 let deep_ctx = StrategyContext {
1066 candles: &candles,
1067 index: 1,
1068 position: None,
1069 equity: 10_000.0,
1070 indicators: &deep_indicators,
1071 extremes: None,
1072 indicator_index: None,
1073 };
1074 let deep_signal = strategy.on_candle(&deep_ctx);
1075 assert_eq!(deep_signal.strength, SignalStrength::strong());
1076
1077 let mut shallow_indicators = HashMap::new();
1078 shallow_indicators.insert("rsi_14".to_string(), vec![Some(28.0), Some(31.0)]);
1079 let shallow_ctx = StrategyContext {
1080 candles: &candles,
1081 index: 1,
1082 position: None,
1083 equity: 10_000.0,
1084 indicators: &shallow_indicators,
1085 extremes: None,
1086 indicator_index: None,
1087 };
1088 let shallow_signal = strategy.on_candle(&shallow_ctx);
1089 assert_eq!(shallow_signal.strength, SignalStrength::weak());
1090 }
1091}