63 lines
1.8 KiB
Python
63 lines
1.8 KiB
Python
"""Value strategy signal (§3.3 — Value).
|
|
|
|
Composite value score from Book/Market, Earnings/Price, and CashFlow/Price.
|
|
|
|
Reference:
|
|
Kakushadze & Serur, "151 Trading Strategies", §3.3
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from .base import BaseStrategy, StrategySignal
|
|
from ._data import get_info
|
|
|
|
|
|
class ValueStrategy(BaseStrategy):
|
|
name = "Value (§3.3)"
|
|
roles = ["fundamentals", "researcher"]
|
|
|
|
def compute(self, ticker: str, date: str, context: dict[str, Any] | None = None) -> StrategySignal | None:
|
|
info = get_info(ticker, context)
|
|
if not info:
|
|
return None
|
|
|
|
scores: list[float] = []
|
|
|
|
# Book/Market (inverse of P/B)
|
|
pb = info.get("priceToBook")
|
|
if pb and pb > 0:
|
|
bm = 1.0 / pb
|
|
scores.append(min(bm, 3.0) / 3.0) # normalize: BM=3 → 1.0
|
|
|
|
# Earnings/Price (inverse of trailing PE)
|
|
pe = info.get("trailingPE")
|
|
if pe and pe > 0:
|
|
ep = 1.0 / pe
|
|
scores.append(min(ep, 0.15) / 0.15)
|
|
|
|
# Free Cash Flow yield proxy
|
|
mcap = info.get("marketCap")
|
|
fcf = info.get("freeCashflow")
|
|
if mcap and fcf and mcap > 0:
|
|
cfy = fcf / mcap
|
|
scores.append(max(-1.0, min(cfy / 0.10, 1.0)))
|
|
|
|
if not scores:
|
|
return None
|
|
|
|
composite = sum(scores) / len(scores)
|
|
# Map [0,1] → [-1,1]: high value = bullish
|
|
strength = max(-1.0, min(1.0, composite * 2 - 1))
|
|
direction = "bullish" if strength > 0.1 else ("bearish" if strength < -0.1 else "neutral")
|
|
|
|
return StrategySignal(
|
|
name=self.name,
|
|
ticker=ticker,
|
|
date=date,
|
|
signal_strength=round(strength, 4),
|
|
direction=direction,
|
|
detail=f"Composite value score: {composite:.2f} from {len(scores)} factors",
|
|
)
|