[docs]classMACDStrategy(BaseStrategy):""" A Moving Average Convergence Divergence (MACD) trading strategy. Generates BUY signals when the MACD line crosses above its Signal line. Generates SELL signals when the MACD line crosses below its Signal line. :param symbol: The financial instrument symbol this strategy will trade. :param fast_period: The period for the fast Exponential Moving Average (EMA) (e.g., 12). :param slow_period: The period for the slow Exponential Moving Average (EMA) (e.g., 26). :param signal_period: The period for the EMA of the MACD line (Signal Line) (e.g., 9). :param kwargs: Arbitrary keyword arguments passed to the base strategy. """def__init__(self,symbol:str,fast_period:int=12,slow_period:int=26,signal_period:int=9,**kwargs:Any):super().__init__(symbol,**kwargs)ifnot(1<=fast_period<slow_period):raiseValueError("Fast period must be less than slow period and positive.")ifsignal_period<=0:raiseValueError("Signal period must be positive.")self.fast_period=fast_periodself.slow_period=slow_periodself.signal_period=signal_periodself._closes_history=deque(maxlen=self.slow_period)self._macd_history=deque(maxlen=self.signal_period)self._prev_fast_ema:Optional[Decimal]=Noneself._prev_slow_ema:Optional[Decimal]=Noneself._prev_signal_line:Optional[Decimal]=Noneself._has_position=Falselogging.info(f"MACDStrategy initialized for {self.symbol} with Fast={fast_period}, "f"Slow={slow_period}, Signal={signal_period}")def_calculate_ema(self,prices:deque,period:int,prev_ema:Optional[Decimal])->Decimal:""" Calculates the Exponential Moving Average (EMA). Uses simple average for the initial EMA. """ifnotprices:returnDecimal("0")# Smoothing factork=Decimal("2")/Decimal(period+1)ifprev_emaisNone:iflen(prices)<period:returnDecimal("0")initial_sma=sum(list(prices)[-period:],Decimal("0"))/Decimal(period)returninitial_smaelse:# EMA formula: (Current_Price - Previous_EMA) * K + Previous_EMAreturn(prices[-1]-prev_ema)*k+prev_ema
[docs]defon_market_event(self,event:MarketEvent):""" Handles incoming market events to update MACD indicator and generate trading signals. :param event: The MarketEvent containing new market data. """ifevent.symbol!=self.symbol:returncurrent_close=Decimal(str(event.data["close"]))self._closes_history.append(current_close)iflen(self._closes_history)<self.slow_period:logging.debug(f"Not enough history for {self.symbol} on {event.timestamp.date()}. Need {self.slow_period} closes for initial MACD. Have {len(self._closes_history)}.")return# Calculate Fast and Slow EMAcurrent_fast_ema=self._calculate_ema(self._closes_history,self.fast_period,self._prev_fast_ema)ifcurrent_fast_ema==Decimal("0")andlen(self._closes_history)<self.fast_period:returncurrent_slow_ema=self._calculate_ema(self._closes_history,self.slow_period,self._prev_slow_ema)ifcurrent_slow_ema==Decimal("0")andlen(self._closes_history)<self.slow_period:return# Update previous EMAs for next iterationself._prev_fast_ema=current_fast_emaself._prev_slow_ema=current_slow_ema# Calculate MACD Linemacd_line=current_fast_ema-current_slow_emaself._macd_history.append(macd_line)iflen(self._macd_history)<self.signal_period:logging.debug(f"Not enough MACD history for {self.symbol} on {event.timestamp.date()}. Need {self.signal_period} MACD values for initial Signal Line.")returnsignal_line=self._calculate_ema(self._macd_history,self.signal_period,self._prev_signal_line)ifsignal_line==Decimal("0")andlen(self._macd_history)<self.signal_period:returnself._prev_signal_line=signal_lineifmacd_line>signal_lineandnotself._has_position:self._put_signal_event(event.timestamp,Signal.BUY)self._has_position=Truelogging.info(f"MACD BUY signal for {self.symbol} at {event.timestamp.date()}. "f"MACD: {macd_line:.4f} > Signal: {signal_line:.4f}")elifmacd_line<signal_lineandself._has_position:self._put_signal_event(event.timestamp,Signal.SELL)self._has_position=Falselogging.info(f"MACD SELL signal for {self.symbol} at {event.timestamp.date()}. "f"MACD: {macd_line:.4f} < Signal: {signal_line:.4f}")else:pass