1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
"""Tests for the EMA Crossover strategy."""
from datetime import datetime, timezone
from decimal import Decimal
from shared.models import Candle, OrderSide
from strategies.ema_crossover_strategy import EmaCrossoverStrategy
def make_candle(close: float) -> Candle:
return Candle(
symbol="BTC/USDT",
timeframe="1m",
open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
open=Decimal(str(close)),
high=Decimal(str(close)),
low=Decimal(str(close)),
close=Decimal(str(close)),
volume=Decimal("1.0"),
)
def _make_strategy(short: int = 3, long: int = 6) -> EmaCrossoverStrategy:
s = EmaCrossoverStrategy()
s.configure({"short_period": short, "long_period": long, "quantity": "0.01"})
return s
def test_ema_warmup_period():
strategy = _make_strategy(short=3, long=6)
assert strategy.warmup_period == 6
def test_ema_no_signal_insufficient_data():
strategy = _make_strategy(short=3, long=6)
# Feed fewer candles than warmup_period
for price in [100, 101, 102, 103, 104]:
result = strategy.on_candle(make_candle(price))
assert result is None
def test_ema_buy_signal_golden_cross():
strategy = _make_strategy(short=3, long=6)
# Declining prices so short EMA stays below long EMA
declining = [100, 98, 96, 94, 92, 90, 88, 86, 84, 82]
for price in declining:
strategy.on_candle(make_candle(price))
# Sharp rise to force short EMA above long EMA (golden cross)
rising = [120, 140, 160]
signal = None
for price in rising:
result = strategy.on_candle(make_candle(price))
if result is not None:
signal = result
assert signal is not None
assert signal.side == OrderSide.BUY
assert "Golden Cross" in signal.reason
def test_ema_sell_signal_death_cross():
strategy = _make_strategy(short=3, long=6)
# Rising prices so short EMA stays above long EMA
rising = [80, 82, 84, 86, 88, 90, 92, 94, 96, 98]
for price in rising:
strategy.on_candle(make_candle(price))
# Sharp decline to force short EMA below long EMA (death cross)
declining = [60, 40, 20]
signal = None
for price in declining:
result = strategy.on_candle(make_candle(price))
if result is not None:
signal = result
assert signal is not None
assert signal.side == OrderSide.SELL
assert "Death Cross" in signal.reason
def test_ema_reset_clears_state():
strategy = _make_strategy(short=3, long=6)
# Feed enough data to produce a signal
for price in [100, 98, 96, 94, 92, 90, 88, 86, 84, 82]:
strategy.on_candle(make_candle(price))
strategy.reset()
# After reset, insufficient data again — no signal
result = strategy.on_candle(make_candle(100))
assert result is None
# Internal state should be cleared
assert len(strategy._closes) == 1
assert strategy._prev_short_above is None
|