80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
"""Event-Driven strategy signal (§5.1 — Event-Driven / Earnings & Dividend Proximity).
|
|
|
|
Flags proximity to upcoming earnings or ex-dividend dates as event catalysts.
|
|
|
|
Reference:
|
|
Kakushadze & Serur, "151 Trading Strategies", §5.1
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from .base import BaseStrategy, StrategySignal
|
|
from ._data import get_info
|
|
|
|
|
|
class EventDrivenStrategy(BaseStrategy):
|
|
name = "Event-Driven (§5.1)"
|
|
roles = ["fundamentals", "news", "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
|
|
|
|
try:
|
|
ref = datetime.strptime(date, "%Y-%m-%d")
|
|
except ValueError:
|
|
return None
|
|
|
|
events: list[str] = []
|
|
days_to_event: int | None = None
|
|
|
|
# Check earnings date proximity
|
|
for key in ("earningsDate", "nextEarningsDate"):
|
|
raw = info.get(key)
|
|
if raw is None:
|
|
continue
|
|
# yfinance may return a timestamp or list
|
|
if isinstance(raw, (list, tuple)) and raw:
|
|
raw = raw[0]
|
|
try:
|
|
dt = datetime.fromtimestamp(int(raw)) if isinstance(raw, (int, float)) else datetime.strptime(str(raw)[:10], "%Y-%m-%d")
|
|
delta = (dt - ref).days
|
|
if 0 <= delta <= 30:
|
|
events.append(f"earnings in {delta}d")
|
|
days_to_event = min(days_to_event, delta) if days_to_event is not None else delta
|
|
except Exception:
|
|
continue
|
|
|
|
# Check ex-dividend date
|
|
ex_div = info.get("exDividendDate")
|
|
if ex_div:
|
|
try:
|
|
dt = datetime.fromtimestamp(int(ex_div)) if isinstance(ex_div, (int, float)) else datetime.strptime(str(ex_div)[:10], "%Y-%m-%d")
|
|
delta = (dt - ref).days
|
|
if 0 <= delta <= 30:
|
|
events.append(f"ex-div in {delta}d")
|
|
days_to_event = min(days_to_event, delta) if days_to_event is not None else delta
|
|
except Exception:
|
|
pass
|
|
|
|
if not events:
|
|
return None
|
|
|
|
# Closer event → stronger signal (event risk / catalyst)
|
|
# Neutral direction — events are catalysts, not directional
|
|
proximity = max(0.0, 1.0 - (days_to_event or 30) / 30.0)
|
|
strength = round(proximity * 0.5, 4) # cap at 0.5 — events are informational
|
|
|
|
return StrategySignal(
|
|
name=self.name,
|
|
ticker=ticker,
|
|
date=date,
|
|
signal_strength=strength,
|
|
direction="neutral",
|
|
detail=f"Upcoming: {', '.join(events)}",
|
|
)
|