56 lines
1.5 KiB
Python
56 lines
1.5 KiB
Python
"""Mean reversion strategy — buy oversold, sell overbought using RSI."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from shared.schemas.trading import MarketSnapshot, SentimentContext, SignalDirection, TradeSignal
|
|
from shared.strategies.base import BaseStrategy
|
|
|
|
|
|
class MeanReversionStrategy(BaseStrategy):
|
|
"""Trade on the assumption that extreme RSI readings will revert to the mean.
|
|
|
|
**Buy signal** (LONG):
|
|
RSI < 30 (oversold).
|
|
|
|
**Sell signal** (SHORT):
|
|
RSI > 70 (overbought).
|
|
|
|
Signal strength is proportional to how far the RSI is from its
|
|
threshold, clamped to [0, 1].
|
|
|
|
* Buy strength = ``(30 - rsi) / 30``
|
|
* Sell strength = ``(rsi - 70) / 30``
|
|
"""
|
|
|
|
name: str = "mean_reversion"
|
|
|
|
async def evaluate(
|
|
self,
|
|
ticker: str,
|
|
market: MarketSnapshot,
|
|
sentiment: SentimentContext | None = None,
|
|
) -> TradeSignal | None:
|
|
if market.rsi is None:
|
|
return None
|
|
|
|
rsi = market.rsi
|
|
|
|
if rsi < 30:
|
|
direction = SignalDirection.LONG
|
|
raw_strength = (30 - rsi) / 30
|
|
elif rsi > 70:
|
|
direction = SignalDirection.SHORT
|
|
raw_strength = (rsi - 70) / 30
|
|
else:
|
|
# RSI in neutral territory — no opinion.
|
|
return None
|
|
|
|
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),
|
|
)
|