fix: align daily matrix ranking universe

This commit is contained in:
2026-07-17 16:39:43 +02:00
parent 27bc8a6631
commit 9800114fc4
3 changed files with 122 additions and 5 deletions
+67 -2
View File
@@ -37,7 +37,7 @@ POLICY_NAMES = (
"gate_reset_improved",
"two_session_confirmation",
)
CACHE_VERSION = "daily-reentry-matrix-v1"
CACHE_VERSION = "daily-reentry-matrix-v2-full-ranking-universe"
def _sqlite_url(path: Path) -> str:
@@ -312,12 +312,16 @@ async def _main() -> None:
cache_path = Path(args.candidate_cache) if args.candidate_cache else None
qualified_candidates: list[dict] | None = None
entry_candidate_count = 0
entry_candidates_by_direction: dict[str, int] = {}
if cache_path is not None and 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:
qualified_candidates = list(cached["qualified_candidates"])
entry_candidate_count = int(cached["entry_candidate_count"])
entry_candidates_by_direction = dict(
cached["entry_candidates_by_direction"]
)
if not args.quiet:
print(f"loaded qualified candidate cache: {cache_path}", flush=True)
elif not args.quiet:
@@ -338,6 +342,7 @@ async def _main() -> None:
benchmark_closes,
replay_start,
"daily",
True,
): symbol
for symbol, columns in prices.items()
}
@@ -347,6 +352,9 @@ async def _main() -> None:
print(f"daily replay: {index}/{len(futures)} tickers", flush=True)
entry_candidate_count = len(candidates)
entry_candidates_by_direction = dict(
Counter(row["direction"] for row in candidates)
)
bt._assign_momentum_percentiles(candidates)
bt._assign_residual_momentum_percentiles(candidates)
bt._assign_low_volatility_percentiles(candidates)
@@ -368,6 +376,9 @@ async def _main() -> None:
{
"key": cache_key,
"entry_candidate_count": entry_candidate_count,
"entry_candidates_by_direction": (
entry_candidates_by_direction
),
"qualified_candidates": qualified_candidates,
},
handle,
@@ -489,6 +500,53 @@ async def _main() -> None:
**row,
})
immediate_baseline_parity: dict
if "immediate" in policies:
direct_daily_baseline = bt._simulate_portfolio(
qualified_candidates,
simulation_prices,
benchmark_closes,
exit_policy,
hold_days,
ranking_key=ranking_key,
max_positions=args.base_capacity,
risk_per_trade=float(entry_config["risk_per_trade"]),
atr_trail_multiplier=trail_multiplier,
cost_per_side=args.base_cost_per_side_pct / 100.0,
start_date=requested_start,
)
if direct_daily_baseline is None:
raise RuntimeError("Direct daily no-lockdown baseline produced no trades")
immediate_all = next(
row
for row in primary
if row["lookback"] == "all" and row["arm"] == "immediate"
)
parity_fields = tuple(sorted(direct_daily_baseline))
parity_differences = {
field: {
"direct_daily_baseline": direct_daily_baseline.get(field),
"immediate_callback": immediate_all.get(field),
}
for field in parity_fields
if direct_daily_baseline.get(field) != immediate_all.get(field)
}
if parity_differences:
raise RuntimeError(
"Immediate callback diverges from direct daily baseline: "
f"{parity_differences}"
)
immediate_baseline_parity = {
"passed": True,
"compared_fields": list(parity_fields),
"direct_daily_baseline": direct_daily_baseline,
}
else:
immediate_baseline_parity = {
"passed": None,
"skipped": "immediate policy was not selected",
}
robustness: list[dict] = []
for cost_pct in all_costs:
for capacity in all_capacities:
@@ -543,10 +601,12 @@ async def _main() -> None:
"tickers_loaded": len(prices),
"tickers_qualified": len(qualified_symbols),
"entry_candidates": entry_candidate_count,
"entry_candidates_by_direction": entry_candidates_by_direction,
"qualified_candidates": len(qualified_candidates),
"params": {
"entry_cadence": "daily",
"target_model": "production_gtl",
"ranking_universe": "all_long_and_short_setups",
"policies": list(policies),
"base_cost_per_side_pct": args.base_cost_per_side_pct,
"base_capacity": args.base_capacity,
@@ -564,12 +624,17 @@ async def _main() -> None:
"ranking_key": ranking_key,
},
"primary_lookback_matrix": primary,
"immediate_baseline_parity": immediate_baseline_parity,
"cost_capacity_robustness": robustness,
"holdout": holdout,
"portfolio_simulations_executed": completed_sims,
"note": (
"The point-in-time daily setup replay and universe ranking are executed "
"once. All arms reuse the identical production-qualified candidate set. "
"once. Long and short setups both contribute to the production-faithful "
"cross-sectional percentiles; only qualified longs are tradable. All arms "
"reuse that identical production-qualified candidate set. The immediate "
"callback, when selected, must match a direct daily no-lockdown "
"simulation exactly. "
"Immediate is the daily no-lockdown baseline; next_session blocks only "
"the stop day; cooldown_5 permits re-entry at wait_sessions=5; gate_reset "
"requires an unqualified close before requalification; "