summaryrefslogtreecommitdiff
path: root/services/strategy-engine/strategies/macd_strategy.py
diff options
context:
space:
mode:
Diffstat (limited to 'services/strategy-engine/strategies/macd_strategy.py')
-rw-r--r--services/strategy-engine/strategies/macd_strategy.py75
1 files changed, 75 insertions, 0 deletions
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