summaryrefslogtreecommitdiff
path: root/services/strategy-engine/tests/test_ema_crossover_strategy.py
blob: 5a403190846d984ad5171fb57e890f6e1d6be660 (plain)
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

import pytest

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