98 lines
2.6 KiB
Python
98 lines
2.6 KiB
Python
"""Value strategy — trade on fundamental valuation metrics."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from shared.schemas.trading import MarketSnapshot, SentimentContext, SignalDirection, TradeSignal
|
|
from shared.strategies.base import BaseStrategy
|
|
|
|
|
|
class ValueStrategy(BaseStrategy):
|
|
"""Generate signals from fundamental financial data.
|
|
|
|
Computes a composite score from PEG ratio, P/E ratio, EPS, revenue
|
|
growth, profit margin, and debt-to-equity ratio.
|
|
|
|
**Buy signal** (LONG):
|
|
Composite score > 0.3 (undervalued).
|
|
|
|
**Sell signal** (SHORT):
|
|
Composite score < -0.3 (overvalued).
|
|
|
|
Signal strength = ``abs(score) / 2.0``, clamped to [0, 1].
|
|
"""
|
|
|
|
name: str = "value"
|
|
|
|
async def evaluate(
|
|
self,
|
|
ticker: str,
|
|
market: MarketSnapshot,
|
|
sentiment: SentimentContext | None = None,
|
|
) -> TradeSignal | None:
|
|
if market.fundamentals is None:
|
|
return None
|
|
|
|
f = market.fundamentals
|
|
|
|
if f.peg_ratio is None or f.pe_ratio is None:
|
|
return None
|
|
|
|
score = 0.0
|
|
|
|
# PEG ratio scoring
|
|
if f.peg_ratio < 1.0:
|
|
score += 0.3
|
|
elif f.peg_ratio > 3.0:
|
|
score -= 0.3
|
|
|
|
# P/E ratio scoring
|
|
if f.pe_ratio < 15:
|
|
score += 0.3
|
|
elif f.pe_ratio > 40:
|
|
score -= 0.3
|
|
|
|
# EPS scoring
|
|
if f.eps_ttm is not None:
|
|
if f.eps_ttm > 0:
|
|
score += 0.2
|
|
elif f.eps_ttm < 0:
|
|
score -= 0.3
|
|
|
|
# Revenue growth scoring
|
|
if f.revenue_growth_yoy is not None:
|
|
if f.revenue_growth_yoy > 0.1:
|
|
score += 0.2
|
|
elif f.revenue_growth_yoy < -0.1:
|
|
score -= 0.2
|
|
|
|
# Profit margin scoring
|
|
if f.profit_margin is not None:
|
|
if f.profit_margin > 0.15:
|
|
score += 0.1
|
|
elif f.profit_margin < 0:
|
|
score -= 0.2
|
|
|
|
# Debt-to-equity scoring
|
|
if f.debt_to_equity is not None:
|
|
if f.debt_to_equity > 3.0:
|
|
score -= 0.2
|
|
elif f.debt_to_equity < 0.5:
|
|
score += 0.1
|
|
|
|
# Determine direction
|
|
if score > 0.3:
|
|
direction = SignalDirection.LONG
|
|
elif score < -0.3:
|
|
direction = SignalDirection.SHORT
|
|
else:
|
|
return None
|
|
|
|
strength = max(0.0, min(1.0, abs(score) / 2.0))
|
|
|
|
return TradeSignal(
|
|
ticker=ticker,
|
|
direction=direction,
|
|
strength=strength,
|
|
strategy_sources=[self.name],
|
|
timestamp=datetime.now(tz=timezone.utc),
|
|
)
|