Compare commits

..
28 changed files with 1604 additions and 1374 deletions
@@ -1,46 +0,0 @@
window,gate,start,end,trade_dates,gate_open,gate_closed,trip_rate,base_ann,base_sharpe,base_maxDD,gated_ann,gated_sharpe,gated_maxDD
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
1 window gate start end trade_dates gate_open gate_closed trip_rate base_ann base_sharpe base_maxDD gated_ann gated_sharpe gated_maxDD
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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
22 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
23 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
24 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
25 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
26 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
27 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
28 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
29 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
30 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
31 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
32 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
33 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
34 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
35 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
36 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
37 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
38 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
39 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
40 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
41 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
42 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
43 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
44 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
45 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
46 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
@@ -1,722 +0,0 @@
[
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
}
]
-293
View File
@@ -1,293 +0,0 @@
"""Signal-quality gate walk-forward backtest.
Gates trades based on whether the model's recent topk predictions were correct
(hit rate). This is a retrospective gate — it measures prediction accuracy,
not market state.
Usage:
cd /app && .venv/bin/python book/scripts/signal_quality_gate_bt.py
"""
from __future__ import annotations
import json
import pathlib
import sys
import time
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")
WINDOWS = [
{"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-quality gate configs: (lookback_days, threshold, name)
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 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")
ret_df = close_df.pct_change()
ret_df.index = pd.to_datetime(ret_df.index).normalize()
results = []
for window in WINDOWS:
wl, ws, we = window["label"], window["start"], window["end"]
pred_path = window["pred"]
print(f"\n=== Window {wl} ({ws} to {we}) ===")
pred = load_pred(pred_path)
print(f" pred shape: {pred.shape}")
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 = {
"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)
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})")
df = pd.DataFrame(results)
out_path = OUT_DIR / "signal_quality_gate_results.csv"
df.to_csv(out_path, index=False)
with open(OUT_DIR / "signal_quality_gate_results.json", "w") as f:
json.dump(df.to_dict(orient="records"), f, indent=2, default=str)
print(f"\nSaved to {out_path}")
print("\n=== Summary: Gated Return by Window ===")
for gate_name in df["gate"].unique():
gdf = df[df["gate"] == gate_name]
print(f"\n{gate_name}:")
for _, r in gdf.iterrows():
print(f" {r['window']}: base={r['base_ann']:+.1%}, gated={r['gated_ann']:+.1%}, "
f"trip={r['trip_rate']:.0%}, diff={r['gated_ann']-r['base_ann']:+.1%}pp")
if __name__ == "__main__":
main()
+16 -15
View File
@@ -1,30 +1,31 @@
# TradeAC custom-qlib-code snapshot (auto-generated) # TradeAC custom-qlib-code snapshot (auto-generated)
# parent repo HEAD : ceb1e196e24761a87d1afe1bde00079b79e02693 # parent repo HEAD : 507846cee16eeee11daf33c4176e8aec79b985b2
# tac-qlib/tac_qlib/contrib # tac-qlib/tac_qlib/contrib
# tac-qlib/tac_qlib/data # tac-qlib/tac_qlib/data
# per-file hashes (git hash-object): # per-file hashes (git hash-object):
1b6298c4a5652f2e863cbdc385a1014a570fcd59 tac-qlib/tac_qlib/contrib/__init__.py 1b6298c4a5652f2e863cbdc385a1014a570fcd59 tac-qlib/tac_qlib/contrib/__init__.py
861592c63edd6a0853a9cb174b5970435b135fc8 tac-qlib/tac_qlib/contrib/__pycache__/__init__.cpython-312.pyc b419ee55ed455a1c45423d1c9025ca5cc0a98576 tac-qlib/tac_qlib/contrib/__pycache__/__init__.cpython-312.pyc
c76a9f17f680e74eea766eff27f7624359749ed6 tac-qlib/tac_qlib/contrib/data/__init__.py c76a9f17f680e74eea766eff27f7624359749ed6 tac-qlib/tac_qlib/contrib/data/__init__.py
5c547a2ef92e075e550fe6d01508a2f1d3f536bc tac-qlib/tac_qlib/contrib/data/__pycache__/__init__.cpython-312.pyc 2f6c67620aa2f9e6aaaef3369361d9b3eac3d6ca tac-qlib/tac_qlib/contrib/data/__pycache__/__init__.cpython-312.pyc
4f656130d167e79dcaaeb7783a121f0b36852374 tac-qlib/tac_qlib/contrib/data/__pycache__/handler.cpython-312.pyc fdd5923a70a399e8680913593ff111641947898e tac-qlib/tac_qlib/contrib/data/__pycache__/handler.cpython-312.pyc
0dd25ef161c6e0f15eafc84886e7e1381deb38c3 tac-qlib/tac_qlib/contrib/data/handler.py 0dd25ef161c6e0f15eafc84886e7e1381deb38c3 tac-qlib/tac_qlib/contrib/data/handler.py
b151d139a0dcde87d74b21e7c4b729176ba5c39b tac-qlib/tac_qlib/contrib/model/__init__.py b151d139a0dcde87d74b21e7c4b729176ba5c39b tac-qlib/tac_qlib/contrib/model/__init__.py
b1489f2fc0dee85f0a4f90b2e6ad545ed9c8967b tac-qlib/tac_qlib/contrib/model/__pycache__/__init__.cpython-312.pyc 08dec87ccdf6bb5d2cf611ca3032a4280aaab8cf tac-qlib/tac_qlib/contrib/model/__pycache__/__init__.cpython-312.pyc
121ef237da1df1b8e21a561c3ad0db200b901339 tac-qlib/tac_qlib/contrib/model/__pycache__/rank_ensemble.cpython-312.pyc 6fb61946ea9a83dfb560de3717f5fbf482c4c00e tac-qlib/tac_qlib/contrib/model/__pycache__/rank_ensemble.cpython-312.pyc
74d0da348cbcc3700c96b6f4fe4391488e61efc5 tac-qlib/tac_qlib/contrib/model/__pycache__/rank_gbdt.cpython-312.pyc 3e80f2e08b661ddd2f58ffe5a6196063fa41ae51 tac-qlib/tac_qlib/contrib/model/__pycache__/rank_gbdt.cpython-312.pyc
d3f051f3a8650c42fedc7b367b966f7c74fb5789 tac-qlib/tac_qlib/contrib/model/rank_ensemble.py d3f051f3a8650c42fedc7b367b966f7c74fb5789 tac-qlib/tac_qlib/contrib/model/rank_ensemble.py
d03e6611338918d4aac5eea4adf26f85a3763652 tac-qlib/tac_qlib/contrib/model/rank_gbdt.py d03e6611338918d4aac5eea4adf26f85a3763652 tac-qlib/tac_qlib/contrib/model/rank_gbdt.py
2c2f167b693f4366a769998e3c9d4804f29e31e0 tac-qlib/tac_qlib/contrib/strategy/__init__.py c4ef84ffda2a611262412fe1127689c667f3d0c1 tac-qlib/tac_qlib/contrib/strategy/__init__.py
c29e45e562256bf786c36f91a971b097467276e9 tac-qlib/tac_qlib/contrib/strategy/__pycache__/__init__.cpython-312.pyc 6ad10c2ebe37c16417e67c7aeb731ad1fcb6da2f tac-qlib/tac_qlib/contrib/strategy/__pycache__/__init__.cpython-312.pyc
6dd1c568a2961842793674390d5abffd1a0e71b8 tac-qlib/tac_qlib/contrib/strategy/__pycache__/optimal_stop.cpython-312.pyc 8d684b3216b040071d9ee4fa920a0e0c7486d278 tac-qlib/tac_qlib/contrib/strategy/__pycache__/optimal_stop.cpython-312.pyc
519a1f4c05dbe0ac018ab8b779eb33d53b4dd545 tac-qlib/tac_qlib/contrib/strategy/ic_gate.py 896ef74ae47bcd1ed388e1e5d9c8d70c28097fe9 tac-qlib/tac_qlib/contrib/strategy/kelly_dropout.py
79aaad9e39fcc740a773f4f63c512ce1086cfde0 tac-qlib/tac_qlib/contrib/strategy/optimal_stop.py 79aaad9e39fcc740a773f4f63c512ce1086cfde0 tac-qlib/tac_qlib/contrib/strategy/optimal_stop.py
7bcee5f0b09cfa721440f1354f16f2dd9a112b12 tac-qlib/tac_qlib/contrib/strategy/regime_gate.py 5b9acfb4340111b204249add7760bd53c6ae03f1 tac-qlib/tac_qlib/contrib/strategy/regime_gate.py
aa1ee880d52ceb5821d65973962099c2254f710a tac-qlib/tac_qlib/contrib/strategy/top_bottom.py
fe60bacdfedd48617863be31f24b7c7daebfac5a tac-qlib/tac_qlib/contrib/strategy/weekly_rebalance.py fe60bacdfedd48617863be31f24b7c7daebfac5a tac-qlib/tac_qlib/contrib/strategy/weekly_rebalance.py
92e6e90eb0cd0a25142034560f27adb6b705b1a8 tac-qlib/tac_qlib/data/__init__.py 92e6e90eb0cd0a25142034560f27adb6b705b1a8 tac-qlib/tac_qlib/data/__init__.py
316bf4aa160cc8d15929ea648be03f4b4999667d tac-qlib/tac_qlib/data/__pycache__/__init__.cpython-312.pyc 7c4e6c345fad1978efe8860c0d977d0c02d6f8d9 tac-qlib/tac_qlib/data/__pycache__/__init__.cpython-312.pyc
554a3f29d181b64effbf49a8161b32e7f93d8d3e tac-qlib/tac_qlib/data/__pycache__/config.cpython-312.pyc 99e602392d51663cb06d5c425000b1ed1e5a916b tac-qlib/tac_qlib/data/__pycache__/config.cpython-312.pyc
8b47f6d78ac046b6b7b2fb07bd7f3382773ffb73 tac-qlib/tac_qlib/data/__pycache__/providers.cpython-312.pyc 020dcdcf288e4832c8cf2386351f78d5ceb4fe13 tac-qlib/tac_qlib/data/__pycache__/providers.cpython-312.pyc
53c9007a928841fd3c3b08450f9a6520ce1ac091 tac-qlib/tac_qlib/data/config.py 53c9007a928841fd3c3b08450f9a6520ce1ac091 tac-qlib/tac_qlib/data/config.py
8d0644f6f0d1efb94798ed444cc73e63b643459b tac-qlib/tac_qlib/data/providers.py 8d0644f6f0d1efb94798ed444cc73e63b643459b tac-qlib/tac_qlib/data/providers.py
@@ -1,11 +1,13 @@
from .ic_gate import ICGateTopkDropoutStrategy # noqa: F401 from .kelly_dropout import FractionalKellyDropoutStrategy # noqa: F401
from .optimal_stop import OptimalStopControl # noqa: F401 from .optimal_stop import OptimalStopControl # noqa: F401
from .regime_gate import RegimeGateTopkDropoutStrategy # noqa: F401 from .regime_gate import RegimeGateDropoutStrategy # noqa: F401
from .top_bottom import TopBottomDropoutStrategy # noqa: F401
from .weekly_rebalance import WeeklyRebalanceDropoutStrategy # noqa: F401 from .weekly_rebalance import WeeklyRebalanceDropoutStrategy # noqa: F401
__all__ = [ __all__ = [
"ICGateTopkDropoutStrategy",
"OptimalStopControl", "OptimalStopControl",
"RegimeGateTopkDropoutStrategy", "FractionalKellyDropoutStrategy",
"WeeklyRebalanceDropoutStrategy", "WeeklyRebalanceDropoutStrategy",
"TopBottomDropoutStrategy",
"RegimeGateDropoutStrategy",
] ]
@@ -1,117 +0,0 @@
"""Realized-IC circuit breaker TopkDropout strategy.
Subclass of ``qlib.contrib.strategy.signal_strategy.TopkDropoutStrategy`` that
holds the book (issues NO orders) while the streaming realized RankIC of the
deployed signal is below threshold — i.e. the model's cross-sectional
predictions are no longer earning against realized forward returns. When the
gate is open it behaves exactly like the reference TopkDropoutStrategy.
The gate is evaluated per trade step on the trailing mean realized RankIC of
the signal over the last ``ic_window`` trading days whose label is fully
realized as of the decision date (no lookahead — a 5d fwd label ``close[t+6]/
close[t+1]-1`` is only known at ``t+6``).
Two wiring modes:
* ``ic_gate``: a precomputed ``pd.Series`` indexed by datetime of booleans
(True = gate open / trade allowed). Computed once by the caller (e.g.
``rd_backtest``) and looked up per step. Missing dates default to open.
* realized-IC self-computation: when ``ic_min_rankic`` is given but no
``ic_gate``, the strategy computes the per-date realized RankIC itself from
``self.signal`` (the pred scores) and the lake 1d bars via
``tac_qlib.risk_limits.realized_rankic_series``, then applies the same
trailing-window comparison. Works when instantiated from a workflow YAML
PortAnaRecord config (``lake_root`` / ``market`` must be provided).
"""
from __future__ import annotations
import pandas as pd
from qlib.backtest.decision import TradeDecisionWO
from qlib.contrib.strategy.signal_strategy import TopkDropoutStrategy
from tac_qlib.risk_limits import ic_circuit_breaker, realized_rankic_series
__all__ = ["ICGateTopkDropoutStrategy"]
class ICGateTopkDropoutStrategy(TopkDropoutStrategy):
"""TopkDropout with a streaming realized-IC circuit breaker.
Parameters
----------
topk, n_drop, method_sell, method_buy, hold_thresh, only_tradable,
forbid_all_trade_at_limit : same as ``TopkDropoutStrategy``.
ic_min_rankic : float — pause new trading while trailing realized RankIC is
below this threshold (0 disables the gate).
ic_window : int — trailing window for the realized RankIC mean (default 22).
ic_label_horizon : int — label horizon in trading days (default 6).
ic_min_obs : int — min realized labels before the gate arms (default 10).
ic_gate : pd.Series, optional — precomputed per-date gate (bool indexed by
datetime). When provided, it overrides self-computation.
lake_root, market : str — lake location for self-computed realized IC.
"""
def __init__(
self,
*,
topk,
n_drop,
ic_min_rankic: float = 0.0,
ic_window: int = 22,
ic_label_horizon: int = 6,
ic_min_obs: int = 10,
ic_gate=None,
lake_root: str = "",
market: str = "US",
**kwargs,
):
super().__init__(topk=topk, n_drop=n_drop, **kwargs)
self.ic_min_rankic = float(ic_min_rankic or 0.0)
self.ic_window = int(ic_window or 22)
self.ic_label_horizon = int(ic_label_horizon or 6)
self.ic_min_obs = int(ic_min_obs or 10)
self._ic_gate = ic_gate
self._realized_ic = None
self.lake_root = lake_root or ""
self.market = market or "US"
def _load_realized_ic(self):
if self._realized_ic is None:
pred_start_time, pred_end_time = self.trade_calendar.get_step_time(
self.trade_calendar.get_trade_step(), shift=-self.ic_label_horizon
)
pred = self.signal.get_signal(start_time=pred_start_time, end_time=pred_end_time)
if isinstance(pred, pd.DataFrame):
pred = pred.iloc[:, 0]
self._realized_ic = realized_rankic_series(
pred, self.lake_root, self.market, label_horizon=self.ic_label_horizon
)
return self._realized_ic
def _gate_open(self, trade_start_time) -> bool:
ts = pd.Timestamp(trade_start_time)
if self._ic_gate is not None:
# precomputed gate series: look up the latest known decision date <= ts
known = self._ic_gate[self._ic_gate.index <= ts]
if len(known):
return bool(known.iloc[-1])
return True
if self.ic_min_rankic <= 0:
return True
realized = self._load_realized_ic()
limits = {
"ic_min_rankic": self.ic_min_rankic,
"ic_window": self.ic_window,
"ic_min_obs": self.ic_min_obs,
}
tripped, _reason, _trail = ic_circuit_breaker(realized, ts, limits)
return not tripped
def generate_trade_decision(self, execute_result=None):
trade_step = self.trade_calendar.get_trade_step()
trade_start_time, _ = self.trade_calendar.get_step_time(trade_step)
if not self._gate_open(trade_start_time):
return TradeDecisionWO([], self)
return super().generate_trade_decision(execute_result)
@@ -0,0 +1,201 @@
"""Fractional-Kelly dropout strategy for cross-sectional signals.
Sizing rule variant of ``qlib.contrib.strategy.signal_strategy.TopkDropoutStrategy``:
the topk/n_drop SELECTION is identical to the reference, but the buy size is
proportional to the score MAGNITUDE (edge) instead of equal-weight, capped at a
fraction ``cap_frac`` of the equal-weight notional so a single name cannot
over-concentrate the book.
``cap_frac`` is the fraction of the equal-weight per-name notional that a top
signal can deploy at most (e.g. 0.5 = at most half the equal-weight size).
Names whose score is below the median of the buy set get a proportionally
smaller slice; the residual stays in cash (that is the point of the rule:
throw away less edge per name, deploy less capital when conviction is low).
"""
from __future__ import annotations
from typing import List
import numpy as np
import pandas as pd
from qlib.backtest import Order
from qlib.backtest.decision import OrderDir, TradeDecisionWO
from qlib.contrib.strategy.signal_strategy import TopkDropoutStrategy
__all__ = ["FractionalKellyDropoutStrategy"]
DEFAULT_CAP_FRAC = 0.5
class FractionalKellyDropoutStrategy(TopkDropoutStrategy):
"""TopkDropout selection with score-magnitude (fractional-Kelly) sizing.
Parameters
----------
topk, n_drop, method_sell, method_buy, hold_thresh, only_tradable,
forbid_all_trade_at_limit : same as ``TopkDropoutStrategy``.
cap_frac : max buy notional as a fraction of the equal-weight notional.
"""
def __init__(self, *, topk, n_drop, cap_frac: float = DEFAULT_CAP_FRAC, **kwargs):
super().__init__(topk=topk, n_drop=n_drop, **kwargs)
self.cap_frac = cap_frac
def generate_trade_decision(self, execute_result=None):
import copy
trade_step = self.trade_calendar.get_trade_step()
trade_start_time, trade_end_time = self.trade_calendar.get_step_time(trade_step)
pred_start_time, pred_end_time = self.trade_calendar.get_step_time(trade_step, shift=1)
pred_score = self.signal.get_signal(start_time=pred_start_time, end_time=pred_end_time)
if isinstance(pred_score, pd.DataFrame):
pred_score = pred_score.iloc[:, 0]
if pred_score is None:
return TradeDecisionWO([], self)
if self.only_tradable:
def get_first_n(li, n, reverse=False):
cur_n = 0
res = []
for si in reversed(li) if reverse else li:
if self.trade_exchange.is_stock_tradable(
stock_id=si, start_time=trade_start_time, end_time=trade_end_time
):
res.append(si)
cur_n += 1
if cur_n >= n:
break
return res[::-1] if reverse else res
def get_last_n(li, n):
return get_first_n(li, n, reverse=True)
def filter_stock(li):
return [
si
for si in li
if self.trade_exchange.is_stock_tradable(
stock_id=si, start_time=trade_start_time, end_time=trade_end_time
)
]
else:
def get_first_n(li, n):
return list(li)[:n]
def get_last_n(li, n):
return list(li)[-n:]
def filter_stock(li):
return li
current_temp: "object" = copy.deepcopy(self.trade_position)
sell_order_list: List[Order] = []
buy_order_list: List[Order] = []
cash = current_temp.get_cash()
current_stock_list = current_temp.get_stock_list()
last = pred_score.reindex(current_stock_list).sort_values(ascending=False).index
if self.method_buy == "top":
today = get_first_n(
pred_score[~pred_score.index.isin(last)].sort_values(ascending=False).index,
self.n_drop + self.topk - len(last),
)
elif self.method_buy == "random":
topk_candi = get_first_n(pred_score.sort_values(ascending=False).index, self.topk)
candi = list(filter(lambda x: x not in last, topk_candi))
n = self.n_drop + self.topk - len(last)
try:
today = np.random.choice(candi, n, replace=False)
except ValueError:
today = candi
else:
raise NotImplementedError(f"This type of input is not supported")
comb = pred_score.reindex(last.union(pd.Index(today))).sort_values(ascending=False).index
if self.method_sell == "bottom":
sell = last[last.isin(get_last_n(comb, self.n_drop))]
elif self.method_sell == "random":
candi = filter_stock(last)
try:
sell = pd.Index(np.random.choice(candi, self.n_drop, replace=False) if len(last) else [])
except ValueError:
sell = candi
else:
raise NotImplementedError(f"This type of input is not supported")
buy = today[: len(sell) + self.topk - len(last)]
for code in current_stock_list:
if not self.trade_exchange.is_stock_tradable(
stock_id=code,
start_time=trade_start_time,
end_time=trade_end_time,
direction=None if self.forbid_all_trade_at_limit else OrderDir.SELL,
):
continue
if code in sell:
time_per_step = self.trade_calendar.get_freq()
if current_temp.get_stock_count(code, bar=time_per_step) < self.hold_thresh:
continue
sell_amount = current_temp.get_stock_amount(code=code)
sell_order = Order(
stock_id=code,
amount=sell_amount,
start_time=trade_start_time,
end_time=trade_end_time,
direction=Order.SELL,
)
if self.trade_exchange.check_order(sell_order):
sell_order_list.append(sell_order)
trade_val, trade_cost, trade_price = self.trade_exchange.deal_order(
sell_order, position=current_temp
)
cash += trade_val - trade_cost
if len(buy) == 0:
return TradeDecisionWO(sell_order_list, self)
# ---- fractional-Kelly sizing --------------------------------------
# equal-weight notional (reference baseline)
eq_notional = cash * self.risk_degree / len(buy)
buy_scores = pred_score.reindex(buy).astype(float)
lo, hi = buy_scores.min(), buy_scores.max()
if hi == lo:
w = pd.Series(1.0, index=buy_scores.index)
else:
w = (buy_scores - lo) / (hi - lo) # [0,1] edge magnitude
w = w.clip(lower=0.0)
w_max = w.max()
w = w / w_max if w_max > 0 else w # max == 1.0
for code in buy:
if not self.trade_exchange.is_stock_tradable(
stock_id=code,
start_time=trade_start_time,
end_time=trade_end_time,
direction=None if self.forbid_all_trade_at_limit else OrderDir.BUY,
):
continue
buy_price = self.trade_exchange.get_deal_price(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time, direction=OrderDir.BUY
)
notional = eq_notional * min(self.cap_frac, float(w.get(code, 0.0)))
buy_amount = notional / buy_price
factor = self.trade_exchange.get_factor(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time
)
buy_amount = self.trade_exchange.round_amount_by_trade_unit(buy_amount, factor)
buy_order = Order(
stock_id=code,
amount=buy_amount,
start_time=trade_start_time,
end_time=trade_end_time,
direction=Order.BUY,
)
buy_order_list.append(buy_order)
return TradeDecisionWO(sell_order_list + buy_order_list, self)
@@ -1,215 +1,231 @@
"""Regime-gate TopkDropout strategy. """HMM-regime overlay TopkDropout strategy.
Subclass of ``qlib.contrib.strategy.signal_strategy.TopkDropoutStrategy`` that Regime-gate overlay on ``qlib.contrib.strategy.signal_strategy.TopkDropoutStrategy``:
holds the book (issues NO orders) while a regime detector says the market is in selection and sizing are identical to the reference, but a name is only BOUGHT
an unfavorable state. When the gate is open it behaves exactly like the (entry gate) when its per-symbol HMM regime posterior ``sp_hmm_p_regime1`` on
reference TopkDropoutStrategy. the signal date is >= ``regime_threshold``; otherwise it is held in cash instead
of being opened.
Three detector types are supported (all causal — no lookahead): The regime posterior is read from the lake feature provider on the fly via
``qlib.data.D.features`` (field ``$sp_hmm_p_regime1``) for the signal window, so
no regime column needs to enter the model's ``feature_fields`` — the gate is a
pure overlay (book ch.01: regime flags regressed as model features, survived
only as an overlay). The HMM itself was fit with ``fit_end=<train end>`` when
the lake features were backfilled, so there is no lookahead.
* ``dispersion``: cross-sectional standard deviation of 22-day rolling returns Names already held are NOT force-sold when the regime turns unfavourable
across the universe. Gate closes when CS dispersion < threshold (low (entry gate only, matching the queue-10 design).
dispersion means the spread between winners and losers is too narrow for
TopkDropout to exploit).
* ``vol``: cross-sectional mean of 22-day rolling realized volatility. Gate
closes when avg vol is outside a band ``[vol_low, vol_high]`` (strategy
needs moderate vol — too calm or too turbulent both hurt).
* ``hmm``: pre-computed HMM posterior for regime 1 (``sp_hmm_p_regime1``).
Gate closes when posterior < threshold (model is not confident the calm
regime is active).
The gate is provided as a precomputed ``pd.Series`` of booleans indexed by
datetime (True = trade allowed). The companion ``compute_regime_gate``
function builds this series from lake bars; call it once before backtesting
and pass the result as the ``regime_gate`` parameter.
""" """
from __future__ import annotations from __future__ import annotations
from typing import List
import numpy as np
import pandas as pd import pandas as pd
from qlib.backtest.decision import TradeDecisionWO from qlib.backtest import Order
from qlib.backtest.decision import OrderDir, TradeDecisionWO
from qlib.contrib.strategy.signal_strategy import TopkDropoutStrategy from qlib.contrib.strategy.signal_strategy import TopkDropoutStrategy
__all__ = ["RegimeGateTopkDropoutStrategy", "compute_regime_gate"] try:
from qlib.data import D
except ImportError: # pragma: no cover - qlib always present in this stack
D = None
__all__ = ["RegimeGateDropoutStrategy"]
DEFAULT_REGIME_THRESHOLD = 0.5
REGIME_FIELD = "$sp_hmm_p_regime1"
class RegimeGateTopkDropoutStrategy(TopkDropoutStrategy): class RegimeGateDropoutStrategy(TopkDropoutStrategy):
"""TopkDropout with a regime-gate circuit breaker. """TopkDropout with an HMM-regime entry gate on buy candidates.
Parameters Parameters
---------- ----------
topk, n_drop, method_sell, method_buy, hold_thresh, only_tradable, topk, n_drop, method_sell, method_buy, hold_thresh, only_tradable,
forbid_all_trade_at_limit : same as ``TopkDropoutStrategy``. forbid_all_trade_at_limit : same as ``TopkDropoutStrategy``.
regime_gate : pd.Series — precomputed per-date gate (bool indexed by regime_threshold : minimum ``sp_hmm_p_regime1`` posterior required to open a
datetime). True = trade allowed, False = no orders. Missing dates new position (default 0.5).
default to open (trade allowed).
""" """
def __init__(self, *, regime_gate=None, **kwargs): def __init__(self, *, topk, n_drop, regime_threshold: float = DEFAULT_REGIME_THRESHOLD, **kwargs):
super().__init__(**kwargs) super().__init__(topk=topk, n_drop=n_drop, **kwargs)
self._regime_gate = regime_gate self.regime_threshold = regime_threshold
def _gate_open(self, trade_start_time) -> bool: def _regime_for(self, codes, pred_start, pred_end) -> pd.Series:
if self._regime_gate is None: """Return {code: sp_hmm_p_regime1} for the signal window (last day)."""
return True if D is None:
ts = pd.Timestamp(trade_start_time) return pd.Series(dtype=float)
known = self._regime_gate[self._regime_gate.index <= ts] try:
if len(known): df = D.features(list(codes), [REGIME_FIELD], start_time=pred_start, end_time=pred_end, freq="day")
return bool(known.iloc[-1]) except Exception: # noqa: BLE001 - a regime read failure should gate open, not crash
return True # default open if no history yet return pd.Series(dtype=float)
if df is None or len(df) == 0:
return pd.Series(dtype=float)
# df index is MultiIndex (datetime, instrument); take the last day's values
df = df.reset_index()
ts_col = "datetime" if "datetime" in df.columns else df.columns[0]
sym_col = "instrument" if "instrument" in df.columns else df.columns[1]
last_ts = df[ts_col].max()
last = df[df[ts_col] == last_ts]
out = {}
for _, row in last.iterrows():
sym = str(row[sym_col]).split("/")[-1].upper()
val = row.iloc[-1]
out[sym] = float(val) if val == val else np.nan
return pd.Series(out)
def generate_trade_decision(self, execute_result=None): def generate_trade_decision(self, execute_result=None):
import copy
trade_step = self.trade_calendar.get_trade_step() trade_step = self.trade_calendar.get_trade_step()
trade_start_time, _ = self.trade_calendar.get_step_time(trade_step) trade_start_time, trade_end_time = self.trade_calendar.get_step_time(trade_step)
if not self._gate_open(trade_start_time): pred_start_time, pred_end_time = self.trade_calendar.get_step_time(trade_step, shift=1)
pred_score = self.signal.get_signal(start_time=pred_start_time, end_time=pred_end_time)
if isinstance(pred_score, pd.DataFrame):
pred_score = pred_score.iloc[:, 0]
if pred_score is None:
return TradeDecisionWO([], self) return TradeDecisionWO([], self)
return super().generate_trade_decision(execute_result)
if self.only_tradable:
# --------------------------------------------------------------------------- def get_first_n(li, n, reverse=False):
# Precomputation helper cur_n = 0
# --------------------------------------------------------------------------- res = []
for si in reversed(li) if reverse else li:
def compute_regime_gate( if self.trade_exchange.is_stock_tradable(
detector: str, stock_id=si, start_time=trade_start_time, end_time=trade_end_time
threshold: float = 0.0, ):
*, res.append(si)
lake_root: str = "", cur_n += 1
market: str = "US", if cur_n >= n:
start: str = "2015-01-03",
end: str = "2026-08-19",
vol_low: float = 0.0,
vol_high: float = 999.0,
hmm_field: str = "sp_hmm_p_regime1",
) -> pd.Series:
"""Build a per-date regime gate series from lake bars.
Parameters
----------
detector : str — ``"dispersion"``, ``"vol"``, or ``"hmm"``.
threshold : float — for ``dispersion``: min CS dispersion to allow trading.
For ``hmm``: min HMM posterior to allow trading.
Ignored for ``vol`` (uses ``vol_low``/``vol_high`` band instead).
lake_root, market : str — lake location.
start, end : str — date window.
vol_low, vol_high : float — annualized vol band for the ``vol`` detector.
hmm_field : str — HMM feature column name for the ``hmm`` detector.
Returns
-------
pd.Series — bool, indexed by datetime. True = trade allowed.
"""
from tac_qlib.data.config import LakeConfig, resolve_lake_root
cfg = LakeConfig(resolve_lake_root(lake_root or None), market)
symbols = _universe_symbols(cfg)
close_df, vol_df = _load_daily_bars(symbols, cfg, start, end)
if close_df.empty:
return pd.Series(dtype=bool)
if detector == "dispersion":
return _dispersion_gate(close_df, threshold)
elif detector == "vol":
return _vol_gate(close_df, vol_low, vol_high)
elif detector == "hmm":
return _hmm_gate(cfg, symbols, threshold, start, end, hmm_field)
else:
raise ValueError(f"Unknown detector: {detector!r}")
def _universe_symbols(cfg) -> list:
"""Read symbols from the lake symbols.parquet."""
import pathlib
sp = cfg.lake_root / "symbols.parquet"
if sp.exists():
df = pd.read_parquet(sp)
col = "symbol" if "symbol" in df.columns else df.columns[0]
return sorted(df[col].astype(str).str.upper().tolist())
return []
def _load_daily_bars(symbols, cfg, start, end):
"""Load daily close prices for all symbols into a wide DataFrame."""
closes = {}
vols = {}
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()
df = df.loc[start:end]
if len(df) < 22:
continue
closes[sym] = df["c"]
if "v" in df.columns:
vols[sym] = df["v"]
close_df = pd.DataFrame(closes)
vol_df = pd.DataFrame(vols) if vols else None
return close_df, vol_df
def _dispersion_gate(close_df, threshold):
"""Cross-sectional dispersion of 22-day rolling returns."""
if close_df.empty or close_df.shape[1] < 2:
return pd.Series(dtype=bool)
ret = close_df.pct_change(22)
cs_disp = ret.std(axis=1)
gate = cs_disp >= threshold
gate.iloc[:22] = True # warmup: allow trading
return gate
def _vol_gate(close_df, vol_low, vol_high):
"""Cross-sectional mean of 22-day rolling realized vol."""
if close_df.empty or close_df.shape[1] < 2:
return pd.Series(dtype=bool)
import numpy as np
log_ret = np.log(close_df / close_df.shift(1))
rv22 = log_ret.rolling(22).std() * (252 ** 0.5)
cs_mean_vol = rv22.mean(axis=1)
gate = (cs_mean_vol >= vol_low) & (cs_mean_vol <= vol_high)
gate.iloc[:22] = True # warmup
return gate
def _hmm_gate(cfg, symbols, threshold, start, end, hmm_field):
"""HMM regime posterior gate from persisted SP features."""
feat_root = cfg.lake_root / "features"
all_posteriors = {}
for sym in symbols:
# check both ta and sp family paths
for family in ("sp", "ta"):
p = feat_root / f"market=US" / f"timeframe=1d" / f"family={family}" / f"symbol={sym}.parquet"
if not p.exists():
continue
try:
df = pd.read_parquet(p)
except Exception:
continue
if hmm_field not in df.columns:
continue
tcol = df["t"] if "t" in df.columns else df["date"]
ts = pd.to_datetime(tcol)
s = pd.Series(df[hmm_field].values, index=ts, name=sym)
s = s.loc[start:end].dropna()
if len(s) > 0:
all_posteriors[sym] = s
break break
if not all_posteriors: return res[::-1] if reverse else res
# no HMM features found — default open
idx = pd.date_range(start, end, freq="B") def get_last_n(li, n):
return pd.Series(True, index=idx) return get_first_n(li, n, reverse=True)
post_df = pd.DataFrame(all_posteriors)
cs_mean = post_df.mean(axis=1) def filter_stock(li):
gate = cs_mean >= threshold return [
return gate si
for si in li
if self.trade_exchange.is_stock_tradable(
stock_id=si, start_time=trade_start_time, end_time=trade_end_time
)
]
else:
def get_first_n(li, n):
return list(li)[:n]
def get_last_n(li, n):
return list(li)[-n:]
def filter_stock(li):
return li
current_temp: "object" = copy.deepcopy(self.trade_position)
sell_order_list: List[Order] = []
buy_order_list: List[Order] = []
cash = current_temp.get_cash()
current_stock_list = current_temp.get_stock_list()
last = pred_score.reindex(current_stock_list).sort_values(ascending=False).index
if self.method_buy == "top":
today = get_first_n(
pred_score[~pred_score.index.isin(last)].sort_values(ascending=False).index,
self.n_drop + self.topk - len(last),
)
elif self.method_buy == "random":
topk_candi = get_first_n(pred_score.sort_values(ascending=False).index, self.topk)
candi = list(filter(lambda x: x not in last, topk_candi))
n = self.n_drop + self.topk - len(last)
try:
today = np.random.choice(candi, n, replace=False)
except ValueError:
today = candi
else:
raise NotImplementedError(f"This type of input is not supported")
comb = pred_score.reindex(last.union(pd.Index(today))).sort_values(ascending=False).index
if self.method_sell == "bottom":
sell = last[last.isin(get_last_n(comb, self.n_drop))]
elif self.method_sell == "random":
candi = filter_stock(last)
try:
sell = pd.Index(np.random.choice(candi, self.n_drop, replace=False) if len(last) else [])
except ValueError:
sell = candi
else:
raise NotImplementedError(f"This type of input is not supported")
buy = today[: len(sell) + self.topk - len(last)]
# ---- regime gate -----------------------------------------------------
if buy:
regime = self._regime_for(buy, pred_start_time, pred_end_time)
gated = [c for c in buy if regime.get(c, np.nan) >= self.regime_threshold]
else:
gated = []
for code in current_stock_list:
if not self.trade_exchange.is_stock_tradable(
stock_id=code,
start_time=trade_start_time,
end_time=trade_end_time,
direction=None if self.forbid_all_trade_at_limit else OrderDir.SELL,
):
continue
if code in sell:
time_per_step = self.trade_calendar.get_freq()
if current_temp.get_stock_count(code, bar=time_per_step) < self.hold_thresh:
continue
sell_amount = current_temp.get_stock_amount(code=code)
sell_order = Order(
stock_id=code,
amount=sell_amount,
start_time=trade_start_time,
end_time=trade_end_time,
direction=Order.SELL,
)
if self.trade_exchange.check_order(sell_order):
sell_order_list.append(sell_order)
trade_val, trade_cost, trade_price = self.trade_exchange.deal_order(
sell_order, position=current_temp
)
cash += trade_val - trade_cost
if len(gated) == 0:
return TradeDecisionWO(sell_order_list, self)
value = cash * self.risk_degree / len(gated)
for code in gated:
if not self.trade_exchange.is_stock_tradable(
stock_id=code,
start_time=trade_start_time,
end_time=trade_end_time,
direction=None if self.forbid_all_trade_at_limit else OrderDir.BUY,
):
continue
buy_price = self.trade_exchange.get_deal_price(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time, direction=OrderDir.BUY
)
buy_amount = value / buy_price
factor = self.trade_exchange.get_factor(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time
)
buy_amount = self.trade_exchange.round_amount_by_trade_unit(buy_amount, factor)
buy_order = Order(
stock_id=code,
amount=buy_amount,
start_time=trade_start_time,
end_time=trade_end_time,
direction=Order.BUY,
)
buy_order_list.append(buy_order)
return TradeDecisionWO(sell_order_list + buy_order_list, self)
@@ -0,0 +1,169 @@
"""Market-neutral top/bottom long-short strategy for cross-sectional signals.
Captures the cross-sectional long-short spread net of costs: buys the top-ranked
``topk`` names and shorts the bottom-ranked ``topk`` names, equal-weight per
side, sized to ``risk_degree`` of total value per side. Rebalances daily to the
current rank (dropout-free: the book converges to the latest top/bottom sets).
The long and short legs use equal notional per side (gross exposure ~2x
``risk_degree`` of NAV, i.e. approximately market neutral before transaction
costs). Benchmark neutrality (SPY beta ~ 0) is the secondary sanity metric.
"""
from __future__ import annotations
from typing import List
import copy
import pandas as pd
from qlib.backtest import Order
from qlib.backtest.decision import OrderDir, TradeDecisionWO
from qlib.contrib.strategy.signal_strategy import BaseSignalStrategy
__all__ = ["TopBottomDropoutStrategy"]
DEFAULT_SHORT_LEG = True
DEFAULT_REBALANCE_DAILY = True
class TopBottomDropoutStrategy(BaseSignalStrategy):
"""Long top-k / short bottom-k equal-weight market-neutral book.
Parameters
----------
topk : number of names on each side (long top-k and short bottom-k).
short_leg : whether to open the short side (if False, long-only topk).
rebalance_daily : if True rebalance to current rank every day; else keep
positions and only refresh on score changes (dropout-style).
risk_degree : fraction of total value deployed per side.
"""
def __init__(
self,
*,
topk: int = 10,
short_leg: bool = DEFAULT_SHORT_LEG,
rebalance_daily: bool = DEFAULT_REBALANCE_DAILY,
**kwargs,
):
super().__init__(**kwargs)
self.topk = topk
self.short_leg = short_leg
self.rebalance_daily = rebalance_daily
self._prev_longs = set()
self._prev_shorts = set()
def generate_trade_decision(self, execute_result=None):
trade_step = self.trade_calendar.get_trade_step()
trade_start_time, trade_end_time = self.trade_calendar.get_step_time(trade_step)
pred_start_time, pred_end_time = self.trade_calendar.get_step_time(trade_step, shift=1)
pred_score = self.signal.get_signal(start_time=pred_start_time, end_time=pred_end_time)
if isinstance(pred_score, pd.DataFrame):
pred_score = pred_score.iloc[:, 0]
if pred_score is None or len(pred_score) == 0:
return TradeDecisionWO([], self)
# rank all names; topk longs and topk shorts
ranked = pred_score.sort_values(ascending=False)
longs = list(ranked.index[: self.topk])
shorts = list(ranked.index[-self.topk :]) if self.short_leg else []
current_temp: "object" = copy.deepcopy(self.trade_position)
current_codes = set(current_temp.get_stock_list())
holdings = {c: current_temp for c in current_codes if abs(current_temp.get_stock_amount(c)) > 1e-6}
sell_orders: List[Order] = []
buy_orders: List[Order] = []
def _tradable(code, direction):
try:
return self.trade_exchange.is_stock_tradable(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time, direction=direction
)
except TypeError:
return self.trade_exchange.is_stock_tradable(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time
)
# determine target set (long/short)
target_longs = set(longs)
target_shorts = set(shorts)
# close positions not in the target book
for code in list(holdings):
if code in target_longs or code in target_shorts:
continue
amt = abs(current_temp.get_stock_amount(code))
o = Order(
stock_id=code,
amount=amt,
start_time=trade_start_time,
end_time=trade_end_time,
direction=Order.SELL if code in target_longs else Order.SELL,
)
if self.trade_exchange.check_order(o):
sell_orders.append(o)
self.trade_exchange.deal_order(o, position=current_temp)
# equal-weight notional per side
total_value = current_temp.get_cash()
for code, pos in holdings.items():
if code in target_longs or code in target_shorts:
mark = self.trade_exchange.get_deal_price(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time, direction=Order.SELL
)
if mark is not None and mark == mark:
total_value += abs(current_temp.get_stock_amount(code)) * mark
side_notional = total_value * self.risk_degree / max(1, self.topk)
for code in longs:
if code in holdings and abs(current_temp.get_stock_amount(code)) > 1e-6:
continue
px = self.trade_exchange.get_deal_price(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time, direction=Order.BUY
)
if px is None or px != px or px <= 0:
continue
amount = side_notional / px
factor = self.trade_exchange.get_factor(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time
)
amount = self.trade_exchange.round_amount_by_trade_unit(amount, factor)
o = Order(
stock_id=code,
amount=amount,
start_time=trade_start_time,
end_time=trade_end_time,
direction=Order.BUY,
)
if self.trade_exchange.check_order(o):
buy_orders.append(o)
if self.short_leg:
for code in shorts:
if code in holdings and abs(current_temp.get_stock_amount(code)) > 1e-6:
continue
px = self.trade_exchange.get_deal_price(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time, direction=Order.SELL
)
if px is None or px != px or px <= 0:
continue
amount = side_notional / px
factor = self.trade_exchange.get_factor(
stock_id=code, start_time=trade_start_time, end_time=trade_end_time
)
amount = self.trade_exchange.round_amount_by_trade_unit(amount, factor)
o = Order(
stock_id=code,
amount=amount,
start_time=trade_start_time,
end_time=trade_end_time,
direction=Order.SELL,
)
if self.trade_exchange.check_order(o):
sell_orders.append(o)
return TradeDecisionWO(sell_orders + buy_orders, self)
+35
View File
@@ -0,0 +1,35 @@
# Q08 — Risk-limit A/B re-validation (trace 40)
**Status:** DONE (verdict: REFUTED as an IR edge; safety-net value retained)
## Input
- Reference signal: exp-26 pred, run `21afc6afdb674a399b59dd76c97628ce` (mlflow exp 25)
- Window: 2026-01-04 → 2026-08-10, Topk10 n_drop1, SPY benchmark, $1M, 5/15bp/$5
- Tool: `rd_risk_calibrate` (A/B + sensitivity grid). Full JSON: `risk_calibration.json`
## Candidate spec (round-3 live spec)
`{"liquidity_floor_adv": 5000000, "size_cap_pct": 0.12, "concentration_cap_pct": 0.95, "drawdown_pause_pct": 0.10}`
## Results (net, with cost)
| Config | IR | Ann. return | Max DD |
|---|---|---|---|
| baseline (no limits) | 1.5804 | +27.50% | −6.91% |
| **candidate (5M floor + caps)** | **1.5121** | +2.20% | **−0.65%** |
| liquidity $10M | 1.5457 | +2.25% | −0.64% |
## Findings
- **Floor binds, not a no-op**: $5M liquidity floor dropped 8 symbols —
`DBA, DBC, ESPO, FDN, REM, TAN, UNG, XAR`.
- **No IR edge from the gate**: candidate IR (1.512) is BELOW baseline (1.580).
The exp-18 direction (floor IR 0.81→0.98) does NOT reproduce on the clean-lake
reference signal.
- **Drawdown cut is pure defunding**: size_cap 0.12 × concentration 0.95 fold
the effective risk_degree to ~0.0095 → ~$9.5k deployed of $1M (~100x less).
Sensitivity grid shows both caps are no-ops (conc 20–50% identical,
size_cap 5–20% identical); only the liquidity floor moves returns, marginally.
- **Conclusion**: keep the live spec as a safety net; there is no risk-limit
gate IR edge to harvest when the signal is the bottleneck (exp-20 pattern).
## Artifacts on this branch
- `evidence/q08-risklimit/risk_calibration.json` — full calibration dump
- `queue/designs/q08_risk_limit_ab.md` — the pre-registered design doc
@@ -0,0 +1,401 @@
{
"rows": [
{
"label": "baseline (no limits)",
"mean": 0.001155,
"std": 0.011279,
"annualized_return": 0.274989,
"information_ratio": 1.580427,
"max_drawdown": -0.069145
},
{
"label": "liquidity $10,000,000",
"mean": 9.4e-05,
"std": 0.000942,
"annualized_return": 0.022464,
"information_ratio": 1.545736,
"max_drawdown": -0.006389
},
{
"label": "conc 20%",
"mean": 0.000115,
"std": 0.001168,
"annualized_return": 0.027285,
"information_ratio": 1.513718,
"max_drawdown": -0.008104
},
{
"label": "conc 30%",
"mean": 0.000115,
"std": 0.001168,
"annualized_return": 0.027285,
"information_ratio": 1.513718,
"max_drawdown": -0.008104
},
{
"label": "conc 40%",
"mean": 0.000115,
"std": 0.001168,
"annualized_return": 0.027285,
"information_ratio": 1.513718,
"max_drawdown": -0.008104
},
{
"label": "conc 50%",
"mean": 0.000115,
"std": 0.001168,
"annualized_return": 0.027285,
"information_ratio": 1.513718,
"max_drawdown": -0.008104
},
{
"label": "candidate {\"liquidity_floor_adv\": 5000000.0, \"size_cap_pct\": 0.12, \"concentration_cap_pct\": 0.95, \"drawdown_pause_pct\": 0.1}",
"mean": 9.2e-05,
"std": 0.000943,
"annualized_return": 0.021991,
"information_ratio": 1.512051,
"max_drawdown": -0.00653
},
{
"label": "size_cap 5%",
"mean": 9.2e-05,
"std": 0.000943,
"annualized_return": 0.021991,
"information_ratio": 1.512051,
"max_drawdown": -0.00653
},
{
"label": "size_cap 10%",
"mean": 9.2e-05,
"std": 0.000943,
"annualized_return": 0.021991,
"information_ratio": 1.512051,
"max_drawdown": -0.00653
},
{
"label": "size_cap 15%",
"mean": 9.2e-05,
"std": 0.000943,
"annualized_return": 0.021991,
"information_ratio": 1.512051,
"max_drawdown": -0.00653
},
{
"label": "size_cap 20%",
"mean": 9.2e-05,
"std": 0.000943,
"annualized_return": 0.021991,
"information_ratio": 1.512051,
"max_drawdown": -0.00653
},
{
"label": "liquidity $5,000,000",
"mean": 9.2e-05,
"std": 0.000943,
"annualized_return": 0.021991,
"information_ratio": 1.512051,
"max_drawdown": -0.00653
},
{
"label": "liquidity $1,000,000",
"mean": 9.1e-05,
"std": 0.000929,
"annualized_return": 0.021625,
"information_ratio": 1.508748,
"max_drawdown": -0.006376
},
{
"label": "liquidity $2,500,000",
"mean": 7.1e-05,
"std": 0.000918,
"annualized_return": 0.017,
"information_ratio": 1.199721,
"max_drawdown": -0.007158
}
],
"runs": {
"baseline": {
"risk": {
"mean": 0.0011554172081987572,
"std": 0.01127853762493476,
"annualized_return": 0.27498929555130425,
"information_ratio": 1.5804272791471323,
"max_drawdown": -0.06914515336341577
},
"applied": {}
},
"candidate": {
"risk": {
"mean": 9.239707947451976e-05,
"std": 0.0009427144352738658,
"annualized_return": 0.0219905049149357,
"information_ratio": 1.5120514373488407,
"max_drawdown": -0.006530482262119444
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"size_cap 5%": {
"risk": {
"mean": 9.239707947451976e-05,
"std": 0.0009427144352738658,
"annualized_return": 0.0219905049149357,
"information_ratio": 1.5120514373488407,
"max_drawdown": -0.006530482262119444
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"size_cap 10%": {
"risk": {
"mean": 9.239707947451976e-05,
"std": 0.0009427144352738658,
"annualized_return": 0.0219905049149357,
"information_ratio": 1.5120514373488407,
"max_drawdown": -0.006530482262119444
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"size_cap 15%": {
"risk": {
"mean": 9.239707947451976e-05,
"std": 0.0009427144352738658,
"annualized_return": 0.0219905049149357,
"information_ratio": 1.5120514373488407,
"max_drawdown": -0.006530482262119444
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"size_cap 20%": {
"risk": {
"mean": 9.239707947451976e-05,
"std": 0.0009427144352738658,
"annualized_return": 0.0219905049149357,
"information_ratio": 1.5120514373488407,
"max_drawdown": -0.006530482262119444
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"conc 20%": {
"risk": {
"mean": 0.00011464156491316718,
"std": 0.0011683839517000441,
"annualized_return": 0.027284692449333788,
"information_ratio": 1.5137180903503433,
"max_drawdown": -0.008103887185240407
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"conc 30%": {
"risk": {
"mean": 0.00011464156491316718,
"std": 0.0011683839517000441,
"annualized_return": 0.027284692449333788,
"information_ratio": 1.5137180903503433,
"max_drawdown": -0.008103887185240407
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"conc 40%": {
"risk": {
"mean": 0.00011464156491316718,
"std": 0.0011683839517000441,
"annualized_return": 0.027284692449333788,
"information_ratio": 1.5137180903503433,
"max_drawdown": -0.008103887185240407
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"conc 50%": {
"risk": {
"mean": 0.00011464156491316718,
"std": 0.0011683839517000441,
"annualized_return": 0.027284692449333788,
"information_ratio": 1.5137180903503433,
"max_drawdown": -0.008103887185240407
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"liquidity $1,000,000": {
"risk": {
"mean": 9.086210454881382e-05,
"std": 0.0009290831160004576,
"annualized_return": 0.021625180882617688,
"information_ratio": 1.508747982736451,
"max_drawdown": -0.006376134679664126
},
"applied": {
"dropped_liquidity": [
"ESPO"
]
}
},
"liquidity $2,500,000": {
"risk": {
"mean": 7.142665167642606e-05,
"std": 0.0009184775632266332,
"annualized_return": 0.016999543098989402,
"information_ratio": 1.1997208834083914,
"max_drawdown": -0.0071582979845040825
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"REM",
"XAR"
]
}
},
"liquidity $5,000,000": {
"risk": {
"mean": 9.239707947451976e-05,
"std": 0.0009427144352738658,
"annualized_return": 0.0219905049149357,
"information_ratio": 1.5120514373488407,
"max_drawdown": -0.006530482262119444
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"REM",
"TAN",
"UNG",
"XAR"
]
}
},
"liquidity $10,000,000": {
"risk": {
"mean": 9.438545151345752e-05,
"std": 0.0009420158170657147,
"annualized_return": 0.02246373746020289,
"information_ratio": 1.5457360696934006,
"max_drawdown": -0.006388809561209335
},
"applied": {
"dropped_liquidity": [
"DBA",
"DBC",
"ESPO",
"FDN",
"ICLN",
"ITA",
"MDY",
"REM",
"SHY",
"TAN",
"UNG",
"XAR"
]
}
}
},
"candidate": {
"liquidity_floor_adv": 5000000.0,
"size_cap_pct": 0.12,
"concentration_cap_pct": 0.95,
"drawdown_pause_pct": 0.1
}
}
+34
View File
@@ -0,0 +1,34 @@
# QUEUE-08 — Risk-limit A/B re-validation: $5M liquidity floor on the exp-26 reference
**Status:** QUEUED · **Priority:** P1 · **Effort:** tool-only (no new code)
## Hypothesis (prove)
The $5M liquidity floor improves net IR and cuts drawdown on the **post-reset**
reference signal (pre-reset exp 18, EVIDENCE#008: net IR 0.81→0.98, cumDD
7.93%→5.44%), while size/concentration caps hurt by cutting deployed capital.
Needs re-validation on the exp-26 lineage because exp 18 is pre-clean-lake and
not comparable (EVIDENCE#009/010). Source: `book/CLAIMS.md` open question +
`book/README.md` `TODO(evidence-needed: reconciliation of exp 18 risk-limit spec
on the post-reset reference signal)`.
## Change vs exp-26 reference (ONE variable)
- Reference: the saved exp-26 prediction (run `21afc6af…`, mlflow exp 25).
- A/B via `rd_risk_calibrate` (runs limit-vs-no-limit A/B + sensitivity grid
over size_cap_pct, concentration_cap_pct, liquidity_floor_adv) and/or
`rd_backtest` with `risk_limits` on the SAME saved `pred.pkl`:
- baseline: no limits (this must reproduce the exp-26 net +2.13% / IR 0.21);
- candidate: `{"liquidity_floor_adv": 5000000, "size_cap_pct": 0.12,
"concentration_cap_pct": 0.95, "drawdown_pause_pct": 0.10}` (round-3 spec).
- Pick the spec (B2 calibration) that keeps live ≈ backtest.
## Acceptance
- Candidate spec: `net_IR > 0.21` AND `net_max_drawdown < 7.69%` vs no-limit on
the same pred. Size/concentration caps expected to REDUCE deployed capital
(record the direction as confirmation of exp 18).
- If the floor is a no-op (gates don't bind at this signal) → report that gates
are no-ops when the signal is the bottleneck (exp 20 pattern) as a PROVEN
clean-lake result.
## Execution prerequisites
- None (uses saved pred + `rd_risk_calibrate`/`rd_backtest`). Trace the A/B as
an experiment; record the spec chosen for the next live round.
@@ -0,0 +1,133 @@
# -----------------------------------------------------------------------------
# ABLATION A (baseline): LightGBM with RankIC early-stopping on the 50-ETF SP-5d
# panel, using ALL 24 sp_* feature columns (ou,hmm,jump,har,trend,hurst,
# signature). Copy of the canonical workflow_lgb_sp5d_rankic.yaml with a
# distinct experiment name so the ablation runs are isolated.
#
# Run:
# rd_run_workflow config_path=tac-qlib/workflows/ablate_baseline_all_sp_fields.yaml \
# experiment_name=tac-rd-rank-ablate
# -----------------------------------------------------------------------------
{%- 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_ou_zscore,sp_ou_half_life,sp_ou_revert,sp_hmm_p_regime1,sp_hmm_state,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-rank-ablate"
task:
model:
class: RankICLGBModel
module_path: tac_qlib.contrib.model.rank_gbdt
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
seed: 42
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: 2015-01-03
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: {}
- class: ProcessInf
kwargs: {}
- class: CSRankNorm
kwargs: {}
- class: ZScoreNorm
kwargs: {}
- class: Fillna
kwargs: {}
segments:
train: [2015-01-03, 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: TopkDropoutStrategy
module_path: qlib.contrib.strategy
kwargs:
signal: "<PRED>"
topk: 10
n_drop: 2
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
@@ -0,0 +1,134 @@
# -----------------------------------------------------------------------------
# ABLATION B (generic-only): same panel/model as the baseline, but feature
# fields restricted to the model-free / generic stochastic-process families
# (jump,har,trend,hurst,signature). Drops the model-specific ou (OU/AR-1
# half-life) and hmm (2-state regime) families to test whether the generic
# families alone dominate the rank dimension.
#
# Run:
# rd_run_workflow config_path=tac-qlib/workflows/ablate_generic_only_sp_fields.yaml \
# experiment_name=tac-rd-rank-ablate
# -----------------------------------------------------------------------------
{%- 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-rank-ablate"
task:
model:
class: RankICLGBModel
module_path: tac_qlib.contrib.model.rank_gbdt
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
seed: 42
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: 2015-01-03
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: {}
- class: ProcessInf
kwargs: {}
- class: CSRankNorm
kwargs: {}
- class: ZScoreNorm
kwargs: {}
- class: Fillna
kwargs: {}
segments:
train: [2015-01-03, 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: TopkDropoutStrategy
module_path: qlib.contrib.strategy
kwargs:
signal: "<PRED>"
topk: 10
n_drop: 2
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
+141
View File
@@ -0,0 +1,141 @@
# -----------------------------------------------------------------------------
# ISOLATION: multi-seed RankIC ensemble, ablate-B generic-only feature set.
#
# Isolates the ensemble effect on the SP-5d rank signal. Same panel, segments,
# history (full backfilled 2016+) and feature set as the exp-9 ablate-B winner
# (generic-only sp_* families: jump,har,trend,hurst,signature), but replaces the
# single RankICLGBModel with a 5-seed RankICEnsembleLGBModel (42,7,2026,99,123)
# that averages per-day predictions.
#
# Differs from exp-15 (tac-rd-rank-ensemble, mlflow exp 15) ONLY by dropping the
# TA subset (rsi_14,roc_10,macd_hist,willr_14,atr_14) and the inter-asset xr_*
# features, so any change vs exp-15 is attributable to the feature set alone,
# and any change vs exp-9 is attributable to the ensemble + full history alone.
#
# Run:
# rd_run_workflow config_path=experiments/workflows/exp12_isolation_ensemble.yaml \
# experiment_name=tac-rd-rank-ensemble-isolated
# -----------------------------------------------------------------------------
{%- 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:///mlruns.db"
default_exp_name: "tac-rd-rank-ensemble-isolated"
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"
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-14
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: {}
- class: ProcessInf
kwargs: {}
- class: CSRankNorm
kwargs: {}
- class: ZScoreNorm
kwargs: {}
- 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: TopkDropoutStrategy
module_path: qlib.contrib.strategy
kwargs:
signal: "<PRED>"
topk: 10
n_drop: 2
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
+141
View File
@@ -0,0 +1,141 @@
# -----------------------------------------------------------------------------
# EXP 18 - Risk-limit control: reference model + TopkDropout baseline (A).
#
# Signal/model identical to the reference (tac-rd-rank-ensemble-isolated,
# run 0cea66d9...): RankICEnsembleLGBModel (parallel, 5 seeds) on the 50-ETF
# SP-5d panel, test 2026-01-04..2026-08-10. This workflow reproduces the
# unconstrained TopkDropout baseline net-of-cost so the risk-limited variant
# (same pred, liquidity/size/concentration caps) can be compared 1:1.
#
# The risk_limits spec itself is applied via rd_backtest / rd_strategy_targets
# (tool-level param, not a YAML key); this run records the unconstrained
# baseline that the limit A/B is measured against.
#
# Run:
# rd_run_workflow config_path=experiments/workflows/exp18-risk-limit/a_baseline.yaml \
# experiment_name=tac-rd-risk-limit
# -----------------------------------------------------------------------------
{%- 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-risk-limit"
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-14
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: {}
- class: ProcessInf
kwargs: {}
- class: CSRankNorm
kwargs: {}
- class: ZScoreNorm
kwargs: {}
- 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: TopkDropoutStrategy
module_path: qlib.contrib.strategy
kwargs:
signal: "<PRED>"
topk: 10
n_drop: 2
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