diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-04-01 16:17:04 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-04-01 16:17:04 +0900 |
| commit | bdffabc630c0cc296fc164d5fa2ca8569626fd7e (patch) | |
| tree | 563b6ee6960f1105a8aa0840f85f92805fd96a97 /services/strategy-engine/strategies | |
| parent | 49e5baaebf2f9ca1ba7b85a80c3451c5789edde4 (diff) | |
feat(strategy): add Bollinger Bands strategy
Diffstat (limited to 'services/strategy-engine/strategies')
| -rw-r--r-- | services/strategy-engine/strategies/bollinger_strategy.py | 86 | ||||
| -rw-r--r-- | services/strategy-engine/strategies/config/bollinger_strategy.yaml | 4 |
2 files changed, 90 insertions, 0 deletions
diff --git a/services/strategy-engine/strategies/bollinger_strategy.py b/services/strategy-engine/strategies/bollinger_strategy.py new file mode 100644 index 0000000..bee7ee4 --- /dev/null +++ b/services/strategy-engine/strategies/bollinger_strategy.py @@ -0,0 +1,86 @@ +from collections import deque +from decimal import Decimal + +import pandas as pd + +from shared.models import Candle, Signal, OrderSide +from strategies.base import BaseStrategy + + +class BollingerStrategy(BaseStrategy): + name: str = "bollinger" + + def __init__(self) -> None: + self._closes: deque[float] = deque(maxlen=500) + self._period: int = 20 + self._num_std: float = 2.0 + self._min_bandwidth: float = 0.02 + self._quantity: Decimal = Decimal("0.01") + self._was_below_lower: bool = False + self._was_above_upper: bool = False + + @property + def warmup_period(self) -> int: + return self._period + + def configure(self, params: dict) -> None: + self._period = int(params.get("period", 20)) + self._num_std = float(params.get("num_std", 2.0)) + self._min_bandwidth = float(params.get("min_bandwidth", 0.02)) + self._quantity = Decimal(str(params.get("quantity", "0.01"))) + + def reset(self) -> None: + self._closes.clear() + self._was_below_lower = False + self._was_above_upper = False + + def on_candle(self, candle: Candle) -> Signal | None: + self._closes.append(float(candle.close)) + + if len(self._closes) < self._period: + return None + + series = pd.Series(list(self._closes)) + sma = series.rolling(window=self._period).mean().iloc[-1] + std = series.rolling(window=self._period).std().iloc[-1] + + upper = sma + self._num_std * std + lower = sma - self._num_std * std + + # Bandwidth filter: skip sideways markets + if sma != 0 and (upper - lower) / sma < self._min_bandwidth: + return None + + price = float(candle.close) + + # Track band penetration + if price < lower: + self._was_below_lower = True + if price > upper: + self._was_above_upper = True + + # BUY: was below lower band and recovered back inside + if self._was_below_lower and price >= lower: + self._was_below_lower = False + return Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.BUY, + price=candle.close, + quantity=self._quantity, + reason=f"Price recovered above lower Bollinger Band ({lower:.2f})", + ) + + # SELL: was above upper band and recovered back inside + if self._was_above_upper and price <= upper: + self._was_above_upper = False + return Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.SELL, + price=candle.close, + quantity=self._quantity, + reason=f"Price recovered below upper Bollinger Band ({upper:.2f})", + ) + + return None diff --git a/services/strategy-engine/strategies/config/bollinger_strategy.yaml b/services/strategy-engine/strategies/config/bollinger_strategy.yaml new file mode 100644 index 0000000..153cf81 --- /dev/null +++ b/services/strategy-engine/strategies/config/bollinger_strategy.yaml @@ -0,0 +1,4 @@ +period: 20 +num_std: 2.0 +min_bandwidth: 0.02 +quantity: "0.01" |
