45 lines
1.4 KiB
Python
45 lines
1.4 KiB
Python
"""Volatility strategy signal (§3.4 — Volatility / Low-Vol Anomaly).
|
|
|
|
Computes realized volatility ranking and flags the low-volatility anomaly.
|
|
|
|
Reference:
|
|
Kakushadze & Serur, "151 Trading Strategies", §3.4
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
|
|
from .base import BaseStrategy, StrategySignal
|
|
from ._data import get_ohlcv
|
|
|
|
|
|
class VolatilityStrategy(BaseStrategy):
|
|
name = "Volatility (§3.4)"
|
|
roles = ["risk", "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) < 63:
|
|
return None
|
|
|
|
close = df["Close"].values[-63:]
|
|
returns = np.diff(np.log(close))
|
|
vol = float(np.std(returns) * np.sqrt(252))
|
|
|
|
# Low-vol anomaly: lower vol → mildly bullish signal
|
|
# Map vol: 0.10→+0.5, 0.30→0, 0.60→-1.0
|
|
strength = max(-1.0, min(1.0, (0.30 - vol) / 0.30))
|
|
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"Realized vol (63d annualized): {vol:.1%}, low-vol anomaly {'active' if vol < 0.25 else 'inactive'}",
|
|
)
|