74 lines
2.3 KiB
Python
74 lines
2.3 KiB
Python
"""Multifactor strategy signal (§3.6 — Multifactor Models).
|
|
|
|
Combined momentum + value + quality + low-vol composite.
|
|
|
|
Reference:
|
|
Kakushadze & Serur, "151 Trading Strategies", §3.6
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
|
|
from .base import BaseStrategy, StrategySignal
|
|
from ._data import get_ohlcv, get_info
|
|
|
|
|
|
class MultifactorStrategy(BaseStrategy):
|
|
name = "Multifactor (§3.6)"
|
|
roles = ["researcher", "risk"]
|
|
|
|
def compute(self, ticker: str, date: str, context: dict[str, Any] | None = None) -> StrategySignal | None:
|
|
df = get_ohlcv(ticker, date, context)
|
|
info = get_info(ticker, context)
|
|
if df is None or len(df) < 252 or not info:
|
|
return None
|
|
|
|
factors: list[float] = []
|
|
details: list[str] = []
|
|
close = df["Close"].values
|
|
|
|
# Momentum factor: 12-1 month return
|
|
if len(close) >= 252:
|
|
mom = (close[-21] - close[-252]) / close[-252]
|
|
factors.append(max(-1.0, min(1.0, mom)))
|
|
details.append(f"mom={mom:+.2%}")
|
|
|
|
# Value factor: inverse PE
|
|
pe = info.get("trailingPE")
|
|
if pe and pe > 0:
|
|
val = min(1.0 / pe / 0.15, 1.0) * 2 - 1
|
|
factors.append(max(-1.0, min(1.0, val)))
|
|
details.append(f"val_pe={pe:.1f}")
|
|
|
|
# Quality factor: ROE
|
|
roe = info.get("returnOnEquity")
|
|
if roe is not None:
|
|
factors.append(max(-1.0, min(1.0, roe * 2)))
|
|
details.append(f"roe={roe:.2%}")
|
|
|
|
# Low-vol factor
|
|
if len(close) >= 63:
|
|
vol = float(np.std(np.diff(np.log(close[-63:]))) * np.sqrt(252))
|
|
lv = max(-1.0, min(1.0, (0.30 - vol) / 0.30))
|
|
factors.append(lv)
|
|
details.append(f"vol={vol:.1%}")
|
|
|
|
if not factors:
|
|
return None
|
|
|
|
strength = round(sum(factors) / len(factors), 4)
|
|
strength = max(-1.0, min(1.0, strength))
|
|
direction = "bullish" if strength > 0.05 else ("bearish" if strength < -0.05 else "neutral")
|
|
|
|
return StrategySignal(
|
|
name=self.name,
|
|
ticker=ticker,
|
|
date=date,
|
|
signal_strength=strength,
|
|
direction=direction,
|
|
detail=f"{len(factors)}-factor composite: {', '.join(details)}",
|
|
)
|