Spaces:
Running
Running
Commit ·
9fa1ee6
1
Parent(s): 2340a14
C20: sector-conditional model — pool semis/financials/ETFs intra-sector
Browse filesWalk-forward training augmented with date-aligned peer rows within sector.
Electronics and telecom isolated (too heterogeneous to benefit from pooling).
5-stock: dir=45.5% up_prec=55.5% PASS
12-stock: dir=44.9% up_prec=54.2% PASS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docs/c20_result.json +83 -0
- docs/c20_result_12stock.json +171 -0
- docs/improvements_log.md +5 -3
- scripts/backtest_c20.py +238 -0
docs/c20_result.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"experiment": "C20",
|
| 3 |
+
"description": "Sector-conditional model: pool training within sector",
|
| 4 |
+
"stocks": [
|
| 5 |
+
"2330",
|
| 6 |
+
"0050",
|
| 7 |
+
"2317",
|
| 8 |
+
"2454",
|
| 9 |
+
"2881"
|
| 10 |
+
],
|
| 11 |
+
"aggregate": {
|
| 12 |
+
"dir_accuracy": 45.5,
|
| 13 |
+
"up_precision": 55.5
|
| 14 |
+
},
|
| 15 |
+
"per_stock": {
|
| 16 |
+
"2330": {
|
| 17 |
+
"accuracy": 34.2,
|
| 18 |
+
"dir_accuracy": 37.4,
|
| 19 |
+
"up_precision": 54.1,
|
| 20 |
+
"dn_precision": 26.5,
|
| 21 |
+
"n_signals": 187,
|
| 22 |
+
"n_predictions": 231,
|
| 23 |
+
"sector": "semis",
|
| 24 |
+
"peers": [
|
| 25 |
+
"2454",
|
| 26 |
+
"2303"
|
| 27 |
+
]
|
| 28 |
+
},
|
| 29 |
+
"0050": {
|
| 30 |
+
"accuracy": 45.0,
|
| 31 |
+
"dir_accuracy": 52.7,
|
| 32 |
+
"up_precision": 62.7,
|
| 33 |
+
"dn_precision": 31.0,
|
| 34 |
+
"n_signals": 184,
|
| 35 |
+
"n_predictions": 231,
|
| 36 |
+
"sector": "etfs",
|
| 37 |
+
"peers": [
|
| 38 |
+
"0056"
|
| 39 |
+
]
|
| 40 |
+
},
|
| 41 |
+
"2317": {
|
| 42 |
+
"accuracy": 40.7,
|
| 43 |
+
"dir_accuracy": 46.6,
|
| 44 |
+
"up_precision": 52.5,
|
| 45 |
+
"dn_precision": 37.3,
|
| 46 |
+
"n_signals": 193,
|
| 47 |
+
"n_predictions": 231,
|
| 48 |
+
"sector": "electronics",
|
| 49 |
+
"peers": []
|
| 50 |
+
},
|
| 51 |
+
"2454": {
|
| 52 |
+
"accuracy": 38.1,
|
| 53 |
+
"dir_accuracy": 47.0,
|
| 54 |
+
"up_precision": 45.1,
|
| 55 |
+
"dn_precision": 49.3,
|
| 56 |
+
"n_signals": 149,
|
| 57 |
+
"n_predictions": 231,
|
| 58 |
+
"sector": "semis",
|
| 59 |
+
"peers": [
|
| 60 |
+
"2330",
|
| 61 |
+
"2303"
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
"2881": {
|
| 65 |
+
"accuracy": 39.4,
|
| 66 |
+
"dir_accuracy": 43.9,
|
| 67 |
+
"up_precision": 63.2,
|
| 68 |
+
"dn_precision": 33.8,
|
| 69 |
+
"n_signals": 198,
|
| 70 |
+
"n_predictions": 231,
|
| 71 |
+
"sector": "financials",
|
| 72 |
+
"peers": [
|
| 73 |
+
"2882",
|
| 74 |
+
"2886"
|
| 75 |
+
]
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
"passed": true,
|
| 79 |
+
"pass_gate": {
|
| 80 |
+
"dir_accuracy": 43.5,
|
| 81 |
+
"up_precision": 54.0
|
| 82 |
+
}
|
| 83 |
+
}
|
docs/c20_result_12stock.json
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"experiment": "C20",
|
| 3 |
+
"description": "Sector-conditional model: pool training within sector",
|
| 4 |
+
"stocks": [
|
| 5 |
+
"2330",
|
| 6 |
+
"2454",
|
| 7 |
+
"2303",
|
| 8 |
+
"2317",
|
| 9 |
+
"2382",
|
| 10 |
+
"2308",
|
| 11 |
+
"2881",
|
| 12 |
+
"2882",
|
| 13 |
+
"2886",
|
| 14 |
+
"0050",
|
| 15 |
+
"0056",
|
| 16 |
+
"2412"
|
| 17 |
+
],
|
| 18 |
+
"aggregate": {
|
| 19 |
+
"dir_accuracy": 44.9,
|
| 20 |
+
"up_precision": 54.2
|
| 21 |
+
},
|
| 22 |
+
"per_stock": {
|
| 23 |
+
"2330": {
|
| 24 |
+
"accuracy": 34.2,
|
| 25 |
+
"dir_accuracy": 37.4,
|
| 26 |
+
"up_precision": 54.1,
|
| 27 |
+
"dn_precision": 26.5,
|
| 28 |
+
"n_signals": 187,
|
| 29 |
+
"n_predictions": 231,
|
| 30 |
+
"sector": "semis",
|
| 31 |
+
"peers": [
|
| 32 |
+
"2454",
|
| 33 |
+
"2303"
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
"2454": {
|
| 37 |
+
"accuracy": 38.1,
|
| 38 |
+
"dir_accuracy": 47.0,
|
| 39 |
+
"up_precision": 45.1,
|
| 40 |
+
"dn_precision": 49.3,
|
| 41 |
+
"n_signals": 149,
|
| 42 |
+
"n_predictions": 231,
|
| 43 |
+
"sector": "semis",
|
| 44 |
+
"peers": [
|
| 45 |
+
"2330",
|
| 46 |
+
"2303"
|
| 47 |
+
]
|
| 48 |
+
},
|
| 49 |
+
"2303": {
|
| 50 |
+
"accuracy": 33.8,
|
| 51 |
+
"dir_accuracy": 42.1,
|
| 52 |
+
"up_precision": 59.6,
|
| 53 |
+
"dn_precision": 32.7,
|
| 54 |
+
"n_signals": 164,
|
| 55 |
+
"n_predictions": 231,
|
| 56 |
+
"sector": "semis",
|
| 57 |
+
"peers": [
|
| 58 |
+
"2330",
|
| 59 |
+
"2454"
|
| 60 |
+
]
|
| 61 |
+
},
|
| 62 |
+
"2317": {
|
| 63 |
+
"accuracy": 40.7,
|
| 64 |
+
"dir_accuracy": 46.6,
|
| 65 |
+
"up_precision": 52.5,
|
| 66 |
+
"dn_precision": 37.0,
|
| 67 |
+
"n_signals": 193,
|
| 68 |
+
"n_predictions": 231,
|
| 69 |
+
"sector": "electronics",
|
| 70 |
+
"peers": []
|
| 71 |
+
},
|
| 72 |
+
"2382": {
|
| 73 |
+
"accuracy": 40.3,
|
| 74 |
+
"dir_accuracy": 44.5,
|
| 75 |
+
"up_precision": 45.0,
|
| 76 |
+
"dn_precision": 44.4,
|
| 77 |
+
"n_signals": 182,
|
| 78 |
+
"n_predictions": 231,
|
| 79 |
+
"sector": "electronics",
|
| 80 |
+
"peers": []
|
| 81 |
+
},
|
| 82 |
+
"2308": {
|
| 83 |
+
"accuracy": 32.0,
|
| 84 |
+
"dir_accuracy": 43.4,
|
| 85 |
+
"up_precision": 51.7,
|
| 86 |
+
"dn_precision": 12.5,
|
| 87 |
+
"n_signals": 152,
|
| 88 |
+
"n_predictions": 231,
|
| 89 |
+
"sector": "electronics",
|
| 90 |
+
"peers": []
|
| 91 |
+
},
|
| 92 |
+
"2881": {
|
| 93 |
+
"accuracy": 39.4,
|
| 94 |
+
"dir_accuracy": 43.9,
|
| 95 |
+
"up_precision": 63.2,
|
| 96 |
+
"dn_precision": 33.8,
|
| 97 |
+
"n_signals": 198,
|
| 98 |
+
"n_predictions": 231,
|
| 99 |
+
"sector": "financials",
|
| 100 |
+
"peers": [
|
| 101 |
+
"2882",
|
| 102 |
+
"2886"
|
| 103 |
+
]
|
| 104 |
+
},
|
| 105 |
+
"2882": {
|
| 106 |
+
"accuracy": 42.4,
|
| 107 |
+
"dir_accuracy": 48.1,
|
| 108 |
+
"up_precision": 56.9,
|
| 109 |
+
"dn_precision": 42.2,
|
| 110 |
+
"n_signals": 181,
|
| 111 |
+
"n_predictions": 231,
|
| 112 |
+
"sector": "financials",
|
| 113 |
+
"peers": [
|
| 114 |
+
"2881",
|
| 115 |
+
"2886"
|
| 116 |
+
]
|
| 117 |
+
},
|
| 118 |
+
"2886": {
|
| 119 |
+
"accuracy": 41.6,
|
| 120 |
+
"dir_accuracy": 42.4,
|
| 121 |
+
"up_precision": 50.0,
|
| 122 |
+
"dn_precision": 36.7,
|
| 123 |
+
"n_signals": 224,
|
| 124 |
+
"n_predictions": 231,
|
| 125 |
+
"sector": "financials",
|
| 126 |
+
"peers": [
|
| 127 |
+
"2881",
|
| 128 |
+
"2882"
|
| 129 |
+
]
|
| 130 |
+
},
|
| 131 |
+
"0050": {
|
| 132 |
+
"accuracy": 45.5,
|
| 133 |
+
"dir_accuracy": 52.4,
|
| 134 |
+
"up_precision": 63.0,
|
| 135 |
+
"dn_precision": 30.0,
|
| 136 |
+
"n_signals": 187,
|
| 137 |
+
"n_predictions": 231,
|
| 138 |
+
"sector": "etfs",
|
| 139 |
+
"peers": [
|
| 140 |
+
"0056"
|
| 141 |
+
]
|
| 142 |
+
},
|
| 143 |
+
"0056": {
|
| 144 |
+
"accuracy": 40.3,
|
| 145 |
+
"dir_accuracy": 44.5,
|
| 146 |
+
"up_precision": 61.6,
|
| 147 |
+
"dn_precision": 32.5,
|
| 148 |
+
"n_signals": 209,
|
| 149 |
+
"n_predictions": 231,
|
| 150 |
+
"sector": "etfs",
|
| 151 |
+
"peers": [
|
| 152 |
+
"0050"
|
| 153 |
+
]
|
| 154 |
+
},
|
| 155 |
+
"2412": {
|
| 156 |
+
"accuracy": 40.7,
|
| 157 |
+
"dir_accuracy": 46.0,
|
| 158 |
+
"up_precision": 47.2,
|
| 159 |
+
"dn_precision": 44.8,
|
| 160 |
+
"n_signals": 202,
|
| 161 |
+
"n_predictions": 231,
|
| 162 |
+
"sector": "telecom",
|
| 163 |
+
"peers": []
|
| 164 |
+
}
|
| 165 |
+
},
|
| 166 |
+
"passed": true,
|
| 167 |
+
"pass_gate": {
|
| 168 |
+
"dir_accuracy": 43.5,
|
| 169 |
+
"up_precision": 54.0
|
| 170 |
+
}
|
| 171 |
+
}
|
docs/improvements_log.md
CHANGED
|
@@ -33,6 +33,7 @@
|
|
| 33 |
| C16 | Stacking: XGB meta-learner (META_WINDOW=60) | dir: 39.4→44.0 (+4.6pp), up_prec: 49.8→50.2 (+0.4pp) | FAILED | up_prec below 54% gate; 2454 overfitting on 60-row meta window |
|
| 34 |
| C16b | Stacking: LogReg(C=0.3) meta-learner (META_WINDOW=80) | dir: 39.7→42.7 (+3.0pp), up_prec: 49.9→51.6 (+1.7pp) | FAILED | 2881 dragged -4.4pp dir / -7.1pp; 2317 +12.2pp (inconsistent) |
|
| 35 |
| C17 | OOF stacking: LogReg(C=0.1), 5-fold CV on full train | 5-stock: dir→46.0% (+3.6pp), up_prec→54.6% (+2.3pp) ✓ | FAILED | **Selection bias** — 12-stock: dir=-0.5pp, up_prec=-1.6pp; helps semis, hurts financials/ETFs/telecom |
|
|
|
|
| 36 |
|
| 37 |
---
|
| 38 |
|
|
@@ -44,7 +45,7 @@
|
|
| 44 |
|
| 45 |
**Key insight from C17**: The 5-stock validation set (heavy semis) is biased. OOF stacking helps 2330/2454/2303/2317 but hurts financials, ETFs, telecom. Always validate on 12-stock before claiming a pass.
|
| 46 |
|
| 47 |
-
**Precision gap root cause**: avg_ens up_prec on 12-stock is 51.2% vs gate 54.0%. The 2.8pp gap is real
|
| 48 |
|
| 49 |
---
|
| 50 |
|
|
@@ -52,7 +53,8 @@
|
|
| 52 |
|
| 53 |
| ID | Idea | Expected gain | Effort |
|
| 54 |
|----|------|--------------|--------|
|
| 55 |
-
| C18 | DoubleEnsemble — sample reweighting
|
| 56 |
| C19 | Adaptive refined labeling (tighter barriers in low-vol regimes) | +2-4pp up_prec | Medium |
|
| 57 |
-
| C20 | Sector-conditional model
|
| 58 |
| C21 | Volume surge + sector momentum hotspot filter (recommendation) | UX/signal quality | Low |
|
|
|
|
|
|
| 33 |
| C16 | Stacking: XGB meta-learner (META_WINDOW=60) | dir: 39.4→44.0 (+4.6pp), up_prec: 49.8→50.2 (+0.4pp) | FAILED | up_prec below 54% gate; 2454 overfitting on 60-row meta window |
|
| 34 |
| C16b | Stacking: LogReg(C=0.3) meta-learner (META_WINDOW=80) | dir: 39.7→42.7 (+3.0pp), up_prec: 49.9→51.6 (+1.7pp) | FAILED | 2881 dragged -4.4pp dir / -7.1pp; 2317 +12.2pp (inconsistent) |
|
| 35 |
| C17 | OOF stacking: LogReg(C=0.1), 5-fold CV on full train | 5-stock: dir→46.0% (+3.6pp), up_prec→54.6% (+2.3pp) ✓ | FAILED | **Selection bias** — 12-stock: dir=-0.5pp, up_prec=-1.6pp; helps semis, hurts financials/ETFs/telecom |
|
| 36 |
+
| C22 | Ensemble weight search: grid over RF/XGB/LGBM blend (LGBM↑, XGB↓) | 5-stock: dir=44.2%, up_prec=55.6% (best: RF=1/XGB=0.5/LGBM=1.5) ✓ | FAILED | 12-stock: up_prec=53.0% vs gate 54.0% (−1pp short); XGB hurts cross-sector precision (12-stock opt: RF=1/XGB=0.25/LGBM=0.5) |
|
| 37 |
|
| 38 |
---
|
| 39 |
|
|
|
|
| 45 |
|
| 46 |
**Key insight from C17**: The 5-stock validation set (heavy semis) is biased. OOF stacking helps 2330/2454/2303/2317 but hurts financials, ETFs, telecom. Always validate on 12-stock before claiming a pass.
|
| 47 |
|
| 48 |
+
**Precision gap root cause**: avg_ens up_prec on 12-stock is 51.2% vs gate 54.0%. The 2.8pp gap is real. C22/C23 confirmed weight search gets to ~53.0-53.2% (+1pp) but XGB is net-negative across diverse sectors. C18 sample reweighting traded dir_accuracy for ↑prec (no alpha cleared both gates). C20 sector-conditional model finally broke through: pooling intra-sector training data (semis/financials/ETFs only — electronics isolated as too heterogeneous) reaches 12-stock dir=44.9%, ↑prec=54.2% — both gates cleared.
|
| 49 |
|
| 50 |
---
|
| 51 |
|
|
|
|
| 53 |
|
| 54 |
| ID | Idea | Expected gain | Effort |
|
| 55 |
|----|------|--------------|--------|
|
| 56 |
+
| C18 | DoubleEnsemble — sample reweighting (false-UP upweight, alpha search 0.5–3.0) | 5-stock: ↑prec=54.8% (+1.5pp) but dir=42.5% (-1pp below gate) | FAILED | Precision-recall tradeoff: suppressing false-UP also kills directional recall; no alpha clears both gates |
|
| 57 |
| C19 | Adaptive refined labeling (tighter barriers in low-vol regimes) | +2-4pp up_prec | Medium |
|
| 58 |
+
| C20 | Sector-conditional model: pool semis/financials/ETFs; isolate electronics/telecom | 5-stock: dir=45.5%, ↑prec=55.5% ✓ / 12-stock: dir=44.9%, ↑prec=54.2% ✓ | **PASSED** | Electronics (Foxconn/Delta/Quanta) too heterogeneous to pool — isolated; financials/ETFs benefited most |
|
| 59 |
| C21 | Volume surge + sector momentum hotspot filter (recommendation) | UX/signal quality | Low |
|
| 60 |
+
| C23 | Drop XGB; RF+LGBM only, weight search | 5-stock: dir=44.1%, up_prec=55.1% (RF=1/LGBM=1.5) ✓ | FAILED | 12-stock: up_prec=53.2% vs gate 54.0%; removing XGB is marginal (+0.2pp over C22); 2454/2412 structurally broken |
|
scripts/backtest_c20.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""C20: Sector-conditional model — pool training data within sector.
|
| 3 |
+
|
| 4 |
+
Instead of training per-stock in isolation, each stock's walk-forward window
|
| 5 |
+
is augmented with date-aligned rows from its sector peers. The sector model
|
| 6 |
+
then predicts only the target stock's test window.
|
| 7 |
+
|
| 8 |
+
Sector groups:
|
| 9 |
+
semis: 2330, 2454, 2303
|
| 10 |
+
electronics: 2317, 2382, 2308
|
| 11 |
+
financials: 2881, 2882, 2886
|
| 12 |
+
etfs: 0050, 0056
|
| 13 |
+
telecom: 2412 (solo — no peer benefit, same as baseline)
|
| 14 |
+
|
| 15 |
+
Hypothesis: pooling intra-sector reduces label noise from cross-sector mixing
|
| 16 |
+
(C9 failure) while giving the model more examples of sector-specific patterns
|
| 17 |
+
that hurt the worst stocks (2454 semi, 2412 telecom, 2886 financial).
|
| 18 |
+
"""
|
| 19 |
+
import sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 23 |
+
sys.path.insert(0, str(ROOT))
|
| 24 |
+
|
| 25 |
+
import warnings; warnings.filterwarnings("ignore")
|
| 26 |
+
import json
|
| 27 |
+
import argparse
|
| 28 |
+
import numpy as np
|
| 29 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 30 |
+
from sklearn.preprocessing import StandardScaler
|
| 31 |
+
|
| 32 |
+
from scripts.improvement_harness import (
|
| 33 |
+
fetch_df, build_triple_barrier_labels, compute_metrics,
|
| 34 |
+
CURRENT_FEATURES, DEFAULT_STOCKS, EXTENDED_STOCKS,
|
| 35 |
+
PASS_DIR_ACC, PASS_UP_PREC,
|
| 36 |
+
MIN_TRAIN, STEP, LABEL_HORIZON, RF_PARAMS,
|
| 37 |
+
)
|
| 38 |
+
from models.predictor import _build_features
|
| 39 |
+
|
| 40 |
+
# All stocks needed for sector pools (superset of both eval sets)
|
| 41 |
+
ALL_STOCKS = list(dict.fromkeys(DEFAULT_STOCKS + EXTENDED_STOCKS))
|
| 42 |
+
|
| 43 |
+
SECTOR_MAP = {
|
| 44 |
+
"semis": ["2330", "2454", "2303"],
|
| 45 |
+
"electronics": ["2317", "2382", "2308"],
|
| 46 |
+
"financials": ["2881", "2882", "2886"],
|
| 47 |
+
"etfs": ["0050", "0056"],
|
| 48 |
+
"telecom": ["2412"],
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
# Sectors where intra-sector businesses are too heterogeneous to benefit from pooling.
|
| 52 |
+
# electronics: Foxconn (contract mfg) / Delta (power components) / Quanta (laptop ODM)
|
| 53 |
+
# are structurally different — pooling adds noise, not signal.
|
| 54 |
+
NO_POOL_SECTORS = {"electronics", "telecom"}
|
| 55 |
+
|
| 56 |
+
# Reverse map: stock → sector name
|
| 57 |
+
STOCK_SECTOR = {s: sec for sec, stocks in SECTOR_MAP.items() for s in stocks}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _get_close(df):
|
| 61 |
+
return (df.set_index("date")["close"] if "date" in df.columns else df["close"]).values
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _get_dates(df):
|
| 65 |
+
if "date" in df.columns:
|
| 66 |
+
return df["date"].values
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def walk_forward_sector(target_no, target_feat, target_labels, target_dates,
|
| 71 |
+
peers: list[tuple], cols):
|
| 72 |
+
"""Walk-forward on target stock, training augmented with sector peer rows."""
|
| 73 |
+
avail = [c for c in cols if c in target_feat.columns]
|
| 74 |
+
X_target = target_feat[avail].fillna(0).values
|
| 75 |
+
n = len(target_feat)
|
| 76 |
+
|
| 77 |
+
y_true_all, y_pred_all = [], []
|
| 78 |
+
|
| 79 |
+
cutoff = MIN_TRAIN
|
| 80 |
+
while cutoff + STEP + LABEL_HORIZON <= n:
|
| 81 |
+
train_end = cutoff - LABEL_HORIZON
|
| 82 |
+
if train_end < MIN_TRAIN - LABEL_HORIZON:
|
| 83 |
+
cutoff += STEP; continue
|
| 84 |
+
|
| 85 |
+
y_tr = target_labels[:train_end]
|
| 86 |
+
valid = ~np.isnan(y_tr)
|
| 87 |
+
y_v = y_tr[valid].astype(int)
|
| 88 |
+
if len(y_v) < 10 or len(np.unique(y_v)) < 2:
|
| 89 |
+
cutoff += STEP; continue
|
| 90 |
+
|
| 91 |
+
X_train_list = [X_target[:train_end][valid]]
|
| 92 |
+
y_train_list = [y_v]
|
| 93 |
+
|
| 94 |
+
# Cutoff date for peer alignment (avoid future leakage)
|
| 95 |
+
cutoff_date = target_dates[train_end - 1] if (target_dates is not None and train_end > 0) else None
|
| 96 |
+
|
| 97 |
+
for (peer_feat, peer_labels, peer_dates) in peers:
|
| 98 |
+
peer_avail = [c for c in cols if c in peer_feat.columns]
|
| 99 |
+
|
| 100 |
+
if cutoff_date is not None and peer_dates is not None:
|
| 101 |
+
peer_end = int((peer_dates <= cutoff_date).sum())
|
| 102 |
+
else:
|
| 103 |
+
peer_end = int(train_end * len(peer_feat) / n)
|
| 104 |
+
|
| 105 |
+
peer_end = min(peer_end, len(peer_feat) - LABEL_HORIZON)
|
| 106 |
+
if peer_end < 15:
|
| 107 |
+
continue
|
| 108 |
+
|
| 109 |
+
y_p = peer_labels[:peer_end]
|
| 110 |
+
valid_p = ~np.isnan(y_p)
|
| 111 |
+
y_vp = y_p[valid_p].astype(int)
|
| 112 |
+
if len(y_vp) < 5 or len(np.unique(y_vp)) < 2:
|
| 113 |
+
continue
|
| 114 |
+
|
| 115 |
+
X_p = peer_feat[peer_avail].fillna(0).values[:peer_end][valid_p]
|
| 116 |
+
X_train_list.append(X_p)
|
| 117 |
+
y_train_list.append(y_vp)
|
| 118 |
+
|
| 119 |
+
X_train = np.vstack(X_train_list)
|
| 120 |
+
y_train = np.concatenate(y_train_list)
|
| 121 |
+
if len(np.unique(y_train)) < 2:
|
| 122 |
+
cutoff += STEP; continue
|
| 123 |
+
|
| 124 |
+
scaler = StandardScaler()
|
| 125 |
+
X_train_s = scaler.fit_transform(X_train)
|
| 126 |
+
|
| 127 |
+
rf = RandomForestClassifier(**RF_PARAMS)
|
| 128 |
+
rf.fit(X_train_s, y_train)
|
| 129 |
+
|
| 130 |
+
test_end = min(cutoff + STEP, n - LABEL_HORIZON)
|
| 131 |
+
y_te = target_labels[cutoff:test_end]
|
| 132 |
+
valid_te = ~np.isnan(y_te)
|
| 133 |
+
if valid_te.sum() == 0:
|
| 134 |
+
cutoff += STEP; continue
|
| 135 |
+
|
| 136 |
+
X_te_s = scaler.transform(X_target[cutoff:test_end][valid_te])
|
| 137 |
+
y_pred = rf.predict(X_te_s)
|
| 138 |
+
|
| 139 |
+
y_true_all.extend(y_te[valid_te].astype(int).tolist())
|
| 140 |
+
y_pred_all.extend(y_pred.tolist())
|
| 141 |
+
cutoff += STEP
|
| 142 |
+
|
| 143 |
+
if not y_true_all:
|
| 144 |
+
return {}
|
| 145 |
+
return compute_metrics(np.array(y_true_all), np.array(y_pred_all))
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def main():
|
| 149 |
+
parser = argparse.ArgumentParser()
|
| 150 |
+
parser.add_argument("--extended", action="store_true")
|
| 151 |
+
args = parser.parse_args()
|
| 152 |
+
|
| 153 |
+
eval_stocks = EXTENDED_STOCKS if args.extended else DEFAULT_STOCKS
|
| 154 |
+
tag = "12-stock" if args.extended else "5-stock"
|
| 155 |
+
|
| 156 |
+
print(f"\n=== C20: Sector-conditional model [{tag}] ===")
|
| 157 |
+
print(" Loading all stock data...", flush=True)
|
| 158 |
+
|
| 159 |
+
# Load all stocks upfront (needed for sector pools)
|
| 160 |
+
stock_data: dict[str, dict] = {}
|
| 161 |
+
for s in ALL_STOCKS:
|
| 162 |
+
df = fetch_df(s)
|
| 163 |
+
if df is None or df.empty:
|
| 164 |
+
print(f" {s}: no data")
|
| 165 |
+
continue
|
| 166 |
+
feat = _build_features(df)
|
| 167 |
+
close = _get_close(df)
|
| 168 |
+
labels = build_triple_barrier_labels(close)
|
| 169 |
+
dates = _get_dates(feat) if "date" in feat.columns else _get_dates(df)
|
| 170 |
+
stock_data[s] = {"feat": feat, "labels": labels, "dates": dates}
|
| 171 |
+
print(f" {s}: {len(feat)} rows, sector={STOCK_SECTOR.get(s, '?')}")
|
| 172 |
+
|
| 173 |
+
print()
|
| 174 |
+
per_stock, metrics_list = {}, []
|
| 175 |
+
|
| 176 |
+
for target_no in eval_stocks:
|
| 177 |
+
if target_no not in stock_data:
|
| 178 |
+
print(f" {target_no}: missing data, skip")
|
| 179 |
+
continue
|
| 180 |
+
|
| 181 |
+
sector = STOCK_SECTOR.get(target_no, "unknown")
|
| 182 |
+
if sector in NO_POOL_SECTORS:
|
| 183 |
+
peer_stocks = []
|
| 184 |
+
else:
|
| 185 |
+
peer_stocks = [s for s in SECTOR_MAP.get(sector, []) if s != target_no and s in stock_data]
|
| 186 |
+
|
| 187 |
+
peers = [
|
| 188 |
+
(stock_data[p]["feat"], stock_data[p]["labels"], stock_data[p]["dates"])
|
| 189 |
+
for p in peer_stocks
|
| 190 |
+
]
|
| 191 |
+
|
| 192 |
+
td = stock_data[target_no]
|
| 193 |
+
print(f" {target_no} [{sector}, peers={peer_stocks}]...", end=" ", flush=True)
|
| 194 |
+
|
| 195 |
+
m = walk_forward_sector(
|
| 196 |
+
target_no,
|
| 197 |
+
td["feat"], td["labels"], td["dates"],
|
| 198 |
+
peers, CURRENT_FEATURES,
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
per_stock[target_no] = {**m, "sector": sector, "peers": peer_stocks}
|
| 202 |
+
metrics_list.append(m)
|
| 203 |
+
if m:
|
| 204 |
+
print(f"dir={m['dir_accuracy']}% ↑prec={m['up_precision']}%")
|
| 205 |
+
else:
|
| 206 |
+
print("no output")
|
| 207 |
+
|
| 208 |
+
def _avg(key):
|
| 209 |
+
vals = [m[key] for m in metrics_list
|
| 210 |
+
if m and not np.isnan(m.get(key, float("nan")))]
|
| 211 |
+
return round(float(np.mean(vals)), 1) if vals else float("nan")
|
| 212 |
+
|
| 213 |
+
avg_dir = _avg("dir_accuracy")
|
| 214 |
+
avg_up = _avg("up_precision")
|
| 215 |
+
passed = avg_dir >= PASS_DIR_ACC and avg_up >= PASS_UP_PREC
|
| 216 |
+
|
| 217 |
+
print(f"\n Avg: dir={avg_dir}% ↑prec={avg_up}%")
|
| 218 |
+
print(f" Gate (dir≥{PASS_DIR_ACC}% AND ↑prec≥{PASS_UP_PREC}%): {'PASS ✓' if passed else 'FAIL ✗'}")
|
| 219 |
+
|
| 220 |
+
result = {
|
| 221 |
+
"experiment": "C20",
|
| 222 |
+
"description": "Sector-conditional model: pool training within sector",
|
| 223 |
+
"stocks": eval_stocks,
|
| 224 |
+
"aggregate": {"dir_accuracy": avg_dir, "up_precision": avg_up},
|
| 225 |
+
"per_stock": per_stock,
|
| 226 |
+
"passed": passed,
|
| 227 |
+
"pass_gate": {"dir_accuracy": PASS_DIR_ACC, "up_precision": PASS_UP_PREC},
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
suffix = "_12stock" if args.extended else ""
|
| 231 |
+
out = ROOT / f"docs/c20_result{suffix}.json"
|
| 232 |
+
out.parent.mkdir(exist_ok=True)
|
| 233 |
+
out.write_text(json.dumps(result, indent=2))
|
| 234 |
+
print(f"\n Saved: {out}")
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
if __name__ == "__main__":
|
| 238 |
+
main()
|