Compare commits

..
28 changed files with 591 additions and 1730 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()
+17 -20
View File
@@ -1,30 +1,27 @@
# TradeAC custom-qlib-code snapshot (auto-generated)
# parent repo HEAD : ceb1e196e24761a87d1afe1bde00079b79e02693
# parent repo HEAD : f9ef005e9aa05e546c11eef760046648cf5a6334
# tac-qlib/tac_qlib/contrib
# tac-qlib/tac_qlib/data
# per-file hashes (git hash-object):
1b6298c4a5652f2e863cbdc385a1014a570fcd59 tac-qlib/tac_qlib/contrib/__init__.py
861592c63edd6a0853a9cb174b5970435b135fc8 tac-qlib/tac_qlib/contrib/__pycache__/__init__.cpython-312.pyc
b8112569f9b2537c45b6535e1a505a207878d322 tac-qlib/tac_qlib/contrib/__pycache__/__init__.cpython-312.pyc
c76a9f17f680e74eea766eff27f7624359749ed6 tac-qlib/tac_qlib/contrib/data/__init__.py
5c547a2ef92e075e550fe6d01508a2f1d3f536bc tac-qlib/tac_qlib/contrib/data/__pycache__/__init__.cpython-312.pyc
4f656130d167e79dcaaeb7783a121f0b36852374 tac-qlib/tac_qlib/contrib/data/__pycache__/handler.cpython-312.pyc
0dd25ef161c6e0f15eafc84886e7e1381deb38c3 tac-qlib/tac_qlib/contrib/data/handler.py
8d5333ebd2b44165c50cba639ca2d4ac3fc7cfec tac-qlib/tac_qlib/contrib/data/__pycache__/__init__.cpython-312.pyc
18cb37c0354184c49fa2e598396d7df0634cce0f tac-qlib/tac_qlib/contrib/data/__pycache__/handler.cpython-312.pyc
871ff1e163c29261f140c3f53d42a41e6504c779 tac-qlib/tac_qlib/contrib/data/handler.py
b151d139a0dcde87d74b21e7c4b729176ba5c39b tac-qlib/tac_qlib/contrib/model/__init__.py
b1489f2fc0dee85f0a4f90b2e6ad545ed9c8967b tac-qlib/tac_qlib/contrib/model/__pycache__/__init__.cpython-312.pyc
121ef237da1df1b8e21a561c3ad0db200b901339 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
ab958203f33a99d12c7d923b6efb435189231666 tac-qlib/tac_qlib/contrib/model/__pycache__/__init__.cpython-312.pyc
9dc36de7e343073b7d511349ee5aede086c38f94 tac-qlib/tac_qlib/contrib/model/__pycache__/rank_ensemble.cpython-312.pyc
9f9014ddd9bce37490061312d51e8e6fe540fec4 tac-qlib/tac_qlib/contrib/model/__pycache__/rank_gbdt.cpython-312.pyc
d3f051f3a8650c42fedc7b367b966f7c74fb5789 tac-qlib/tac_qlib/contrib/model/rank_ensemble.py
d03e6611338918d4aac5eea4adf26f85a3763652 tac-qlib/tac_qlib/contrib/model/rank_gbdt.py
2c2f167b693f4366a769998e3c9d4804f29e31e0 tac-qlib/tac_qlib/contrib/strategy/__init__.py
c29e45e562256bf786c36f91a971b097467276e9 tac-qlib/tac_qlib/contrib/strategy/__pycache__/__init__.cpython-312.pyc
6dd1c568a2961842793674390d5abffd1a0e71b8 tac-qlib/tac_qlib/contrib/strategy/__pycache__/optimal_stop.cpython-312.pyc
519a1f4c05dbe0ac018ab8b779eb33d53b4dd545 tac-qlib/tac_qlib/contrib/strategy/ic_gate.py
ccfe7d554989aa7f3e5a2128ae663e51b2207149 tac-qlib/tac_qlib/contrib/model/rank_gbdt.py
4afcf9058231111c412925f4c4b84e81d656db87 tac-qlib/tac_qlib/contrib/strategy/__init__.py
74e5ecbbbb20bb71fd5cd083383de4ce88476712 tac-qlib/tac_qlib/contrib/strategy/__pycache__/__init__.cpython-312.pyc
afaf562aeaa12cebc8529cd916153252e7e3c38a tac-qlib/tac_qlib/contrib/strategy/__pycache__/optimal_stop.cpython-312.pyc
79aaad9e39fcc740a773f4f63c512ce1086cfde0 tac-qlib/tac_qlib/contrib/strategy/optimal_stop.py
7bcee5f0b09cfa721440f1354f16f2dd9a112b12 tac-qlib/tac_qlib/contrib/strategy/regime_gate.py
fe60bacdfedd48617863be31f24b7c7daebfac5a tac-qlib/tac_qlib/contrib/strategy/weekly_rebalance.py
92e6e90eb0cd0a25142034560f27adb6b705b1a8 tac-qlib/tac_qlib/data/__init__.py
316bf4aa160cc8d15929ea648be03f4b4999667d tac-qlib/tac_qlib/data/__pycache__/__init__.cpython-312.pyc
554a3f29d181b64effbf49a8161b32e7f93d8d3e tac-qlib/tac_qlib/data/__pycache__/config.cpython-312.pyc
8b47f6d78ac046b6b7b2fb07bd7f3382773ffb73 tac-qlib/tac_qlib/data/__pycache__/providers.cpython-312.pyc
53c9007a928841fd3c3b08450f9a6520ce1ac091 tac-qlib/tac_qlib/data/config.py
8d0644f6f0d1efb94798ed444cc73e63b643459b tac-qlib/tac_qlib/data/providers.py
0ed1ead6c1314a3f25784d453e54a15a8a04baaa tac-qlib/tac_qlib/data/__pycache__/__init__.cpython-312.pyc
9609782800944c45b78bb58eaa7b51ba1b7f8f43 tac-qlib/tac_qlib/data/__pycache__/config.cpython-312.pyc
a85628d71d12cfe5b18b1c884c5d829c89594579 tac-qlib/tac_qlib/data/__pycache__/providers.cpython-312.pyc
686d36f6d101c547491ca866aa143aa542e17518 tac-qlib/tac_qlib/data/config.py
d9f839be30026f337754a3f015425a8efdbe8e2a tac-qlib/tac_qlib/data/providers.py
+4 -22
View File
@@ -64,13 +64,9 @@ def check_transform_proc(proc_l, fit_start_time, fit_end_time):
def get_common_feature_fields(lake_root=None, market="US", timeframe="1d") -> List[str]:
"""Discover feature columns present in *every* feature file of the lake.
"""Discover ta-lib columns present in *every* features parquet file of the lake.
Walks the `family=ta|sp` partition layout (plus any legacy flat files).
TA and SP columns are disjoint by construction, so the common set is
computed per family (columns shared by all symbol files of that family),
then the per-family results are unioned. Returns sorted field names
(without the ``$`` prefix). Empty if no features are persisted.
Returns sorted field names (without the ``$`` prefix). Empty if no features are persisted.
"""
cfg = LakeConfig(lake_root, market)
feat_dir = cfg.features_dir(timeframe)
@@ -78,9 +74,8 @@ def get_common_feature_fields(lake_root=None, market="US", timeframe="1d") -> Li
return []
import pyarrow.parquet as pq
def _family_common(fam_dir: Path) -> set:
common = None
for p in sorted(fam_dir.glob("symbol=*.parquet")):
for p in sorted(feat_dir.glob("symbol=*.parquet")):
try:
cols = set(pq.read_schema(p).names) - set(NON_FEATURE_COLUMNS)
except Exception: # pragma: no cover - skip unreadable files
@@ -88,20 +83,7 @@ def get_common_feature_fields(lake_root=None, market="US", timeframe="1d") -> Li
common = cols if common is None else (common & cols)
if not common:
break
return common or set()
common: set = set()
# family tier: features/market=*/timeframe=*/family=*/symbol=*.parquet
for fam in ("ta", "sp"):
fam_dir = feat_dir / f"family={fam}"
if fam_dir.is_dir():
common |= _family_common(fam_dir)
# legacy flat: features/market=*/timeframe=*/symbol=*.parquet
if (feat_dir / "family=ta").exists() or (feat_dir / "family=sp").exists():
pass # family layout already covered
else:
common |= _family_common(feat_dir)
return sorted(common)
return sorted(common) if common else []
class DropAllNaN(processor_module.Processor):
@@ -53,61 +53,23 @@ from qlib.workflow import R
__all__ = ["RankICLGBModel", "rankic_feval"]
def _group_averaged_rank(values: np.ndarray, gid: np.ndarray, offs: np.ndarray) -> np.ndarray:
"""Averaged (tie-corrected) rank of ``values`` within each group, vectorized.
``gid`` maps each row to its group id; ``offs`` holds the cumulative row
offsets so that group ``i`` occupies rows ``[offs[i], offs[i+1])``. Returns
the same result as ``pandas.Series.rank(method='average')`` applied per
group, but in one pass (``np.lexsort`` is the only non-linear step).
"""
n = len(values)
order = np.lexsort((values, gid))
ord_rank = np.empty(n, dtype=np.float64)
ord_rank[order] = np.arange(n, dtype=np.float64) - offs[gid[order]] + 1.0
sg = gid[order]
sv = values[order]
newblock = np.empty(n, dtype=bool)
newblock[0] = True
newblock[1:] = (sg[1:] != sg[:-1]) | (sv[1:] != sv[:-1])
blockid = np.cumsum(newblock) - 1
block_mean = np.bincount(blockid, weights=ord_rank[order]) / np.bincount(blockid)
out = np.empty(n)
out[order] = block_mean[blockid]
return out
def _per_day_spearman(preds: np.ndarray, labels: np.ndarray, group: np.ndarray) -> float:
"""Mean per-day Spearman rank correlation of preds vs labels.
``group`` holds the number of rows of each trading day (query group), in
order. Days with <3 valid rows or a constant pred/label are skipped.
Vectorized: per-day Spearman == Pearson of the per-day rank transforms,
and the Pearson moments (``sum``, ``sum`` of products/squares) aggregate
over each day with ``np.bincount``. Runs ~10x faster than the per-day
``pd.Series.rank()`` loop that preceded it — this feval is invoked on the
train and valid panels every boosting round, per seed.
"""
if group is None or len(group) == 0:
return 0.0
offs = np.concatenate([[0], np.cumsum(group.astype(int))])
gid = np.repeat(np.arange(len(group)), group.astype(int))
rp = _group_averaged_rank(preds, gid, offs)
rl = _group_averaged_rank(labels, gid, offs)
n_g = group.astype(float)
s_p = np.bincount(gid, weights=rp)
s_l = np.bincount(gid, weights=rl)
s_pl = np.bincount(gid, weights=rp * rl)
s_pp = np.bincount(gid, weights=rp * rp)
s_ll = np.bincount(gid, weights=rl * rl)
cov = n_g * s_pl - s_p * s_l
var_p = n_g * s_pp - s_p ** 2
var_l = n_g * s_ll - s_l ** 2
denom = np.sqrt(var_p * var_l)
valid = (n_g >= 3) & (denom > 0)
corr = np.where(valid, cov / np.where(denom == 0, 1, denom), 0.0)
return float(corr[valid].mean()) if valid.any() else 0.0
vals = []
for i in range(len(group)):
s = slice(offs[i], offs[i + 1])
p, l = preds[s], labels[s]
if len(p) < 3 or np.std(p) == 0 or np.std(l) == 0:
continue
vals.append(np.corrcoef(pd.Series(p).rank(), pd.Series(l).rank())[0, 1])
return float(np.mean(vals)) if vals else 0.0
def rankic_feval(preds, dataset):
@@ -1,11 +1,3 @@
from .ic_gate import ICGateTopkDropoutStrategy # noqa: F401
from .optimal_stop import OptimalStopControl # noqa: F401
from .regime_gate import RegimeGateTopkDropoutStrategy # noqa: F401
from .weekly_rebalance import WeeklyRebalanceDropoutStrategy # noqa: F401
__all__ = [
"ICGateTopkDropoutStrategy",
"OptimalStopControl",
"RegimeGateTopkDropoutStrategy",
"WeeklyRebalanceDropoutStrategy",
]
__all__ = ["OptimalStopControl"]
@@ -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)
@@ -1,215 +0,0 @@
"""Regime-gate TopkDropout strategy.
Subclass of ``qlib.contrib.strategy.signal_strategy.TopkDropoutStrategy`` that
holds the book (issues NO orders) while a regime detector says the market is in
an unfavorable state. When the gate is open it behaves exactly like the
reference TopkDropoutStrategy.
Three detector types are supported (all causal — no lookahead):
* ``dispersion``: cross-sectional standard deviation of 22-day rolling returns
across the universe. Gate closes when CS dispersion < threshold (low
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
import pandas as pd
from qlib.backtest.decision import TradeDecisionWO
from qlib.contrib.strategy.signal_strategy import TopkDropoutStrategy
__all__ = ["RegimeGateTopkDropoutStrategy", "compute_regime_gate"]
class RegimeGateTopkDropoutStrategy(TopkDropoutStrategy):
"""TopkDropout with a regime-gate circuit breaker.
Parameters
----------
topk, n_drop, method_sell, method_buy, hold_thresh, only_tradable,
forbid_all_trade_at_limit : same as ``TopkDropoutStrategy``.
regime_gate : pd.Series — precomputed per-date gate (bool indexed by
datetime). True = trade allowed, False = no orders. Missing dates
default to open (trade allowed).
"""
def __init__(self, *, regime_gate=None, **kwargs):
super().__init__(**kwargs)
self._regime_gate = regime_gate
def _gate_open(self, trade_start_time) -> bool:
if self._regime_gate is None:
return True
ts = pd.Timestamp(trade_start_time)
known = self._regime_gate[self._regime_gate.index <= ts]
if len(known):
return bool(known.iloc[-1])
return True # default open if no history yet
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)
# ---------------------------------------------------------------------------
# Precomputation helper
# ---------------------------------------------------------------------------
def compute_regime_gate(
detector: str,
threshold: float = 0.0,
*,
lake_root: str = "",
market: str = "US",
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
if not all_posteriors:
# no HMM features found — default open
idx = pd.date_range(start, end, freq="B")
return pd.Series(True, index=idx)
post_df = pd.DataFrame(all_posteriors)
cs_mean = post_df.mean(axis=1)
gate = cs_mean >= threshold
return gate
@@ -1,202 +0,0 @@
"""Weekly-rebalance TopkDropout strategy.
Turnover-reduction variant of ``qlib.contrib.strategy.signal_strategy.TopkDropoutStrategy``:
the topk/n_drop selection and sizing are identical to the reference, but the
target book is recomputed only on the first trading day of each ISO week; on the
other days the strategy issues NO orders (holds the book untouched).
The weekly cadence is derived from the qlib trade calendar: a rebalance happens
when the current trade step's date belongs to a different ISO ``(year, week)``
than the previous trade step. ``hold_band_pct`` (default 0) optionally skips
tiny rebalances: when a name's existing position differs from the new target by
less than this fraction, no order is generated for it.
"""
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__ = ["WeeklyRebalanceDropoutStrategy"]
DEFAULT_HOLD_BAND_PCT = 0.0
class WeeklyRebalanceDropoutStrategy(TopkDropoutStrategy):
"""TopkDropout rebalanced once per ISO week; holds otherwise.
Parameters
----------
topk, n_drop, method_sell, method_buy, hold_thresh, only_tradable,
forbid_all_trade_at_limit : same as ``TopkDropoutStrategy``.
hold_band_pct : skip order for a name whose deviation from target weight is
below this fraction of the target (no-trade buffer band).
"""
def __init__(self, *, topk, n_drop, hold_band_pct: float = DEFAULT_HOLD_BAND_PCT, **kwargs):
super().__init__(topk=topk, n_drop=n_drop, **kwargs)
self.hold_band_pct = hold_band_pct
@staticmethod
def _iso_week(ts) -> tuple:
return (ts.year, ts.week)
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)
cur_week = self._iso_week(trade_start_time)
prev_week = getattr(self, "_last_week", None)
self._last_week = cur_week
if prev_week is not None and prev_week == cur_week:
# not the first trading day of this ISO week -> hold
return TradeDecisionWO([], self)
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)
value = cash * self.risk_degree / len(buy)
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
)
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)
+2 -29
View File
@@ -6,11 +6,10 @@ The lake is a hive-partitioned parquet store (see ``tac-engine/skills/tradeac-la
├── market=US/
│ └── timeframe=1d/
│ └── symbol=AAPL.parquet # OHLCV bars: t, date, o, h, l, c, v, n, vw
├── features/ # indicators, wide format, family tier
├── features/ # ta-lib indicators, wide format
│ └── market=US/
│ └── timeframe=1d/
│ ├── family=ta/symbol=AAPL.parquet # t, sma_5, sma_20, rsi_14, ...
│ └── family=sp/symbol=AAPL.parquet # t, sp_ou_*, sp_hmm_*, ...
│ └── symbol=AAPL.parquet # t, sma_5, sma_20, rsi_14, ...
├── calendar.parquet # trading days per market
├── coverage.parquet # per (market,timeframe,symbol) loaded windows
└── symbols.parquet # asset master
@@ -108,34 +107,8 @@ class LakeConfig:
return self.lake_root / "features" / f"market={self.market}" / f"timeframe={timeframe}"
def features_path(self, timeframe: str, symbol: str) -> Path:
# Legacy flat path (no family tier). Prefer `load_features` which
# resolves the family=ta|sp partition layout.
return self.features_dir(timeframe) / f"symbol={str(symbol).upper()}.parquet"
def load_features(self, timeframe: str, symbol: str) -> pd.DataFrame:
"""All feature columns for a symbol, merging the `family=ta` and
`family=sp` partitions by timestamp. Returns an empty frame when no
feature files exist (legacy flat layout falls back transparently)."""
sym = str(symbol).upper()
frames = []
for family in ("ta", "sp"):
p = self.features_dir(timeframe) / f"family={family}" / f"symbol={sym}.parquet"
if p.exists():
frames.append(pd.read_parquet(p))
if not frames:
flat = self.features_dir(timeframe) / f"symbol={sym}.parquet"
if flat.exists():
return pd.read_parquet(flat)
return pd.DataFrame()
if len(frames) == 1:
return frames[0]
merged = frames[0]
for extra in frames[1:]:
merged = merged.merge(extra, on="t", how="outer", suffixes=("", "_dup"))
for c in [c for c in merged.columns if c.endswith("_dup")]:
merged = merged.drop(columns=c)
return merged
def calendar_path(self) -> Path:
return self.lake_root / "calendar.parquet"
+2 -1
View File
@@ -173,7 +173,8 @@ class LakeFeatureProvider(FeatureProvider):
def _load_feature_df(self, instrument: str, timeframe: str) -> pd.DataFrame:
key = (instrument, timeframe)
if key not in self._feature_cache:
self._feature_cache[key] = self.cfg.load_features(timeframe, instrument)
p = self.cfg.features_path(timeframe, instrument)
self._feature_cache[key] = pd.read_parquet(p) if p.exists() else pd.DataFrame()
return self._feature_cache[key]
@staticmethod
@@ -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