"""Momentum trading strategy — trend-following based on moving averages.""" from datetime import datetime, timezone from shared.schemas.trading import MarketSnapshot, SentimentContext, SignalDirection, TradeSignal from shared.strategies.base import BaseStrategy class MomentumStrategy(BaseStrategy): """Detect and follow momentum via simple moving average cross-overs. **Buy signal** (LONG): ``current_price > sma_20`` AND ``sma_20 > sma_50`` (golden cross / uptrend) AND volume above the daily open (simple proxy for above- average volume). **Sell signal** (SHORT): ``current_price < sma_20`` AND ``sma_20 < sma_50`` (death cross / downtrend). Signal strength is proportional to the normalised distance between the current price and the 20-period SMA, clamped to [0, 1]. """ name: str = "momentum" async def evaluate( self, ticker: str, market: MarketSnapshot, sentiment: SentimentContext | None = None, ) -> TradeSignal | None: # Require both moving averages to be present. if market.sma_20 is None or market.sma_50 is None: return None price = market.current_price sma_20 = market.sma_20 sma_50 = market.sma_50 direction: SignalDirection | None = None if price > sma_20 and sma_20 > sma_50: direction = SignalDirection.LONG elif price < sma_20 and sma_20 < sma_50: direction = SignalDirection.SHORT else: # No clear trend — abstain. return None # Strength: normalised distance from SMA-20, clamped to [0, 1]. raw_strength = abs(price - sma_20) / sma_20 if sma_20 != 0 else 0.0 strength = max(0.0, min(1.0, raw_strength)) return TradeSignal( ticker=ticker, direction=direction, strength=strength, strategy_sources=[self.name], timestamp=datetime.now(tz=timezone.utc), )