trading/shared/strategies/momentum.py

61 lines
2 KiB
Python

"""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),
)