[docs]classBollingerBandsStrategy(BaseStrategy):""" A Bollinger Bands trading strategy. Generates BUY signals when the close price crosses below the lower band and SELL signals when it crosses above the upper band :param symbol: The financial instrument symbol this strategy will trade. :param bb_period: The period over which to calculate the Simple Moving Average (SMA) for the middle band. (e.g., 20) :param num_std_dev: The number of standard deviations for the upper and lower bands. (e.g., 2) :param kwargs: Arbitrary keyword arguments passed to the base strategy. """def__init__(self,symbol:str,bb_period:int=20,num_std_dev:Decimal=Decimal("2"),**kwargs:Any):super().__init__(symbol,**kwargs)ifbb_period<=1:raiseValueError("Bollinger Bands period must be greater than 1.")ifnum_std_dev<=0:raiseValueError("Number of standard deviations must be positive.")self.bb_period=bb_periodself.num_std_dev=num_std_devself._closes_history=deque(maxlen=self.bb_period)self._has_position=Falselogging.info(f"BollingerBandsStrategy initialized for {self.symbol} with period={bb_period}, "f"std_dev={num_std_dev}")
[docs]defon_market_event(self,event:MarketEvent):ifevent.symbol!=self.symbol:returncurrent_close=Decimal(str(event.data["close"]))self._closes_history.append(current_close)iflen(self._closes_history)<self.bb_period:logging.debug(f"Not enough history for {self.symbol} on {event.timestamp.date()}. Need {self.bb_period} closes for BB calculation. Have {len(self._closes_history)}.")returniflen(self._closes_history)<2andself.bb_period>=2:logging.debug(f"Insufficient data points for standard deviation calculation for {self.symbol} on {event.timestamp.date()}. Need at least 2.")returnmiddle_band=self._calculate_sma(self._closes_history)std_dev=self._calculate_std_dev(self._closes_history,middle_band)upper_band=middle_band+(std_dev*self.num_std_dev)lower_band=middle_band-(std_dev*self.num_std_dev)lower_band=max(Decimal("0"),lower_band)ifcurrent_close<lower_bandandnotself._has_position:self._put_signal_event(event.timestamp,Signal.BUY)self._has_position=Truelogging.info(f"BB BUY signal for {self.symbol} at {event.timestamp.date()}. "f"Close: {current_close:.2f} < Lower Band: {lower_band:.2f}")elifcurrent_close>upper_bandandself._has_position:self._put_signal_event(event.timestamp,Signal.SELL)self._has_position=Falselogging.info(f"BB SELL signal for {self.symbol} at {event.timestamp.date()}. "f"Close: {current_close:.2f} > Upper Band: {upper_band:.2f}")else:pass
def_calculate_sma(self,prices:deque)->Decimal:"""Calculates the Simple Moving Average (SMA)."""ifnotprices:returnDecimal("0")returnsum(prices,Decimal("0"))/Decimal(len(prices))def_calculate_std_dev(self,prices:deque,sma:Decimal)->Decimal:"""Calculates the standard deviation."""ifnotpricesorlen(prices)<2:returnDecimal("0")variance=sum([(p-sma)**2forpinprices],Decimal("0"))/Decimal(len(prices)-1)returnvariance.sqrt()