85 lines
3.2 KiB
Python
85 lines
3.2 KiB
Python
'''Shared production-style historical ranking helpers for research runners.'''
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
|
|
|
|
def _period_percentiles(
|
|
observations: list[dict], value_key: str
|
|
) -> dict[tuple[str, str], float]:
|
|
'''Rank one deterministic ticker observation per historical period.'''
|
|
by_period: dict[tuple, list[dict]] = {}
|
|
seen: set[tuple[str, str]] = set()
|
|
for row in observations:
|
|
identity = (str(row['symbol']), str(row['date']))
|
|
if identity in seen:
|
|
raise ValueError(f'Duplicate universe rank observation: {identity}')
|
|
seen.add(identity)
|
|
if row.get(value_key) is None:
|
|
continue
|
|
period = tuple(row['ranking_period'])
|
|
by_period.setdefault(period, []).append(row)
|
|
|
|
result: dict[tuple[str, str], float] = {}
|
|
for group in by_period.values():
|
|
ordered = sorted(
|
|
group,
|
|
key=lambda row: (float(row[value_key]), str(row['symbol'])),
|
|
)
|
|
denominator = len(ordered) - 1
|
|
for rank, row in enumerate(ordered):
|
|
result[(str(row['symbol']), str(row['date']))] = round(
|
|
rank / denominator * 100.0 if denominator > 0 else 100.0,
|
|
2,
|
|
)
|
|
return result
|
|
|
|
|
|
def _live_universe_rank_map(
|
|
observations: list[dict],
|
|
benchmark_closes: dict[date, float],
|
|
momentum_weight: float,
|
|
) -> dict[tuple[str, str], dict[str, float | None]]:
|
|
'''Historical equivalent of production compute_activation_ranks.
|
|
|
|
Every ticker contributes at most once per session. Residual momentum starts
|
|
only once 252 benchmark closes were point-in-time available; earlier dates
|
|
use the same raw-momentum fallback as production.
|
|
'''
|
|
identities = [(str(row['symbol']), str(row['date'])) for row in observations]
|
|
if len(identities) != len(set(identities)):
|
|
raise ValueError('Universe ranking requires one observation per ticker/date')
|
|
|
|
raw_pct = _period_percentiles(observations, 'momentum')
|
|
residual_pct = _period_percentiles(observations, 'residual_momentum')
|
|
vol_pct = _period_percentiles(observations, 'vol_6m')
|
|
benchmark_ords = sorted(value.toordinal() for value in benchmark_closes)
|
|
residual_start_ord = benchmark_ords[251] if len(benchmark_ords) >= 252 else None
|
|
|
|
ranks: dict[tuple[str, str], dict[str, float | None]] = {}
|
|
for row in observations:
|
|
identity = (str(row['symbol']), str(row['date']))
|
|
asof_ord = date.fromisoformat(identity[1]).toordinal()
|
|
momentum_pct = (
|
|
residual_pct.get(identity)
|
|
if residual_start_ord is not None and asof_ord >= residual_start_ord
|
|
else raw_pct.get(identity)
|
|
)
|
|
volatility_pct = vol_pct.get(identity)
|
|
strategy_rank = (
|
|
round(
|
|
momentum_pct * momentum_weight
|
|
+ volatility_pct * (1.0 - momentum_weight),
|
|
2,
|
|
)
|
|
if momentum_pct is not None and volatility_pct is not None
|
|
else momentum_pct
|
|
)
|
|
ranks[identity] = {
|
|
'momentum_percentile': momentum_pct,
|
|
'volatility_percentile': volatility_pct,
|
|
'strategy_rank': strategy_rank,
|
|
}
|
|
return ranks
|