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
100
101
|
"""Tests for the Bollinger Bands strategy."""
from datetime import datetime, timezone
from decimal import Decimal
import pytest
from shared.models import Candle, OrderSide
from strategies.bollinger_strategy import BollingerStrategy
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() -> BollingerStrategy:
s = BollingerStrategy()
s.configure({"period": 5, "num_std": 1.0, "min_bandwidth": 0.0})
return s
def test_bollinger_warmup_period():
strategy = BollingerStrategy()
strategy.configure({"period": 20})
assert strategy.warmup_period == 20
strategy.configure({"period": 5})
assert strategy.warmup_period == 5
def test_bollinger_no_signal_insufficient_data():
strategy = _make_strategy()
# Feed fewer candles than the period
for price in [100.0, 101.0, 102.0, 101.0]:
result = strategy.on_candle(make_candle(price))
assert result is None
def test_bollinger_buy_on_lower_band_recovery():
strategy = _make_strategy()
# Feed stable prices to build up the window
for _ in range(5):
strategy.on_candle(make_candle(100.0))
# Drop well below the lower band
signal = strategy.on_candle(make_candle(50.0))
# No buy yet -- still below lower band
assert signal is None
# Recover back inside the bands
signal = strategy.on_candle(make_candle(100.0))
assert signal is not None
assert signal.side == OrderSide.BUY
assert "lower" in signal.reason.lower() or "bollinger" in signal.reason.lower()
def test_bollinger_sell_on_upper_band_recovery():
strategy = _make_strategy()
# Feed stable prices to build up the window
for _ in range(5):
strategy.on_candle(make_candle(100.0))
# Spike well above the upper band
signal = strategy.on_candle(make_candle(150.0))
# No sell yet -- still above upper band
assert signal is None
# Recover back inside the bands
signal = strategy.on_candle(make_candle(100.0))
assert signal is not None
assert signal.side == OrderSide.SELL
assert "upper" in signal.reason.lower() or "bollinger" in signal.reason.lower()
def test_bollinger_reset_clears_state():
strategy = _make_strategy()
# Build some state
for _ in range(5):
strategy.on_candle(make_candle(100.0))
strategy.on_candle(make_candle(50.0)) # penetrate lower band
strategy.reset()
# After reset, insufficient data again
result = strategy.on_candle(make_candle(100.0))
assert result is None
# Internal state should be cleared
assert len(strategy._closes) == 1
assert strategy._was_below_lower is False
assert strategy._was_above_upper is False
|