Files
dennisthiessenandClaude Opus 5 f5d4b516ab
Deploy / lint (push) Successful in 10s
Deploy / test (push) Successful in 1m21s
Deploy / deploy (push) Successful in 37s
docs: land capacity-study evidence and share the rank-map helper
Brings the durable artifacts of research/portfolio-capacity-rebalancing onto
main so the rationale for raising the count cap lives with the code that cites
it. The matrix runner, the research simulator hooks and the study's unit tests
are deliberately left behind; they remain at tag research/portfolio-capacity-final.

Corrects conclusions that were reached on EV per trade and are now superseded:
the findings doc's decisions 1 (keep cap 10) and 4 (run the risk-floor A/B) are
struck through and answered in a new correction section, and the research README
and phase-A matrix entries are updated to match. The frozen specification itself
is untouched -- its recorded SHA-256 f1e37783 still verifies.

effective-risk-floor-ab.md is retained but marked CLOSED/NEGATIVE: the study it
proposes is already answered by cap15 vs cash_unbounded (-0.753pp CAGR while
EV/trade rises), and its EV-based pass rule would have shipped it.

scripts/research_rankings.py replaces a fourth copy of the historical rank-map
helper; run_research_matrix, run_execution_recovery_matrix and
run_daily_reentry_matrix now share it. The shared version adds a duplicate
observation guard and a deterministic symbol tie-break the copies lacked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:08:29 +02:00

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