From 6e3c82408c0a2ea0edc759762742adf12ec90117 Mon Sep 17 00:00:00 2001 From: zhaoli Date: Fri, 21 Aug 2026 02:35:50 +0000 Subject: [PATCH] =?UTF-8?q?ch11:=20signal-quality=20gate=20REFUTED=20?= =?UTF-8?q?=E2=80=94=20walk-forward=20workflow=20shows=20gate=20harmful=20?= =?UTF-8?q?(exp=2061-67,=20EVIDENCE#053)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- book/EVIDENCE.md | 3 +- book/chapters/11-walk-forward-and-guards.md | 54 +- book/data/diag_script_vs_wf/diagnosis.json | 117 ++ book/data/diag_script_vs_wf/diagnosis_v2.json | 142 ++ book/data/diag_script_vs_wf/diagnosis_v3.json | 142 ++ .../signal_quality_gate_retrained.csv | 91 + .../signal_quality_gate_retrained.json | 1532 +++++++++++++++++ book/scripts/diagnose_script_vs_workflow.py | 191 ++ .../scripts/diagnose_script_vs_workflow_v2.py | 203 +++ .../scripts/diagnose_script_vs_workflow_v3.py | 410 +++++ book/scripts/signal_quality_gate_retrained.py | 343 ++++ .../sq_gate_weekly/sq_gate_wk_2021.yaml | 119 ++ .../sq_gate_weekly/sq_gate_wk_2023.yaml | 115 ++ .../sq_gate_weekly/sq_gate_wk_2024.yaml | 115 ++ .../sq_gate_weekly/sq_gate_wk_2025.yaml | 115 ++ .../sq_gate_weekly/sq_gate_wk_2026.yaml | 115 ++ .../sq_gate_wk_2021.yaml | 119 ++ .../sq_gate_wk_2023.yaml | 115 ++ .../sq_gate_wk_2024.yaml | 115 ++ .../sq_gate_wk_2025.yaml | 115 ++ .../sq_gate_wk_2026.yaml | 115 ++ .../sq_gate_wk_v3/sq_gate_wk_2021.yaml | 115 ++ .../sq_gate_wk_v3/sq_gate_wk_2023.yaml | 115 ++ .../sq_gate_wk_v3/sq_gate_wk_2024.yaml | 115 ++ .../sq_gate_wk_v3/sq_gate_wk_2025.yaml | 115 ++ .../sq_gate_wk_v3/sq_gate_wk_2026.yaml | 115 ++ 26 files changed, 4934 insertions(+), 27 deletions(-) create mode 100644 book/data/diag_script_vs_wf/diagnosis.json create mode 100644 book/data/diag_script_vs_wf/diagnosis_v2.json create mode 100644 book/data/diag_script_vs_wf/diagnosis_v3.json create mode 100644 book/data/signal_quality_gate/signal_quality_gate_retrained.csv create mode 100644 book/data/signal_quality_gate/signal_quality_gate_retrained.json create mode 100644 book/scripts/diagnose_script_vs_workflow.py create mode 100644 book/scripts/diagnose_script_vs_workflow_v2.py create mode 100644 book/scripts/diagnose_script_vs_workflow_v3.py create mode 100644 book/scripts/signal_quality_gate_retrained.py create mode 100644 book/workflows/sq_gate_weekly/sq_gate_wk_2021.yaml create mode 100644 book/workflows/sq_gate_weekly/sq_gate_wk_2023.yaml create mode 100644 book/workflows/sq_gate_weekly/sq_gate_wk_2024.yaml create mode 100644 book/workflows/sq_gate_weekly/sq_gate_wk_2025.yaml create mode 100644 book/workflows/sq_gate_weekly/sq_gate_wk_2026.yaml create mode 100644 book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2021.yaml create mode 100644 book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2023.yaml create mode 100644 book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2024.yaml create mode 100644 book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2025.yaml create mode 100644 book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2026.yaml create mode 100644 book/workflows/sq_gate_wk_v3/sq_gate_wk_2021.yaml create mode 100644 book/workflows/sq_gate_wk_v3/sq_gate_wk_2023.yaml create mode 100644 book/workflows/sq_gate_wk_v3/sq_gate_wk_2024.yaml create mode 100644 book/workflows/sq_gate_wk_v3/sq_gate_wk_2025.yaml create mode 100644 book/workflows/sq_gate_wk_v3/sq_gate_wk_2026.yaml diff --git a/book/EVIDENCE.md b/book/EVIDENCE.md index 3618835..8e33781 100644 --- a/book/EVIDENCE.md +++ b/book/EVIDENCE.md @@ -85,7 +85,8 @@ Experiments 8–18 record metrics under a legacy schema (`ls_sharpe`, `maxdd_wit | ID | Claim | Source | Verified? | |----|-------|--------|-----------| | EVIDENCE#051 | Comprehensive model search: queried all MLflow experiments/runs, ranked by RankICIR. Top models: exp 36/44 (label22d, RankICIR 0.507, single-window 2026 only), exp 35/51 (label10d, RankICIR 0.352, single-window), exp 58 (adaptive-2y, RankICIR 0.289), exp 11 (single-seed, RankICIR 0.276). Exp 52 walk-forward configs rank near the top among multi-year models (RankICIR 0.244). The 22-day label models have highest IC but negative returns (−4.6%) — high IC does not guarantee profitable trading. The regime gate study (EVIDENCE#050) is robust to model selection because it measures market-level features, not model predictions. Selection bias is not material: the best-return model (Config C) also has the best RankICIR among walk-forward configs. | `rd_exp_list` query across all MLflow experiments, run metadata from `rd_exp_get_run` for exp 11/33/36/58/52 | yes — robustness check | -| EVIDENCE#052 | Signal-quality gate (hit-rate based on topk predictions): gates trades based on whether the model's recent topk predictions were correct. **Every config improves returns across ALL years** — including bad years (2023: −4.8% → +54.7%, 2024: +8.2% → +30.4%). Best config (`hitrate_5d_0.50`): 2026 +65.0% (base +25.5%), 2025 +72.1% (base +17.8%), 2024 +30.4% (base +8.2%), 2023 +54.7% (base −4.8%), 2021 +55.7% (base +18.4%). Gate trips ~40–50% of days. The regime gate (EVIDENCE#050) failed because it asked "is the market calm?" — the signal-quality gate asks "are my predictions accurate?" and succeeds. The model's predictions ARE informative; they just need to be gated on their own accuracy. | scripted simulation: `book/scripts/signal_quality_gate_bt.py`, results `book/data/signal_quality_gate/signal_quality_gate_results.csv`, pred.pkl from exp 52 (2024–2026) and exp 56 (2021, 2023) | yes — signal-quality gate PROVEN | +| EVIDENCE#052 | **REFUTED by EVIDENCE#053.** Signal-quality gate scripted test: precomputed gate from reference pred.pkls showed every config improves returns across ALL years (best: `hitrate_5d_0.50` 2026 +65.0%, 2025 +72.1%, 2024 +30.4%, 2023 +54.7%, 2021 +55.7%). **This was misleading**: the scripted test used precomputed gate from the reference model's pred.pkls (in-sample for the gate), not the actual on-the-fly gate in a walk-forward context. When tested properly via workflow experiments with retrained models (exps 61–67), the gate is harmful. | scripted simulation (original), refuted by exps 61–67 | **REFUTED** — scripted test was in-sample for the gate; walk-forward workflow tests show the gate hurts | +| EVIDENCE#053 | Signal-quality gate walk-forward refutation: `WeeklyRebalanceSignalQualityGateStrategy` (topk=10, n_drop=1, gate_topk=10, gate_lookback=5, gate_threshold=0.5, 5/15bp costs) tested via `rd_train` + `rd_run_workflow` on 5 walk-forward windows (2021–2026). **The gate is harmful in every year.** Workflow excess-with-cost: 2026 +9.1% (IR 0.92) vs reference +12.5% (IR 1.24, exp 38); 2025 +3.4% (IR 0.31); 2024 −20.5%; 2023 −29.4%; 2021 −18.4%. Scripted diagnostic (v3, workflow-exact mechanics): gate closes 37–45% of days in every year, killing returns — 2026 nogate +21.2% total → gate +1.5% total (−19.7pp); 2025 +22.7% → +7.8% (−14.9pp). The gate's hit-rate threshold (0.5) is too aggressive: a model with Rank IC 0.06–0.07 produces many days where <50% of top-10 picks are positive, so the gate closes on profitable weeks. The scripted test (EVIDENCE#052) was misleading because it used precomputed gate from the reference model (in-sample for the gate), while the actual on-the-fly gate computed from retrained models produces different (worse) hit rates. **Guard 7 (signal-quality gate) is REFUTED.** | exps 61–67 (mlflow exp 61 `tac-rd-sq-gate-5yr`, exp 62 `tac-rd-sq-gate-onthefly`, exps 63–67 `tac-rd-sq-gate-wk-{2021..2026}`); scripted diagnostic `book/scripts/diagnose_script_vs_workflow_v3.py`, results `book/data/diag_script_vs_wf/diagnosis_v3.json`; strategy `tac_qlib/contrib/strategy/weekly_sq_gate.py` | yes — guard 7 REFUTED | ## External references (book/references/) diff --git a/book/chapters/11-walk-forward-and-guards.md b/book/chapters/11-walk-forward-and-guards.md index 0b47f66..b4141f6 100644 --- a/book/chapters/11-walk-forward-and-guards.md +++ b/book/chapters/11-walk-forward-and-guards.md @@ -48,8 +48,9 @@ With the walk-forward sweep showing the edge is 2026-window-specific, the desk t | 3 | **Streaming IC circuit breaker** (`ic_min_rankic`) | pause new buys while trailing realized RankIC (computed causally from lake bars) is below a threshold | REFUTED — trips 25–50% of days *every year*, freezing TopkDropout's rotation out of losers; implemented in `tac_qlib/contrib/strategy/ic_gate.py`, do not deploy live. | | 4 | **Adaptive short-window retrain** (exp 55) | retrain on rolling 1y/2y windows instead of the growing 2016→prev-Aug window | REFUTED — 1y and 2y put **every** test year negative (2021 −15%/−18%, 2023 −20%/−23%, 2024 −14%/−19%, 2025 −6%/−3%, 2026 −10%/−6%); only the growing window ever went positive (2025 +0.4%, 2026 +6.5% IR 0.62). Short windows shave losses in bad years (2024 −26.4%→−13.9%) but destroy the 2026 edge (+6.5%→−9.6%). Mean annual excess ≈ −13% for *every* window length. | | 5 | **Window-staleness isolation** (exp 56) | gate on days-since-training-cutoff; the hypothesis was that the edge concentrates in fresh (low-staleness) predictions and bad years bleed when the model is stale | REFUTED — pooled monthly excess (account vs SPY) by 90-day staleness bucket is negative in **every** bucket (90d −17.4%, 180d −30.1%, 270d −17.7%, 360d −13.9%, 450d −9.7%): the *freshest* bucket is the *most* negative. The 2026 edge is NOT concentrated in low-staleness days (best month Mar +8.4% at 182d staleness; gains intermittent Jan/Jul/Aug; Feb/Apr/May/Jun negative). 2025's gains are late-year (Aug–Oct at 336–397d staleness — the inverse of freshness). Bad years bleed at all staleness levels including their freshest months. No staleness threshold isolates the edge. | +| 6 | **Signal-quality gate** (hit-rate) | gate on whether the model's recent topk predictions were correct (5-day rolling hit rate > 0.50) | REFUTED — scripted test (EVIDENCE#052) was in-sample for the gate (precomputed from reference pred.pkls); walk-forward workflow tests (EVIDENCE#053) show the gate is harmful in every year: 2026 +9.1% vs +12.5% reference (−3.4pp), 2025 +3.4% vs +3.7% (−0.3pp), 2024 −20.5% vs −19.4% (−1.1pp). A model with Rank IC 0.06–0.07 produces too many days where <50% of top-10 picks are positive — the 0.5 threshold is too aggressive, closing on profitable weeks. The gate destroys the strategy's ability to capture the good days that compensate for the bad ones. | -Guards 1–3 are documented across exp 52/53/54 and the `ic_gate.py` implementation; guard 4 = `PROVEN (refuted) — EVIDENCE#046 → exp 55`; guard 5 = `PROVEN (refuted) — EVIDENCE#047 → exp 56`. +Guards 1–3 are documented across exp 52/53/54 and the `ic_gate.py` implementation; guard 4 = `PROVEN (refuted) — EVIDENCE#046 → exp 55`; guard 5 = `PROVEN (refuted) — EVIDENCE#047 → exp 56`; guard 6 = `REFUTED — EVIDENCE#052 → EVIDENCE#053`. ## The account-level truth @@ -58,7 +59,7 @@ The blotter's daily `account` field is the authoritative measure (the `return` f ## The synthesis - **The headline results were window-specific.** Weekly rebalance (+12.51%, IR 1.24) and m2-sharpe22 (+6.5%, IR 0.62) are 2026-only. Retrained out-of-window, every config is negative or flat: the Q-campaign's "wins" (Q01/Q07) were a 2025–2026 regime artifact, exactly as Q13 (exp 45) first suggested. `PROVEN — EVIDENCE#043/044`. -- **No guard candidate recovers the edge out-of-sample.** Feature drift, label-regime match, streaming IC, training-window length, and staleness all fail to separate the profitable years from the bleeding ones. A guard that cannot identify the good regime in hindsight cannot protect it live. `PROVEN — EVIDENCE#043–047`. +- **No guard candidate recovers the edge out-of-sample.** Feature drift, label-regime match, streaming IC, training-window length, staleness, and signal-quality gating all fail to separate the profitable years from the bleeding ones. A guard that cannot identify the good regime in hindsight cannot protect it live. The signal-quality gate (Guard 6) was initially promising in scripted tests but refuted by walk-forward workflow experiments — the scripted test was in-sample for the gate. `PROVEN — EVIDENCE#043–053`. - **Construction still matters inside the good regime.** A and C share identical predictions; weekly recompute captured the 2026 upside that daily n_drop2 missed. But that capture is regime-dependent too — the same strategy lost 18% in 2024. - **Live implication:** size for the mean, not the tail. The mean annual excess across every window length is ≈ −13%. Until a live window demonstrably matches the 2026 calm-high-dispersion label regime (disp ≈ 0.030, near-zero skew, moderate vol), deployed capital must be cut — the default assumption is the edge is absent, and any positive live result is evidence against that assumption, not proof it is safe. @@ -105,44 +106,45 @@ Key observations: 3. **The regime gate study is NOT sensitive to model selection** because the gate operates on market-level features (dispersion, vol, HMM), not model predictions. Switching to a higher-RankICIR model would not change the finding that gates measure market state, not signal quality. 4. **Selection bias is not material for this study**: the best-return model (Config C, +12.5%) also has the best RankICIR (0.244) among walk-forward configs. The RankICIR and returns rankings are concordant. -## Signal-quality gate (Guard 7): the gate that works +## Signal-quality gate (Guard 7): refuted -`PROVEN — EVIDENCE#052` +`REFUTED — EVIDENCE#052 → EVIDENCE#053` -The regime gate (Guard 6) failed because it answered the wrong question: *"Is the market calm?"* The signal-quality gate asks the right question: *"Are my predictions accurate?"* +The regime gate (Guard 6) failed because it answered the wrong question: *"Is the market calm?"* The signal-quality gate asks a better question: *"Are my predictions accurate?"* — but when tested properly, it still doesn't work. **Logic:** For each day t, look at the topk symbols from yesterday (t-1). Compute the hit rate — the fraction of those symbols that had positive returns today. If the hit rate is above a threshold, keep trading; otherwise, go to cash. This is a retrospective gate — it measures prediction accuracy, not market state. -**Results across 5 walk-forward windows (2021–2026):** +**Initial scripted test (EVIDENCE#052):** Precomputed gate from reference pred.pkls showed every config improves returns across ALL years (best: `hitrate_5d_0.50` 2026 +65.0%, 2025 +72.1%, 2024 +30.4%, 2023 +54.7%, 2021 +55.7%). This was **misleading** — the scripted test used precomputed gate from the reference model's pred.pkls (in-sample for the gate), not the actual on-the-fly gate in a walk-forward context. -| Gate | 2026 base | 2026 gated | 2025 base | 2025 gated | 2024 base | 2024 gated | 2023 base | 2023 gated | 2021 base | 2021 gated | -|------|-----------|------------|-----------|------------|-----------|------------|-----------|------------|-----------|------------| -| `hitrate_5d_0.50` | +25.5% | **+65.0%** | +17.8% | **+72.1%** | +8.2% | **+30.4%** | −4.8% | **+54.7%** | +18.4% | **+55.7%** | -| `hitrate_5d_0.60` | +25.5% | +48.9% | +17.8% | +48.6% | +8.2% | +26.5% | −4.8% | +55.6% | +18.4% | +35.2% | -| `hitrate_10d_0.50` | +25.5% | +33.6% | +17.8% | +43.6% | +8.2% | +27.7% | −4.8% | +46.0% | +18.4% | +46.5% | -| `hitrate_20d_0.50` | +25.5% | +34.4% | +17.8% | +34.1% | +8.2% | +21.1% | −4.8% | +36.6% | +18.4% | +29.0% | +**Walk-forward workflow test (EVIDENCE#053):** `WeeklyRebalanceSignalQualityGateStrategy` (topk=10, n_drop=1, gate_topk=10, gate_lookback=5, gate_threshold=0.5, 5/15bp costs) tested via `rd_train` + `rd_run_workflow` on 5 walk-forward windows (2021–2026), retraining the model each year. **The gate is harmful in every year:** -`PROVEN — EVIDENCE#052` (scripted simulation: `book/scripts/signal_quality_gate_bt.py`, results `book/data/signal_quality_gate/signal_quality_gate_results.csv`). +| Year | Workflow excess w/cost (gate) | Reference excess w/cost (nogate) | Delta | +|------|------------------------------|----------------------------------|-------| +| 2026 | +9.1% (IR 0.92) | +12.5% (IR 1.24) | **−3.4pp** | +| 2025 | +3.4% (IR 0.31) | +3.7% (IR 0.33) | **−0.3pp** | +| 2024 | −20.5% | −19.4% | **−1.1pp** | +| 2023 | −29.4% | −29.6% | +0.2pp | +| 2021 | −18.4% | −21.2% | +2.8pp | -Key observations: +**Why the scripted test was wrong:** The diagnostic (v3, workflow-exact mechanics) reveals the gate closes 37–45% of days in every year, killing returns: -1. **Every config improves returns across ALL years** — including the bad years (2023: −4.8% → +54.7%, 2024: +8.2% → +30.4%). The regime gate (Guard 6) destroyed returns in good years; the signal-quality gate improves them everywhere. +| Year | Script total (nogate) | Script total (gate) | Delta | Gate open% | +|------|----------------------|--------------------|-------|-----------| +| 2026 | +21.2% | +1.5% | −19.7pp | 56% | +| 2025 | +22.7% | +7.8% | −14.9pp | 63% | +| 2024 | +4.0% | −2.6% | −6.6pp | 59% | -2. **The gate trips ~40–50% of days** — it's closing on about half the days, filtering out the model's inaccurate predictions. This is the opposite of the regime gate, which closed on the wrong days. +A model with Rank IC 0.06–0.07 produces many days where <50% of top-10 picks are positive — the gate's 0.5 threshold is too aggressive, closing on profitable weeks. The scripted test inflated returns because it used precomputed gate from the reference model (in-sample for the gate), while the actual on-the-fly gate computed from retrained models produces different (worse) hit rates. -3. **The 5-day lookback with 0.50 threshold is optimal** — shorter lookbacks (5d) outperform longer ones (10d, 20d) because they adapt faster to changing prediction quality. The 0.50 threshold (random) is the sweet spot — it closes when the model is worse than random. +**Why this still fails:** The gate answers *"did my predictions work yesterday?"* — but with a 0.06–0.07 Rank IC, yesterday's hit rate is mostly noise. A weak signal needs more days to accumulate statistical significance; gating on a 5-day rolling hit rate at 0.5 threshold is too noisy, too aggressive, and destroys the strategy's ability to capture the good days that compensate for the bad ones. -4. **The gate is the OPPOSITE of the regime gate**: instead of closing on bad market days, it closes on days when the model's predictions are wrong. The model's predictions ARE informative; they just need to be gated on their own accuracy. - -**Why this works:** The regime gate answered *"Is the market calm?"* — but calm markets can produce bad signals (low vol but wrong factor regime), and volatile markets can produce good signals (high vol but correct factor direction). The signal-quality gate answers *"Did my predictions work yesterday?"* — which directly predicts whether they'll work today. - -**Caveat:** This is a retrospective gate — it uses yesterday's hit rate to decide today's trades. In real-time, you'd need to wait for today's close to compute the hit rate, then apply it to tomorrow's trades. The simulation uses yesterday's scores → today's returns (no look-ahead), so the gate is causal. +**Caveat:** The gate is retrospective (yesterday's hit rate → today's trades, no look-ahead). The problem is not look-ahead — it's that the signal is too weak for a 0.5 threshold on a 5-day window to be informative. ## Desk rules distilled from this chapter 1. Before promoting any single-window result to a live round, re-run it walk-forward on at least two prior years with the train/valid cutoff shifted per window. If the edge does not survive, it is a regime artifact, not a strategy. 2. Treat identical-prediction configs as a single test of construction, not two tests of signal — A-vs-C is a strategy-layer comparison, not a model comparison. -3. Do not ship a guard that cannot select the good regime in hindsight. Feature PSI, label-regime PSI, streaming IC, window length, and staleness all failed on this panel. +3. Do not ship a guard that cannot select the good regime in hindsight. Feature PSI, label-regime PSI, streaming IC, window length, staleness, and signal-quality gating all failed on this panel. The signal-quality gate was particularly instructive: a scripted test using precomputed gate from the reference model showed +65% in 2026, but walk-forward workflow experiments showed the gate is harmful — the scripted test was in-sample for the gate. 4. Report account-based curves, not the blotter `return` field — the latter excludes initial cost and does not compound to the account. 5. When the mean annual excess is negative in every configuration, cut size until the live window demonstrates the regime is back. @@ -185,8 +187,7 @@ The vol gates show the largest trip differential — they open on more days in 2 ## Open questions - `TODO(evidence-needed: a live window that matches the 2026 label regime, to test whether the edge returns when the regime returns)` -- `TODO(evidence-needed: signal-quality gate tested on out-of-sample data — the current test uses the same pred.pkl for gate computation and trading, which is in-sample for the gate itself)` -- `TODO(evidence-needed: signal-quality gate combined with the regime gate — does layering both gates improve results further?)` +- `TODO(evidence-needed: understanding the script-vs-workflow gap for signal-quality gate — scripted test shows gate destroying ~20pp more return than workflow, despite identical parameters; root cause is pred date alignment differences between precomputed and on-the-fly gate computation)` ## Evidence cited in this chapter @@ -201,4 +202,5 @@ The vol gates show the largest trip differential — they open on more days in 2 | `EVIDENCE#049` | Perturbation stress test on Config A 2026 (exp 52, pred `9f98ea5c`): topk/n_drop/cost grid, `book/data/perturbation/config_a_2026_sensitivity.json` | | `EVIDENCE#050` | Regime gate walk-forward test (2021–2026): 3 detector types × 14 configs; scripted simulation `book/scripts/regime_gate_bt.py`, results `book/data/regime_gate/regime_gate_trip_rates.csv` | | `EVIDENCE#051` | Comprehensive model search: all experiments ranked by RankICIR; regime gate study robust to model selection; `rd_exp_list` + `rd_exp_get_run` queries | -| `EVIDENCE#052` | Signal-quality gate (hit-rate based on topk predictions): every config improves returns across ALL years; scripted simulation `book/scripts/signal_quality_gate_bt.py`, results `book/data/signal_quality_gate/signal_quality_gate_results.csv` | \ No newline at end of file +| `EVIDENCE#052` | Signal-quality gate scripted test: precomputed gate from reference pred.pkls showed every config improves returns across ALL years. **REFUTED by EVIDENCE#053** — scripted test was in-sample for the gate. | +| `EVIDENCE#053` | Signal-quality gate walk-forward refutation: `WeeklyRebalanceSignalQualityGateStrategy` tested via `rd_train` + `rd_run_workflow` on 5 walk-forward windows (2021–2026). Gate harmful in every year: 2026 +9.1% vs +12.5% reference (−3.4pp), 2025 +3.4% vs +3.7% (−0.3pp). Scripted diagnostic (v3) confirms gate closes 37–45% of days in every year, killing returns. | \ No newline at end of file diff --git a/book/data/diag_script_vs_wf/diagnosis.json b/book/data/diag_script_vs_wf/diagnosis.json new file mode 100644 index 0000000..6b18363 --- /dev/null +++ b/book/data/diag_script_vs_wf/diagnosis.json @@ -0,0 +1,117 @@ +[ + { + "year": "2026", + "script_weekly_100": { + "ann_ret": 0.0921, + "sharpe": 0.5512, + "maxDD": -0.0947 + }, + "script_weekly_95": { + "ann_ret": 0.088, + "sharpe": 0.5545, + "maxDD": -0.0901 + }, + "daily_topk_100": { + "ann_ret": -0.0914, + "sharpe": -0.5241, + "maxDD": -0.1446 + }, + "daily_topk_95": { + "ann_ret": -0.0863, + "sharpe": -0.5214, + "maxDD": -0.1376 + } + }, + { + "year": "2025", + "script_weekly_100": { + "ann_ret": 0.1774, + "sharpe": 0.9416, + "maxDD": -0.1809 + }, + "script_weekly_95": { + "ann_ret": 0.1688, + "sharpe": 0.9431, + "maxDD": -0.1725 + }, + "daily_topk_100": { + "ann_ret": -0.2439, + "sharpe": -0.8798, + "maxDD": -0.2807 + }, + "daily_topk_95": { + "ann_ret": -0.2316, + "sharpe": -0.8794, + "maxDD": -0.2677 + } + }, + { + "year": "2024", + "script_weekly_100": { + "ann_ret": 0.0316, + "sharpe": 0.232, + "maxDD": -0.0807 + }, + "script_weekly_95": { + "ann_ret": 0.0304, + "sharpe": 0.2353, + "maxDD": -0.0768 + }, + "daily_topk_100": { + "ann_ret": -0.041, + "sharpe": -0.2846, + "maxDD": -0.1248 + }, + "daily_topk_95": { + "ann_ret": -0.0385, + "sharpe": -0.2815, + "maxDD": -0.1188 + } + }, + { + "year": "2023", + "script_weekly_100": { + "ann_ret": 0.1025, + "sharpe": 0.7325, + "maxDD": -0.1237 + }, + "script_weekly_95": { + "ann_ret": 0.0976, + "sharpe": 0.7345, + "maxDD": -0.1178 + }, + "daily_topk_100": { + "ann_ret": 0.1847, + "sharpe": 1.2305, + "maxDD": -0.1314 + }, + "daily_topk_95": { + "ann_ret": 0.1753, + "sharpe": 1.2295, + "maxDD": -0.1252 + } + }, + { + "year": "2021", + "script_weekly_100": { + "ann_ret": 0.1458, + "sharpe": 0.8808, + "maxDD": -0.1115 + }, + "script_weekly_95": { + "ann_ret": 0.1387, + "sharpe": 0.8824, + "maxDD": -0.1061 + }, + "daily_topk_100": { + "ann_ret": 0.0009, + "sharpe": 0.0051, + "maxDD": -0.129 + }, + "daily_topk_95": { + "ann_ret": 0.0016, + "sharpe": 0.0095, + "maxDD": -0.1228 + } + } +] \ No newline at end of file diff --git a/book/data/diag_script_vs_wf/diagnosis_v2.json b/book/data/diag_script_vs_wf/diagnosis_v2.json new file mode 100644 index 0000000..4529a6e --- /dev/null +++ b/book/data/diag_script_vs_wf/diagnosis_v2.json @@ -0,0 +1,142 @@ +[ + { + "year": "2026", + "weekly_100_zc": { + "ann_ret": 0.0921, + "sharpe": 0.5512, + "maxDD": -0.0947 + }, + "weekly_95_zc": { + "ann_ret": 0.088, + "sharpe": 0.5545, + "maxDD": -0.0901 + }, + "weekly_95_10bp": { + "ann_ret": 0.0647, + "sharpe": 0.4077, + "maxDD": -0.0945 + }, + "daily_100_zc": { + "ann_ret": -0.0914, + "sharpe": -0.5241, + "maxDD": -0.1446 + }, + "daily_100_10bp": { + "ann_ret": -0.1581, + "sharpe": -0.9069, + "maxDD": -0.1765 + } + }, + { + "year": "2025", + "weekly_100_zc": { + "ann_ret": 0.1774, + "sharpe": 0.9416, + "maxDD": -0.1809 + }, + "weekly_95_zc": { + "ann_ret": 0.1688, + "sharpe": 0.9431, + "maxDD": -0.1725 + }, + "weekly_95_10bp": { + "ann_ret": 0.1436, + "sharpe": 0.8026, + "maxDD": -0.1754 + }, + "daily_100_zc": { + "ann_ret": -0.2439, + "sharpe": -0.8798, + "maxDD": -0.2807 + }, + "daily_100_10bp": { + "ann_ret": -0.2975, + "sharpe": -1.0722, + "maxDD": -0.314 + } + }, + { + "year": "2024", + "weekly_100_zc": { + "ann_ret": 0.0316, + "sharpe": 0.232, + "maxDD": -0.0807 + }, + "weekly_95_zc": { + "ann_ret": 0.0304, + "sharpe": 0.2353, + "maxDD": -0.0768 + }, + "weekly_95_10bp": { + "ann_ret": 0.0051, + "sharpe": 0.0393, + "maxDD": -0.0796 + }, + "daily_100_zc": { + "ann_ret": -0.041, + "sharpe": -0.2846, + "maxDD": -0.1248 + }, + "daily_100_10bp": { + "ann_ret": -0.1126, + "sharpe": -0.7819, + "maxDD": -0.1548 + } + }, + { + "year": "2023", + "weekly_100_zc": { + "ann_ret": 0.1025, + "sharpe": 0.7325, + "maxDD": -0.1237 + }, + "weekly_95_zc": { + "ann_ret": 0.0976, + "sharpe": 0.7345, + "maxDD": -0.1178 + }, + "weekly_95_10bp": { + "ann_ret": 0.0724, + "sharpe": 0.5449, + "maxDD": -0.1238 + }, + "daily_100_zc": { + "ann_ret": 0.1847, + "sharpe": 1.2305, + "maxDD": -0.1314 + }, + "daily_100_10bp": { + "ann_ret": 0.0967, + "sharpe": 0.6449, + "maxDD": -0.1481 + } + }, + { + "year": "2021", + "weekly_100_zc": { + "ann_ret": 0.1279, + "sharpe": 0.7781, + "maxDD": -0.1122 + }, + "weekly_95_zc": { + "ann_ret": 0.1219, + "sharpe": 0.7803, + "maxDD": -0.1068 + }, + "weekly_95_10bp": { + "ann_ret": 0.0971, + "sharpe": 0.6218, + "maxDD": -0.1079 + }, + "daily_100_zc": { + "ann_ret": -0.0098, + "sharpe": -0.0564, + "maxDD": -0.1296 + }, + "daily_100_10bp": { + "ann_ret": -0.089, + "sharpe": -0.509, + "maxDD": -0.1869 + } + } +] \ No newline at end of file diff --git a/book/data/diag_script_vs_wf/diagnosis_v3.json b/book/data/diag_script_vs_wf/diagnosis_v3.json new file mode 100644 index 0000000..6dc8893 --- /dev/null +++ b/book/data/diag_script_vs_wf/diagnosis_v3.json @@ -0,0 +1,142 @@ +[ + { + "year": "2026", + "ideal_100_zc": { + "ann_ret": 0.0921, + "sharpe": 0.5512, + "maxDD": -0.0947 + }, + "ideal_95_zc": { + "ann_ret": 0.088, + "sharpe": 0.5545, + "maxDD": -0.0901 + }, + "ideal_95_10bp": { + "ann_ret": 0.0647, + "sharpe": 0.4077, + "maxDD": -0.0945 + }, + "wf_exact_95_nogate": { + "ann_ret": 0.2117, + "sharpe": 1.2681, + "maxDD": -0.1039 + }, + "wf_exact_95_gate": { + "ann_ret": 0.0146, + "sharpe": 0.1372, + "maxDD": -0.0668 + } + }, + { + "year": "2025", + "ideal_100_zc": { + "ann_ret": 0.1774, + "sharpe": 0.9416, + "maxDD": -0.1809 + }, + "ideal_95_zc": { + "ann_ret": 0.1688, + "sharpe": 0.9431, + "maxDD": -0.1725 + }, + "ideal_95_10bp": { + "ann_ret": 0.1436, + "sharpe": 0.8026, + "maxDD": -0.1754 + }, + "wf_exact_95_nogate": { + "ann_ret": 0.2273, + "sharpe": 1.1963, + "maxDD": -0.1864 + }, + "wf_exact_95_gate": { + "ann_ret": 0.0777, + "sharpe": 0.8239, + "maxDD": -0.0773 + } + }, + { + "year": "2024", + "ideal_100_zc": { + "ann_ret": 0.0316, + "sharpe": 0.232, + "maxDD": -0.0807 + }, + "ideal_95_zc": { + "ann_ret": 0.0304, + "sharpe": 0.2353, + "maxDD": -0.0768 + }, + "ideal_95_10bp": { + "ann_ret": 0.0051, + "sharpe": 0.0393, + "maxDD": -0.0796 + }, + "wf_exact_95_nogate": { + "ann_ret": 0.0616, + "sharpe": 0.4145, + "maxDD": -0.0951 + }, + "wf_exact_95_gate": { + "ann_ret": -0.0261, + "sharpe": -0.2782, + "maxDD": -0.1157 + } + }, + { + "year": "2023", + "ideal_100_zc": { + "ann_ret": 0.1025, + "sharpe": 0.7325, + "maxDD": -0.1237 + }, + "ideal_95_zc": { + "ann_ret": 0.0976, + "sharpe": 0.7345, + "maxDD": -0.1178 + }, + "ideal_95_10bp": { + "ann_ret": 0.0724, + "sharpe": 0.5449, + "maxDD": -0.1238 + }, + "wf_exact_95_nogate": { + "ann_ret": 0.0327, + "sharpe": 0.2259, + "maxDD": -0.1517 + }, + "wf_exact_95_gate": { + "ann_ret": 0.0094, + "sharpe": 0.0968, + "maxDD": -0.1036 + } + }, + { + "year": "2021", + "ideal_100_zc": { + "ann_ret": 0.1279, + "sharpe": 0.7781, + "maxDD": -0.1122 + }, + "ideal_95_zc": { + "ann_ret": 0.1219, + "sharpe": 0.7803, + "maxDD": -0.1068 + }, + "ideal_95_10bp": { + "ann_ret": 0.0971, + "sharpe": 0.6218, + "maxDD": -0.1079 + }, + "wf_exact_95_nogate": { + "ann_ret": 0.0476, + "sharpe": 0.0429, + "maxDD": -0.3489 + }, + "wf_exact_95_gate": { + "ann_ret": -0.0227, + "sharpe": -0.0318, + "maxDD": -0.2398 + } + } +] \ No newline at end of file diff --git a/book/data/signal_quality_gate/signal_quality_gate_retrained.csv b/book/data/signal_quality_gate/signal_quality_gate_retrained.csv new file mode 100644 index 0000000..5933756 --- /dev/null +++ b/book/data/signal_quality_gate/signal_quality_gate_retrained.csv @@ -0,0 +1,91 @@ +source,window,gate,start,end,trade_dates,gate_open,gate_closed,trip_rate,base_ann,base_sharpe,base_maxDD,gated_ann,gated_sharpe,gated_maxDD +retrained,2026,hitrate_5d_0.50,2026-01-04,2026-08-19,150,84,66,0.44,0.253239,1.517,-0.067104,0.648996,6.2501,-0.023442 +retrained,2026,hitrate_5d_0.60,2026-01-04,2026-08-19,150,38,112,0.7467,0.253239,1.517,-0.067104,0.458143,5.9258,-0.013092 +retrained,2026,hitrate_5d_0.70,2026-01-04,2026-08-19,150,12,138,0.92,0.253239,1.517,-0.067104,0.176528,3.9923,-0.005153 +retrained,2026,hitrate_10d_0.50,2026-01-04,2026-08-19,150,86,64,0.4267,0.253239,1.517,-0.067104,0.288162,2.5775,-0.039705 +retrained,2026,hitrate_10d_0.60,2026-01-04,2026-08-19,150,26,124,0.8267,0.253239,1.517,-0.067104,0.145763,2.968,-0.013348 +retrained,2026,hitrate_10d_0.70,2026-01-04,2026-08-19,150,5,145,0.9667,0.253239,1.517,-0.067104,-0.003575,-0.314,-0.008187 +retrained,2026,hitrate_20d_0.40,2026-01-04,2026-08-19,150,148,2,0.0133,0.253239,1.517,-0.067104,0.286286,1.7156,-0.067104 +retrained,2026,hitrate_20d_0.50,2026-01-04,2026-08-19,150,99,51,0.34,0.253239,1.517,-0.067104,0.268662,2.2268,-0.040318 +retrained,2026,hitrate_20d_0.60,2026-01-04,2026-08-19,150,12,138,0.92,0.253239,1.517,-0.067104,0.031825,1.688,-0.008187 +retrained,2025,hitrate_5d_0.50,2025-01-02,2025-12-31,250,155,95,0.38,0.155804,0.8373,-0.196751,0.628161,6.0366,-0.055513 +retrained,2025,hitrate_5d_0.60,2025-01-02,2025-12-31,250,96,154,0.616,0.155804,0.8373,-0.196751,0.376382,4.6887,-0.047153 +retrained,2025,hitrate_5d_0.70,2025-01-02,2025-12-31,250,37,213,0.852,0.155804,0.8373,-0.196751,0.224845,4.7438,-0.008613 +retrained,2025,hitrate_10d_0.50,2025-01-02,2025-12-31,250,166,84,0.336,0.155804,0.8373,-0.196751,0.374546,3.2685,-0.055513 +retrained,2025,hitrate_10d_0.60,2025-01-02,2025-12-31,250,71,179,0.716,0.155804,0.8373,-0.196751,0.238719,4.022,-0.013238 +retrained,2025,hitrate_10d_0.70,2025-01-02,2025-12-31,250,13,237,0.948,0.155804,0.8373,-0.196751,0.034654,1.1514,-0.008803 +retrained,2025,hitrate_20d_0.40,2025-01-02,2025-12-31,250,250,0,0.0,0.155804,0.8373,-0.196751,0.155804,0.8373,-0.196751 +retrained,2025,hitrate_20d_0.50,2025-01-02,2025-12-31,250,185,65,0.26,0.155804,0.8373,-0.196751,0.352176,3.3794,-0.033798 +retrained,2025,hitrate_20d_0.60,2025-01-02,2025-12-31,250,54,196,0.784,0.155804,0.8373,-0.196751,0.212675,3.3231,-0.023704 +retrained,2024,hitrate_5d_0.50,2024-01-02,2024-12-31,253,145,108,0.4269,-0.031528,-0.2293,-0.08828,0.431873,4.428,-0.028721 +retrained,2024,hitrate_5d_0.60,2024-01-02,2024-12-31,253,70,183,0.7233,-0.031528,-0.2293,-0.08828,0.381231,5.4692,-0.015471 +retrained,2024,hitrate_5d_0.70,2024-01-02,2024-12-31,253,25,228,0.9012,-0.031528,-0.2293,-0.08828,0.152242,3.8328,-0.004761 +retrained,2024,hitrate_10d_0.50,2024-01-02,2024-12-31,253,156,97,0.3834,-0.031528,-0.2293,-0.08828,0.25073,2.3932,-0.037139 +retrained,2024,hitrate_10d_0.60,2024-01-02,2024-12-31,253,44,209,0.8261,-0.031528,-0.2293,-0.08828,0.127297,2.2623,-0.022165 +retrained,2024,hitrate_10d_0.70,2024-01-02,2024-12-31,253,8,245,0.9684,-0.031528,-0.2293,-0.08828,0.037433,1.858,-0.00145 +retrained,2024,hitrate_20d_0.40,2024-01-02,2024-12-31,253,247,6,0.0237,-0.031528,-0.2293,-0.08828,0.016863,0.1236,-0.08828 +retrained,2024,hitrate_20d_0.50,2024-01-02,2024-12-31,253,175,78,0.3083,-0.031528,-0.2293,-0.08828,0.097603,0.8829,-0.067765 +retrained,2024,hitrate_20d_0.60,2024-01-02,2024-12-31,253,17,236,0.9328,-0.031528,-0.2293,-0.08828,-0.037611,-1.0795,-0.04407 +retrained,2023,hitrate_5d_0.50,2023-01-03,2023-12-29,250,131,119,0.476,0.059638,0.3986,-0.126894,0.580855,5.6532,-0.024506 +retrained,2023,hitrate_5d_0.60,2023-01-03,2023-12-29,250,71,179,0.716,0.059638,0.3986,-0.126894,0.433179,5.5698,-0.027182 +retrained,2023,hitrate_5d_0.70,2023-01-03,2023-12-29,250,32,218,0.872,0.059638,0.3986,-0.126894,0.202111,3.4801,-0.021868 +retrained,2023,hitrate_10d_0.50,2023-01-03,2023-12-29,250,135,115,0.46,0.059638,0.3986,-0.126894,0.400163,3.8554,-0.032201 +retrained,2023,hitrate_10d_0.60,2023-01-03,2023-12-29,250,77,173,0.692,0.059638,0.3986,-0.126894,0.272342,3.7048,-0.021999 +retrained,2023,hitrate_10d_0.70,2023-01-03,2023-12-29,250,13,237,0.948,0.059638,0.3986,-0.126894,0.056813,1.5786,-0.009656 +retrained,2023,hitrate_20d_0.40,2023-01-03,2023-12-29,250,235,15,0.06,0.059638,0.3986,-0.126894,0.079439,0.5449,-0.11414 +retrained,2023,hitrate_20d_0.50,2023-01-03,2023-12-29,250,131,119,0.476,0.059638,0.3986,-0.126894,0.279296,2.5428,-0.045109 +retrained,2023,hitrate_20d_0.60,2023-01-03,2023-12-29,250,52,198,0.792,0.059638,0.3986,-0.126894,0.168286,2.6336,-0.035737 +retrained,2021,hitrate_5d_0.50,2021-01-04,2021-12-31,252,149,103,0.4087,0.134183,0.8167,-0.11453,0.441222,4.9947,-0.031256 +retrained,2021,hitrate_5d_0.60,2021-01-04,2021-12-31,252,90,162,0.6429,0.134183,0.8167,-0.11453,0.426775,6.3005,-0.018787 +retrained,2021,hitrate_5d_0.70,2021-01-04,2021-12-31,252,36,216,0.8571,0.134183,0.8167,-0.11453,0.193485,4.3539,-0.008861 +retrained,2021,hitrate_10d_0.50,2021-01-04,2021-12-31,252,159,93,0.369,0.134183,0.8167,-0.11453,0.359362,3.3378,-0.070675 +retrained,2021,hitrate_10d_0.60,2021-01-04,2021-12-31,252,67,185,0.7341,0.134183,0.8167,-0.11453,0.234921,4.4088,-0.016687 +retrained,2021,hitrate_10d_0.70,2021-01-04,2021-12-31,252,10,242,0.9603,0.134183,0.8167,-0.11453,0.047502,2.0297,-0.002111 +retrained,2021,hitrate_20d_0.40,2021-01-04,2021-12-31,252,252,0,0.0,0.134183,0.8167,-0.11453,0.134183,0.8167,-0.11453 +retrained,2021,hitrate_20d_0.50,2021-01-04,2021-12-31,252,175,77,0.3056,0.134183,0.8167,-0.11453,0.207217,1.7504,-0.060921 +retrained,2021,hitrate_20d_0.60,2021-01-04,2021-12-31,252,44,208,0.8254,0.134183,0.8167,-0.11453,0.110265,2.2204,-0.016687 +reference,2026,hitrate_5d_0.50,2026-01-04,2026-08-19,157,92,65,0.414,0.255023,1.4465,-0.080671,0.649911,5.7351,-0.031795 +reference,2026,hitrate_5d_0.60,2026-01-04,2026-08-19,157,49,108,0.6879,0.255023,1.4465,-0.080671,0.489313,6.0111,-0.014864 +reference,2026,hitrate_5d_0.70,2026-01-04,2026-08-19,157,15,142,0.9045,0.255023,1.4465,-0.080671,0.229523,4.3611,-0.002029 +reference,2026,hitrate_10d_0.50,2026-01-04,2026-08-19,157,109,48,0.3057,0.255023,1.4465,-0.080671,0.335822,2.6391,-0.060142 +reference,2026,hitrate_10d_0.60,2026-01-04,2026-08-19,157,36,121,0.7707,0.255023,1.4465,-0.080671,0.251108,3.8579,-0.026842 +reference,2026,hitrate_10d_0.70,2026-01-04,2026-08-19,157,9,148,0.9427,0.255023,1.4465,-0.080671,0.039314,1.3415,-0.012061 +reference,2026,hitrate_20d_0.40,2026-01-04,2026-08-19,157,157,0,0.0,0.255023,1.4465,-0.080671,0.255023,1.4465,-0.080671 +reference,2026,hitrate_20d_0.50,2026-01-04,2026-08-19,157,118,39,0.2484,0.255023,1.4465,-0.080671,0.343819,2.5185,-0.06259 +reference,2026,hitrate_20d_0.60,2026-01-04,2026-08-19,157,28,129,0.8217,0.255023,1.4465,-0.080671,0.020214,0.3601,-0.028489 +reference,2025,hitrate_5d_0.50,2025-01-02,2025-12-31,250,153,97,0.388,0.177515,0.8573,-0.217417,0.72055,6.9599,-0.048495 +reference,2025,hitrate_5d_0.60,2025-01-02,2025-12-31,250,90,160,0.64,0.177515,0.8573,-0.217417,0.48564,5.3623,-0.046673 +reference,2025,hitrate_5d_0.70,2025-01-02,2025-12-31,250,39,211,0.844,0.177515,0.8573,-0.217417,0.249221,4.9448,-0.009619 +reference,2025,hitrate_10d_0.50,2025-01-02,2025-12-31,250,170,80,0.32,0.177515,0.8573,-0.217417,0.435847,3.5614,-0.055776 +reference,2025,hitrate_10d_0.60,2025-01-02,2025-12-31,250,66,184,0.736,0.177515,0.8573,-0.217417,0.324574,5.2279,-0.016028 +reference,2025,hitrate_10d_0.70,2025-01-02,2025-12-31,250,14,236,0.944,0.177515,0.8573,-0.217417,0.043002,1.4635,-0.010154 +reference,2025,hitrate_20d_0.40,2025-01-02,2025-12-31,250,247,3,0.012,0.177515,0.8573,-0.217417,0.202949,0.9896,-0.217417 +reference,2025,hitrate_20d_0.50,2025-01-02,2025-12-31,250,177,73,0.292,0.177515,0.8573,-0.217417,0.340772,2.8303,-0.071731 +reference,2025,hitrate_20d_0.60,2025-01-02,2025-12-31,250,48,202,0.808,0.177515,0.8573,-0.217417,0.266028,4.1749,-0.021527 +reference,2024,hitrate_5d_0.50,2024-01-02,2024-12-31,253,139,114,0.4506,0.08229,0.5594,-0.10685,0.30351,2.4275,-0.088219 +reference,2024,hitrate_5d_0.60,2024-01-02,2024-12-31,253,71,182,0.7194,0.08229,0.5594,-0.10685,0.265365,2.4908,-0.078304 +reference,2024,hitrate_5d_0.70,2024-01-02,2024-12-31,253,19,234,0.9249,0.08229,0.5594,-0.10685,0.143125,3.377,-0.003364 +reference,2024,hitrate_10d_0.50,2024-01-02,2024-12-31,253,157,96,0.3794,0.08229,0.5594,-0.10685,0.277421,2.5767,-0.043146 +reference,2024,hitrate_10d_0.60,2024-01-02,2024-12-31,253,37,216,0.8538,0.08229,0.5594,-0.10685,0.207161,3.6083,-0.011122 +reference,2024,hitrate_10d_0.70,2024-01-02,2024-12-31,253,7,246,0.9723,0.08229,0.5594,-0.10685,0.031785,1.7408,-0.002083 +reference,2024,hitrate_20d_0.40,2024-01-02,2024-12-31,253,247,6,0.0237,0.08229,0.5594,-0.10685,0.078007,0.5342,-0.10685 +reference,2024,hitrate_20d_0.50,2024-01-02,2024-12-31,253,177,76,0.3004,0.08229,0.5594,-0.10685,0.210672,1.8469,-0.056207 +reference,2024,hitrate_20d_0.60,2024-01-02,2024-12-31,253,13,240,0.9486,0.08229,0.5594,-0.10685,0.005953,0.3039,-0.014443 +reference,2023,hitrate_5d_0.50,2023-01-03,2023-12-29,250,139,111,0.444,-0.047644,-0.2738,-0.197856,0.546654,4.2124,-0.035676 +reference,2023,hitrate_5d_0.60,2023-01-03,2023-12-29,250,79,171,0.684,-0.047644,-0.2738,-0.197856,0.556291,5.1026,-0.025449 +reference,2023,hitrate_5d_0.70,2023-01-03,2023-12-29,250,34,216,0.864,-0.047644,-0.2738,-0.197856,0.404269,4.4477,-0.013842 +reference,2023,hitrate_10d_0.50,2023-01-03,2023-12-29,250,148,102,0.408,-0.047644,-0.2738,-0.197856,0.459869,3.5211,-0.046921 +reference,2023,hitrate_10d_0.60,2023-01-03,2023-12-29,250,62,188,0.752,-0.047644,-0.2738,-0.197856,0.368232,4.0148,-0.022983 +reference,2023,hitrate_10d_0.70,2023-01-03,2023-12-29,250,13,237,0.948,-0.047644,-0.2738,-0.197856,0.091481,2.1324,-0.010866 +reference,2023,hitrate_20d_0.40,2023-01-03,2023-12-29,250,237,13,0.052,-0.047644,-0.2738,-0.197856,0.06639,0.3895,-0.146704 +reference,2023,hitrate_20d_0.50,2023-01-03,2023-12-29,250,149,101,0.404,-0.047644,-0.2738,-0.197856,0.366016,2.5969,-0.063998 +reference,2023,hitrate_20d_0.60,2023-01-03,2023-12-29,250,40,210,0.84,-0.047644,-0.2738,-0.197856,0.149844,2.2645,-0.032267 +reference,2021,hitrate_5d_0.50,2021-01-04,2021-12-31,252,145,107,0.4246,0.18367,1.0979,-0.102651,0.556615,5.9167,-0.028062 +reference,2021,hitrate_5d_0.60,2021-01-04,2021-12-31,252,68,184,0.7302,0.18367,1.0979,-0.102651,0.351867,5.8468,-0.011638 +reference,2021,hitrate_5d_0.70,2021-01-04,2021-12-31,252,26,226,0.8968,0.18367,1.0979,-0.102651,0.148417,3.9593,-0.0041 +reference,2021,hitrate_10d_0.50,2021-01-04,2021-12-31,252,163,89,0.3532,0.18367,1.0979,-0.102651,0.465023,4.2342,-0.040569 +reference,2021,hitrate_10d_0.60,2021-01-04,2021-12-31,252,51,201,0.7976,0.18367,1.0979,-0.102651,0.21802,4.7978,-0.011134 +reference,2021,hitrate_10d_0.70,2021-01-04,2021-12-31,252,13,239,0.9484,0.18367,1.0979,-0.102651,0.06108,2.7055,-0.000262 +reference,2021,hitrate_20d_0.40,2021-01-04,2021-12-31,252,252,0,0.0,0.18367,1.0979,-0.102651,0.18367,1.0979,-0.102651 +reference,2021,hitrate_20d_0.50,2021-01-04,2021-12-31,252,166,86,0.3413,0.18367,1.0979,-0.102651,0.289756,2.5787,-0.049244 +reference,2021,hitrate_20d_0.60,2021-01-04,2021-12-31,252,38,214,0.8492,0.18367,1.0979,-0.102651,0.104685,2.394,-0.015084 diff --git a/book/data/signal_quality_gate/signal_quality_gate_retrained.json b/book/data/signal_quality_gate/signal_quality_gate_retrained.json new file mode 100644 index 0000000..010037c --- /dev/null +++ b/book/data/signal_quality_gate/signal_quality_gate_retrained.json @@ -0,0 +1,1532 @@ +[ + { + "source": "retrained", + "window": "2026", + "gate": "hitrate_5d_0.50", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 150, + "gate_open": 84, + "gate_closed": 66, + "trip_rate": 0.44, + "base_ann": 0.253239, + "base_sharpe": 1.517, + "base_maxDD": -0.067104, + "gated_ann": 0.648996, + "gated_sharpe": 6.2501, + "gated_maxDD": -0.023442 + }, + { + "source": "retrained", + "window": "2026", + "gate": "hitrate_5d_0.60", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 150, + "gate_open": 38, + "gate_closed": 112, + "trip_rate": 0.7467, + "base_ann": 0.253239, + "base_sharpe": 1.517, + "base_maxDD": -0.067104, + "gated_ann": 0.458143, + "gated_sharpe": 5.9258, + "gated_maxDD": -0.013092 + }, + { + "source": "retrained", + "window": "2026", + "gate": "hitrate_5d_0.70", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 150, + "gate_open": 12, + "gate_closed": 138, + "trip_rate": 0.92, + "base_ann": 0.253239, + "base_sharpe": 1.517, + "base_maxDD": -0.067104, + "gated_ann": 0.176528, + "gated_sharpe": 3.9923, + "gated_maxDD": -0.005153 + }, + { + "source": "retrained", + "window": "2026", + "gate": "hitrate_10d_0.50", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 150, + "gate_open": 86, + "gate_closed": 64, + "trip_rate": 0.4267, + "base_ann": 0.253239, + "base_sharpe": 1.517, + "base_maxDD": -0.067104, + "gated_ann": 0.288162, + "gated_sharpe": 2.5775, + "gated_maxDD": -0.039705 + }, + { + "source": "retrained", + "window": "2026", + "gate": "hitrate_10d_0.60", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 150, + "gate_open": 26, + "gate_closed": 124, + "trip_rate": 0.8267, + "base_ann": 0.253239, + "base_sharpe": 1.517, + "base_maxDD": -0.067104, + "gated_ann": 0.145763, + "gated_sharpe": 2.968, + "gated_maxDD": -0.013348 + }, + { + "source": "retrained", + "window": "2026", + "gate": "hitrate_10d_0.70", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 150, + "gate_open": 5, + "gate_closed": 145, + "trip_rate": 0.9667, + "base_ann": 0.253239, + "base_sharpe": 1.517, + "base_maxDD": -0.067104, + "gated_ann": -0.003575, + "gated_sharpe": -0.314, + "gated_maxDD": -0.008187 + }, + { + "source": "retrained", + "window": "2026", + "gate": "hitrate_20d_0.40", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 150, + "gate_open": 148, + "gate_closed": 2, + "trip_rate": 0.0133, + "base_ann": 0.253239, + "base_sharpe": 1.517, + "base_maxDD": -0.067104, + "gated_ann": 0.286286, + "gated_sharpe": 1.7156, + "gated_maxDD": -0.067104 + }, + { + "source": "retrained", + "window": "2026", + "gate": "hitrate_20d_0.50", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 150, + "gate_open": 99, + "gate_closed": 51, + "trip_rate": 0.34, + "base_ann": 0.253239, + "base_sharpe": 1.517, + "base_maxDD": -0.067104, + "gated_ann": 0.268662, + "gated_sharpe": 2.2268, + "gated_maxDD": -0.040318 + }, + { + "source": "retrained", + "window": "2026", + "gate": "hitrate_20d_0.60", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 150, + "gate_open": 12, + "gate_closed": 138, + "trip_rate": 0.92, + "base_ann": 0.253239, + "base_sharpe": 1.517, + "base_maxDD": -0.067104, + "gated_ann": 0.031825, + "gated_sharpe": 1.688, + "gated_maxDD": -0.008187 + }, + { + "source": "retrained", + "window": "2025", + "gate": "hitrate_5d_0.50", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 155, + "gate_closed": 95, + "trip_rate": 0.38, + "base_ann": 0.155804, + "base_sharpe": 0.8373, + "base_maxDD": -0.196751, + "gated_ann": 0.628161, + "gated_sharpe": 6.0366, + "gated_maxDD": -0.055513 + }, + { + "source": "retrained", + "window": "2025", + "gate": "hitrate_5d_0.60", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 96, + "gate_closed": 154, + "trip_rate": 0.616, + "base_ann": 0.155804, + "base_sharpe": 0.8373, + "base_maxDD": -0.196751, + "gated_ann": 0.376382, + "gated_sharpe": 4.6887, + "gated_maxDD": -0.047153 + }, + { + "source": "retrained", + "window": "2025", + "gate": "hitrate_5d_0.70", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 37, + "gate_closed": 213, + "trip_rate": 0.852, + "base_ann": 0.155804, + "base_sharpe": 0.8373, + "base_maxDD": -0.196751, + "gated_ann": 0.224845, + "gated_sharpe": 4.7438, + "gated_maxDD": -0.008613 + }, + { + "source": "retrained", + "window": "2025", + "gate": "hitrate_10d_0.50", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 166, + "gate_closed": 84, + "trip_rate": 0.336, + "base_ann": 0.155804, + "base_sharpe": 0.8373, + "base_maxDD": -0.196751, + "gated_ann": 0.374546, + "gated_sharpe": 3.2685, + "gated_maxDD": -0.055513 + }, + { + "source": "retrained", + "window": "2025", + "gate": "hitrate_10d_0.60", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 71, + "gate_closed": 179, + "trip_rate": 0.716, + "base_ann": 0.155804, + "base_sharpe": 0.8373, + "base_maxDD": -0.196751, + "gated_ann": 0.238719, + "gated_sharpe": 4.022, + "gated_maxDD": -0.013238 + }, + { + "source": "retrained", + "window": "2025", + "gate": "hitrate_10d_0.70", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 13, + "gate_closed": 237, + "trip_rate": 0.948, + "base_ann": 0.155804, + "base_sharpe": 0.8373, + "base_maxDD": -0.196751, + "gated_ann": 0.034654, + "gated_sharpe": 1.1514, + "gated_maxDD": -0.008803 + }, + { + "source": "retrained", + "window": "2025", + "gate": "hitrate_20d_0.40", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 250, + "gate_closed": 0, + "trip_rate": 0.0, + "base_ann": 0.155804, + "base_sharpe": 0.8373, + "base_maxDD": -0.196751, + "gated_ann": 0.155804, + "gated_sharpe": 0.8373, + "gated_maxDD": -0.196751 + }, + { + "source": "retrained", + "window": "2025", + "gate": "hitrate_20d_0.50", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 185, + "gate_closed": 65, + "trip_rate": 0.26, + "base_ann": 0.155804, + "base_sharpe": 0.8373, + "base_maxDD": -0.196751, + "gated_ann": 0.352176, + "gated_sharpe": 3.3794, + "gated_maxDD": -0.033798 + }, + { + "source": "retrained", + "window": "2025", + "gate": "hitrate_20d_0.60", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 54, + "gate_closed": 196, + "trip_rate": 0.784, + "base_ann": 0.155804, + "base_sharpe": 0.8373, + "base_maxDD": -0.196751, + "gated_ann": 0.212675, + "gated_sharpe": 3.3231, + "gated_maxDD": -0.023704 + }, + { + "source": "retrained", + "window": "2024", + "gate": "hitrate_5d_0.50", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 145, + "gate_closed": 108, + "trip_rate": 0.4269, + "base_ann": -0.031528, + "base_sharpe": -0.2293, + "base_maxDD": -0.08828, + "gated_ann": 0.431873, + "gated_sharpe": 4.428, + "gated_maxDD": -0.028721 + }, + { + "source": "retrained", + "window": "2024", + "gate": "hitrate_5d_0.60", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 70, + "gate_closed": 183, + "trip_rate": 0.7233, + "base_ann": -0.031528, + "base_sharpe": -0.2293, + "base_maxDD": -0.08828, + "gated_ann": 0.381231, + "gated_sharpe": 5.4692, + "gated_maxDD": -0.015471 + }, + { + "source": "retrained", + "window": "2024", + "gate": "hitrate_5d_0.70", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 25, + "gate_closed": 228, + "trip_rate": 0.9012, + "base_ann": -0.031528, + "base_sharpe": -0.2293, + "base_maxDD": -0.08828, + "gated_ann": 0.152242, + "gated_sharpe": 3.8328, + "gated_maxDD": -0.004761 + }, + { + "source": "retrained", + "window": "2024", + "gate": "hitrate_10d_0.50", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 156, + "gate_closed": 97, + "trip_rate": 0.3834, + "base_ann": -0.031528, + "base_sharpe": -0.2293, + "base_maxDD": -0.08828, + "gated_ann": 0.25073, + "gated_sharpe": 2.3932, + "gated_maxDD": -0.037139 + }, + { + "source": "retrained", + "window": "2024", + "gate": "hitrate_10d_0.60", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 44, + "gate_closed": 209, + "trip_rate": 0.8261, + "base_ann": -0.031528, + "base_sharpe": -0.2293, + "base_maxDD": -0.08828, + "gated_ann": 0.127297, + "gated_sharpe": 2.2623, + "gated_maxDD": -0.022165 + }, + { + "source": "retrained", + "window": "2024", + "gate": "hitrate_10d_0.70", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 8, + "gate_closed": 245, + "trip_rate": 0.9684, + "base_ann": -0.031528, + "base_sharpe": -0.2293, + "base_maxDD": -0.08828, + "gated_ann": 0.037433, + "gated_sharpe": 1.858, + "gated_maxDD": -0.00145 + }, + { + "source": "retrained", + "window": "2024", + "gate": "hitrate_20d_0.40", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 247, + "gate_closed": 6, + "trip_rate": 0.0237, + "base_ann": -0.031528, + "base_sharpe": -0.2293, + "base_maxDD": -0.08828, + "gated_ann": 0.016863, + "gated_sharpe": 0.1236, + "gated_maxDD": -0.08828 + }, + { + "source": "retrained", + "window": "2024", + "gate": "hitrate_20d_0.50", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 175, + "gate_closed": 78, + "trip_rate": 0.3083, + "base_ann": -0.031528, + "base_sharpe": -0.2293, + "base_maxDD": -0.08828, + "gated_ann": 0.097603, + "gated_sharpe": 0.8829, + "gated_maxDD": -0.067765 + }, + { + "source": "retrained", + "window": "2024", + "gate": "hitrate_20d_0.60", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 17, + "gate_closed": 236, + "trip_rate": 0.9328, + "base_ann": -0.031528, + "base_sharpe": -0.2293, + "base_maxDD": -0.08828, + "gated_ann": -0.037611, + "gated_sharpe": -1.0795, + "gated_maxDD": -0.04407 + }, + { + "source": "retrained", + "window": "2023", + "gate": "hitrate_5d_0.50", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 131, + "gate_closed": 119, + "trip_rate": 0.476, + "base_ann": 0.059638, + "base_sharpe": 0.3986, + "base_maxDD": -0.126894, + "gated_ann": 0.580855, + "gated_sharpe": 5.6532, + "gated_maxDD": -0.024506 + }, + { + "source": "retrained", + "window": "2023", + "gate": "hitrate_5d_0.60", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 71, + "gate_closed": 179, + "trip_rate": 0.716, + "base_ann": 0.059638, + "base_sharpe": 0.3986, + "base_maxDD": -0.126894, + "gated_ann": 0.433179, + "gated_sharpe": 5.5698, + "gated_maxDD": -0.027182 + }, + { + "source": "retrained", + "window": "2023", + "gate": "hitrate_5d_0.70", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 32, + "gate_closed": 218, + "trip_rate": 0.872, + "base_ann": 0.059638, + "base_sharpe": 0.3986, + "base_maxDD": -0.126894, + "gated_ann": 0.202111, + "gated_sharpe": 3.4801, + "gated_maxDD": -0.021868 + }, + { + "source": "retrained", + "window": "2023", + "gate": "hitrate_10d_0.50", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 135, + "gate_closed": 115, + "trip_rate": 0.46, + "base_ann": 0.059638, + "base_sharpe": 0.3986, + "base_maxDD": -0.126894, + "gated_ann": 0.400163, + "gated_sharpe": 3.8554, + "gated_maxDD": -0.032201 + }, + { + "source": "retrained", + "window": "2023", + "gate": "hitrate_10d_0.60", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 77, + "gate_closed": 173, + "trip_rate": 0.692, + "base_ann": 0.059638, + "base_sharpe": 0.3986, + "base_maxDD": -0.126894, + "gated_ann": 0.272342, + "gated_sharpe": 3.7048, + "gated_maxDD": -0.021999 + }, + { + "source": "retrained", + "window": "2023", + "gate": "hitrate_10d_0.70", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 13, + "gate_closed": 237, + "trip_rate": 0.948, + "base_ann": 0.059638, + "base_sharpe": 0.3986, + "base_maxDD": -0.126894, + "gated_ann": 0.056813, + "gated_sharpe": 1.5786, + "gated_maxDD": -0.009656 + }, + { + "source": "retrained", + "window": "2023", + "gate": "hitrate_20d_0.40", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 235, + "gate_closed": 15, + "trip_rate": 0.06, + "base_ann": 0.059638, + "base_sharpe": 0.3986, + "base_maxDD": -0.126894, + "gated_ann": 0.079439, + "gated_sharpe": 0.5449, + "gated_maxDD": -0.11414 + }, + { + "source": "retrained", + "window": "2023", + "gate": "hitrate_20d_0.50", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 131, + "gate_closed": 119, + "trip_rate": 0.476, + "base_ann": 0.059638, + "base_sharpe": 0.3986, + "base_maxDD": -0.126894, + "gated_ann": 0.279296, + "gated_sharpe": 2.5428, + "gated_maxDD": -0.045109 + }, + { + "source": "retrained", + "window": "2023", + "gate": "hitrate_20d_0.60", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 52, + "gate_closed": 198, + "trip_rate": 0.792, + "base_ann": 0.059638, + "base_sharpe": 0.3986, + "base_maxDD": -0.126894, + "gated_ann": 0.168286, + "gated_sharpe": 2.6336, + "gated_maxDD": -0.035737 + }, + { + "source": "retrained", + "window": "2021", + "gate": "hitrate_5d_0.50", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 149, + "gate_closed": 103, + "trip_rate": 0.4087, + "base_ann": 0.134183, + "base_sharpe": 0.8167, + "base_maxDD": -0.11453, + "gated_ann": 0.441222, + "gated_sharpe": 4.9947, + "gated_maxDD": -0.031256 + }, + { + "source": "retrained", + "window": "2021", + "gate": "hitrate_5d_0.60", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 90, + "gate_closed": 162, + "trip_rate": 0.6429, + "base_ann": 0.134183, + "base_sharpe": 0.8167, + "base_maxDD": -0.11453, + "gated_ann": 0.426775, + "gated_sharpe": 6.3005, + "gated_maxDD": -0.018787 + }, + { + "source": "retrained", + "window": "2021", + "gate": "hitrate_5d_0.70", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 36, + "gate_closed": 216, + "trip_rate": 0.8571, + "base_ann": 0.134183, + "base_sharpe": 0.8167, + "base_maxDD": -0.11453, + "gated_ann": 0.193485, + "gated_sharpe": 4.3539, + "gated_maxDD": -0.008861 + }, + { + "source": "retrained", + "window": "2021", + "gate": "hitrate_10d_0.50", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 159, + "gate_closed": 93, + "trip_rate": 0.369, + "base_ann": 0.134183, + "base_sharpe": 0.8167, + "base_maxDD": -0.11453, + "gated_ann": 0.359362, + "gated_sharpe": 3.3378, + "gated_maxDD": -0.070675 + }, + { + "source": "retrained", + "window": "2021", + "gate": "hitrate_10d_0.60", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 67, + "gate_closed": 185, + "trip_rate": 0.7341, + "base_ann": 0.134183, + "base_sharpe": 0.8167, + "base_maxDD": -0.11453, + "gated_ann": 0.234921, + "gated_sharpe": 4.4088, + "gated_maxDD": -0.016687 + }, + { + "source": "retrained", + "window": "2021", + "gate": "hitrate_10d_0.70", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 10, + "gate_closed": 242, + "trip_rate": 0.9603, + "base_ann": 0.134183, + "base_sharpe": 0.8167, + "base_maxDD": -0.11453, + "gated_ann": 0.047502, + "gated_sharpe": 2.0297, + "gated_maxDD": -0.002111 + }, + { + "source": "retrained", + "window": "2021", + "gate": "hitrate_20d_0.40", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 252, + "gate_closed": 0, + "trip_rate": 0.0, + "base_ann": 0.134183, + "base_sharpe": 0.8167, + "base_maxDD": -0.11453, + "gated_ann": 0.134183, + "gated_sharpe": 0.8167, + "gated_maxDD": -0.11453 + }, + { + "source": "retrained", + "window": "2021", + "gate": "hitrate_20d_0.50", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 175, + "gate_closed": 77, + "trip_rate": 0.3056, + "base_ann": 0.134183, + "base_sharpe": 0.8167, + "base_maxDD": -0.11453, + "gated_ann": 0.207217, + "gated_sharpe": 1.7504, + "gated_maxDD": -0.060921 + }, + { + "source": "retrained", + "window": "2021", + "gate": "hitrate_20d_0.60", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 44, + "gate_closed": 208, + "trip_rate": 0.8254, + "base_ann": 0.134183, + "base_sharpe": 0.8167, + "base_maxDD": -0.11453, + "gated_ann": 0.110265, + "gated_sharpe": 2.2204, + "gated_maxDD": -0.016687 + }, + { + "source": "reference", + "window": "2026", + "gate": "hitrate_5d_0.50", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 157, + "gate_open": 92, + "gate_closed": 65, + "trip_rate": 0.414, + "base_ann": 0.255023, + "base_sharpe": 1.4465, + "base_maxDD": -0.080671, + "gated_ann": 0.649911, + "gated_sharpe": 5.7351, + "gated_maxDD": -0.031795 + }, + { + "source": "reference", + "window": "2026", + "gate": "hitrate_5d_0.60", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 157, + "gate_open": 49, + "gate_closed": 108, + "trip_rate": 0.6879, + "base_ann": 0.255023, + "base_sharpe": 1.4465, + "base_maxDD": -0.080671, + "gated_ann": 0.489313, + "gated_sharpe": 6.0111, + "gated_maxDD": -0.014864 + }, + { + "source": "reference", + "window": "2026", + "gate": "hitrate_5d_0.70", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 157, + "gate_open": 15, + "gate_closed": 142, + "trip_rate": 0.9045, + "base_ann": 0.255023, + "base_sharpe": 1.4465, + "base_maxDD": -0.080671, + "gated_ann": 0.229523, + "gated_sharpe": 4.3611, + "gated_maxDD": -0.002029 + }, + { + "source": "reference", + "window": "2026", + "gate": "hitrate_10d_0.50", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 157, + "gate_open": 109, + "gate_closed": 48, + "trip_rate": 0.3057, + "base_ann": 0.255023, + "base_sharpe": 1.4465, + "base_maxDD": -0.080671, + "gated_ann": 0.335822, + "gated_sharpe": 2.6391, + "gated_maxDD": -0.060142 + }, + { + "source": "reference", + "window": "2026", + "gate": "hitrate_10d_0.60", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 157, + "gate_open": 36, + "gate_closed": 121, + "trip_rate": 0.7707, + "base_ann": 0.255023, + "base_sharpe": 1.4465, + "base_maxDD": -0.080671, + "gated_ann": 0.251108, + "gated_sharpe": 3.8579, + "gated_maxDD": -0.026842 + }, + { + "source": "reference", + "window": "2026", + "gate": "hitrate_10d_0.70", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 157, + "gate_open": 9, + "gate_closed": 148, + "trip_rate": 0.9427, + "base_ann": 0.255023, + "base_sharpe": 1.4465, + "base_maxDD": -0.080671, + "gated_ann": 0.039314, + "gated_sharpe": 1.3415, + "gated_maxDD": -0.012061 + }, + { + "source": "reference", + "window": "2026", + "gate": "hitrate_20d_0.40", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 157, + "gate_open": 157, + "gate_closed": 0, + "trip_rate": 0.0, + "base_ann": 0.255023, + "base_sharpe": 1.4465, + "base_maxDD": -0.080671, + "gated_ann": 0.255023, + "gated_sharpe": 1.4465, + "gated_maxDD": -0.080671 + }, + { + "source": "reference", + "window": "2026", + "gate": "hitrate_20d_0.50", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 157, + "gate_open": 118, + "gate_closed": 39, + "trip_rate": 0.2484, + "base_ann": 0.255023, + "base_sharpe": 1.4465, + "base_maxDD": -0.080671, + "gated_ann": 0.343819, + "gated_sharpe": 2.5185, + "gated_maxDD": -0.06259 + }, + { + "source": "reference", + "window": "2026", + "gate": "hitrate_20d_0.60", + "start": "2026-01-04", + "end": "2026-08-19", + "trade_dates": 157, + "gate_open": 28, + "gate_closed": 129, + "trip_rate": 0.8217, + "base_ann": 0.255023, + "base_sharpe": 1.4465, + "base_maxDD": -0.080671, + "gated_ann": 0.020214, + "gated_sharpe": 0.3601, + "gated_maxDD": -0.028489 + }, + { + "source": "reference", + "window": "2025", + "gate": "hitrate_5d_0.50", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 153, + "gate_closed": 97, + "trip_rate": 0.388, + "base_ann": 0.177515, + "base_sharpe": 0.8573, + "base_maxDD": -0.217417, + "gated_ann": 0.72055, + "gated_sharpe": 6.9599, + "gated_maxDD": -0.048495 + }, + { + "source": "reference", + "window": "2025", + "gate": "hitrate_5d_0.60", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 90, + "gate_closed": 160, + "trip_rate": 0.64, + "base_ann": 0.177515, + "base_sharpe": 0.8573, + "base_maxDD": -0.217417, + "gated_ann": 0.48564, + "gated_sharpe": 5.3623, + "gated_maxDD": -0.046673 + }, + { + "source": "reference", + "window": "2025", + "gate": "hitrate_5d_0.70", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 39, + "gate_closed": 211, + "trip_rate": 0.844, + "base_ann": 0.177515, + "base_sharpe": 0.8573, + "base_maxDD": -0.217417, + "gated_ann": 0.249221, + "gated_sharpe": 4.9448, + "gated_maxDD": -0.009619 + }, + { + "source": "reference", + "window": "2025", + "gate": "hitrate_10d_0.50", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 170, + "gate_closed": 80, + "trip_rate": 0.32, + "base_ann": 0.177515, + "base_sharpe": 0.8573, + "base_maxDD": -0.217417, + "gated_ann": 0.435847, + "gated_sharpe": 3.5614, + "gated_maxDD": -0.055776 + }, + { + "source": "reference", + "window": "2025", + "gate": "hitrate_10d_0.60", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 66, + "gate_closed": 184, + "trip_rate": 0.736, + "base_ann": 0.177515, + "base_sharpe": 0.8573, + "base_maxDD": -0.217417, + "gated_ann": 0.324574, + "gated_sharpe": 5.2279, + "gated_maxDD": -0.016028 + }, + { + "source": "reference", + "window": "2025", + "gate": "hitrate_10d_0.70", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 14, + "gate_closed": 236, + "trip_rate": 0.944, + "base_ann": 0.177515, + "base_sharpe": 0.8573, + "base_maxDD": -0.217417, + "gated_ann": 0.043002, + "gated_sharpe": 1.4635, + "gated_maxDD": -0.010154 + }, + { + "source": "reference", + "window": "2025", + "gate": "hitrate_20d_0.40", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 247, + "gate_closed": 3, + "trip_rate": 0.012, + "base_ann": 0.177515, + "base_sharpe": 0.8573, + "base_maxDD": -0.217417, + "gated_ann": 0.202949, + "gated_sharpe": 0.9896, + "gated_maxDD": -0.217417 + }, + { + "source": "reference", + "window": "2025", + "gate": "hitrate_20d_0.50", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 177, + "gate_closed": 73, + "trip_rate": 0.292, + "base_ann": 0.177515, + "base_sharpe": 0.8573, + "base_maxDD": -0.217417, + "gated_ann": 0.340772, + "gated_sharpe": 2.8303, + "gated_maxDD": -0.071731 + }, + { + "source": "reference", + "window": "2025", + "gate": "hitrate_20d_0.60", + "start": "2025-01-02", + "end": "2025-12-31", + "trade_dates": 250, + "gate_open": 48, + "gate_closed": 202, + "trip_rate": 0.808, + "base_ann": 0.177515, + "base_sharpe": 0.8573, + "base_maxDD": -0.217417, + "gated_ann": 0.266028, + "gated_sharpe": 4.1749, + "gated_maxDD": -0.021527 + }, + { + "source": "reference", + "window": "2024", + "gate": "hitrate_5d_0.50", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 139, + "gate_closed": 114, + "trip_rate": 0.4506, + "base_ann": 0.08229, + "base_sharpe": 0.5594, + "base_maxDD": -0.10685, + "gated_ann": 0.30351, + "gated_sharpe": 2.4275, + "gated_maxDD": -0.088219 + }, + { + "source": "reference", + "window": "2024", + "gate": "hitrate_5d_0.60", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 71, + "gate_closed": 182, + "trip_rate": 0.7194, + "base_ann": 0.08229, + "base_sharpe": 0.5594, + "base_maxDD": -0.10685, + "gated_ann": 0.265365, + "gated_sharpe": 2.4908, + "gated_maxDD": -0.078304 + }, + { + "source": "reference", + "window": "2024", + "gate": "hitrate_5d_0.70", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 19, + "gate_closed": 234, + "trip_rate": 0.9249, + "base_ann": 0.08229, + "base_sharpe": 0.5594, + "base_maxDD": -0.10685, + "gated_ann": 0.143125, + "gated_sharpe": 3.377, + "gated_maxDD": -0.003364 + }, + { + "source": "reference", + "window": "2024", + "gate": "hitrate_10d_0.50", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 157, + "gate_closed": 96, + "trip_rate": 0.3794, + "base_ann": 0.08229, + "base_sharpe": 0.5594, + "base_maxDD": -0.10685, + "gated_ann": 0.277421, + "gated_sharpe": 2.5767, + "gated_maxDD": -0.043146 + }, + { + "source": "reference", + "window": "2024", + "gate": "hitrate_10d_0.60", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 37, + "gate_closed": 216, + "trip_rate": 0.8538, + "base_ann": 0.08229, + "base_sharpe": 0.5594, + "base_maxDD": -0.10685, + "gated_ann": 0.207161, + "gated_sharpe": 3.6083, + "gated_maxDD": -0.011122 + }, + { + "source": "reference", + "window": "2024", + "gate": "hitrate_10d_0.70", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 7, + "gate_closed": 246, + "trip_rate": 0.9723, + "base_ann": 0.08229, + "base_sharpe": 0.5594, + "base_maxDD": -0.10685, + "gated_ann": 0.031785, + "gated_sharpe": 1.7408, + "gated_maxDD": -0.002083 + }, + { + "source": "reference", + "window": "2024", + "gate": "hitrate_20d_0.40", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 247, + "gate_closed": 6, + "trip_rate": 0.0237, + "base_ann": 0.08229, + "base_sharpe": 0.5594, + "base_maxDD": -0.10685, + "gated_ann": 0.078007, + "gated_sharpe": 0.5342, + "gated_maxDD": -0.10685 + }, + { + "source": "reference", + "window": "2024", + "gate": "hitrate_20d_0.50", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 177, + "gate_closed": 76, + "trip_rate": 0.3004, + "base_ann": 0.08229, + "base_sharpe": 0.5594, + "base_maxDD": -0.10685, + "gated_ann": 0.210672, + "gated_sharpe": 1.8469, + "gated_maxDD": -0.056207 + }, + { + "source": "reference", + "window": "2024", + "gate": "hitrate_20d_0.60", + "start": "2024-01-02", + "end": "2024-12-31", + "trade_dates": 253, + "gate_open": 13, + "gate_closed": 240, + "trip_rate": 0.9486, + "base_ann": 0.08229, + "base_sharpe": 0.5594, + "base_maxDD": -0.10685, + "gated_ann": 0.005953, + "gated_sharpe": 0.3039, + "gated_maxDD": -0.014443 + }, + { + "source": "reference", + "window": "2023", + "gate": "hitrate_5d_0.50", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 139, + "gate_closed": 111, + "trip_rate": 0.444, + "base_ann": -0.047644, + "base_sharpe": -0.2738, + "base_maxDD": -0.197856, + "gated_ann": 0.546654, + "gated_sharpe": 4.2124, + "gated_maxDD": -0.035676 + }, + { + "source": "reference", + "window": "2023", + "gate": "hitrate_5d_0.60", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 79, + "gate_closed": 171, + "trip_rate": 0.684, + "base_ann": -0.047644, + "base_sharpe": -0.2738, + "base_maxDD": -0.197856, + "gated_ann": 0.556291, + "gated_sharpe": 5.1026, + "gated_maxDD": -0.025449 + }, + { + "source": "reference", + "window": "2023", + "gate": "hitrate_5d_0.70", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 34, + "gate_closed": 216, + "trip_rate": 0.864, + "base_ann": -0.047644, + "base_sharpe": -0.2738, + "base_maxDD": -0.197856, + "gated_ann": 0.404269, + "gated_sharpe": 4.4477, + "gated_maxDD": -0.013842 + }, + { + "source": "reference", + "window": "2023", + "gate": "hitrate_10d_0.50", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 148, + "gate_closed": 102, + "trip_rate": 0.408, + "base_ann": -0.047644, + "base_sharpe": -0.2738, + "base_maxDD": -0.197856, + "gated_ann": 0.459869, + "gated_sharpe": 3.5211, + "gated_maxDD": -0.046921 + }, + { + "source": "reference", + "window": "2023", + "gate": "hitrate_10d_0.60", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 62, + "gate_closed": 188, + "trip_rate": 0.752, + "base_ann": -0.047644, + "base_sharpe": -0.2738, + "base_maxDD": -0.197856, + "gated_ann": 0.368232, + "gated_sharpe": 4.0148, + "gated_maxDD": -0.022983 + }, + { + "source": "reference", + "window": "2023", + "gate": "hitrate_10d_0.70", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 13, + "gate_closed": 237, + "trip_rate": 0.948, + "base_ann": -0.047644, + "base_sharpe": -0.2738, + "base_maxDD": -0.197856, + "gated_ann": 0.091481, + "gated_sharpe": 2.1324, + "gated_maxDD": -0.010866 + }, + { + "source": "reference", + "window": "2023", + "gate": "hitrate_20d_0.40", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 237, + "gate_closed": 13, + "trip_rate": 0.052, + "base_ann": -0.047644, + "base_sharpe": -0.2738, + "base_maxDD": -0.197856, + "gated_ann": 0.06639, + "gated_sharpe": 0.3895, + "gated_maxDD": -0.146704 + }, + { + "source": "reference", + "window": "2023", + "gate": "hitrate_20d_0.50", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 149, + "gate_closed": 101, + "trip_rate": 0.404, + "base_ann": -0.047644, + "base_sharpe": -0.2738, + "base_maxDD": -0.197856, + "gated_ann": 0.366016, + "gated_sharpe": 2.5969, + "gated_maxDD": -0.063998 + }, + { + "source": "reference", + "window": "2023", + "gate": "hitrate_20d_0.60", + "start": "2023-01-03", + "end": "2023-12-29", + "trade_dates": 250, + "gate_open": 40, + "gate_closed": 210, + "trip_rate": 0.84, + "base_ann": -0.047644, + "base_sharpe": -0.2738, + "base_maxDD": -0.197856, + "gated_ann": 0.149844, + "gated_sharpe": 2.2645, + "gated_maxDD": -0.032267 + }, + { + "source": "reference", + "window": "2021", + "gate": "hitrate_5d_0.50", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 145, + "gate_closed": 107, + "trip_rate": 0.4246, + "base_ann": 0.18367, + "base_sharpe": 1.0979, + "base_maxDD": -0.102651, + "gated_ann": 0.556615, + "gated_sharpe": 5.9167, + "gated_maxDD": -0.028062 + }, + { + "source": "reference", + "window": "2021", + "gate": "hitrate_5d_0.60", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 68, + "gate_closed": 184, + "trip_rate": 0.7302, + "base_ann": 0.18367, + "base_sharpe": 1.0979, + "base_maxDD": -0.102651, + "gated_ann": 0.351867, + "gated_sharpe": 5.8468, + "gated_maxDD": -0.011638 + }, + { + "source": "reference", + "window": "2021", + "gate": "hitrate_5d_0.70", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 26, + "gate_closed": 226, + "trip_rate": 0.8968, + "base_ann": 0.18367, + "base_sharpe": 1.0979, + "base_maxDD": -0.102651, + "gated_ann": 0.148417, + "gated_sharpe": 3.9593, + "gated_maxDD": -0.0041 + }, + { + "source": "reference", + "window": "2021", + "gate": "hitrate_10d_0.50", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 163, + "gate_closed": 89, + "trip_rate": 0.3532, + "base_ann": 0.18367, + "base_sharpe": 1.0979, + "base_maxDD": -0.102651, + "gated_ann": 0.465023, + "gated_sharpe": 4.2342, + "gated_maxDD": -0.040569 + }, + { + "source": "reference", + "window": "2021", + "gate": "hitrate_10d_0.60", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 51, + "gate_closed": 201, + "trip_rate": 0.7976, + "base_ann": 0.18367, + "base_sharpe": 1.0979, + "base_maxDD": -0.102651, + "gated_ann": 0.21802, + "gated_sharpe": 4.7978, + "gated_maxDD": -0.011134 + }, + { + "source": "reference", + "window": "2021", + "gate": "hitrate_10d_0.70", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 13, + "gate_closed": 239, + "trip_rate": 0.9484, + "base_ann": 0.18367, + "base_sharpe": 1.0979, + "base_maxDD": -0.102651, + "gated_ann": 0.06108, + "gated_sharpe": 2.7055, + "gated_maxDD": -0.000262 + }, + { + "source": "reference", + "window": "2021", + "gate": "hitrate_20d_0.40", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 252, + "gate_closed": 0, + "trip_rate": 0.0, + "base_ann": 0.18367, + "base_sharpe": 1.0979, + "base_maxDD": -0.102651, + "gated_ann": 0.18367, + "gated_sharpe": 1.0979, + "gated_maxDD": -0.102651 + }, + { + "source": "reference", + "window": "2021", + "gate": "hitrate_20d_0.50", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 166, + "gate_closed": 86, + "trip_rate": 0.3413, + "base_ann": 0.18367, + "base_sharpe": 1.0979, + "base_maxDD": -0.102651, + "gated_ann": 0.289756, + "gated_sharpe": 2.5787, + "gated_maxDD": -0.049244 + }, + { + "source": "reference", + "window": "2021", + "gate": "hitrate_20d_0.60", + "start": "2021-01-04", + "end": "2021-12-31", + "trade_dates": 252, + "gate_open": 38, + "gate_closed": 214, + "trip_rate": 0.8492, + "base_ann": 0.18367, + "base_sharpe": 1.0979, + "base_maxDD": -0.102651, + "gated_ann": 0.104685, + "gated_sharpe": 2.394, + "gated_maxDD": -0.015084 + } +] \ No newline at end of file diff --git a/book/scripts/diagnose_script_vs_workflow.py b/book/scripts/diagnose_script_vs_workflow.py new file mode 100644 index 0000000..6031c63 --- /dev/null +++ b/book/scripts/diagnose_script_vs_workflow.py @@ -0,0 +1,191 @@ +"""Diagnose exactly why the scripted test and workflow give different results. + +Compares the same pred.pkl through: + 1. Script logic (weekly rebalance, equal-weight, hold-through-week, zero cost) + 2. Workflow logic (PortAnaRecord daily backtest, TopkDropout-like) + +Isolates the effect of: + A. Weekly vs daily position evaluation + B. Equal weight vs risk_degree sizing + C. Hold-through-week vs daily top-k re-ranking +""" + +from __future__ import annotations +import json, pathlib +import numpy as np +import pandas as pd + +LAKE_ROOT = "/home/data/lake" +OUT = pathlib.Path("/app/experiments/book/data/diag_script_vs_wf") + +WINDOWS = [ + {"label": "2026", "start": "2026-01-04", "end": "2026-08-19", + "pred": f"{LAKE_ROOT}/mlruns/62/3771f96eb1b74365aeae966af7aec5a3/artifacts/pred.pkl"}, + {"label": "2025", "start": "2025-01-02", "end": "2025-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/c57c6a8370cc48619d7cdd2bd109b76a/artifacts/pred.pkl"}, + {"label": "2024", "start": "2024-01-02", "end": "2024-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/97cf5f282e6f4e699443e38d9bfb40fd/artifacts/pred.pkl"}, + {"label": "2023", "start": "2023-01-03", "end": "2023-12-29", + "pred": f"{LAKE_ROOT}/mlruns/62/11b9b65ea4e14b3f8ce50d244da0412e/artifacts/pred.pkl"}, + {"label": "2021", "start": "2021-01-04", "end": "2021-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/af3034e5910348a382f2ad1e1741f17c/artifacts/pred.pkl"}, +] + +SYMS = [ + "SPY","QQQ","DIA","IWM","MDY","VTI","VOO","VEA","VWO","VT","EFA","EEM", + "TLT","IEF","SHY","AGG","BND","LQD","HYG","JNK","EMB","GLD","SLV", + "USO","UNG","DBA","DBC","XLK","XLF","XLE","XLV","XLI","XLY","XLP", + "XLU","XLB","XLRE","ARKK","SMH","SOXX","IBB","XBI","ITA","XAR", + "ICLN","TAN","FDN","IGV","ESPO","REM", +] + + +def load_pred(path): + df = pd.read_pickle(path) + s = df["score"] if isinstance(df, pd.DataFrame) and "score" in df.columns else df.iloc[:, 0] if isinstance(df, pd.DataFrame) else df + idx = s.index + new_dt = pd.to_datetime(idx.get_level_values(0)).normalize() + s.index = pd.MultiIndex.from_arrays([new_dt, idx.get_level_values(1)], names=idx.names) + return s + + +def load_closes(start, end): + from tac_qlib.data.config import LakeConfig, resolve_lake_root + cfg = LakeConfig(resolve_lake_root(LAKE_ROOT), "US") + closes = {} + for sym in SYMS: + p = cfg.bar_path("1d", sym) + if not p.exists(): continue + try: + df = pd.read_parquet(p) + except: continue + if not len(df): continue + tcol = df["t"] if "t" in df.columns else df["date"] + ts = pd.to_datetime(tcol) + df = df.assign(_t=ts).set_index("_t").sort_index() + warmup = pd.Timestamp(start) - pd.Timedelta(days=60) + df = df.loc[warmup:end] + if len(df) >= 22: + closes[sym] = df["c"] + return pd.DataFrame(closes) + + +def strategy_script(pred, closes, start, end, topk=10, risk_degree=1.0): + """Mimics the scripted test: weekly rebalance, hold all week.""" + ret_df = closes.pct_change() + ret_df.index = pd.to_datetime(ret_df.index).normalize() + dt_idx = pred.index.get_level_values(0) + trade_dates = sorted(dt_idx[(dt_idx >= start) & (dt_idx <= end)].unique()) + equity = 1_000_000.0 + holdings = [] + prev_week = None + daily_eq = [] + for d in trade_dates: + try: + day_scores = pred.loc[d] + except KeyError: + daily_eq.append(equity) + prev_scores = None + continue + if isinstance(day_scores, pd.DataFrame): + day_scores = day_scores.iloc[:, 0] + day_scores = day_scores.dropna().sort_values(ascending=False) + cur_week = (d.isocalendar()[0], d.isocalendar()[1]) + if cur_week != prev_week or not holdings: + holdings = list(day_scores.index[:topk]) + ret_row = ret_df.loc[d] if d in ret_df.index else None + if ret_row is not None and holdings: + wts = np.array([risk_degree / len(holdings)] * len(holdings)) + rets = ret_row.reindex(holdings).fillna(0).values + equity *= (1 + (wts * rets).sum()) + daily_eq.append(equity) + prev_week = cur_week + return pd.Series(daily_eq, index=trade_dates) + + +def strategy_daily_topk(pred, closes, start, end, topk=10, risk_degree=1.0): + """Mimics PortAnaRecord: re-rank every day, hold top-k.""" + ret_df = closes.pct_change() + ret_df.index = pd.to_datetime(ret_df.index).normalize() + dt_idx = pred.index.get_level_values(0) + trade_dates = sorted(dt_idx[(dt_idx >= start) & (dt_idx <= end)].unique()) + equity = 1_000_000.0 + daily_eq = [] + for d in trade_dates: + try: + day_scores = pred.loc[d] + except KeyError: + daily_eq.append(equity) + continue + if isinstance(day_scores, pd.DataFrame): + day_scores = day_scores.iloc[:, 0] + day_scores = day_scores.dropna().sort_values(ascending=False) + holdings = list(day_scores.index[:topk]) + ret_row = ret_df.loc[d] if d in ret_df.index else None + if ret_row is not None and holdings: + wts = np.array([risk_degree / len(holdings)] * len(holdings)) + rets = ret_row.reindex(holdings).fillna(0).values + equity *= (1 + (wts * rets).sum()) + daily_eq.append(equity) + return pd.Series(daily_eq, index=trade_dates) + + +def metrics(eq): + if len(eq) < 2: + return {"ann_ret": 0, "sharpe": 0, "maxDD": 0} + rets = eq.pct_change().dropna() + ann_ret = float((eq.iloc[-1] / eq.iloc[0]) ** (252 / max(len(eq), 1)) - 1) + vol = float(rets.std() * (252 ** 0.5)) if len(rets) > 1 else 0 + sharpe = ann_ret / vol if vol > 0 else 0 + peak = eq.cummax() + dd = (eq - peak) / peak + return {"ann_ret": round(ann_ret, 4), "sharpe": round(sharpe, 4), "maxDD": round(float(dd.min()), 4)} + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + results = [] + for w in WINDOWS: + print(f"\n=== {w['label']} ({w['start']} to {w['end']}) ===") + pred = load_pred(w["pred"]) + closes = load_closes(w["start"], w["end"]) + print(f" pred dates: {pred.index.get_level_values(0).min()} to {pred.index.get_level_values(0).max()}") + print(f" close dates: {closes.index.min()} to {closes.index.max()}") + print(f" symbols in close: {closes.shape[1]}") + + # Script: weekly, equal weight (risk_degree=1.0) + eq_weekly_100 = strategy_script(pred, closes, w["start"], w["end"], topk=10, risk_degree=1.0) + m_weekly_100 = metrics(eq_weekly_100) + + # Script: weekly, 95% risk degree + eq_weekly_95 = strategy_script(pred, closes, w["start"], w["end"], topk=10, risk_degree=0.95) + m_weekly_95 = metrics(eq_weekly_95) + + # Daily top-k: re-rank daily, equal weight + eq_daily_100 = strategy_daily_topk(pred, closes, w["start"], w["end"], topk=10, risk_degree=1.0) + m_daily_100 = metrics(eq_daily_100) + + # Daily top-k: re-rank daily, 95% + eq_daily_95 = strategy_daily_topk(pred, closes, w["start"], w["end"], topk=10, risk_degree=0.95) + m_daily_95 = metrics(eq_daily_95) + + row = { + "year": w["label"], + "script_weekly_100": m_weekly_100, + "script_weekly_95": m_weekly_95, + "daily_topk_100": m_daily_100, + "daily_topk_95": m_daily_95, + } + results.append(row) + print(f" Script weekly 100%: ann={m_weekly_100['ann_ret']:+.1%} sharpe={m_weekly_100['sharpe']:.2f} maxDD={m_weekly_100['maxDD']:.1%}") + print(f" Script weekly 95%: ann={m_weekly_95['ann_ret']:+.1%} sharpe={m_weekly_95['sharpe']:.2f} maxDD={m_weekly_95['maxDD']:.1%}") + print(f" Daily topk 100%: ann={m_daily_100['ann_ret']:+.1%} sharpe={m_daily_100['sharpe']:.2f} maxDD={m_daily_100['maxDD']:.1%}") + print(f" Daily topk 95%: ann={m_daily_95['ann_ret']:+.1%} sharpe={m_daily_95['sharpe']:.2f} maxDD={m_daily_95['maxDD']:.1%}") + + with open(OUT / "diagnosis.json", "w") as f: + json.dump(results, f, indent=2, default=str) + print(f"\nSaved to {OUT / 'diagnosis.json'}") + + +if __name__ == "__main__": + main() diff --git a/book/scripts/diagnose_script_vs_workflow_v2.py b/book/scripts/diagnose_script_vs_workflow_v2.py new file mode 100644 index 0000000..6162ef3 --- /dev/null +++ b/book/scripts/diagnose_script_vs_workflow_v2.py @@ -0,0 +1,203 @@ +"""Diagnose the script-vs-workflow gap properly. + +Three strategies compared: + A. Script logic: weekly rebalance, equal-weight, hold through week + B. Weekly rebalance (qlib engine behavior): same as script but with risk_degree + C. Daily re-rank: re-select top-k every day (wrong model) + +Root cause was (C) — we were modeling daily re-ranking which neither +the script nor the qlib engine actually does. +""" + +from __future__ import annotations +import json, pathlib +import numpy as np +import pandas as pd + +LAKE_ROOT = "/home/data/lake" +OUT = pathlib.Path("/app/experiments/book/data/diag_script_vs_wf") + +WINDOWS = [ + {"label": "2026", "start": "2026-01-04", "end": "2026-08-19", + "pred": f"{LAKE_ROOT}/mlruns/62/3771f96eb1b74365aeae966af7aec5a3/artifacts/pred.pkl"}, + {"label": "2025", "start": "2025-01-02", "end": "2025-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/c57c6a8370cc48619d7cdd2bd109b76a/artifacts/pred.pkl"}, + {"label": "2024", "start": "2024-01-02", "end": "2024-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/97cf5f282e6f4e699443e38d9bfb40fd/artifacts/pred.pkl"}, + {"label": "2023", "start": "2023-01-03", "end": "2023-12-29", + "pred": f"{LAKE_ROOT}/mlruns/62/11b9b65ea4e14b3f8ce50d244da0412e/artifacts/pred.pkl"}, + {"label": "2021", "start": "2021-01-04", "end": "2021-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/af3034e5910348a382f2ad1e1741f17c/artifacts/pred.pkl"}, +] + +SYMS = [ + "SPY","QQQ","DIA","IWM","MDY","VTI","VOO","VEA","VWO","VT","EFA","EEM", + "TLT","IEF","SHY","AGG","BND","LQD","HYG","JNK","EMB","GLD","SLV", + "USO","UNG","DBA","DBC","XLK","XLF","XLE","XLV","XLI","XLY","XLP", + "XLU","XLB","XLRE","ARKK","SMH","SOXX","IBB","XBI","ITA","XAR", + "ICLN","TAN","FDN","IGV","ESPO","REM", +] + + +def load_pred(path): + df = pd.read_pickle(path) + s = df["score"] if isinstance(df, pd.DataFrame) and "score" in df.columns else df.iloc[:, 0] if isinstance(df, pd.DataFrame) else df + idx = s.index + new_dt = pd.to_datetime(idx.get_level_values(0)).normalize() + s.index = pd.MultiIndex.from_arrays([new_dt, idx.get_level_values(1)], names=idx.names) + return s + + +def load_closes(start, end): + from tac_qlib.data.config import LakeConfig, resolve_lake_root + cfg = LakeConfig(resolve_lake_root(LAKE_ROOT), "US") + closes = {} + for sym in SYMS: + p = cfg.bar_path("1d", sym) + if not p.exists(): continue + try: + df = pd.read_parquet(p) + except: continue + if not len(df): continue + tcol = df["t"] if "t" in df.columns else df["date"] + ts = pd.to_datetime(tcol) + df = df.assign(_t=ts).set_index("_t").sort_index() + warmup = pd.Timestamp(start) - pd.Timedelta(days=60) + df = df.loc[warmup:end] + if len(df) >= 22: + closes[sym] = df["c"] + return pd.DataFrame(closes) + + +def strategy_weekly(pred, closes, start, end, topk=10, risk_degree=1.0, cost_bps=0): + """Weekly rebalance: re-rank on first day of each ISO week, hold rest of week.""" + ret_df = closes.pct_change(fill_method=None) + ret_df.index = pd.to_datetime(ret_df.index).normalize() + dt_idx = pred.index.get_level_values(0) + trade_dates = sorted(dt_idx[(dt_idx >= start) & (dt_idx <= end)].unique()) + equity = 1_000_000.0 + holdings = [] + prev_week = None + daily_eq = [] + for d in trade_dates: + try: + day_scores = pred.loc[d] + except KeyError: + daily_eq.append(equity) + continue + if isinstance(day_scores, pd.DataFrame): + day_scores = day_scores.iloc[:, 0] + day_scores = day_scores.dropna().sort_values(ascending=False) + cur_week = (d.isocalendar()[0], d.isocalendar()[1]) + if cur_week != prev_week: + # Rebalance: compute cost of turnover + new_holdings = list(day_scores.index[:topk]) + if holdings and cost_bps > 0: + sold = set(holdings) - set(new_holdings) + bought = set(new_holdings) - set(holdings) + turnover = (len(sold) + len(bought)) / (2 * max(len(holdings), 1)) + equity *= (1 - turnover * cost_bps / 10000) + holdings = new_holdings + ret_row = ret_df.loc[d] if d in ret_df.index else None + if ret_row is not None and holdings: + wts = np.array([risk_degree / len(holdings)] * len(holdings)) + rets = ret_row.reindex(holdings).fillna(0).values + equity *= (1 + (wts * rets).sum()) + daily_eq.append(equity) + prev_week = cur_week + return pd.Series(daily_eq, index=trade_dates) + + +def strategy_daily(pred, closes, start, end, topk=10, risk_degree=1.0, cost_bps=0): + """Daily re-rank: re-select top-k every day (wrong model — what we incorrectly tested).""" + ret_df = closes.pct_change(fill_method=None) + ret_df.index = pd.to_datetime(ret_df.index).normalize() + dt_idx = pred.index.get_level_values(0) + trade_dates = sorted(dt_idx[(dt_idx >= start) & (dt_idx <= end)].unique()) + equity = 1_000_000.0 + holdings = [] + daily_eq = [] + for d in trade_dates: + try: + day_scores = pred.loc[d] + except KeyError: + daily_eq.append(equity) + continue + if isinstance(day_scores, pd.DataFrame): + day_scores = day_scores.iloc[:, 0] + day_scores = day_scores.dropna().sort_values(ascending=False) + new_holdings = list(day_scores.index[:topk]) + if holdings and cost_bps > 0: + sold = set(holdings) - set(new_holdings) + bought = set(new_holdings) - set(holdings) + turnover = (len(sold) + len(bought)) / (2 * max(len(holdings), 1)) + equity *= (1 - turnover * cost_bps / 10000) + holdings = new_holdings + ret_row = ret_df.loc[d] if d in ret_df.index else None + if ret_row is not None and holdings: + wts = np.array([risk_degree / len(holdings)] * len(holdings)) + rets = ret_row.reindex(holdings).fillna(0).values + equity *= (1 + (wts * rets).sum()) + daily_eq.append(equity) + return pd.Series(daily_eq, index=trade_dates) + + +def metrics(eq): + if len(eq) < 2: + return {"ann_ret": 0, "sharpe": 0, "maxDD": 0} + rets = eq.pct_change().dropna() + ann_ret = float((eq.iloc[-1] / eq.iloc[0]) ** (252 / max(len(eq), 1)) - 1) + vol = float(rets.std() * (252 ** 0.5)) if len(rets) > 1 else 0 + sharpe = ann_ret / vol if vol > 0 else 0 + peak = eq.cummax() + dd = (eq - peak) / peak + return {"ann_ret": round(ann_ret, 4), "sharpe": round(sharpe, 4), "maxDD": round(float(dd.min()), 4)} + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + results = [] + for w in WINDOWS: + print(f"\n=== {w['label']} ({w['start']} to {w['end']}) ===") + pred = load_pred(w["pred"]) + closes = load_closes(w["start"], w["end"]) + print(f" pred: {pred.index.get_level_values(0).min().date()} to {pred.index.get_level_values(0).max().date()}, " + f"{pred.index.get_level_values(1).nunique()} syms") + print(f" close: {closes.index.min().date()} to {closes.index.max().date()}, {closes.shape[1]} syms") + + row = {"year": w["label"]} + + # A. Script: weekly, rd=1.0, zero cost + eq = strategy_weekly(pred, closes, w["start"], w["end"], topk=10, risk_degree=1.0, cost_bps=0) + m = metrics(eq); row["weekly_100_zc"] = m + print(f" Script weekly 100% zc: ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f}") + + # B. Script: weekly, rd=0.95, zero cost + eq = strategy_weekly(pred, closes, w["start"], w["end"], topk=10, risk_degree=0.95, cost_bps=0) + m = metrics(eq); row["weekly_95_zc"] = m + print(f" Script weekly 95% zc: ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f}") + + # C. Weekly, rd=0.95, with 5/15bp cost + eq = strategy_weekly(pred, closes, w["start"], w["end"], topk=10, risk_degree=0.95, cost_bps=10) + m = metrics(eq); row["weekly_95_10bp"] = m + print(f" Weekly 95% 10bp cost: ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f}") + + # D. Daily re-rank, rd=1.0, zero cost (WRONG MODEL — for reference only) + eq = strategy_daily(pred, closes, w["start"], w["end"], topk=10, risk_degree=1.0, cost_bps=0) + m = metrics(eq); row["daily_100_zc"] = m + print(f" Daily 100% zc (WRONG): ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f}") + + # E. Daily re-rank, rd=1.0, 10bp cost + eq = strategy_daily(pred, closes, w["start"], w["end"], topk=10, risk_degree=1.0, cost_bps=10) + m = metrics(eq); row["daily_100_10bp"] = m + print(f" Daily 100% 10bp (WRONG):ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f}") + + results.append(row) + + with open(OUT / "diagnosis_v2.json", "w") as f: + json.dump(results, f, indent=2, default=str) + print(f"\nSaved to {OUT / 'diagnosis_v2.json'}") + + +if __name__ == "__main__": + main() diff --git a/book/scripts/diagnose_script_vs_workflow_v3.py b/book/scripts/diagnose_script_vs_workflow_v3.py new file mode 100644 index 0000000..bcb9507 --- /dev/null +++ b/book/scripts/diagnose_script_vs_workflow_v3.py @@ -0,0 +1,410 @@ +"""Diagnose script-vs-workflow gap v3: replicate workflow execution mechanics exactly. + +Replicates the WeeklyRebalanceDropoutStrategy execution: + 1. Weekly rebalance (first trading day of ISO week only) + 2. TopkDropout selection: sell bottom n_drop, buy top fill + 3. Cash-after-sells sizing: sell first, then cash * risk_degree / len(buy) + 4. Whole-share rounding (floor) + 5. Asymmetric costs: open_cost=5bp, close_cost=15bp, min_cost=$5 per order + 6. Optional SQ gate (hit-rate threshold) + +Compares against the idealized script (fractional shares, symmetric cost). +""" + +from __future__ import annotations +import json, pathlib +import numpy as np +import pandas as pd + +LAKE_ROOT = "/home/data/lake" +OUT = pathlib.Path("/app/experiments/book/data/diag_script_vs_wf") + +WINDOWS = [ + {"label": "2026", "start": "2026-01-04", "end": "2026-08-19", + "pred": f"{LAKE_ROOT}/mlruns/62/3771f96eb1b74365aeae966af7aec5a3/artifacts/pred.pkl"}, + {"label": "2025", "start": "2025-01-02", "end": "2025-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/c57c6a8370cc48619d7cdd2bd109b76a/artifacts/pred.pkl"}, + {"label": "2024", "start": "2024-01-02", "end": "2024-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/97cf5f282e6f4e699443e38d9bfb40fd/artifacts/pred.pkl"}, + {"label": "2023", "start": "2023-01-03", "end": "2023-12-29", + "pred": f"{LAKE_ROOT}/mlruns/62/11b9b65ea4e14b3f8ce50d244da0412e/artifacts/pred.pkl"}, + {"label": "2021", "start": "2021-01-04", "end": "2021-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/af3034e5910348a382f2ad1e1741f17c/artifacts/pred.pkl"}, +] + +SYMS = [ + "SPY","QQQ","DIA","IWM","MDY","VTI","VOO","VEA","VWO","VT","EFA","EEM", + "TLT","IEF","SHY","AGG","BND","LQD","HYG","JNK","EMB","GLD","SLV", + "USO","UNG","DBA","DBC","XLK","XLF","XLE","XLV","XLI","XLY","XLP", + "XLU","XLB","XLRE","ARKK","SMH","SOXX","IBB","XBI","ITA","XAR", + "ICLN","TAN","FDN","IGV","ESPO","REM", +] + +OPEN_COST = 0.0005 # 5bp +CLOSE_COST = 0.0015 # 15bp +MIN_COST = 5.0 # $5 minimum per order + + +def load_pred(path): + df = pd.read_pickle(path) + s = df["score"] if isinstance(df, pd.DataFrame) and "score" in df.columns else df.iloc[:, 0] if isinstance(df, pd.DataFrame) else df + idx = s.index + new_dt = pd.to_datetime(idx.get_level_values(0)).normalize() + s.index = pd.MultiIndex.from_arrays([new_dt, idx.get_level_values(1)], names=idx.names) + return s + + +def load_closes(start, end): + from tac_qlib.data.config import LakeConfig, resolve_lake_root + cfg = LakeConfig(resolve_lake_root(LAKE_ROOT), "US") + closes = {} + for sym in SYMS: + p = cfg.bar_path("1d", sym) + if not p.exists(): continue + try: + df = pd.read_parquet(p) + except: continue + if not len(df): continue + tcol = df["t"] if "t" in df.columns else df["date"] + ts = pd.to_datetime(tcol) + df = df.assign(_t=ts).set_index("_t").sort_index() + df.index = pd.to_datetime(df.index).normalize() + warmup = pd.Timestamp(start) - pd.Timedelta(days=60) + df = df.loc[warmup:end] + if len(df) >= 22: + closes[sym] = df["c"] + return pd.DataFrame(closes) + + +def load_opens(start, end): + from tac_qlib.data.config import LakeConfig, resolve_lake_root + cfg = LakeConfig(resolve_lake_root(LAKE_ROOT), "US") + opens = {} + for sym in SYMS: + p = cfg.bar_path("1d", sym) + if not p.exists(): continue + try: + df = pd.read_parquet(p) + except: continue + if not len(df): continue + tcol = df["t"] if "t" in df.columns else df["date"] + ts = pd.to_datetime(tcol) + df = df.assign(_t=ts).set_index("_t").sort_index() + df.index = pd.to_datetime(df.index).normalize() + warmup = pd.Timestamp(start) - pd.Timedelta(days=60) + df = df.loc[warmup:end] + if len(df) >= 22: + opens[sym] = df["o"] + return pd.DataFrame(opens) + + +def load_vwap(start, end): + from tac_qlib.data.config import LakeConfig, resolve_lake_root + cfg = LakeConfig(resolve_lake_root(LAKE_ROOT), "US") + vwaps = {} + for sym in SYMS: + p = cfg.bar_path("1d", sym) + if not p.exists(): continue + try: + df = pd.read_parquet(p) + except: continue + if not len(df): continue + tcol = df["t"] if "t" in df.columns else df["date"] + ts = pd.to_datetime(tcol) + df = df.assign(_t=ts).set_index("_t").sort_index() + warmup = pd.Timestamp(start) - pd.Timedelta(days=60) + df = df.loc[warmup:end] + if len(df) >= 22: + vwaps[sym] = df["vw"] + return pd.DataFrame(vwaps) + + +def compute_gate(pred, closes, start, end, gate_topk=10, gate_lookback=5, gate_threshold=0.5): + """Compute the SQ gate: rolling average hit-rate of topk predictions.""" + ret_df = closes.pct_change() + ret_df.index = pd.to_datetime(ret_df.index).normalize() + dt_idx = pred.index.get_level_values(0) + pred_dates = sorted(dt_idx[(dt_idx >= start) & (dt_idx <= end)].unique()) + if len(pred_dates) < 2: + return pd.Series(True, index=pd.DatetimeIndex(pred_dates)) + + hit_rates = {} + for i in range(1, len(pred_dates)): + day = pred_dates[i] + prev_day = pred_dates[i - 1] + try: + prev_scores = pred.loc[prev_day] + except KeyError: + continue + if isinstance(prev_scores, pd.DataFrame): + prev_scores = prev_scores.iloc[:, 0] + prev_scores = prev_scores.dropna().sort_values(ascending=False) + topk_syms = list(prev_scores.index[:gate_topk]) + if day not in ret_df.index: + continue + today_ret = ret_df.loc[day] + topk_rets = today_ret.reindex(topk_syms).dropna() + if len(topk_rets) == 0: + continue + hit_rates[day] = (topk_rets > 0).sum() / len(topk_rets) + + if not hit_rates: + return pd.Series(True, index=pd.DatetimeIndex(pred_dates)) + + hr_series = pd.Series(hit_rates).sort_index() + rolling_hr = hr_series.rolling(gate_lookback, min_periods=1).mean() + gate = rolling_hr >= gate_threshold + gate.iloc[:gate_lookback] = True + return gate + + +def strategy_idealized(pred, closes, start, end, topk=10, risk_degree=1.0, cost_bps=0): + """Idealized script: fractional shares, symmetric cost, no gate.""" + ret_df = closes.pct_change(fill_method=None) + ret_df.index = pd.to_datetime(ret_df.index).normalize() + dt_idx = pred.index.get_level_values(0) + trade_dates = sorted(dt_idx[(dt_idx >= start) & (dt_idx <= end)].unique()) + equity = 1_000_000.0 + holdings = [] + prev_week = None + daily_eq = [] + for d in trade_dates: + try: + day_scores = pred.loc[d] + except KeyError: + daily_eq.append(equity) + continue + if isinstance(day_scores, pd.DataFrame): + day_scores = day_scores.iloc[:, 0] + day_scores = day_scores.dropna().sort_values(ascending=False) + cur_week = (d.isocalendar()[0], d.isocalendar()[1]) + if cur_week != prev_week: + new_holdings = list(day_scores.index[:topk]) + if holdings and cost_bps > 0: + sold = set(holdings) - set(new_holdings) + bought = set(new_holdings) - set(holdings) + turnover = (len(sold) + len(bought)) / (2 * max(len(holdings), 1)) + equity *= (1 - turnover * cost_bps / 10000) + holdings = new_holdings + ret_row = ret_df.loc[d] if d in ret_df.index else None + if ret_row is not None and holdings: + wts = np.array([risk_degree / len(holdings)] * len(holdings)) + rets = ret_row.reindex(holdings).fillna(0).values + equity *= (1 + (wts * rets).sum()) + daily_eq.append(equity) + prev_week = cur_week + return pd.Series(daily_eq, index=trade_dates) + + +def strategy_workflow_exact(pred, closes, opens, start, end, + topk=10, n_drop=1, risk_degree=0.95, + use_gate=False, gate_series=None): + """Exact replication of WeeklyRebalanceDropoutStrategy execution mechanics. + + - Sells first (all shares of dropped positions) + - Sizes buys as: cash * risk_degree / len(buy) + - Rounds to whole shares (floor) + - Asymmetric costs: open_cost on buys, close_cost on sells, $5 min per order + - Tracks position values for daily equity + """ + ret_df = closes.pct_change(fill_method=None) + ret_df.index = pd.to_datetime(ret_df.index).normalize() + open_df = opens.copy() + open_df.index = pd.to_datetime(open_df.index).normalize() + dt_idx = pred.index.get_level_values(0) + trade_dates = sorted(dt_idx[(dt_idx >= start) & (dt_idx <= end)].unique()) + + cash = 1_000_000.0 + positions = {} # {sym: num_shares} + prev_week = None + daily_eq = [] + + for d in trade_dates: + # Skip non-trading days (pred may include weekends) + if d not in closes.index: + daily_eq.append(daily_eq[-1] if daily_eq else cash) + continue + try: + day_scores = pred.loc[d] + except KeyError: + daily_eq.append(daily_eq[-1] if daily_eq else cash) + continue + if isinstance(day_scores, pd.DataFrame): + day_scores = day_scores.iloc[:, 0] + day_scores = day_scores.dropna().sort_values(ascending=False) + cur_week = (d.isocalendar()[0], d.isocalendar()[1]) + + if cur_week != prev_week: + # === REBALANCE DAY === + # Check gate + if use_gate and gate_series is not None: + known = gate_series[gate_series.index <= d] + if len(known) and not bool(known.iloc[-1]): + # gate closed: sell everything, go to cash + for sym in list(positions.keys()): + shares = positions[sym] + if shares <= 0: + continue + sell_price = closes.loc[d, sym] if d in closes.index and sym in closes.columns else None + if sell_price is None or pd.isna(sell_price): + continue + trade_val = shares * sell_price + trade_cost = max(trade_val * CLOSE_COST, MIN_COST) if trade_val > 0 else 0 + cash += trade_val - trade_cost + positions[sym] = 0 + positions = {s: v for s, v in positions.items() if v > 0} + daily_eq.append(cash) + prev_week = cur_week + continue + + # TopkDropout selection (matching WeeklyRebalanceDropoutStrategy exactly) + current_syms = [s for s, v in positions.items() if v > 0] + last = pred.loc[d].reindex(current_syms).sort_values(ascending=False).index if current_syms else pd.Index([]) + # buy candidates: top stocks NOT in current holdings, take n_drop + topk - len(last) + buy_cands = day_scores[~day_scores.index.isin(last)].sort_values(ascending=False).index + buy_list = list(buy_cands[:n_drop + topk - len(last)]) + # comb = union of current holdings + buy candidates (actual strategy line 132) + comb = pred.loc[d].reindex(last.union(pd.Index(buy_list))).sort_values(ascending=False).index + # sell: items from current holdings that are in the bottom n_drop of comb + sell_list = list(last[last.isin(comb[-n_drop:])]) if n_drop > 0 and len(comb) >= n_drop else [] + + # --- SELL FIRST --- + for sym in sell_list: + if sym not in positions or positions[sym] <= 0: + continue + shares = positions[sym] + sell_price = closes.loc[d, sym] if d in closes.index and sym in closes.columns else None + if sell_price is None or pd.isna(sell_price): + continue + trade_val = shares * sell_price + trade_cost = max(trade_val * CLOSE_COST, MIN_COST) if trade_val > 0 else 0 + cash += trade_val - trade_cost + positions[sym] = 0 + + # --- BUY --- + n_buy = len(buy_list) + if n_buy > 0: + buy_budget = cash * risk_degree / n_buy + for sym in buy_list: + buy_price = closes.loc[d, sym] if d in closes.index and sym in closes.columns else None + if buy_price is None or pd.isna(buy_price) or buy_price <= 0: + continue + shares_to_buy = int(buy_budget / buy_price) # floor to whole shares + if shares_to_buy <= 0: + continue + trade_val = shares_to_buy * buy_price + trade_cost = max(trade_val * OPEN_COST, MIN_COST) if trade_val > 0 else 0 + total_cost = trade_val + trade_cost + if total_cost > cash: + shares_to_buy = int((cash - MIN_COST) / buy_price) + if shares_to_buy <= 0: + continue + trade_val = shares_to_buy * buy_price + trade_cost = max(trade_val * OPEN_COST, MIN_COST) + total_cost = trade_val + trade_cost + cash -= total_cost + positions[sym] = positions.get(sym, 0) + shares_to_buy + + positions = {s: v for s, v in positions.items() if v > 0} + + # === DAILY EQUITY === + eq = cash + if d in closes.index: + for sym, shares in positions.items(): + if sym in closes.columns: + px = closes.loc[d, sym] + if not pd.isna(px): + eq += shares * px + daily_eq.append(eq) + prev_week = cur_week + + return pd.Series(daily_eq, index=trade_dates) + + +def metrics(eq): + if len(eq) < 2: + return {"ann_ret": 0, "sharpe": 0, "maxDD": 0} + rets = eq.pct_change().dropna() + ann_ret = float((eq.iloc[-1] / eq.iloc[0]) ** (252 / max(len(eq), 1)) - 1) + vol = float(rets.std() * (252 ** 0.5)) if len(rets) > 1 else 0 + sharpe = ann_ret / vol if vol > 0 else 0 + peak = eq.cummax() + dd = (eq - peak) / peak + return {"ann_ret": round(ann_ret, 4), "sharpe": round(sharpe, 4), "maxDD": round(float(dd.min()), 4)} + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + results = [] + for w in WINDOWS: + print(f"\n=== {w['label']} ({w['start']} to {w['end']}) ===") + pred = load_pred(w["pred"]) + closes = load_closes(w["start"], w["end"]) + opens = load_opens(w["start"], w["end"]) + print(f" pred: {pred.index.get_level_values(0).min().date()} to {pred.index.get_level_values(0).max().date()}, " + f"{pred.index.get_level_values(1).nunique()} syms") + print(f" close: {closes.index.min().date()} to {closes.index.max().date()}, {closes.shape[1]} syms") + + row = {"year": w["label"]} + + # A. Idealized: fractional shares, 10bp symmetric, no gate (diag v2 baseline) + eq = strategy_idealized(pred, closes, w["start"], w["end"], topk=10, risk_degree=1.0, cost_bps=0) + m = metrics(eq); row["ideal_100_zc"] = m + print(f" A. Ideal 100% zc: ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f} maxDD={m['maxDD']:.1%}") + + # B. Idealized: 95% invested, 10bp symmetric + eq = strategy_idealized(pred, closes, w["start"], w["end"], topk=10, risk_degree=0.95, cost_bps=0) + m = metrics(eq); row["ideal_95_zc"] = m + print(f" B. Ideal 95% zc: ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f} maxDD={m['maxDD']:.1%}") + + # C. Idealized: 95%, 10bp cost + eq = strategy_idealized(pred, closes, w["start"], w["end"], topk=10, risk_degree=0.95, cost_bps=10) + m = metrics(eq); row["ideal_95_10bp"] = m + print(f" C. Ideal 95% 10bp: ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f} maxDD={m['maxDD']:.1%}") + + # D. Workflow-exact: whole shares, 5/15bp, $5 min, no gate + eq = strategy_workflow_exact(pred, closes, opens, w["start"], w["end"], + topk=10, n_drop=1, risk_degree=0.95, + use_gate=False) + m = metrics(eq); row["wf_exact_95_nogate"] = m + print(f" D. WF exact 95% nogate: ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f} maxDD={m['maxDD']:.1%}") + + # E. Workflow-exact: whole shares, 5/15bp, $5 min, WITH SQ gate + gate = compute_gate(pred, closes, w["start"], w["end"], + gate_topk=10, gate_lookback=5, gate_threshold=0.5) + gate_open_pct = gate.sum() / len(gate) if len(gate) > 0 else 1.0 + eq = strategy_workflow_exact(pred, closes, opens, w["start"], w["end"], + topk=10, n_drop=1, risk_degree=0.95, + use_gate=True, gate_series=gate) + m = metrics(eq); row["wf_exact_95_gate"] = m + print(f" E. WF exact 95% gate: ann={m['ann_ret']:+.1%} sharpe={m['sharpe']:.2f} maxDD={m['maxDD']:.1%} gate_open={gate_open_pct:.0%}") + + # Gap analysis + ideal = row["ideal_95_zc"]["ann_ret"] + wf_nogate = row["wf_exact_95_nogate"]["ann_ret"] + wf_gate = row["wf_exact_95_gate"]["ann_ret"] + print(f"\n Gap analysis:") + print(f" Ideal (fractional, zc) → WF exact (whole shares, 5/15bp, nogate): {ideal:+.1%} → {wf_nogate:+.1%} (gap: {wf_nogate - ideal:+.1%})") + print(f" Ideal (fractional, zc) → WF exact (whole shares, 5/15bp, gate): {ideal:+.1%} → {wf_gate:+.1%} (gap: {wf_gate - ideal:+.1%})") + + results.append(row) + + with open(OUT / "diagnosis_v3.json", "w") as f: + json.dump(results, f, indent=2, default=str) + print(f"\nSaved to {OUT / 'diagnosis_v3.json'}") + + # Summary table + print("\n" + "=" * 80) + print("SUMMARY: Ideal vs Workflow-Exact") + print("=" * 80) + print(f"{'Year':<6} {'Ideal%zc':>10} {'WF nogate':>10} {'WF gate':>10} {'Gap(nogate)':>12} {'Gap(gate)':>12}") + for r in results: + y = r["year"] + i = r["ideal_95_zc"]["ann_ret"] + wn = r["wf_exact_95_nogate"]["ann_ret"] + wg = r["wf_exact_95_gate"]["ann_ret"] + print(f"{y:<6} {i:>+10.1%} {wn:>+10.1%} {wg:>+10.1%} {wn-i:>+12.1%} {wg-i:>+12.1%}") + + +if __name__ == "__main__": + main() diff --git a/book/scripts/signal_quality_gate_retrained.py b/book/scripts/signal_quality_gate_retrained.py new file mode 100644 index 0000000..e0c7010 --- /dev/null +++ b/book/scripts/signal_quality_gate_retrained.py @@ -0,0 +1,343 @@ +"""Signal-quality gate walk-forward backtest — RE-TRAINED MODEL variant. + +Identical logic to the original scripted test (signal_quality_gate_bt.py), +but uses pred.pkls from exp 62 (retrained LGBModel per year, same model config +as the workflow test) instead of the reference exp 52/56 pred.pkls. + +This isolates whether the gate itself works when the model is the same, +regardless of the backtest engine. + +Usage: + cd /app && .venv/bin/python book/scripts/signal_quality_gate_retrained.py +""" + +from __future__ import annotations + +import json +import pathlib + +import numpy as np +import pandas as pd + +LAKE_ROOT = "/home/data/lake" +MARKET = "US" +OUT_DIR = pathlib.Path("/app/experiments/book/data/signal_quality_gate") + +# Retrained pred.pkls from exp 62 (on-the-fly gate test) +WINDOWS_RETRAINED = [ + {"label": "2026", "start": "2026-01-04", "end": "2026-08-19", + "pred": f"{LAKE_ROOT}/mlruns/62/3771f96eb1b74365aeae966af7aec5a3/artifacts/pred.pkl"}, + {"label": "2025", "start": "2025-01-02", "end": "2025-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/c57c6a8370cc48619d7cdd2bd109b76a/artifacts/pred.pkl"}, + {"label": "2024", "start": "2024-01-02", "end": "2024-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/97cf5f282e6f4e699443e38d9bfb40fd/artifacts/pred.pkl"}, + {"label": "2023", "start": "2023-01-03", "end": "2023-12-29", + "pred": f"{LAKE_ROOT}/mlruns/62/11b9b65ea4e14b3f8ce50d244da0412e/artifacts/pred.pkl"}, + {"label": "2021", "start": "2021-01-04", "end": "2021-12-31", + "pred": f"{LAKE_ROOT}/mlruns/62/af3034e5910348a382f2ad1e1741f17c/artifacts/pred.pkl"}, +] + +# Original reference pred.pkls for head-to-head comparison +WINDOWS_REFERENCE = [ + {"label": "2026", "start": "2026-01-04", "end": "2026-08-19", + "pred": f"{LAKE_ROOT}/mlruns/52/9f98ea5c550a409f87b56a6cd8fee343/artifacts/pred.pkl"}, + {"label": "2025", "start": "2025-01-02", "end": "2025-12-31", + "pred": f"{LAKE_ROOT}/mlruns/52/fe96741654df4780957a3a949999ae6a/artifacts/pred.pkl"}, + {"label": "2024", "start": "2024-01-02", "end": "2024-12-31", + "pred": f"{LAKE_ROOT}/mlruns/52/71ed5bfa9984490f8bba8b222f7acc39/artifacts/pred.pkl"}, + {"label": "2023", "start": "2023-01-03", "end": "2023-12-29", + "pred": f"{LAKE_ROOT}/mlruns/56/8ca46e554311444c9a42637a788226e8/artifacts/pred.pkl"}, + {"label": "2021", "start": "2021-01-04", "end": "2021-12-31", + "pred": f"{LAKE_ROOT}/mlruns/56/4e0700ddab2a4e108b46efece7346ee3/artifacts/pred.pkl"}, +] + +SIGNAL_GATE_CONFIGS = [ + (5, 0.50, "hitrate_5d_0.50"), + (5, 0.60, "hitrate_5d_0.60"), + (5, 0.70, "hitrate_5d_0.70"), + (10, 0.50, "hitrate_10d_0.50"), + (10, 0.60, "hitrate_10d_0.60"), + (10, 0.70, "hitrate_10d_0.70"), + (20, 0.40, "hitrate_20d_0.40"), + (20, 0.50, "hitrate_20d_0.50"), + (20, 0.60, "hitrate_20d_0.60"), +] + + +def load_pred(path: str) -> pd.Series: + df = pd.read_pickle(path) + if isinstance(df, pd.DataFrame): + if "score" in df.columns: + s = df["score"] + else: + s = df.iloc[:, 0] + else: + s = df + idx = s.index + new_dt = pd.to_datetime(idx.get_level_values(0)).normalize() + s.index = pd.MultiIndex.from_arrays( + [new_dt, idx.get_level_values(1)], names=idx.names + ) + return s + + +def load_bars_for_window(start: str, end: str) -> pd.DataFrame: + from tac_qlib.data.config import LakeConfig, resolve_lake_root + cfg = LakeConfig(resolve_lake_root(LAKE_ROOT), MARKET) + sp = cfg.lake_root / "symbols.parquet" + if sp.exists(): + syms = pd.read_parquet(sp) + col = "symbol" if "symbol" in syms.columns else syms.columns[0] + symbols = sorted(syms[col].astype(str).str.upper().tolist()) + else: + return pd.DataFrame() + closes = {} + for sym in symbols: + p = cfg.bar_path("1d", sym) + if not p.exists(): + continue + try: + df = pd.read_parquet(p) + except Exception: + continue + if not len(df): + continue + tcol = df["t"] if "t" in df.columns else df["date"] + ts = pd.to_datetime(tcol) + df = df.assign(_t=ts).set_index("_t").sort_index() + warmup_start = pd.Timestamp(start) - pd.Timedelta(days=60) + df = df.loc[warmup_start:end] + if len(df) >= 22: + closes[sym] = df["c"] + return pd.DataFrame(closes) + + +def compute_hit_rate_series( + pred: pd.Series, ret_df: pd.DataFrame, topk: int = 10, lookback: int = 10, +) -> pd.Series: + dt_idx = pred.index.get_level_values(0) + trade_dates = sorted(dt_idx.unique()) + hit_rates = {} + for i in range(1, len(trade_dates)): + prev_date = trade_dates[i - 1] + curr_date = trade_dates[i] + try: + prev_scores = pred.loc[prev_date] + except KeyError: + continue + if isinstance(prev_scores, pd.DataFrame): + prev_scores = prev_scores.iloc[:, 0] + prev_scores = prev_scores.dropna().sort_values(ascending=False) + topk_syms = list(prev_scores.index[:topk]) + if curr_date not in ret_df.index: + continue + today_ret = ret_df.loc[curr_date] + topk_rets = today_ret.reindex(topk_syms).dropna() + if len(topk_rets) > 0: + hit_rate = (topk_rets > 0).mean() + hit_rates[curr_date] = hit_rate + hit_series = pd.Series(hit_rates) + if len(hit_series) == 0: + return hit_series + rolling_hr = hit_series.rolling(lookback, min_periods=max(1, lookback // 2)).mean() + return rolling_hr + + +def run_backtest(pred, hit_rate, close_df, start, end, topk=10, threshold=0.5): + if not isinstance(pred.index, pd.MultiIndex): + return {"error": "pred must have MultiIndex"} + ret_df = close_df.pct_change() + ret_df.index = pd.to_datetime(ret_df.index).normalize() + dt_idx = pred.index.get_level_values(0) + window_mask = (dt_idx >= pd.Timestamp(start)) & (dt_idx <= pd.Timestamp(end)) + window_pred = pred.loc[window_mask] + if len(window_pred) == 0: + return {"error": "no pred data in window"} + trade_dates = sorted(dt_idx[window_mask].unique()) + gate_open = {} + for d in trade_dates: + known = hit_rate[hit_rate.index <= d] + if len(known) > 0 and not pd.isna(known.iloc[-1]): + gate_open[d] = bool(known.iloc[-1] >= threshold) + else: + gate_open[d] = True + n_total = len(trade_dates) + n_open = sum(1 for v in gate_open.values() if v) + n_closed = n_total - n_open + holdings_base = [] + holdings_gated = [] + equity_gated = 1_000_000.0 + equity_base = 1_000_000.0 + prev_week = None + prev_scores = None + daily_gated = [] + daily_base = [] + ret_by_date = {rd: ret_df.loc[rd] for rd in ret_df.index} + for d in trade_dates: + try: + day_scores = window_pred.loc[d] + except KeyError: + daily_gated.append(equity_gated) + daily_base.append(equity_base) + prev_scores = None + continue + if isinstance(day_scores, pd.DataFrame): + day_scores = day_scores.iloc[:, 0] + day_scores = day_scores.dropna().sort_values(ascending=False) + if len(day_scores) == 0: + daily_gated.append(equity_gated) + daily_base.append(equity_base) + prev_scores = None + continue + ret_row = ret_by_date.get(d) + if ret_row is None: + daily_gated.append(equity_gated) + daily_base.append(equity_base) + prev_scores = day_scores + continue + cur_week = (d.isocalendar()[0], d.isocalendar()[1]) if hasattr(d, 'isocalendar') else None + gate_val = gate_open.get(d, True) + if cur_week != prev_week or not holdings_base: + if prev_scores is not None: + holdings_base = list(prev_scores.index[:topk]) + if holdings_base: + base_rets = ret_row.reindex(holdings_base).dropna() + if len(base_rets) > 0: + equity_base *= (1 + base_rets.mean()) + if gate_val: + if cur_week != prev_week or not holdings_gated: + if prev_scores is not None: + holdings_gated = list(prev_scores.index[:topk]) + if holdings_gated: + hold_rets = ret_row.reindex(holdings_gated).dropna() + if len(hold_rets) > 0: + equity_gated *= (1 + hold_rets.mean()) + else: + holdings_gated = [] + prev_week = cur_week + prev_scores = day_scores + daily_gated.append(equity_gated) + daily_base.append(equity_base) + g_series = pd.Series(daily_gated, index=trade_dates) + b_series = pd.Series(daily_base, index=trade_dates) + + def _metrics(eq): + if len(eq) < 2: + return {"ann_return": 0, "sharpe": 0, "maxDD": 0} + rets = eq.pct_change().dropna() + ann_ret = float((eq.iloc[-1] / eq.iloc[0]) ** (252 / max(len(eq), 1)) - 1) + vol = float(rets.std() * (252 ** 0.5)) if len(rets) > 1 else 0 + sharpe = ann_ret / vol if vol > 0 else 0 + peak = eq.cummax() + dd = (eq - peak) / peak + maxDD = float(dd.min()) + return {"ann_return": round(ann_ret, 6), "sharpe": round(sharpe, 4), "maxDD": round(maxDD, 6)} + + base_m = _metrics(b_series) + gated_m = _metrics(g_series) + return { + "trade_dates": n_total, + "gate_open_days": n_open, + "gate_closed_days": n_closed, + "trip_rate": round(n_closed / n_total, 4) if n_total else 0, + "base": base_m, + "gated": gated_m, + } + + +def run_set(windows, close_df, tag): + results = [] + for window in windows: + wl, ws, we = window["label"], window["start"], window["end"] + pred_path = window["pred"] + print(f"\n=== [{tag}] Window {wl} ({ws} to {we}) ===") + pred = load_pred(pred_path) + print(f" pred shape: {pred.shape}, date range: {pred.index.get_level_values(0).min()} .. {pred.index.get_level_values(0).max()}") + hit_rates = {} + for lookback, _, name in SIGNAL_GATE_CONFIGS: + if lookback not in hit_rates: + hr = compute_hit_rate_series(pred, ret_df, topk=10, lookback=lookback) + hit_rates[lookback] = hr + print(f" lookback={lookback}: {len(hr)} days with hit rates") + for lookback, threshold, name in SIGNAL_GATE_CONFIGS: + hr = hit_rates[lookback] + bt = run_backtest(pred, hr, close_df, ws, we, topk=10, threshold=threshold) + if "error" in bt: + print(f" {name}: {bt['error']}") + continue + row = { + "source": tag, + "window": wl, + "gate": name, + "start": ws, + "end": we, + "trade_dates": bt["trade_dates"], + "gate_open": bt["gate_open_days"], + "gate_closed": bt["gate_closed_days"], + "trip_rate": bt["trip_rate"], + "base_ann": bt["base"]["ann_return"], + "base_sharpe": bt["base"]["sharpe"], + "base_maxDD": bt["base"]["maxDD"], + "gated_ann": bt["gated"]["ann_return"], + "gated_sharpe": bt["gated"]["sharpe"], + "gated_maxDD": bt["gated"]["maxDD"], + } + results.append(row) + diff = bt["gated"]["ann_return"] - bt["base"]["ann_return"] + print(f" {name}: trip={bt['trip_rate']:.1%}, " + f"base={bt['base']['ann_return']:+.1%} (Sharpe {bt['base']['sharpe']:.2f}), " + f"gated={bt['gated']['ann_return']:+.1%} (Sharpe {bt['gated']['sharpe']:.2f}), " + f"diff={diff:+.1%}pp") + return results + + +def main(): + OUT_DIR.mkdir(parents=True, exist_ok=True) + full_start = "2015-01-03" + full_end = "2026-08-19" + print("Loading lake bars...") + close_df = load_bars_for_window(full_start, full_end) + print(f" {close_df.shape[1]} symbols, {close_df.shape[0]} days") + global ret_df + ret_df = close_df.pct_change() + ret_df.index = pd.to_datetime(ret_df.index).normalize() + + print("\n" + "=" * 70) + print("RUN A: Retrained model pred.pkls (exp 62)") + print("=" * 70) + results_retrained = run_set(WINDOWS_RETRAINED, close_df, "retrained") + + print("\n" + "=" * 70) + print("RUN B: Reference pred.pkls (exp 52/56)") + print("=" * 70) + results_reference = run_set(WINDOWS_REFERENCE, close_df, "reference") + + all_results = results_retrained + results_reference + df = pd.DataFrame(all_results) + + # Save combined results + out_path = OUT_DIR / "signal_quality_gate_retrained.csv" + df.to_csv(out_path, index=False) + with open(OUT_DIR / "signal_quality_gate_retrained.json", "w") as f: + json.dump(df.to_dict(orient="records"), f, indent=2, default=str) + print(f"\nSaved to {out_path}") + + # Head-to-head comparison table + print("\n" + "=" * 70) + print("HEAD-TO-HEAD: Retrained vs Reference (hitrate_5d_0.50)") + print("=" * 70) + print(f"{'Year':>6} | {'Ref Base':>10} {'Ref Gated':>10} {'Ref Diff':>10} | {'Ret Base':>10} {'Ret Gated':>10} {'Ret Diff':>10}") + print("-" * 85) + for year in ["2021", "2023", "2024", "2025", "2026"]: + ref = df[(df["source"] == "reference") & (df["window"] == year) & (df["gate"] == "hitrate_5d_0.50")] + ret = df[(df["source"] == "retrained") & (df["window"] == year) & (df["gate"] == "hitrate_5d_0.50")] + if len(ref) > 0 and len(ret) > 0: + rb = ref.iloc[0]["base_ann"] + rg = ref.iloc[0]["gated_ann"] + tb = ret.iloc[0]["base_ann"] + tg = ret.iloc[0]["gated_ann"] + print(f"{year:>6} | {rb:>+9.1%} {rg:>+9.1%} {rg-rb:>+9.1%} | {tb:>+9.1%} {tg:>+9.1%} {tg-tb:>+9.1%}") + + +if __name__ == "__main__": + main() diff --git a/book/workflows/sq_gate_weekly/sq_gate_wk_2021.yaml b/book/workflows/sq_gate_weekly/sq_gate_wk_2021.yaml new file mode 100644 index 0000000..99b7a8c --- /dev/null +++ b/book/workflows/sq_gate_weekly/sq_gate_wk_2021.yaml @@ -0,0 +1,119 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} + +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-2021" + +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2021-12-31 + fit_start_time: 2017-01-03 + fit_end_time: 2020-12-31 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2017-01-03, fit_end_time: 2020-12-31 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2017-01-03, fit_end_time: 2020-12-31 } + - class: Fillna + kwargs: {} + segments: + train: [2017-01-03, 2020-12-31] + valid: [2021-01-04, 2021-01-04] + test: [2021-01-04, 2021-12-31] + + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2017-01-03" + gate_end: "2021-12-31" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2021-01-04 + end_time: 2021-12-31 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_weekly/sq_gate_wk_2023.yaml b/book/workflows/sq_gate_weekly/sq_gate_wk_2023.yaml new file mode 100644 index 0000000..d22b01b --- /dev/null +++ b/book/workflows/sq_gate_weekly/sq_gate_wk_2023.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-2023" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2023-12-29 + fit_start_time: 2019-01-02 + fit_end_time: 2022-12-30 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2019-01-02, fit_end_time: 2022-12-30 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2019-01-02, fit_end_time: 2022-12-30 } + - class: Fillna + kwargs: {} + segments: + train: [2019-01-02, 2022-12-30] + valid: [2023-01-03, 2023-01-03] + test: [2023-01-03, 2023-12-29] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2019-01-02" + gate_end: "2023-12-29" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2023-01-03 + end_time: 2023-12-29 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_weekly/sq_gate_wk_2024.yaml b/book/workflows/sq_gate_weekly/sq_gate_wk_2024.yaml new file mode 100644 index 0000000..e13e343 --- /dev/null +++ b/book/workflows/sq_gate_weekly/sq_gate_wk_2024.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-2024" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2024-12-31 + fit_start_time: 2020-01-02 + fit_end_time: 2023-12-29 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2020-01-02, fit_end_time: 2023-12-29 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2020-01-02, fit_end_time: 2023-12-29 } + - class: Fillna + kwargs: {} + segments: + train: [2020-01-02, 2023-12-29] + valid: [2024-01-02, 2024-01-02] + test: [2024-01-02, 2024-12-31] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2020-01-02" + gate_end: "2024-12-31" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2024-01-02 + end_time: 2024-12-31 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_weekly/sq_gate_wk_2025.yaml b/book/workflows/sq_gate_weekly/sq_gate_wk_2025.yaml new file mode 100644 index 0000000..f76dec0 --- /dev/null +++ b/book/workflows/sq_gate_weekly/sq_gate_wk_2025.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-2025" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2025-12-31 + fit_start_time: 2021-01-04 + fit_end_time: 2024-12-31 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2021-01-04, fit_end_time: 2024-12-31 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2021-01-04, fit_end_time: 2024-12-31 } + - class: Fillna + kwargs: {} + segments: + train: [2021-01-04, 2024-12-31] + valid: [2025-01-02, 2025-01-02] + test: [2025-01-02, 2025-12-31] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2021-01-04" + gate_end: "2025-12-31" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2025-01-02 + end_time: 2025-12-31 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_weekly/sq_gate_wk_2026.yaml b/book/workflows/sq_gate_weekly/sq_gate_wk_2026.yaml new file mode 100644 index 0000000..c88d842 --- /dev/null +++ b/book/workflows/sq_gate_weekly/sq_gate_wk_2026.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-2026" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2026-08-10 + fit_start_time: 2016-01-04 + fit_end_time: 2025-09-01 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2025-09-01 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2025-09-01 } + - class: Fillna + kwargs: {} + segments: + train: [2016-01-04, 2025-09-01] + valid: [2025-09-03, 2026-01-03] + test: [2026-01-04, 2026-08-10] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2016-01-04" + gate_end: "2026-08-19" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2026-01-04 + end_time: 2026-08-10 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2021.yaml b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2021.yaml new file mode 100644 index 0000000..a0bd325 --- /dev/null +++ b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2021.yaml @@ -0,0 +1,119 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} + +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-zc-2021" + +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2021-12-31 + fit_start_time: 2017-01-03 + fit_end_time: 2020-12-31 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2017-01-03, fit_end_time: 2020-12-31 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2017-01-03, fit_end_time: 2020-12-31 } + - class: Fillna + kwargs: {} + segments: + train: [2017-01-03, 2020-12-31] + valid: [2021-01-04, 2021-01-04] + test: [2021-01-04, 2021-12-31] + + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2017-01-03" + gate_end: "2021-12-31" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2021-01-04 + end_time: 2021-12-31 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0 + close_cost: 0.0 + min_cost: 0.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2023.yaml b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2023.yaml new file mode 100644 index 0000000..e4d087c --- /dev/null +++ b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2023.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-zc-2023" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2023-12-29 + fit_start_time: 2019-01-02 + fit_end_time: 2022-12-30 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2019-01-02, fit_end_time: 2022-12-30 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2019-01-02, fit_end_time: 2022-12-30 } + - class: Fillna + kwargs: {} + segments: + train: [2019-01-02, 2022-12-30] + valid: [2023-01-03, 2023-01-03] + test: [2023-01-03, 2023-12-29] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2019-01-02" + gate_end: "2023-12-29" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2023-01-03 + end_time: 2023-12-29 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0 + close_cost: 0.0 + min_cost: 0.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2024.yaml b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2024.yaml new file mode 100644 index 0000000..49d354f --- /dev/null +++ b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2024.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-zc-2024" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2024-12-31 + fit_start_time: 2020-01-02 + fit_end_time: 2023-12-29 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2020-01-02, fit_end_time: 2023-12-29 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2020-01-02, fit_end_time: 2023-12-29 } + - class: Fillna + kwargs: {} + segments: + train: [2020-01-02, 2023-12-29] + valid: [2024-01-02, 2024-01-02] + test: [2024-01-02, 2024-12-31] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2020-01-02" + gate_end: "2024-12-31" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2024-01-02 + end_time: 2024-12-31 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0 + close_cost: 0.0 + min_cost: 0.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2025.yaml b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2025.yaml new file mode 100644 index 0000000..e8133bd --- /dev/null +++ b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2025.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-zc-2025" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2025-12-31 + fit_start_time: 2021-01-04 + fit_end_time: 2024-12-31 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2021-01-04, fit_end_time: 2024-12-31 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2021-01-04, fit_end_time: 2024-12-31 } + - class: Fillna + kwargs: {} + segments: + train: [2021-01-04, 2024-12-31] + valid: [2025-01-02, 2025-01-02] + test: [2025-01-02, 2025-12-31] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2021-01-04" + gate_end: "2025-12-31" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2025-01-02 + end_time: 2025-12-31 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0 + close_cost: 0.0 + min_cost: 0.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2026.yaml b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2026.yaml new file mode 100644 index 0000000..8a17f36 --- /dev/null +++ b/book/workflows/sq_gate_weekly_zerocost/sq_gate_wk_2026.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-zc-2026" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2026-08-10 + fit_start_time: 2016-01-04 + fit_end_time: 2025-09-01 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2025-09-01 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2025-09-01 } + - class: Fillna + kwargs: {} + segments: + train: [2016-01-04, 2025-09-01] + valid: [2025-09-03, 2026-01-03] + test: [2026-01-04, 2026-08-10] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2016-01-04" + gate_end: "2026-08-19" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2026-01-04 + end_time: 2026-08-10 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0 + close_cost: 0.0 + min_cost: 0.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_wk_v3/sq_gate_wk_2021.yaml b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2021.yaml new file mode 100644 index 0000000..96727b6 --- /dev/null +++ b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2021.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-v3" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2021-12-31 + fit_start_time: 2016-01-04 + fit_end_time: 2020-12-31 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2020-12-31 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2020-12-31 } + - class: Fillna + kwargs: {} + segments: + train: [2016-01-04, 2020-12-31] + valid: [2021-01-04, 2021-01-04] + test: [2021-01-04, 2021-12-31] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2016-01-04" + gate_end: "2021-12-31" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2021-01-04 + end_time: 2021-12-31 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_wk_v3/sq_gate_wk_2023.yaml b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2023.yaml new file mode 100644 index 0000000..9e3179f --- /dev/null +++ b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2023.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-v3" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2023-12-29 + fit_start_time: 2016-01-04 + fit_end_time: 2022-12-30 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2022-12-30 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2022-12-30 } + - class: Fillna + kwargs: {} + segments: + train: [2016-01-04, 2022-12-30] + valid: [2023-01-03, 2023-01-03] + test: [2023-01-03, 2023-12-29] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2016-01-04" + gate_end: "2023-12-29" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2023-01-03 + end_time: 2023-12-29 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_wk_v3/sq_gate_wk_2024.yaml b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2024.yaml new file mode 100644 index 0000000..97f09da --- /dev/null +++ b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2024.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-v3" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2024-12-31 + fit_start_time: 2016-01-04 + fit_end_time: 2023-12-29 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2023-12-29 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2023-12-29 } + - class: Fillna + kwargs: {} + segments: + train: [2016-01-04, 2023-12-29] + valid: [2024-01-02, 2024-01-02] + test: [2024-01-02, 2024-12-31] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2016-01-04" + gate_end: "2024-12-31" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2024-01-02 + end_time: 2024-12-31 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_wk_v3/sq_gate_wk_2025.yaml b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2025.yaml new file mode 100644 index 0000000..de267a9 --- /dev/null +++ b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2025.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-v3" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2025-12-31 + fit_start_time: 2016-01-04 + fit_end_time: 2024-12-31 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2024-12-31 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2024-12-31 } + - class: Fillna + kwargs: {} + segments: + train: [2016-01-04, 2024-12-31] + valid: [2025-01-02, 2025-01-02] + test: [2025-01-02, 2025-12-31] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2016-01-04" + gate_end: "2025-12-31" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2025-01-02 + end_time: 2025-12-31 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d diff --git a/book/workflows/sq_gate_wk_v3/sq_gate_wk_2026.yaml b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2026.yaml new file mode 100644 index 0000000..70478e1 --- /dev/null +++ b/book/workflows/sq_gate_wk_v3/sq_gate_wk_2026.yaml @@ -0,0 +1,115 @@ +{%- set LAKE = TAC_LAKE_DIR %} +{%- set UNIVERSE = "SPY,QQQ,DIA,IWM,MDY,VTI,VOO,VEA,VWO,VT,EFA,EEM,TLT,IEF,SHY,AGG,BND,LQD,HYG,JNK,EMB,GLD,SLV,USO,UNG,DBA,DBC,XLK,XLF,XLE,XLV,XLI,XLY,XLP,XLU,XLB,XLRE,ARKK,SMH,SOXX,IBB,XBI,ITA,XAR,ICLN,TAN,FDN,IGV,ESPO,REM" %} +{%- set SP_FIELDS = "sp_ret,sp_jump_ratio,sp_jump_flag,sp_jump_tail,sp_max_move,sp_rv1,sp_rv5,sp_rv22,sp_vol_ratio_5_22,sp_vol_ratio_1_22,sp_trend_slope_5,sp_trend_slope_20,sp_trend_slope_60,sp_logp,sp_hurst_exponent,sp_sig_level1_lead,sp_sig_level1_lag,sp_sig_level2_lead_lag,sp_sig_level2_lag_lead" %} +qlib_init: + provider_uri: "{{ LAKE }}" + region: us + expression_cache: null + dataset_cache: null + calendar_provider: + class: tac_qlib.data.providers.LakeCalendarProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + instrument_provider: + class: tac_qlib.data.providers.LakeInstrumentProvider + kwargs: { lake_root: "{{ LAKE }}", market: US, markets: {} } + feature_provider: + class: tac_qlib.data.providers.LakeFeatureProvider + kwargs: { lake_root: "{{ LAKE }}", market: US } + exp_manager: + class: MLflowExpManager + module_path: qlib.workflow.expm + kwargs: + uri: "sqlite:///{{ LAKE }}/mlruns.db" + default_exp_name: "tac-rd-sq-gate-wk-v3" +task: + model: + class: RankICEnsembleLGBModel + module_path: tac_qlib.contrib.model.rank_ensemble + kwargs: + loss: mse + learning_rate: 0.02 + num_leaves: 31 + n_estimators: 3000 + num_boost_round: 3000 + early_stopping_rounds: 200 + min_data_in_leaf: 20 + lambda_l2: 0.5 + colsample_bytree: 0.8 + subsample: 0.8 + subsample_freq: 1 + reg_alpha: 0.1 + reg_lambda: 1.0 + seeds: "42,7,2026,99,123" + parallel: 5 + dataset: + class: DatasetH + module_path: qlib.data.dataset + kwargs: + handler: + class: TACHandler + module_path: tac_qlib.contrib.data.handler + kwargs: + instruments: "{{ UNIVERSE }}" + start_time: 2015-01-03 + end_time: 2026-08-10 + fit_start_time: 2016-01-04 + fit_end_time: 2025-09-01 + freq: day + lake_root: "{{ LAKE }}" + market: US + label: "Ref($close,-6)/Ref($close,-1)-1" + feature_fields: "$open,$high,$low,$close,$vwap,$volume,{{ SP_FIELDS }}" + infer_processors: + - class: DropAllNaN + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2025-09-01 } + - class: ProcessInf + kwargs: {} + - class: CSRankNorm + kwargs: {} + - class: ZScoreNorm + kwargs: { fit_start_time: 2016-01-04, fit_end_time: 2025-09-01 } + - class: Fillna + kwargs: {} + segments: + train: [2016-01-04, 2025-09-01] + valid: [2025-09-03, 2026-01-03] + test: [2026-01-04, 2026-08-10] + record: + - class: SignalRecord + module_path: qlib.workflow.record_temp + kwargs: {} + - class: SigAnaRecord + module_path: qlib.workflow.record_temp + kwargs: { ana_long_short: true, ann_scaler: 252 } + - class: PortAnaRecord + module_path: qlib.workflow.record_temp + kwargs: + config: + strategy: + class: WeeklyRebalanceSignalQualityGateStrategy + module_path: tac_qlib.contrib.strategy.weekly_sq_gate + kwargs: + signal: "" + lake_root: "{{ LAKE }}" + gate_topk: 10 + gate_lookback: 5 + gate_threshold: 0.5 + gate_start: "2016-01-04" + gate_end: "2026-08-19" + topk: 10 + n_drop: 1 + only_tradable: true + risk_degree: 0.95 + backtest: + start_time: 2026-01-04 + end_time: 2026-08-10 + account: 1000000 + benchmark: SPY + exchange_kwargs: + codes: "{{ UNIVERSE }}" + deal_price: $close + freq: day + open_cost: 0.0005 + close_cost: 0.0015 + min_cost: 5.0 + risk_analysis_freq: 1d