62 lines
1.8 KiB
Python
62 lines
1.8 KiB
Python
"""Momentum trading strategy.
|
|
|
|
Buy when price crosses above N-period SMA with increasing volume.
|
|
Sell when price crosses below SMA. Signal strength is proportional
|
|
to the distance from the SMA.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from shared.schemas.trading import MarketSnapshot, SentimentContext, SignalDirection, TradeSignal
|
|
from shared.strategies.base import BaseStrategy
|
|
|
|
|
|
class MomentumStrategy(BaseStrategy):
|
|
"""Trend-following momentum strategy based on SMA crossover."""
|
|
|
|
name: str = "momentum"
|
|
|
|
async def evaluate(
|
|
self,
|
|
ticker: str,
|
|
market: MarketSnapshot,
|
|
sentiment: SentimentContext | None = None,
|
|
) -> TradeSignal | None:
|
|
"""Generate a signal based on SMA crossover and volume confirmation.
|
|
|
|
Uses the 20-period SMA by default. Signal strength is the
|
|
normalised distance from the SMA (capped at 1.0).
|
|
"""
|
|
if market.sma_20 is None or market.sma_20 == 0:
|
|
return None
|
|
|
|
price = market.current_price
|
|
sma = market.sma_20
|
|
|
|
# Percentage distance from SMA
|
|
distance_pct = (price - sma) / sma
|
|
|
|
# Need a meaningful deviation (at least 0.5%)
|
|
if abs(distance_pct) < 0.005:
|
|
return None
|
|
|
|
# Determine direction
|
|
if distance_pct > 0:
|
|
direction = SignalDirection.LONG
|
|
else:
|
|
direction = SignalDirection.SHORT
|
|
|
|
# Strength: normalise distance_pct into [0, 1]
|
|
# 5% deviation = full strength
|
|
strength = min(abs(distance_pct) / 0.05, 1.0)
|
|
|
|
return TradeSignal(
|
|
ticker=ticker,
|
|
direction=direction,
|
|
strength=round(strength, 4),
|
|
strategy_sources=[self.name],
|
|
sentiment_context=None,
|
|
timestamp=datetime.now(timezone.utc),
|
|
)
|