From 2faec2f1f1631bd286a5c55051e582f8abe2898c Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:16:03 +0900 Subject: feat(strategy): add MACD strategy --- .../strategies/config/macd_strategy.yaml | 4 ++ .../strategy-engine/strategies/macd_strategy.py | 75 ++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 services/strategy-engine/strategies/config/macd_strategy.yaml create mode 100644 services/strategy-engine/strategies/macd_strategy.py (limited to 'services/strategy-engine/strategies') diff --git a/services/strategy-engine/strategies/config/macd_strategy.yaml b/services/strategy-engine/strategies/config/macd_strategy.yaml new file mode 100644 index 0000000..9d55c39 --- /dev/null +++ b/services/strategy-engine/strategies/config/macd_strategy.yaml @@ -0,0 +1,4 @@ +fast_period: 12 +slow_period: 26 +signal_period: 9 +quantity: "0.01" diff --git a/services/strategy-engine/strategies/macd_strategy.py b/services/strategy-engine/strategies/macd_strategy.py new file mode 100644 index 0000000..049574e --- /dev/null +++ b/services/strategy-engine/strategies/macd_strategy.py @@ -0,0 +1,75 @@ +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 MacdStrategy(BaseStrategy): + name: str = "macd" + + def __init__(self) -> None: + self._fast_period: int = 12 + self._slow_period: int = 26 + self._signal_period: int = 9 + self._quantity: Decimal = Decimal("0.01") + self._closes: deque[float] = deque(maxlen=500) + self._prev_histogram: float | None = None + + @property + def warmup_period(self) -> int: + return self._slow_period + self._signal_period + + def configure(self, params: dict) -> None: + self._fast_period = int(params.get("fast_period", 12)) + self._slow_period = int(params.get("slow_period", 26)) + self._signal_period = int(params.get("signal_period", 9)) + self._quantity = Decimal(str(params.get("quantity", "0.01"))) + + def reset(self) -> None: + self._closes.clear() + self._prev_histogram = None + + def on_candle(self, candle: Candle) -> Signal | None: + self._closes.append(float(candle.close)) + + if len(self._closes) < self.warmup_period: + return None + + series = pd.Series(list(self._closes)) + + fast_ema = series.ewm(span=self._fast_period, adjust=False).mean() + slow_ema = series.ewm(span=self._slow_period, adjust=False).mean() + macd_line = fast_ema - slow_ema + signal_line = macd_line.ewm(span=self._signal_period, adjust=False).mean() + histogram = macd_line - signal_line + + current_histogram = float(histogram.iloc[-1]) + signal = None + + if self._prev_histogram is not None: + # Bullish crossover: histogram crosses from negative to positive + if self._prev_histogram <= 0 and current_histogram > 0: + signal = Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.BUY, + price=candle.close, + quantity=self._quantity, + reason=f"MACD bullish crossover: histogram {self._prev_histogram:.6f} -> {current_histogram:.6f}", + ) + # Bearish crossover: histogram crosses from positive to negative + elif self._prev_histogram >= 0 and current_histogram < 0: + signal = Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.SELL, + price=candle.close, + quantity=self._quantity, + reason=f"MACD bearish crossover: histogram {self._prev_histogram:.6f} -> {current_histogram:.6f}", + ) + + self._prev_histogram = current_histogram + return signal -- cgit v1.2.3