research: add focused portfolio capacity matrix

This commit is contained in:
2026-08-05 08:28:30 +02:00
parent 07d864cf64
commit 1ace6688dd
12 changed files with 2893 additions and 236 deletions
+665
View File
@@ -0,0 +1,665 @@
'''Pure helpers for the focused daily portfolio-capacity research matrix.'''
from __future__ import annotations
import hashlib
import math
import random
import statistics
from collections import defaultdict
from datetime import date, timedelta
from typing import Any, Iterable
ARMS: tuple[dict[str, Any], ...] = (
{
'id': 'cap10_incumbent',
'label': 'Cap 10, arrival-order incumbents',
'max_positions': 10,
'min_initial_risk_fraction': None,
'weekly_top_n_rebalance': False,
},
{
'id': 'cash_unbounded',
'label': 'Cash-constrained, no count cap',
'max_positions': None,
'min_initial_risk_fraction': 0.005,
'weekly_top_n_rebalance': False,
},
{
'id': 'cap10_weekly_top10',
'label': 'Cap 10, weekly current-rank top 10',
'max_positions': 10,
'min_initial_risk_fraction': None,
'weekly_top_n_rebalance': True,
},
{
'id': 'cap15_incumbent',
'label': 'Cap 15, arrival-order incumbents',
'max_positions': 15,
'min_initial_risk_fraction': None,
'weekly_top_n_rebalance': False,
},
)
ARM_BY_ID = {arm['id']: arm for arm in ARMS}
COSTS_PER_SIDE_PCT = (0.1, 0.2)
ANCHOR_YEARS = tuple(range(2019, 2026))
SCORING_SESSIONS = 504
MEASUREMENT_SESSIONS = 252
RESIDUAL_BENCHMARK_SESSIONS = 252
WARM_SEED_MIN_OFFSET = 63
WARM_SEED_MAX_OFFSET = 126
BOOTSTRAP_REPLICATES = 10_000
BOOTSTRAP_SEED = 20260805
PRIMARY_METRICS = (
'ev_net_r',
'calmar',
'profit_factor',
'gain_to_pain',
'sortino',
)
PAIRED_METRICS = (
*PRIMARY_METRICS,
'cagr_pct',
'max_drawdown_pct',
'total_return_pct',
'sharpe',
)
def _end_exclusive(
sessions: list[date], start_index: int, count: int
) -> date:
end_index = start_index + count
if end_index < len(sessions):
return sessions[end_index]
return sessions[-1] + timedelta(days=1)
def build_cohort_manifest(session_dates: Iterable[date]) -> dict[str, Any]:
sessions = sorted(set(session_dates))
minimum = RESIDUAL_BENCHMARK_SESSIONS + SCORING_SESSIONS
if len(sessions) <= minimum + MEASUREMENT_SESSIONS:
raise ValueError('Snapshot is too short for the frozen cohort design')
index_of = {session: index for index, session in enumerate(sessions)}
first_eligible_index = RESIDUAL_BENCHMARK_SESSIONS - 1 + SCORING_SESSIONS
last_eligible_index = len(sessions) - MEASUREMENT_SESSIONS
first_by_month: dict[tuple[int, int], date] = {}
for session in sessions:
first_by_month.setdefault((session.year, session.month), session)
empty: list[dict[str, Any]] = []
for (year, month), session in sorted(first_by_month.items()):
index = index_of[session]
if year not in ANCHOR_YEARS:
continue
if index < first_eligible_index or index > last_eligible_index:
continue
empty.append({
'protocol': 'empty_book',
'path_id': f'empty-{year:04d}-{month:02d}',
'cluster': year,
'simulation_start': session.isoformat(),
'measurement_start': session.isoformat(),
'hard_end_exclusive': _end_exclusive(
sessions, index, MEASUREMENT_SESSIONS
).isoformat(),
})
first_by_year: dict[int, date] = {}
for session in sessions:
first_by_year.setdefault(session.year, session)
warm: list[dict[str, Any]] = []
warm_seed_counts: dict[str, int] = {}
for year in ANCHOR_YEARS:
anchor = first_by_year.get(year)
if anchor is None:
continue
anchor_index = index_of[anchor]
if (
anchor_index < WARM_SEED_MAX_OFFSET
or anchor_index > last_eligible_index
):
continue
seed_window = sessions[
anchor_index - WARM_SEED_MAX_OFFSET:
anchor_index - WARM_SEED_MIN_OFFSET + 1
]
first_by_iso_week: dict[tuple[int, int], date] = {}
for session in seed_window:
iso = session.isocalendar()
first_by_iso_week.setdefault((iso.year, iso.week), session)
seeds = sorted(first_by_iso_week.values())
warm_seed_counts[str(year)] = len(seeds)
for seed_index, seed in enumerate(seeds, 1):
warm.append({
'protocol': 'warm_book',
'path_id': f'warm-{year}-seed-{seed_index:02d}',
'cluster': year,
'simulation_start': seed.isoformat(),
'measurement_start': anchor.isoformat(),
'hard_end_exclusive': _end_exclusive(
sessions, anchor_index, MEASUREMENT_SESSIONS
).isoformat(),
'seed_offset_sessions': anchor_index - index_of[seed],
})
return {
'snapshot_first_session': sessions[0].isoformat(),
'snapshot_last_session': sessions[-1].isoformat(),
'session_count': len(sessions),
'expected_clusters': list(ANCHOR_YEARS),
'empty_book': empty,
'warm_book': warm,
'empty_cluster_counts': dict(
sorted(
(
str(year),
sum(1 for row in empty if row['cluster'] == year),
)
for year in {row['cluster'] for row in empty}
)
),
'warm_seed_counts': warm_seed_counts,
'empty_cluster_count': len({row['cluster'] for row in empty}),
'warm_cluster_count': len({row['cluster'] for row in warm}),
}
def validate_cohort_manifest(manifest: dict[str, Any]) -> list[str]:
errors: list[str] = []
expected = set(ANCHOR_YEARS)
empty_clusters = {row['cluster'] for row in manifest['empty_book']}
warm_clusters = {row['cluster'] for row in manifest['warm_book']}
if empty_clusters != expected:
errors.append(
f'empty-book clusters {sorted(empty_clusters)} != {sorted(expected)}'
)
if warm_clusters != expected:
errors.append(
f'warm-book clusters {sorted(warm_clusters)} != {sorted(expected)}'
)
for year in ANCHOR_YEARS:
seed_count = int(manifest['warm_seed_counts'].get(str(year), 0))
if seed_count < 12:
errors.append(f'warm anchor {year} has only {seed_count} seeds')
return errors
def build_cells(manifest: dict[str, Any]) -> list[dict[str, Any]]:
paths = [*manifest['empty_book'], *manifest['warm_book']]
cells: list[dict[str, Any]] = []
for cost in COSTS_PER_SIDE_PCT:
for path in paths:
for arm in ARMS:
cell_id = (
f'{arm["id"]}|{path["protocol"]}|{path["path_id"]}'
f'|cost={cost:.1f}'
)
cells.append({
**path,
'cell_id': cell_id,
'arm_id': arm['id'],
'cost_per_side_pct': cost,
})
return cells
def percentile(values: Iterable[float], probability: float) -> float | None:
ordered = sorted(float(value) for value in values if value is not None)
if not ordered:
return None
if len(ordered) == 1:
return ordered[0]
location = (len(ordered) - 1) * probability
lower = math.floor(location)
upper = math.ceil(location)
if lower == upper:
return ordered[lower]
weight = location - lower
return ordered[lower] * (1.0 - weight) + ordered[upper] * weight
def iqr(values: Iterable[float]) -> float | None:
q25 = percentile(values, 0.25)
q75 = percentile(values, 0.75)
if q25 is None or q75 is None:
return None
return q75 - q25
def median(values: Iterable[float | None]) -> float | None:
clean = [float(value) for value in values if value is not None]
return statistics.median(clean) if clean else None
def _safe_ratio(numerator: float | None, denominator: float | None) -> float | None:
if numerator is None or denominator is None:
return None
if abs(denominator) <= 1e-12:
return 1.0 if abs(numerator) <= 1e-12 else None
return numerator / denominator
def _stable_seed(*parts: object) -> int:
digest = hashlib.sha256('|'.join(map(str, parts)).encode('utf-8')).digest()
return BOOTSTRAP_SEED + int.from_bytes(digest[:4], 'big')
def bootstrap_median_interval(
values: Iterable[float | None],
*,
seed_parts: tuple[object, ...],
replicates: int = BOOTSTRAP_REPLICATES,
) -> dict[str, float | int | None]:
clean = [float(value) for value in values if value is not None]
if not clean:
return {'n': 0, 'point': None, 'p05': None, 'p95': None}
rng = random.Random(_stable_seed(*seed_parts))
draws = [
statistics.median(rng.choices(clean, k=len(clean)))
for _ in range(replicates)
]
return {
'n': len(clean),
'replicates': replicates,
'point': statistics.median(clean),
'p05': percentile(draws, 0.05),
'p95': percentile(draws, 0.95),
}
def _monthly_returns(
equity_curve: list[dict[str, Any]], base_equity: float
) -> list[float]:
month_ends: dict[tuple[int, int], float] = {}
for point in equity_curve:
point_date = date.fromisoformat(str(point['date']))
month_ends[(point_date.year, point_date.month)] = float(point['equity'])
previous = float(base_equity)
returns: list[float] = []
for month in sorted(month_ends):
equity = month_ends[month]
if previous > 0:
returns.append(equity / previous - 1.0)
previous = equity
return returns
def _time_underwater(equities: list[float]) -> tuple[int, float]:
peak = float('-inf')
current = 0
longest = 0
underwater = 0
for equity in equities:
peak = max(peak, equity)
if peak > 0 and equity < peak - 1e-9:
current += 1
underwater += 1
longest = max(longest, current)
else:
current = 0
percentage = underwater / len(equities) * 100.0 if equities else 0.0
return longest, percentage
def summarize_simulation(sim: dict[str, Any]) -> dict[str, Any]:
trades = list(sim.get('trade_details') or [])
equity_curve = list(sim.get('equity_curve') or [])
net_rs = [float(trade['net_r']) for trade in trades]
positive_rs = [value for value in net_rs if value > 0]
negative_rs = [value for value in net_rs if value < 0]
ev_net_r = statistics.fmean(net_rs) if net_rs else None
profit_factor = (
sum(positive_rs) / abs(sum(negative_rs))
if negative_rs
else None
)
base_equity = float(
sim.get('measurement_start_equity') or sim.get('starting_capital') or 0.0
)
curve_equities = [float(point['equity']) for point in equity_curve]
daily_equities = [base_equity, *curve_equities]
daily_returns = [
current / previous - 1.0
for previous, current in zip(daily_equities, daily_equities[1:])
if previous > 0
]
downside_deviation = (
math.sqrt(
statistics.fmean(min(value, 0.0) ** 2 for value in daily_returns)
)
if daily_returns
else None
)
sortino = (
statistics.fmean(daily_returns) / downside_deviation * math.sqrt(252.0)
if downside_deviation is not None and downside_deviation > 0
else None
)
monthly_returns = _monthly_returns(equity_curve, base_equity)
negative_monthly = sum(value for value in monthly_returns if value < 0)
gain_to_pain = (
sum(monthly_returns) / abs(negative_monthly)
if negative_monthly < 0
else None
)
longest_underwater, underwater_pct = _time_underwater(daily_equities)
transaction_cost = sum(
float(trade.get('transaction_cost') or 0.0) for trade in trades
)
traded_notional = sum(
float(trade.get('shares') or 0.0)
* (float(trade.get('entry') or 0.0) + float(trade.get('fill') or 0.0))
for trade in trades
)
turnover_multiple = (
traded_notional / base_equity if base_equity > 0 else None
)
ordered_rs = sorted(net_rs, reverse=True)
ev_without_best: dict[str, float | None] = {}
for count in (1, 5, 10):
remaining = ordered_rs[count:]
ev_without_best[str(count)] = (
statistics.fmean(remaining) if remaining else None
)
events = list(sim.get('weekly_rebalance_events') or [])
entrant_sizes = [int(event['fresh_entrant_pool']) for event in events]
eligible_sizes = [
int(event['rank_eligible_entrant_pool']) for event in events
]
replacements = [int(event['replacements']) for event in events]
capacity_skips = int(
sim.get('measurement_skipped_book_full', sim.get('skipped_book_full', 0))
)
opened = int(sim.get('opened_positions', sim.get('trades', 0)))
capacity_opportunities = opened + capacity_skips
result = {
'start_date': sim.get('start_date'),
'end_date': sim.get('end_date'),
'simulation_start_date': sim.get('simulation_start_date'),
'measurement_start_equity': base_equity,
'measurement_start_positions': sim.get('measurement_start_positions', 0),
'trades': len(trades),
'ev_net_r': ev_net_r,
'profit_factor': profit_factor,
'gain_to_pain': gain_to_pain,
'sortino': sortino,
'ev_without_best': ev_without_best,
'total_return_pct': sim.get('total_return_pct'),
'cagr_pct': sim.get('cagr_pct'),
'max_drawdown_pct': sim.get('max_drawdown_pct'),
'calmar': sim.get('calmar'),
'sharpe': sim.get('sharpe'),
'win_rate': sim.get('win_rate'),
'avg_hold_days': sim.get('avg_hold_days'),
'longest_underwater_sessions': longest_underwater,
'underwater_pct': underwater_pct,
'transaction_cost': transaction_cost,
'turnover_multiple': turnover_multiple,
'skipped_book_full': capacity_skips,
'opened_positions': opened,
'capacity_opportunities': capacity_opportunities,
'blocked_fraction': (
capacity_skips / capacity_opportunities
if capacity_opportunities
else 0.0
),
'skipped_min_initial_risk': int(
sim.get('measurement_skipped_min_initial_risk', 0)
),
'avg_positions': sim.get('avg_positions'),
'peak_positions': sim.get('peak_positions'),
'sessions_at_capacity': sim.get('sessions_at_capacity'),
'sessions_measured': sim.get('sessions_measured'),
'avg_cash_pct': sim.get('avg_cash_pct'),
'avg_gross_exposure_pct': sim.get('avg_gross_exposure_pct'),
'exit_reasons': sim.get('exit_reasons'),
}
if events:
result['weekly_rebalance'] = {
'events': len(events),
'zero_entrant_fraction': (
sum(1 for value in entrant_sizes if value == 0) / len(events)
),
'entrant_pool_mean': statistics.fmean(entrant_sizes),
'entrant_pool_median': statistics.median(entrant_sizes),
'entrant_pool_p90': percentile(entrant_sizes, 0.9),
'eligible_pool_mean': statistics.fmean(eligible_sizes),
'replacements': sum(replacements),
'weekly_rank_rejected_entries': int(
sim.get('weekly_rank_rejected_entries', 0)
),
'reentries_within_5_sessions': int(
sim.get('rebalance_reentries_within_5_sessions', 0)
),
'reentries_within_10_sessions': int(
sim.get('rebalance_reentries_within_10_sessions', 0)
),
'reentries_within_20_sessions': int(
sim.get('rebalance_reentries_within_20_sessions', 0)
),
}
return result
def _cluster_rows(
cells: list[dict[str, Any]],
*,
arm_id: str,
protocol: str,
cost: float,
) -> list[dict[str, Any]]:
treatment = {
row['path_id']: row
for row in cells
if row['arm_id'] == arm_id
and row['protocol'] == protocol
and float(row['cost_per_side_pct']) == cost
}
control = {
row['path_id']: row
for row in cells
if row['arm_id'] == 'cap10_incumbent'
and row['protocol'] == protocol
and float(row['cost_per_side_pct']) == cost
}
shared_paths = sorted(set(treatment) & set(control))
by_cluster: dict[int, list[tuple[dict, dict]]] = defaultdict(list)
for path_id in shared_paths:
row = treatment[path_id]
by_cluster[int(row['cluster'])].append((row, control[path_id]))
summaries: list[dict[str, Any]] = []
for cluster, pairs in sorted(by_cluster.items()):
metrics: dict[str, Any] = {}
for metric in PAIRED_METRICS:
arm_values = [
pair[0]['metrics'].get(metric)
for pair in pairs
if pair[0]['metrics'].get(metric) is not None
and math.isfinite(float(pair[0]['metrics'][metric]))
]
control_values = [
pair[1]['metrics'].get(metric)
for pair in pairs
if pair[1]['metrics'].get(metric) is not None
and math.isfinite(float(pair[1]['metrics'][metric]))
]
deltas = [
float(arm['metrics'][metric])
- float(base['metrics'][metric])
for arm, base in pairs
if arm['metrics'].get(metric) is not None
and base['metrics'].get(metric) is not None
and math.isfinite(float(arm['metrics'][metric]))
and math.isfinite(float(base['metrics'][metric]))
]
arm_median = median(arm_values)
control_median = median(control_values)
metrics[metric] = {
'arm_median': arm_median,
'control_median': control_median,
'paired_delta_median': median(deltas),
'arm_control_ratio': _safe_ratio(
arm_median, control_median
),
'paired_paths': len(deltas),
}
summaries.append({
'cluster': cluster,
'paths': len(pairs),
'metrics': metrics,
})
return summaries
def aggregate_results(cells: list[dict[str, Any]]) -> dict[str, Any]:
paired: list[dict[str, Any]] = []
for cost in COSTS_PER_SIDE_PCT:
for protocol in ('empty_book', 'warm_book'):
for arm in ARMS:
arm_id = str(arm['id'])
clusters = _cluster_rows(
cells,
arm_id=arm_id,
protocol=protocol,
cost=float(cost),
)
headline: dict[str, Any] = {}
for metric in PAIRED_METRICS:
deltas = [
cluster['metrics'][metric]['paired_delta_median']
for cluster in clusters
]
arm_levels = [
cluster['metrics'][metric]['arm_median']
for cluster in clusters
]
control_levels = [
cluster['metrics'][metric]['control_median']
for cluster in clusters
]
arm_level = median(arm_levels)
control_level = median(control_levels)
metric_summary: dict[str, Any] = {
'paired_delta_median': median(deltas),
'arm_median': arm_level,
'control_median': control_level,
'arm_control_ratio': _safe_ratio(
arm_level, control_level
),
}
if metric in ('ev_net_r', 'calmar'):
metric_summary['bootstrap_90'] = (
bootstrap_median_interval(
deltas,
seed_parts=(
arm_id,
protocol,
cost,
metric,
'paired-delta',
),
)
)
headline[metric] = metric_summary
paired.append({
'arm_id': arm_id,
'protocol': protocol,
'cost_per_side_pct': cost,
'clusters': clusters,
'headline': headline,
})
warm_rows = [
row for row in cells if row['protocol'] == 'warm_book'
]
warm_dispersion: list[dict[str, Any]] = []
for cost in COSTS_PER_SIDE_PCT:
for arm in ARMS:
arm_id = str(arm['id'])
anchor_rows: list[dict[str, Any]] = []
for cluster in ANCHOR_YEARS:
arm_paths = [
row
for row in warm_rows
if row['arm_id'] == arm_id
and int(row['cluster']) == cluster
and float(row['cost_per_side_pct']) == float(cost)
]
control_by_path = {
row['path_id']: row
for row in warm_rows
if row['arm_id'] == 'cap10_incumbent'
and int(row['cluster']) == cluster
and float(row['cost_per_side_pct']) == float(cost)
}
metric_rows: dict[str, Any] = {}
for metric in ('ev_net_r', 'calmar'):
arm_spread = iqr(
row['metrics'].get(metric) for row in arm_paths
)
control_spread = iqr(
control_by_path[row['path_id']]['metrics'].get(metric)
for row in arm_paths
if row['path_id'] in control_by_path
)
metric_rows[metric] = {
'arm_iqr': arm_spread,
'control_iqr': control_spread,
'iqr_ratio': _safe_ratio(
arm_spread, control_spread
),
}
anchor_rows.append({
'cluster': cluster,
'seeds': len(arm_paths),
'metrics': metric_rows,
})
headline: dict[str, Any] = {}
for metric in ('ev_net_r', 'calmar'):
ratios = [
row['metrics'][metric]['iqr_ratio']
for row in anchor_rows
]
headline[metric] = {
'median_iqr_ratio': median(ratios),
'bootstrap_90': bootstrap_median_interval(
ratios,
seed_parts=(
arm_id,
cost,
metric,
'warm-iqr-ratio',
),
),
}
warm_dispersion.append({
'arm_id': arm_id,
'cost_per_side_pct': cost,
'anchors': anchor_rows,
'headline': headline,
})
return {
'paired_per_year': paired,
'warm_seed_dispersion': warm_dispersion,
'bootstrap': {
'replicates': BOOTSTRAP_REPLICATES,
'seed': BOOTSTRAP_SEED,
'interval': 'central 90% percentile, context only',
'resampling_unit': 'seven annual paired summaries',
},
}
+84
View File
@@ -0,0 +1,84 @@
'''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
+5 -79
View File
@@ -29,6 +29,11 @@ ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts.research_rankings import ( # noqa: E402
_live_universe_rank_map,
_period_percentiles,
)
POLICY_NAMES = (
"immediate",
"next_session",
@@ -107,85 +112,6 @@ def _default_output_path() -> Path:
return Path("reports") / f"daily-reentry-matrix-{stamp}.json"
def _period_percentiles(
observations: list[dict], value_key: str
) -> dict[tuple[str, str], float]:
"""Production-style percentiles, one deterministic symbol row per 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 ``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
class PrecomputedDailyEngine:
"""Exact date/symbol lookup over the already-ranked production gate."""
+5 -60
View File
@@ -55,6 +55,11 @@ ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts.research_rankings import ( # noqa: E402
_live_universe_rank_map,
_period_percentiles,
)
# Must match Phase A cache when reusing research-cands.pkl
CACHE_VERSION = "research-matrix-v1-daily-prod"
@@ -104,66 +109,6 @@ def _parse_args() -> argparse.Namespace:
return p.parse_args()
def _period_percentiles(
observations: list[dict], value_key: str
) -> dict[tuple[str, str], float]:
by_period: dict[tuple, list[dict]] = {}
for row in observations:
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]]:
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
def _window(arm: dict, name: str) -> dict | None:
for row in arm.get("windows") or []:
if row.get("window") == name:
@@ -0,0 +1,952 @@
'''Run the focused four-arm daily portfolio-capacity research matrix.
The expensive point-in-time daily replay and full-universe ranks are cached
once. Empty-book monthly paths and warm-book weekly seeds are then evaluated
under cap 10, cap 15, cash-only unbounded, and weekly current-rank top 10.
'''
from __future__ import annotations
import argparse
import asyncio
import hashlib
import json
import multiprocessing
import os
import pickle
import platform
import subprocess
import sys
from collections import Counter
from concurrent.futures import ProcessPoolExecutor, as_completed
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts.portfolio_capacity_research import ( # noqa: E402
ANCHOR_YEARS,
ARM_BY_ID,
ARMS,
BOOTSTRAP_REPLICATES,
BOOTSTRAP_SEED,
COSTS_PER_SIDE_PCT,
aggregate_results,
build_cells,
build_cohort_manifest,
median,
summarize_simulation,
validate_cohort_manifest,
)
from scripts.research_rankings import _live_universe_rank_map # noqa: E402
CACHE_VERSION = 'portfolio-capacity-candidates-v1-zero-horizon'
RUNNER_VERSION = 'portfolio-capacity-bracket-v1'
SPEC_PATH = ROOT / 'docs' / 'research' / 'portfolio-capacity-bracket.md'
DEFAULT_RUN_ID = 'prod505-capacity-bracket-daily-v1'
_WORKER_CONTEXT: dict[str, Any] | None = None
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('snapshot', help='SQLite backtest snapshot')
parser.add_argument('--run-id', default=DEFAULT_RUN_ID)
parser.add_argument(
'--workers',
default='auto',
help='Worker count or auto',
)
parser.add_argument('--resume', action='store_true')
parser.add_argument('--validate-only', action='store_true')
parser.add_argument('--candidate-cache', default=None)
parser.add_argument('--checkpoint', default=None)
parser.add_argument('--out', default=None)
parser.add_argument('--quiet', action='store_true')
return parser.parse_args()
def _worker_count(raw: str) -> int:
if str(raw).lower() == 'auto':
return max(1, min(6, (multiprocessing.cpu_count() or 2) - 1))
value = int(raw)
if value <= 0:
raise ValueError('--workers must be positive or auto')
return value
def _sqlite_url(path: Path) -> str:
return f'sqlite+aiosqlite:///{path.resolve().as_posix()}'
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open('rb') as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b''):
digest.update(chunk)
return digest.hexdigest()
def _json_hash(value: Any) -> str:
payload = json.dumps(
value,
sort_keys=True,
separators=(',', ':'),
default=str,
).encode('utf-8')
return hashlib.sha256(payload).hexdigest()
def _atomic_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + '.tmp')
with temporary.open('w', encoding='utf-8', newline='\n') as handle:
json.dump(value, handle, indent=2, sort_keys=True, allow_nan=False)
handle.write('\n')
temporary.replace(path)
def _atomic_text(path: Path, value: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + '.tmp')
with temporary.open('w', encoding='utf-8', newline='\n') as handle:
handle.write(value.rstrip() + '\n')
temporary.replace(path)
def _atomic_pickle(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + '.tmp')
with temporary.open('wb') as handle:
pickle.dump(value, handle, protocol=pickle.HIGHEST_PROTOCOL)
temporary.replace(path)
def _git_output(*args: str) -> str:
completed = subprocess.run(
['git', *args],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return completed.stdout.strip()
def _assert_clean_worktree() -> None:
dirty = _git_output('status', '--porcelain')
if dirty:
raise SystemExit(
'Authoritative research refuses a dirty worktree; commit first:\n'
+ dirty
)
def _worker_init(context: dict[str, Any]) -> None:
global _WORKER_CONTEXT
os.environ['BACKTEST_SNAPSHOT_OFFLINE'] = '1'
os.environ['BACKTEST_ALLOW_SPAWN'] = '1'
from app.services import backtest_service as bt
context = dict(context)
context['post_stop_reentry_fn'] = bt._make_gate_reset_reentry_fn(
context['qualified_candidates'],
context['prices'],
cadence='daily',
ranking_key=context['ranking_key'],
evaluation_horizon_sessions=0,
)
_WORKER_CONTEXT = context
def _worker_run_cell(cell: dict[str, Any]) -> dict[str, Any]:
if _WORKER_CONTEXT is None:
raise RuntimeError('Portfolio-capacity worker was not initialized')
from app.services import backtest_service as bt
context = _WORKER_CONTEXT
arm = ARM_BY_ID[str(cell['arm_id'])]
measurement_start = date.fromisoformat(str(cell['measurement_start']))
hard_end = date.fromisoformat(str(cell['hard_end_exclusive']))
sim = bt._simulate_portfolio(
context['qualified_candidates'],
context['prices'],
context['benchmark_closes'],
context['exit_policy'],
context['hold_days'],
ranking_key=context['ranking_key'],
max_positions=arm['max_positions'],
risk_per_trade=context['risk_per_trade'],
atr_trail_multiplier=context['atr_trail_multiplier'],
cost_per_side=float(cell['cost_per_side_pct']) / 100.0,
post_stop_reentry_fn=context['post_stop_reentry_fn'],
start_date=date.fromisoformat(str(cell['simulation_start'])),
end_date=hard_end,
measurement_start_date=measurement_start,
hard_end_date=hard_end,
fill_mode=bt.FILL_MODE_CLOSE,
min_initial_risk_fraction=arm['min_initial_risk_fraction'],
weekly_top_n_rebalance=bool(arm['weekly_top_n_rebalance']),
daily_rank_map=(
context['daily_rank_map']
if arm['weekly_top_n_rebalance']
else None
),
include_curve=True,
include_trades=True,
include_capacity_diagnostics=True,
)
if sim is None:
raise RuntimeError(f'Cell produced no trades: {cell["cell_id"]}')
return {
**cell,
'metrics': summarize_simulation(sim),
}
async def _load_snapshot(
snapshot: Path,
*,
quiet: bool,
) -> dict[str, Any]:
from app.models.ticker import Ticker
from app.services import backtest_service as bt
from app.services.admin_service import get_activation_config
from app.services.paper_trade_service import get_exit_policy
from app.services.recommendation_service import get_recommendation_config
engine = create_async_engine(_sqlite_url(snapshot), pool_pre_ping=True)
Session = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
try:
async with Session() as db:
recommendation_config = await get_recommendation_config(db)
activation = await get_activation_config(db)
exit_config = await get_exit_policy(db)
benchmark_closes = await bt._load_benchmark_closes_for_backtest(
db,
days=None,
refresh=False,
)
ticker_result = await db.execute(
select(Ticker).order_by(Ticker.symbol)
)
symbols = [
ticker.symbol for ticker in ticker_result.scalars().all()
]
prices: dict[str, tuple] = {}
for index, symbol in enumerate(symbols, 1):
columns = await bt._fetch_columns(db, symbol)
if columns is not None:
prices[symbol] = columns
if not quiet and index % 50 == 0:
print(
f'loaded prices: {index}/{len(symbols)}',
flush=True,
)
finally:
await engine.dispose()
if not prices or not benchmark_closes:
raise SystemExit('Snapshot has no usable prices or benchmark history')
strategy = next(
row
for row in bt.PORTFOLIO_MONITOR_STRATEGIES
if row.get('is_production')
)
entry_config = bt._entry_variant_config(str(strategy['entry_variant']))
if entry_config is None:
raise RuntimeError('Production entry configuration is missing')
ranking_key = str(
entry_config.get('ranking_key') or entry_config['percentile_key']
)
exit_policy = bt.LIVE_EXIT_MODE_TO_SIM.get(
str(exit_config.get('mode', 'atr_trailing')),
'atr_trail3',
)
hold_days = int(exit_config.get('hold_days', 30))
risk_per_trade = float(entry_config['risk_per_trade'])
atr_trail_multiplier = float(
exit_config.get('atr_multiplier', bt.ATR_TRAIL_MULTIPLIER)
)
threshold = float(
activation.get('min_momentum_percentile', 80.0)
)
expected = {
'ranking_key': bt.RESIDUAL_HIGH_VOL_BLEND_80_20_KEY,
'exit_policy': 'atr_trail3',
'hold_days': 30,
'risk_per_trade': 0.01,
'max_positions': 10,
'threshold': 80.0,
}
realized = {
'ranking_key': ranking_key,
'exit_policy': exit_policy,
'hold_days': hold_days,
'risk_per_trade': risk_per_trade,
'max_positions': int(entry_config['max_positions']),
'threshold': threshold,
}
if realized != expected:
raise SystemExit(
'Snapshot runtime configuration is not the frozen Phase A control: '
+ json.dumps({'expected': expected, 'realized': realized}, sort_keys=True)
)
return {
'recommendation_config': recommendation_config,
'activation': activation,
'exit_config': exit_config,
'benchmark_closes': benchmark_closes,
'prices': prices,
'symbols': symbols,
'universe_manifest': {
'ticker_rows': len(symbols),
'symbols_with_prices': len(prices),
'symbols_sha256': _json_hash(sorted(symbols)),
},
'ranking_key': ranking_key,
'exit_policy': exit_policy,
'hold_days': hold_days,
'risk_per_trade': risk_per_trade,
'atr_trail_multiplier': atr_trail_multiplier,
'threshold': threshold,
'runtime_config': realized,
}
def _build_candidate_cache(
snapshot_data: dict[str, Any],
*,
snapshot: Path,
snapshot_sha256: str,
cache_path: Path,
workers: int,
quiet: bool,
) -> dict[str, Any]:
from app.services import backtest_service as bt
cache_key = {
'version': CACHE_VERSION,
'snapshot': str(snapshot.resolve()),
'snapshot_sha256': snapshot_sha256,
'cadence': 'daily',
'outcome_horizon_sessions': 0,
'recommendation_config_hash': _json_hash(
snapshot_data['recommendation_config']
),
'activation_hash': _json_hash(snapshot_data['activation']),
'runtime_config': snapshot_data['runtime_config'],
'universe_manifest': snapshot_data['universe_manifest'],
}
if cache_path.exists():
with cache_path.open('rb') as handle:
cached = pickle.load(handle) # noqa: S301 - trusted local cache
if cached.get('key') == cache_key:
if not quiet:
print(f'loaded candidate/rank cache: {cache_path}', flush=True)
return cached
if not quiet:
print(f'candidate cache mismatch; rebuilding: {cache_path}', flush=True)
replay_rows: list[dict[str, Any]] = []
prices = snapshot_data['prices']
replay_args = [
(
symbol,
columns,
snapshot_data['recommendation_config'],
snapshot_data['activation'],
snapshot_data['benchmark_closes'],
date(1900, 1, 1),
'daily',
True,
True,
0,
)
for symbol, columns in prices.items()
]
if workers == 1:
for index, args in enumerate(replay_args, 1):
replay_rows.extend(bt._replay_candidates_for_period(*args))
if not quiet and index % 25 == 0:
print(
f'daily replay: {index}/{len(replay_args)} tickers',
flush=True,
)
else:
context = bt._mp_context() or multiprocessing.get_context('spawn')
with ProcessPoolExecutor(
max_workers=workers,
mp_context=context,
) as pool:
futures = [
pool.submit(bt._replay_candidates_for_period, *args)
for args in replay_args
]
for index, future in enumerate(as_completed(futures), 1):
replay_rows.extend(future.result())
if not quiet and index % 25 == 0:
print(
f'daily replay: {index}/{len(futures)} tickers',
flush=True,
)
setup_candidates = [
row for row in replay_rows if not row.get('_rank_only')
]
rank_observations = [
row for row in replay_rows if row.get('_universe_rank_observation')
]
daily_rank_map = _live_universe_rank_map(
rank_observations,
snapshot_data['benchmark_closes'],
bt.STRATEGY_RANK_MOMENTUM_WEIGHT,
)
qualified: list[dict[str, Any]] = []
for setup in setup_candidates:
if setup.get('direction') != 'long':
continue
identity = (str(setup['symbol']), str(setup['date']))
rank = daily_rank_map.get(identity)
if rank is None:
continue
candidate = {
key: value
for key, value in setup.items()
if not key.startswith('_universe_')
}
candidate[bt.PRODUCTION_PERCENTILE_KEY] = rank['momentum_percentile']
candidate[bt.VOL_PERCENTILE_KEY] = rank['volatility_percentile']
candidate[bt.RESIDUAL_HIGH_VOL_BLEND_80_20_KEY] = rank['strategy_rank']
candidate['qualified'] = bt._momentum_qualifies(
candidate,
snapshot_data['threshold'],
)
if candidate['qualified']:
qualified.append(candidate)
qualified.sort(
key=lambda row: (
str(row['date']),
-float(row.get(snapshot_data['ranking_key']) or 0.0),
str(row['symbol']),
float(row.get('entry') or 0.0),
float(row.get('stop') or 0.0),
float(row.get('target') or 0.0),
str(row.get('action') or ''),
)
)
rank_dates = sorted({identity[1] for identity in daily_rank_map})
cached = {
'key': cache_key,
'qualified_candidates': qualified,
'daily_rank_map': daily_rank_map,
'setup_candidate_count': len(setup_candidates),
'qualified_long_count': len(qualified),
'entry_candidates_by_direction': dict(
Counter(str(row['direction']) for row in setup_candidates)
),
'rank_observation_count': len(rank_observations),
'rank_first_date': rank_dates[0] if rank_dates else None,
'rank_last_date': rank_dates[-1] if rank_dates else None,
}
_atomic_pickle(cache_path, cached)
if not quiet:
print(f'wrote candidate/rank cache: {cache_path}', flush=True)
return cached
def _checkpoint_state(
checkpoint_dir: Path,
fingerprint: str,
*,
resume: bool,
) -> dict[str, dict[str, Any]]:
manifest_path = checkpoint_dir / 'manifest.json'
if checkpoint_dir.exists() and not resume:
existing_cells = list(checkpoint_dir.glob('cell-*.json'))
if existing_cells:
raise SystemExit(
f'Checkpoint cells already exist at {checkpoint_dir}; use --resume '
'or a new --run-id'
)
checkpoint_dir.mkdir(parents=True, exist_ok=True)
if manifest_path.exists():
existing = json.loads(manifest_path.read_text(encoding='utf-8'))
if existing.get('fingerprint') != fingerprint:
raise SystemExit(
f'Checkpoint fingerprint mismatch at {checkpoint_dir}'
)
else:
_atomic_json(
manifest_path,
{
'runner_version': RUNNER_VERSION,
'fingerprint': fingerprint,
'created_at': datetime.now(timezone.utc).isoformat(),
},
)
completed: dict[str, dict[str, Any]] = {}
if resume:
for path in sorted(checkpoint_dir.glob('cell-*.json')):
row = json.loads(path.read_text(encoding='utf-8'))
completed[str(row['cell_id'])] = row
return completed
def _write_cell_checkpoint(
checkpoint_dir: Path,
row: dict[str, Any],
) -> None:
name = hashlib.sha256(str(row['cell_id']).encode('utf-8')).hexdigest()
_atomic_json(checkpoint_dir / f'cell-{name}.json', row)
def _fmt(value: Any, digits: int = 3) -> str:
if value is None:
return 'n/a'
if isinstance(value, float):
return f'{value:.{digits}f}'
return str(value)
def _operational_summary(cells: list[dict[str, Any]]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for arm in ARMS:
arm_cells = [
row
for row in cells
if row['arm_id'] == arm['id']
and float(row['cost_per_side_pct']) == 0.1
]
weekly = [
row['metrics']['weekly_rebalance']
for row in arm_cells
if row['metrics'].get('weekly_rebalance')
]
rows.append({
'arm_id': arm['id'],
'paths': len(arm_cells),
'median_trades': median(
row['metrics'].get('trades') for row in arm_cells
),
'median_blocked_fraction': median(
row['metrics'].get('blocked_fraction') for row in arm_cells
),
'median_avg_positions': median(
row['metrics'].get('avg_positions') for row in arm_cells
),
'peak_positions': max(
(
int(row['metrics'].get('peak_positions') or 0)
for row in arm_cells
),
default=0,
),
'median_turnover_multiple': median(
row['metrics'].get('turnover_multiple') for row in arm_cells
),
'min_risk_rejections': sum(
int(row['metrics'].get('skipped_min_initial_risk') or 0)
for row in arm_cells
),
'weekly_zero_entrant_fraction': median(
item.get('zero_entrant_fraction') for item in weekly
),
'weekly_entrant_pool_median': median(
item.get('entrant_pool_median') for item in weekly
),
'weekly_replacements': sum(
int(item.get('replacements') or 0) for item in weekly
),
'weekly_reentries_within_10_sessions': sum(
int(item.get('reentries_within_10_sessions') or 0)
for item in weekly
),
})
return rows
def _markdown(report: dict[str, Any]) -> str:
lines = [
'# Focused daily portfolio-capacity matrix',
'',
f'Generated: {report["generated_at"]}',
'',
'## Question',
'',
'The current daily Phase A control admitted 472 trades and rejected 519 '
'qualified opportunities because the ten-slot book was full. This run '
'brackets the economic cost of that binding constraint; it has no formal '
'promotion gate.',
'',
'> Universe caveat: today\'s production membership is projected backward. '
'Use paired arm-versus-control differences, not absolute profitability, '
'for construction conclusions.',
'',
'## Paired annual medians',
'',
]
paired = report['analysis']['paired_per_year']
for protocol in ('empty_book', 'warm_book'):
lines.extend([
f'### {protocol.replace("_", " ").title()} — 0.10% per fill',
'',
'| Arm | ΔEV net R | 90% context | ΔCalmar | 90% context |',
'|---|---:|---:|---:|---:|',
])
for arm in ARMS:
row = next(
item
for item in paired
if item['arm_id'] == arm['id']
and item['protocol'] == protocol
and float(item['cost_per_side_pct']) == 0.1
)
ev = row['headline']['ev_net_r']
calmar = row['headline']['calmar']
ev_ci = ev['bootstrap_90']
calmar_ci = calmar['bootstrap_90']
lines.append(
f'| {arm["id"]} | {_fmt(ev["paired_delta_median"])} | '
f'[{_fmt(ev_ci["p05"])}, {_fmt(ev_ci["p95"])}] | '
f'{_fmt(calmar["paired_delta_median"])} | '
f'[{_fmt(calmar_ci["p05"])}, {_fmt(calmar_ci["p95"])}] |'
)
lines.append('')
lines.extend([
'| Arm | ΔPF | ΔGain-to-Pain | ΔSortino | ΔCAGR pp | ΔMaxDD pp |',
'|---|---:|---:|---:|---:|---:|',
])
for arm in ARMS:
row = next(
item
for item in paired
if item['arm_id'] == arm['id']
and item['protocol'] == protocol
and float(item['cost_per_side_pct']) == 0.1
)
headline = row['headline']
lines.append(
f'| {arm["id"]} | '
f'{_fmt(headline["profit_factor"]["paired_delta_median"])} | '
f'{_fmt(headline["gain_to_pain"]["paired_delta_median"])} | '
f'{_fmt(headline["sortino"]["paired_delta_median"])} | '
f'{_fmt(headline["cagr_pct"]["paired_delta_median"])} | '
f'{_fmt(headline["max_drawdown_pct"]["paired_delta_median"])} |'
)
lines.append('')
lines.extend([
'## Warm-seed initialization dispersion',
'',
'| Arm | Cost/fill | Median EV IQR ratio | Median Calmar IQR ratio |',
'|---|---:|---:|---:|',
])
for row in report['analysis']['warm_seed_dispersion']:
lines.append(
f'| {row["arm_id"]} | {row["cost_per_side_pct"]:.2f}% | '
f'{_fmt(row["headline"]["ev_net_r"]["median_iqr_ratio"])} | '
f'{_fmt(row["headline"]["calmar"]["median_iqr_ratio"])} |'
)
lines.extend([
'',
'## Capacity and operations — 0.10% per fill',
'',
'| Arm | Median trades | Median blocked | Median positions | Peak | '
'Turnover | Min-risk rejects |',
'|---|---:|---:|---:|---:|---:|---:|',
])
for row in report['operational_summary']:
blocked = row['median_blocked_fraction']
blocked_text = (
f'{float(blocked) * 100.0:.1f}%' if blocked is not None else 'n/a'
)
lines.append(
f'| {row["arm_id"]} | {_fmt(row["median_trades"], 1)} | '
f'{blocked_text} | {_fmt(row["median_avg_positions"], 2)} | '
f'{row["peak_positions"]} | '
f'{_fmt(row["median_turnover_multiple"], 2)} | '
f'{row["min_risk_rejections"]} |'
)
weekly = next(
row
for row in report['operational_summary']
if row['arm_id'] == 'cap10_weekly_top10'
)
lines.extend([
'',
'## Weekly-ranking opportunity set',
'',
f'- Median fresh entrant pool: '
f'{_fmt(weekly["weekly_entrant_pool_median"], 1)}.',
f'- Median zero-entrant fraction: '
f'{_fmt(weekly["weekly_zero_entrant_fraction"], 3)}.',
f'- Replacements across reported paths: {weekly["weekly_replacements"]}.',
f'- Same-symbol re-entries within 10 sessions: '
f'{weekly["weekly_reentries_within_10_sessions"]}.',
'',
'Bootstrap intervals above resample seven annual summaries and are '
'descriptive context only. They are not gates or independent-population '
'confidence claims.',
])
return '\n'.join(lines)
async def _main() -> None:
args = _parse_args()
snapshot = Path(args.snapshot)
if not snapshot.exists():
raise SystemExit(f'Snapshot does not exist: {snapshot}')
if not SPEC_PATH.exists():
raise SystemExit(f'Frozen specification is missing: {SPEC_PATH}')
os.environ['BACKTEST_SNAPSHOT_OFFLINE'] = '1'
os.environ['BACKTEST_ALLOW_SPAWN'] = '1'
workers = _worker_count(str(args.workers))
snapshot_sha256 = _sha256_file(snapshot)
specification_sha256 = _sha256_file(SPEC_PATH)
snapshot_data = await _load_snapshot(snapshot, quiet=bool(args.quiet))
cache_path = (
Path(args.candidate_cache)
if args.candidate_cache
else ROOT / 'reports' / '.cache' / f'{args.run_id}-candidates.pkl'
)
candidate_cache = _build_candidate_cache(
snapshot_data,
snapshot=snapshot,
snapshot_sha256=snapshot_sha256,
cache_path=cache_path,
workers=workers,
quiet=bool(args.quiet),
)
cohort_manifest = build_cohort_manifest(
snapshot_data['benchmark_closes'].keys()
)
cohort_errors = validate_cohort_manifest(cohort_manifest)
cells = build_cells(cohort_manifest)
validation_payload = {
'runner_version': RUNNER_VERSION,
'snapshot': str(snapshot.resolve()),
'snapshot_sha256': snapshot_sha256,
'snapshot_sessions': {
'count': cohort_manifest['session_count'],
'first': cohort_manifest['snapshot_first_session'],
'last': cohort_manifest['snapshot_last_session'],
},
'candidate_rank_coverage': {
'first': candidate_cache['rank_first_date'],
'last': candidate_cache['rank_last_date'],
'observations': candidate_cache['rank_observation_count'],
'qualified_longs': candidate_cache['qualified_long_count'],
},
'universe_manifest': snapshot_data['universe_manifest'],
'empty_cluster_counts': cohort_manifest['empty_cluster_counts'],
'warm_seed_counts': cohort_manifest['warm_seed_counts'],
'empty_cluster_count': cohort_manifest['empty_cluster_count'],
'warm_cluster_count': cohort_manifest['warm_cluster_count'],
'expected_clusters': list(ANCHOR_YEARS),
'matrix_cells': len(cells),
'cache_path': str(cache_path.resolve()),
'cache_key_hash': _json_hash(candidate_cache['key']),
'errors': cohort_errors,
}
print(json.dumps(validation_payload, indent=2, sort_keys=True), flush=True)
if cohort_errors:
raise SystemExit(
'Cohort validation failed; revise and re-hash the specification'
)
if args.validate_only:
return
_assert_clean_worktree()
git_commit = _git_output('rev-parse', 'HEAD')
fingerprint_payload = {
'runner_version': RUNNER_VERSION,
'git_commit': git_commit,
'snapshot_sha256': snapshot_sha256,
'specification_sha256': specification_sha256,
'candidate_cache_key': candidate_cache['key'],
'universe_manifest': snapshot_data['universe_manifest'],
'cohort_manifest': cohort_manifest,
'arms': list(ARMS),
'costs_per_side_pct': list(COSTS_PER_SIDE_PCT),
'bootstrap': {
'replicates': BOOTSTRAP_REPLICATES,
'seed': BOOTSTRAP_SEED,
},
}
fingerprint = _json_hash(fingerprint_payload)
checkpoint_dir = (
Path(args.checkpoint)
if args.checkpoint
else ROOT / 'reports' / '.cache' / f'{args.run_id}-checkpoint'
)
completed = _checkpoint_state(
checkpoint_dir,
fingerprint,
resume=bool(args.resume),
)
expected_ids = {str(cell['cell_id']) for cell in cells}
unknown = set(completed) - expected_ids
if unknown:
raise SystemExit(
f'Checkpoint contains {len(unknown)} unknown matrix cells'
)
remaining = [
cell for cell in cells if str(cell['cell_id']) not in completed
]
if not args.quiet:
print(
f'matrix cells: {len(completed)} resumed, {len(remaining)} remaining',
flush=True,
)
worker_context = {
'qualified_candidates': candidate_cache['qualified_candidates'],
'daily_rank_map': candidate_cache['daily_rank_map'],
'prices': snapshot_data['prices'],
'benchmark_closes': snapshot_data['benchmark_closes'],
'ranking_key': snapshot_data['ranking_key'],
'exit_policy': snapshot_data['exit_policy'],
'hold_days': snapshot_data['hold_days'],
'risk_per_trade': snapshot_data['risk_per_trade'],
'atr_trail_multiplier': snapshot_data['atr_trail_multiplier'],
}
if workers == 1:
_worker_init(worker_context)
for index, cell in enumerate(remaining, 1):
row = _worker_run_cell(cell)
completed[str(row['cell_id'])] = row
_write_cell_checkpoint(checkpoint_dir, row)
if not args.quiet:
print(
f'portfolio cells: {len(completed)}/{len(cells)} '
f'({row["cell_id"]})',
flush=True,
)
elif remaining:
context = multiprocessing.get_context('spawn')
with ProcessPoolExecutor(
max_workers=workers,
mp_context=context,
initializer=_worker_init,
initargs=(worker_context,),
) as pool:
futures = {
pool.submit(_worker_run_cell, cell): str(cell['cell_id'])
for cell in remaining
}
for future in as_completed(futures):
row = future.result()
completed[str(row['cell_id'])] = row
_write_cell_checkpoint(checkpoint_dir, row)
if not args.quiet:
print(
f'portfolio cells: {len(completed)}/{len(cells)} '
f'({row["cell_id"]})',
flush=True,
)
missing = expected_ids - set(completed)
if missing:
raise RuntimeError(
f'Incomplete matrix: {len(missing)} cells are missing'
)
result_cells = sorted(
completed.values(),
key=lambda row: str(row['cell_id']),
)
analysis = aggregate_results(result_cells)
requirements_path = ROOT / 'requirements.txt'
report: dict[str, Any] = {
'run_id': args.run_id,
'status': 'complete',
'generated_at': datetime.now(timezone.utc).isoformat(),
'research_question': (
'Bracket the economic cost of the binding ten-position cap and '
'test whether weekly current-rank selection beats arrival order.'
),
'decision_rule': (
'No formal promotion gate. Report paired annual medians, warm-seed '
'EV/Calmar IQR ratios, and simple bootstrap intervals as context.'
),
'survivorship_bias_caveat': (
'The current production universe is projected backward; construction '
'conclusions rely on paired relative comparisons, not absolute levels.'
),
'motivation': {
'source': 'reports/research-matrix-phase-a.json a0_control full window',
'trades': 472,
'skipped_book_full': 519,
'blocked_fraction': 519 / (519 + 472),
'stale_claim_corrected': (
'The older weekly pre-gate-reset claim that cap 10 never bound '
'does not apply to the current daily configuration.'
),
},
'fingerprint': fingerprint,
'fingerprint_payload': fingerprint_payload,
'environment': {
'python': sys.version,
'platform': platform.platform(),
'requirements_sha256': (
_sha256_file(requirements_path)
if requirements_path.exists()
else None
),
'command': [sys.executable, *sys.argv],
},
'validation': validation_payload,
'runtime_config': snapshot_data['runtime_config'],
'universe_manifest': snapshot_data['universe_manifest'],
'candidate_cache': {
key: value
for key, value in candidate_cache.items()
if key
not in {
'qualified_candidates',
'daily_rank_map',
}
},
'cohort_manifest': cohort_manifest,
'arms': list(ARMS),
'costs_per_side_pct': list(COSTS_PER_SIDE_PCT),
'cell_count': len(result_cells),
'cells': result_cells,
'analysis': analysis,
'operational_summary': _operational_summary(result_cells),
}
out_path = (
Path(args.out)
if args.out
else ROOT
/ 'reports'
/ f'portfolio-construction-{args.run_id}.json'
)
_atomic_json(out_path, report)
_atomic_text(out_path.with_suffix('.md'), _markdown(report))
if not args.quiet:
print(f'wrote {out_path}', flush=True)
print(f'wrote {out_path.with_suffix(".md")}', flush=True)
if __name__ == '__main__':
asyncio.run(_main())
+5 -60
View File
@@ -68,6 +68,11 @@ ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts.research_rankings import ( # noqa: E402
_live_universe_rank_map,
_period_percentiles,
)
CACHE_VERSION = "research-matrix-v1-daily-prod"
# Pre-registered arm catalogue (order is report order). Control is a0.
@@ -210,66 +215,6 @@ def _sqlite_url(path: Path) -> str:
return f"sqlite+aiosqlite:///{path.resolve().as_posix()}"
def _period_percentiles(
observations: list[dict], value_key: str
) -> dict[tuple[str, str], float]:
by_period: dict[tuple, list[dict]] = {}
for row in observations:
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]]:
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
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=__doc__,