48 lines
1.5 KiB
Python
48 lines
1.5 KiB
Python
"""Trend Following strategy signal (§3.10 — Time-Series Momentum / Trend Following).
|
|
|
|
Multi-timeframe trend strength using short, medium, and long lookbacks.
|
|
|
|
Reference:
|
|
Kakushadze & Serur, "151 Trading Strategies", §3.10
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from .base import BaseStrategy, StrategySignal
|
|
from ._data import get_ohlcv
|
|
|
|
|
|
class TrendFollowingStrategy(BaseStrategy):
|
|
name = "Trend Following (§3.10)"
|
|
roles = ["market", "researcher"]
|
|
|
|
def compute(self, ticker: str, date: str, context: dict[str, Any] | None = None) -> StrategySignal | None:
|
|
df = get_ohlcv(ticker, date, context)
|
|
if df is None or len(df) < 252:
|
|
return None
|
|
|
|
close = df["Close"].values
|
|
scores: list[float] = []
|
|
details: list[str] = []
|
|
|
|
for label, period in [("21d", 21), ("63d", 63), ("252d", 252)]:
|
|
ret = (close[-1] - close[-period]) / close[-period]
|
|
s = max(-1.0, min(1.0, ret * (252 / period) ** 0.5)) # vol-scale
|
|
scores.append(s)
|
|
details.append(f"{label}={ret:+.1%}")
|
|
|
|
strength = round(sum(scores) / len(scores), 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"Multi-TF trend: {', '.join(details)}",
|
|
)
|