ifieryarrows commited on
Commit
545a1d6
·
verified ·
1 Parent(s): f89b3b2

Sync from GitHub (tests passed)

Browse files
app/main.py CHANGED
@@ -1639,17 +1639,21 @@ async def get_pipeline_run_status(
1639
  "/api/analysis/consensus",
1640
  response_model=ConsensusSignal,
1641
  summary="Get consensus signal",
1642
- description="Combines XGBoost and TFT-ASRO signals into a directional consensus."
 
 
 
1643
  )
1644
  async def get_consensus(
1645
  symbol: str = Query(default=TARGET_SYMBOL, description="Trading symbol")
1646
  ):
1647
- from deep_learning.inference.predictor import ensemble_directional_vote, generate_tft_analysis
1648
 
1649
- # 1. Get TFT analysis
 
 
1650
  try:
1651
- with SessionLocal() as session:
1652
- tft_result = generate_tft_analysis(session, symbol)
1653
 
1654
  if "error" in tft_result:
1655
  raise HTTPException(status_code=500, detail=tft_result["error"])
 
1639
  "/api/analysis/consensus",
1640
  response_model=ConsensusSignal,
1641
  summary="Get consensus signal",
1642
+ description=(
1643
+ "Combines the latest XGBoost snapshot and snapshot-first TFT-ASRO "
1644
+ "signal into a directional consensus."
1645
+ ),
1646
  )
1647
  async def get_consensus(
1648
  symbol: str = Query(default=TARGET_SYMBOL, description="Trading symbol")
1649
  ):
1650
+ from deep_learning.inference.predictor import ensemble_directional_vote
1651
 
1652
+ # 1. Reuse the TFT endpoint's snapshot-first contract. This keeps the
1653
+ # consensus response semantically identical while avoiding a fresh model
1654
+ # inference when the pipeline already persisted a valid prediction.
1655
  try:
1656
+ tft_result = await get_tft_analysis(symbol=symbol, source="snapshot")
 
1657
 
1658
  if "error" in tft_result:
1659
  raise HTTPException(status_code=500, detail=tft_result["error"])
config/symbol_sets/challenger.json CHANGED
@@ -1,10 +1,10 @@
1
  {
2
- "run_id": "scr-20260907-47e71d",
3
- "generated_at": "2026-09-07T07:49:34Z",
4
- "screener_run_id": "scr-20260907-47e71d",
5
- "content_fingerprint": "sha256:b7d332601e036e5e6ddf2aa399d963bec8d8014df3dbe004b3599ebb3ade1f9a",
6
- "output_fingerprint": "sha256:27c40ee31ca83bac542e54d11c139b630915fbb65d757f86ae6422e38939a543",
7
- "universe_content_fingerprint": "sha256:9d9fa74eda4fb62f607299c7c411b45dc4d7c9fa1c9087dea82014c39490ecc7",
8
  "selection_rules_version": "3.2.0",
9
  "target": "HG=F",
10
  "total_candidates": 141,
@@ -75,204 +75,204 @@
75
  "category": "macro_currency",
76
  "selection_source": "mandatory",
77
  "rank": 113,
78
- "score_composite": 0.3322,
79
  "is_pearson": -0.4365658515972885,
80
  "is_n_obs": 312,
81
  "is_best_lag": 0,
82
- "oos_pearson": -0.26481112850985244,
83
  "oos_pearson_sign": "negative",
84
- "oos_n_obs": 140,
85
- "oos_partial_corr": 0.12771342511863104,
86
- "oos_rolling_std": 0.09139103113852662,
87
  "oos_frozen_lag": 0,
88
- "oos_lag_corr_at_frozen": -0.26481112850985244
89
  },
90
  {
91
  "ticker": "CL=F",
92
  "category": "commodity_energy",
93
  "selection_source": "mandatory",
94
  "rank": 139,
95
- "score_composite": 0.2283,
96
  "is_pearson": 0.34649573464844113,
97
  "is_n_obs": 312,
98
  "is_best_lag": 0,
99
- "oos_pearson": -0.009725612150347122,
100
  "oos_pearson_sign": "negative",
101
- "oos_n_obs": 140,
102
- "oos_partial_corr": 0.026040931590577132,
103
- "oos_rolling_std": 0.2904904934635282,
104
  "oos_frozen_lag": 0,
105
- "oos_lag_corr_at_frozen": -0.009725612150347122
106
  },
107
  {
108
  "ticker": "COPX",
109
  "category": "etf_copper",
110
  "selection_source": "screener",
111
  "rank": 2,
112
- "score_composite": 0.7528,
113
- "is_pearson": 0.7841777665901276,
114
  "is_n_obs": 312,
115
  "is_best_lag": 0,
116
- "oos_pearson": 0.6702190340297001,
117
  "oos_pearson_sign": "positive",
118
- "oos_n_obs": 140,
119
- "oos_partial_corr": 0.5892097405886964,
120
- "oos_rolling_std": 0.11536717604218495,
121
  "oos_frozen_lag": 0,
122
- "oos_lag_corr_at_frozen": 0.6702190340297001
123
  },
124
  {
125
  "ticker": "PICK",
126
  "category": "etf_miners",
127
  "selection_source": "screener",
128
  "rank": 3,
129
- "score_composite": 0.7084,
130
- "is_pearson": 0.7269351028456664,
131
  "is_n_obs": 312,
132
  "is_best_lag": 0,
133
- "oos_pearson": 0.645425266898279,
134
  "oos_pearson_sign": "positive",
135
- "oos_n_obs": 140,
136
- "oos_partial_corr": 0.547376717358926,
137
- "oos_rolling_std": 0.10044822702864298,
138
  "oos_frozen_lag": 0,
139
- "oos_lag_corr_at_frozen": 0.645425266898279
140
  },
141
  {
142
  "ticker": "FCX",
143
  "category": "miner_major",
144
  "selection_source": "screener",
145
  "rank": 4,
146
- "score_composite": 0.6995,
147
- "is_pearson": 0.6989203225741722,
148
  "is_n_obs": 312,
149
  "is_best_lag": 0,
150
- "oos_pearson": 0.6115947330459959,
151
  "oos_pearson_sign": "positive",
152
- "oos_n_obs": 140,
153
- "oos_partial_corr": 0.5036797627061815,
154
- "oos_rolling_std": 0.16570944891424616,
155
  "oos_frozen_lag": 0,
156
- "oos_lag_corr_at_frozen": 0.6115947330459959
157
  },
158
  {
159
  "ticker": "SCCO",
160
  "category": "miner_major",
161
  "selection_source": "screener",
162
  "rank": 5,
163
- "score_composite": 0.6975,
164
- "is_pearson": 0.7085866952903818,
165
  "is_n_obs": 312,
166
  "is_best_lag": 0,
167
- "oos_pearson": 0.592165816993722,
168
  "oos_pearson_sign": "positive",
169
- "oos_n_obs": 140,
170
- "oos_partial_corr": 0.5039968942966784,
171
- "oos_rolling_std": 0.14606867230985837,
172
  "oos_frozen_lag": 0,
173
- "oos_lag_corr_at_frozen": 0.592165816993722
174
  },
175
  {
176
  "ticker": "RIO",
177
  "category": "miner_major",
178
  "selection_source": "screener",
179
  "rank": 6,
180
- "score_composite": 0.6608,
181
- "is_pearson": 0.6828594972987202,
182
  "is_n_obs": 312,
183
  "is_best_lag": 0,
184
- "oos_pearson": 0.5668930737915945,
185
  "oos_pearson_sign": "positive",
186
- "oos_n_obs": 140,
187
- "oos_partial_corr": 0.4367660236404706,
188
- "oos_rolling_std": 0.14679906832009698,
189
  "oos_frozen_lag": 0,
190
- "oos_lag_corr_at_frozen": 0.5668930737915945
191
  },
192
  {
193
  "ticker": "HBM.TO",
194
  "category": "miner_mid",
195
  "selection_source": "screener",
196
  "rank": 7,
197
- "score_composite": 0.6601,
198
- "is_pearson": 0.656051010118594,
199
  "is_n_obs": 312,
200
  "is_best_lag": 0,
201
- "oos_pearson": 0.5475275362874713,
202
  "oos_pearson_sign": "positive",
203
- "oos_n_obs": 140,
204
- "oos_partial_corr": 0.4546578699383272,
205
- "oos_rolling_std": 0.15329984991467707,
206
  "oos_frozen_lag": 0,
207
- "oos_lag_corr_at_frozen": 0.5475275362874713
208
  },
209
  {
210
  "ticker": "BHP",
211
  "category": "miner_major",
212
  "selection_source": "screener",
213
  "rank": 8,
214
- "score_composite": 0.6473,
215
- "is_pearson": 0.6635622305490159,
216
  "is_n_obs": 312,
217
  "is_best_lag": 0,
218
- "oos_pearson": 0.5376411945718109,
219
  "oos_pearson_sign": "positive",
220
- "oos_n_obs": 140,
221
- "oos_partial_corr": 0.41040121050568446,
222
- "oos_rolling_std": 0.18622367162062195,
223
  "oos_frozen_lag": 0,
224
- "oos_lag_corr_at_frozen": 0.5376411945718109
225
  },
226
  {
227
  "ticker": "LUN.TO",
228
  "category": "miner_mid",
229
  "selection_source": "screener",
230
  "rank": 9,
231
- "score_composite": 0.6332,
232
- "is_pearson": 0.6287677490628221,
233
  "is_n_obs": 312,
234
  "is_best_lag": 0,
235
- "oos_pearson": 0.48622276669490333,
236
  "oos_pearson_sign": "positive",
237
- "oos_n_obs": 140,
238
- "oos_partial_corr": 0.39539446882770857,
239
- "oos_rolling_std": 0.22798193850613174,
240
  "oos_frozen_lag": 0,
241
- "oos_lag_corr_at_frozen": 0.48622276669490333
242
  },
243
  {
244
  "ticker": "XME",
245
  "category": "etf_metals",
246
  "selection_source": "screener",
247
  "rank": 12,
248
- "score_composite": 0.613,
249
- "is_pearson": 0.5943770811582014,
250
  "is_n_obs": 312,
251
  "is_best_lag": 0,
252
- "oos_pearson": 0.46939602284833737,
253
  "oos_pearson_sign": "positive",
254
- "oos_n_obs": 140,
255
- "oos_partial_corr": 0.32592932042220457,
256
- "oos_rolling_std": 0.17807951788305412,
257
  "oos_frozen_lag": 0,
258
- "oos_lag_corr_at_frozen": 0.46939602284833737
259
  },
260
  {
261
  "ticker": "FXA",
262
  "category": "macro_currency",
263
  "selection_source": "screener",
264
  "rank": 17,
265
- "score_composite": 0.576,
266
- "is_pearson": 0.5554676133481979,
267
  "is_n_obs": 312,
268
  "is_best_lag": 0,
269
- "oos_pearson": 0.4867358920236742,
270
  "oos_pearson_sign": "positive",
271
- "oos_n_obs": 140,
272
- "oos_partial_corr": 0.3014904458743425,
273
- "oos_rolling_std": 0.11602988271302624,
274
  "oos_frozen_lag": 0,
275
- "oos_lag_corr_at_frozen": 0.4867358920236742
276
  },
277
  {
278
  "ticker": "VALE",
@@ -280,84 +280,84 @@
280
  "selection_source": "screener",
281
  "rank": 25,
282
  "score_composite": 0.561,
283
- "is_pearson": 0.6012555070028014,
284
  "is_n_obs": 312,
285
  "is_best_lag": 0,
286
- "oos_pearson": 0.4466655590992748,
287
  "oos_pearson_sign": "positive",
288
- "oos_n_obs": 140,
289
- "oos_partial_corr": 0.2887976221115926,
290
- "oos_rolling_std": 0.1935832334479645,
291
  "oos_frozen_lag": 0,
292
- "oos_lag_corr_at_frozen": 0.4466655590992748
293
  },
294
  {
295
  "ticker": "GXC",
296
  "category": "macro_china",
297
  "selection_source": "screener",
298
  "rank": 35,
299
- "score_composite": 0.5116,
300
- "is_pearson": 0.5168833751333366,
301
  "is_n_obs": 312,
302
  "is_best_lag": 0,
303
- "oos_pearson": 0.4333670088956817,
304
  "oos_pearson_sign": "positive",
305
- "oos_n_obs": 140,
306
- "oos_partial_corr": 0.30581422834854566,
307
- "oos_rolling_std": 0.121199484856543,
308
  "oos_frozen_lag": 0,
309
- "oos_lag_corr_at_frozen": 0.4333670088956817
310
  },
311
  {
312
  "ticker": "CEW",
313
  "category": "macro_em",
314
  "selection_source": "screener",
315
  "rank": 37,
316
- "score_composite": 0.5054,
317
- "is_pearson": 0.5536232108061646,
318
  "is_n_obs": 312,
319
  "is_best_lag": 0,
320
- "oos_pearson": 0.3976319130333007,
321
  "oos_pearson_sign": "positive",
322
- "oos_n_obs": 140,
323
- "oos_partial_corr": 0.17028678827216104,
324
- "oos_rolling_std": 0.14252821376933172,
325
  "oos_frozen_lag": 0,
326
- "oos_lag_corr_at_frozen": 0.3976319130333007
327
  },
328
  {
329
  "ticker": "PL=F",
330
  "category": "commodity_precious",
331
  "selection_source": "screener",
332
  "rank": 44,
333
- "score_composite": 0.4786,
334
  "is_pearson": 0.48368775603200487,
335
  "is_n_obs": 312,
336
  "is_best_lag": 0,
337
- "oos_pearson": 0.4675441187246214,
338
  "oos_pearson_sign": "positive",
339
- "oos_n_obs": 140,
340
- "oos_partial_corr": 0.3902405085328851,
341
- "oos_rolling_std": 0.10480371701034612,
342
  "oos_frozen_lag": 0,
343
- "oos_lag_corr_at_frozen": 0.4675441187246214
344
  },
345
  {
346
  "ticker": "ALI=F",
347
  "category": "commodity_base",
348
  "selection_source": "screener",
349
  "rank": 82,
350
- "score_composite": 0.3856,
351
  "is_pearson": 0.5018667089390725,
352
  "is_n_obs": 312,
353
  "is_best_lag": 0,
354
- "oos_pearson": 0.416083472244696,
355
  "oos_pearson_sign": "positive",
356
- "oos_n_obs": 140,
357
- "oos_partial_corr": 0.37599757846352727,
358
- "oos_rolling_std": 0.1308183978751278,
359
  "oos_frozen_lag": 0,
360
- "oos_lag_corr_at_frozen": 0.416083472244696
361
  }
362
  ]
363
  }
 
1
  {
2
+ "run_id": "scr-20260914-1b4575",
3
+ "generated_at": "2026-09-14T08:27:01Z",
4
+ "screener_run_id": "scr-20260914-1b4575",
5
+ "content_fingerprint": "sha256:c97b5ba04c019cc9b1cd437062b7ae7f3d29b62c5bd55d3be6d165d0bf851b83",
6
+ "output_fingerprint": "sha256:d5391b0594cabeeec50e14e9d67208389a33911eaeb04b552632a590caa9c78e",
7
+ "universe_content_fingerprint": "sha256:0fd8dc62f0d844427de19e4a55e54c1a8cf4129d28d95cbd86c231d20faa0f75",
8
  "selection_rules_version": "3.2.0",
9
  "target": "HG=F",
10
  "total_candidates": 141,
 
75
  "category": "macro_currency",
76
  "selection_source": "mandatory",
77
  "rank": 113,
78
+ "score_composite": 0.3321,
79
  "is_pearson": -0.4365658515972885,
80
  "is_n_obs": 312,
81
  "is_best_lag": 0,
82
+ "oos_pearson": -0.26430676889238325,
83
  "oos_pearson_sign": "negative",
84
+ "oos_n_obs": 141,
85
+ "oos_partial_corr": 0.12367444280609856,
86
+ "oos_rolling_std": 0.09105194847720606,
87
  "oos_frozen_lag": 0,
88
+ "oos_lag_corr_at_frozen": -0.26430676889238325
89
  },
90
  {
91
  "ticker": "CL=F",
92
  "category": "commodity_energy",
93
  "selection_source": "mandatory",
94
  "rank": 139,
95
+ "score_composite": 0.2302,
96
  "is_pearson": 0.34649573464844113,
97
  "is_n_obs": 312,
98
  "is_best_lag": 0,
99
+ "oos_pearson": -0.016127694916842482,
100
  "oos_pearson_sign": "negative",
101
+ "oos_n_obs": 141,
102
+ "oos_partial_corr": 0.0191421576303892,
103
+ "oos_rolling_std": 0.2913467841041788,
104
  "oos_frozen_lag": 0,
105
+ "oos_lag_corr_at_frozen": -0.016127694916842482
106
  },
107
  {
108
  "ticker": "COPX",
109
  "category": "etf_copper",
110
  "selection_source": "screener",
111
  "rank": 2,
112
+ "score_composite": 0.753,
113
+ "is_pearson": 0.7841778917513906,
114
  "is_n_obs": 312,
115
  "is_best_lag": 0,
116
+ "oos_pearson": 0.6711073866179069,
117
  "oos_pearson_sign": "positive",
118
+ "oos_n_obs": 141,
119
+ "oos_partial_corr": 0.5901986177405685,
120
+ "oos_rolling_std": 0.11491662730551566,
121
  "oos_frozen_lag": 0,
122
+ "oos_lag_corr_at_frozen": 0.6711073866179069
123
  },
124
  {
125
  "ticker": "PICK",
126
  "category": "etf_miners",
127
  "selection_source": "screener",
128
  "rank": 3,
129
+ "score_composite": 0.7087,
130
+ "is_pearson": 0.7269352758776466,
131
  "is_n_obs": 312,
132
  "is_best_lag": 0,
133
+ "oos_pearson": 0.646618739434024,
134
  "oos_pearson_sign": "positive",
135
+ "oos_n_obs": 141,
136
+ "oos_partial_corr": 0.5489059575210063,
137
+ "oos_rolling_std": 0.10030383876885804,
138
  "oos_frozen_lag": 0,
139
+ "oos_lag_corr_at_frozen": 0.646618739434024
140
  },
141
  {
142
  "ticker": "FCX",
143
  "category": "miner_major",
144
  "selection_source": "screener",
145
  "rank": 4,
146
+ "score_composite": 0.6997,
147
+ "is_pearson": 0.6989203826512825,
148
  "is_n_obs": 312,
149
  "is_best_lag": 0,
150
+ "oos_pearson": 0.6123498748865847,
151
  "oos_pearson_sign": "positive",
152
+ "oos_n_obs": 141,
153
+ "oos_partial_corr": 0.5040578448259175,
154
+ "oos_rolling_std": 0.16506359290430392,
155
  "oos_frozen_lag": 0,
156
+ "oos_lag_corr_at_frozen": 0.6123498748865847
157
  },
158
  {
159
  "ticker": "SCCO",
160
  "category": "miner_major",
161
  "selection_source": "screener",
162
  "rank": 5,
163
+ "score_composite": 0.6978,
164
+ "is_pearson": 0.7085867959347079,
165
  "is_n_obs": 312,
166
  "is_best_lag": 0,
167
+ "oos_pearson": 0.5932490392779859,
168
  "oos_pearson_sign": "positive",
169
+ "oos_n_obs": 141,
170
+ "oos_partial_corr": 0.5051401730950509,
171
+ "oos_rolling_std": 0.14549743380882185,
172
  "oos_frozen_lag": 0,
173
+ "oos_lag_corr_at_frozen": 0.5932490392779859
174
  },
175
  {
176
  "ticker": "RIO",
177
  "category": "miner_major",
178
  "selection_source": "screener",
179
  "rank": 6,
180
+ "score_composite": 0.6613,
181
+ "is_pearson": 0.6828593761093451,
182
  "is_n_obs": 312,
183
  "is_best_lag": 0,
184
+ "oos_pearson": 0.5685016016688824,
185
  "oos_pearson_sign": "positive",
186
+ "oos_n_obs": 141,
187
+ "oos_partial_corr": 0.4389788782565666,
188
+ "oos_rolling_std": 0.1462264046748876,
189
  "oos_frozen_lag": 0,
190
+ "oos_lag_corr_at_frozen": 0.5685016016688824
191
  },
192
  {
193
  "ticker": "HBM.TO",
194
  "category": "miner_mid",
195
  "selection_source": "screener",
196
  "rank": 7,
197
+ "score_composite": 0.6605,
198
+ "is_pearson": 0.6560508716990718,
199
  "is_n_obs": 312,
200
  "is_best_lag": 0,
201
+ "oos_pearson": 0.5487563903427952,
202
  "oos_pearson_sign": "positive",
203
+ "oos_n_obs": 141,
204
+ "oos_partial_corr": 0.45592625353736893,
205
+ "oos_rolling_std": 0.15272683664076614,
206
  "oos_frozen_lag": 0,
207
+ "oos_lag_corr_at_frozen": 0.5487563903427952
208
  },
209
  {
210
  "ticker": "BHP",
211
  "category": "miner_major",
212
  "selection_source": "screener",
213
  "rank": 8,
214
+ "score_composite": 0.6478,
215
+ "is_pearson": 0.6635622138870493,
216
  "is_n_obs": 312,
217
  "is_best_lag": 0,
218
+ "oos_pearson": 0.5393694863399422,
219
  "oos_pearson_sign": "positive",
220
+ "oos_n_obs": 141,
221
+ "oos_partial_corr": 0.41269070419122456,
222
+ "oos_rolling_std": 0.18559142511544605,
223
  "oos_frozen_lag": 0,
224
+ "oos_lag_corr_at_frozen": 0.5393694863399422
225
  },
226
  {
227
  "ticker": "LUN.TO",
228
  "category": "miner_mid",
229
  "selection_source": "screener",
230
  "rank": 9,
231
+ "score_composite": 0.6338,
232
+ "is_pearson": 0.6287674535731311,
233
  "is_n_obs": 312,
234
  "is_best_lag": 0,
235
+ "oos_pearson": 0.48823972975553603,
236
  "oos_pearson_sign": "positive",
237
+ "oos_n_obs": 141,
238
+ "oos_partial_corr": 0.39775226596186225,
239
+ "oos_rolling_std": 0.2275092586828792,
240
  "oos_frozen_lag": 0,
241
+ "oos_lag_corr_at_frozen": 0.48823972975553603
242
  },
243
  {
244
  "ticker": "XME",
245
  "category": "etf_metals",
246
  "selection_source": "screener",
247
  "rank": 12,
248
+ "score_composite": 0.6136,
249
+ "is_pearson": 0.5943770868985795,
250
  "is_n_obs": 312,
251
  "is_best_lag": 0,
252
+ "oos_pearson": 0.47157452355251056,
253
  "oos_pearson_sign": "positive",
254
+ "oos_n_obs": 141,
255
+ "oos_partial_corr": 0.32883502642083673,
256
+ "oos_rolling_std": 0.17774006238393442,
257
  "oos_frozen_lag": 0,
258
+ "oos_lag_corr_at_frozen": 0.47157452355251056
259
  },
260
  {
261
  "ticker": "FXA",
262
  "category": "macro_currency",
263
  "selection_source": "screener",
264
  "rank": 17,
265
+ "score_composite": 0.5763,
266
+ "is_pearson": 0.5554644517228883,
267
  "is_n_obs": 312,
268
  "is_best_lag": 0,
269
+ "oos_pearson": 0.4876982661494697,
270
  "oos_pearson_sign": "positive",
271
+ "oos_n_obs": 141,
272
+ "oos_partial_corr": 0.30302585615698363,
273
+ "oos_rolling_std": 0.11582806074717024,
274
  "oos_frozen_lag": 0,
275
+ "oos_lag_corr_at_frozen": 0.4876982661494697
276
  },
277
  {
278
  "ticker": "VALE",
 
280
  "selection_source": "screener",
281
  "rank": 25,
282
  "score_composite": 0.561,
283
+ "is_pearson": 0.6012555157666103,
284
  "is_n_obs": 312,
285
  "is_best_lag": 0,
286
+ "oos_pearson": 0.44658191248463813,
287
  "oos_pearson_sign": "positive",
288
+ "oos_n_obs": 141,
289
+ "oos_partial_corr": 0.28800956163151104,
290
+ "oos_rolling_std": 0.19298190394297499,
291
  "oos_frozen_lag": 0,
292
+ "oos_lag_corr_at_frozen": 0.44658191248463813
293
  },
294
  {
295
  "ticker": "GXC",
296
  "category": "macro_china",
297
  "selection_source": "screener",
298
  "rank": 35,
299
+ "score_composite": 0.5124,
300
+ "is_pearson": 0.5168832599054346,
301
  "is_n_obs": 312,
302
  "is_best_lag": 0,
303
+ "oos_pearson": 0.43581070140912315,
304
  "oos_pearson_sign": "positive",
305
+ "oos_n_obs": 141,
306
+ "oos_partial_corr": 0.30889120868524156,
307
+ "oos_rolling_std": 0.12280568468881828,
308
  "oos_frozen_lag": 0,
309
+ "oos_lag_corr_at_frozen": 0.43581070140912315
310
  },
311
  {
312
  "ticker": "CEW",
313
  "category": "macro_em",
314
  "selection_source": "screener",
315
  "rank": 37,
316
+ "score_composite": 0.5057,
317
+ "is_pearson": 0.5536216688550951,
318
  "is_n_obs": 312,
319
  "is_best_lag": 0,
320
+ "oos_pearson": 0.3985481291989441,
321
  "oos_pearson_sign": "positive",
322
+ "oos_n_obs": 141,
323
+ "oos_partial_corr": 0.17175256592658594,
324
+ "oos_rolling_std": 0.14250725378780832,
325
  "oos_frozen_lag": 0,
326
+ "oos_lag_corr_at_frozen": 0.3985481291989441
327
  },
328
  {
329
  "ticker": "PL=F",
330
  "category": "commodity_precious",
331
  "selection_source": "screener",
332
  "rank": 44,
333
+ "score_composite": 0.4789,
334
  "is_pearson": 0.48368775603200487,
335
  "is_n_obs": 312,
336
  "is_best_lag": 0,
337
+ "oos_pearson": 0.4683634854783861,
338
  "oos_pearson_sign": "positive",
339
+ "oos_n_obs": 141,
340
+ "oos_partial_corr": 0.39111085029770926,
341
+ "oos_rolling_std": 0.10455541201142945,
342
  "oos_frozen_lag": 0,
343
+ "oos_lag_corr_at_frozen": 0.4683634854783861
344
  },
345
  {
346
  "ticker": "ALI=F",
347
  "category": "commodity_base",
348
  "selection_source": "screener",
349
  "rank": 82,
350
+ "score_composite": 0.386,
351
  "is_pearson": 0.5018667089390725,
352
  "is_n_obs": 312,
353
  "is_best_lag": 0,
354
+ "oos_pearson": 0.41765145706182727,
355
  "oos_pearson_sign": "positive",
356
+ "oos_n_obs": 141,
357
+ "oos_partial_corr": 0.37763895895846994,
358
+ "oos_rolling_std": 0.13058008151506226,
359
  "oos_frozen_lag": 0,
360
+ "oos_lag_corr_at_frozen": 0.41765145706182727
361
  }
362
  ]
363
  }
deep_learning/data/embeddings.py CHANGED
@@ -180,6 +180,58 @@ def bytes_to_embedding(data: bytes, dim: int = _EMBEDDING_DIM) -> np.ndarray:
180
  # DB backfill logic
181
  # ---------------------------------------------------------------------------
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  def backfill_embeddings(
184
  *,
185
  days: int = 180,
@@ -193,7 +245,7 @@ def backfill_embeddings(
193
  1. Query news_processed rows that lack a corresponding news_embeddings row.
194
  2. Extract CLS embeddings in batches.
195
  3. Fit or load PCA model.
196
- 4. Store reduced embeddings in news_embeddings table.
197
  """
198
  from app.db import SessionLocal
199
  from app.models import NewsEmbedding, NewsProcessed, NewsRaw
@@ -205,8 +257,6 @@ def backfill_embeddings(
205
  stats = {"embedded": 0, "skipped": 0, "pca_fitted": False}
206
 
207
  with SessionLocal() as session:
208
- from sqlalchemy import text as sa_text
209
-
210
  cutoff = datetime.now(timezone.utc) - timedelta(days=days)
211
 
212
  rows = (
@@ -258,32 +308,31 @@ def backfill_embeddings(
258
 
259
  reduced = reduce_embeddings(full_embeddings, pca)
260
 
261
- try:
262
- from app.models import NewsEmbedding
263
- except ImportError:
264
- logger.error("NewsEmbedding model not found - run DB migration first")
265
- return stats
266
-
267
- for idx, article_id in enumerate(ids):
268
- existing = session.query(NewsEmbedding).filter(
269
- NewsEmbedding.news_processed_id == article_id
270
- ).first()
271
- if existing:
272
- stats["skipped"] += 1
273
- continue
274
-
275
- emb = NewsEmbedding(
276
- news_processed_id=article_id,
277
- embedding_full=embedding_to_bytes(full_embeddings[idx]),
278
- embedding_pca=embedding_to_bytes(reduced[idx]),
279
- pca_version=f"pca{pca_dim}_v1",
 
 
 
 
 
280
  )
281
- session.add(emb)
282
- stats["embedded"] += 1
283
-
284
- if stats["embedded"] % 200 == 0:
285
- session.commit()
286
- logger.info("Committed %d embeddings so far", stats["embedded"])
287
 
288
  session.commit()
289
 
 
180
  # DB backfill logic
181
  # ---------------------------------------------------------------------------
182
 
183
+ def _insert_embedding_batch(
184
+ session,
185
+ payloads: Sequence[dict[str, object]],
186
+ ) -> tuple[int, int]:
187
+ """Insert a batch without per-row existence queries.
188
+
189
+ ``news_processed_id`` is unique, so PostgreSQL and SQLite can safely
190
+ ignore a concurrent duplicate at the database boundary. The fallback
191
+ keeps the function usable with other SQLAlchemy dialects while still
192
+ reducing existence checks to one query per batch.
193
+
194
+ Returns:
195
+ ``(inserted_count, skipped_count)`` for the attempted payloads.
196
+ """
197
+ if not payloads:
198
+ return 0, 0
199
+
200
+ from app.models import NewsEmbedding
201
+
202
+ dialect = session.get_bind().dialect.name
203
+ if dialect == "postgresql":
204
+ from sqlalchemy.dialects.postgresql import insert as dialect_insert
205
+ elif dialect == "sqlite":
206
+ from sqlalchemy.dialects.sqlite import insert as dialect_insert
207
+ else:
208
+ article_ids = [int(payload["news_processed_id"]) for payload in payloads]
209
+ existing_ids = {
210
+ int(row[0])
211
+ for row in session.query(NewsEmbedding.news_processed_id)
212
+ .filter(NewsEmbedding.news_processed_id.in_(article_ids))
213
+ .all()
214
+ }
215
+ pending = [
216
+ NewsEmbedding(**payload)
217
+ for payload in payloads
218
+ if int(payload["news_processed_id"]) not in existing_ids
219
+ ]
220
+ session.add_all(pending)
221
+ return len(pending), len(payloads) - len(pending)
222
+
223
+ statement = (
224
+ dialect_insert(NewsEmbedding)
225
+ .values(list(payloads))
226
+ .on_conflict_do_nothing(
227
+ index_elements=[NewsEmbedding.news_processed_id]
228
+ )
229
+ )
230
+ result = session.execute(statement)
231
+ inserted = max(0, int(result.rowcount or 0))
232
+ return inserted, len(payloads) - inserted
233
+
234
+
235
  def backfill_embeddings(
236
  *,
237
  days: int = 180,
 
245
  1. Query news_processed rows that lack a corresponding news_embeddings row.
246
  2. Extract CLS embeddings in batches.
247
  3. Fit or load PCA model.
248
+ 4. Store reduced embeddings in race-safe batches.
249
  """
250
  from app.db import SessionLocal
251
  from app.models import NewsEmbedding, NewsProcessed, NewsRaw
 
257
  stats = {"embedded": 0, "skipped": 0, "pca_fitted": False}
258
 
259
  with SessionLocal() as session:
 
 
260
  cutoff = datetime.now(timezone.utc) - timedelta(days=days)
261
 
262
  rows = (
 
308
 
309
  reduced = reduce_embeddings(full_embeddings, pca)
310
 
311
+ payloads = [
312
+ {
313
+ "news_processed_id": article_id,
314
+ "embedding_full": embedding_to_bytes(full_embeddings[idx]),
315
+ "embedding_pca": embedding_to_bytes(reduced[idx]),
316
+ "pca_version": f"pca{pca_dim}_v1",
317
+ }
318
+ for idx, article_id in enumerate(ids)
319
+ ]
320
+
321
+ # Keep transactions bounded while reducing the previous N existence
322
+ # queries to one database-level upsert per persistence batch.
323
+ persist_batch_size = 200
324
+ for start in range(0, len(payloads), persist_batch_size):
325
+ batch = payloads[start : start + persist_batch_size]
326
+ inserted, skipped = _insert_embedding_batch(session, batch)
327
+ stats["embedded"] += inserted
328
+ stats["skipped"] += skipped
329
+ session.commit()
330
+ logger.info(
331
+ "Committed embedding batch: %d inserted, %d skipped, %d total",
332
+ inserted,
333
+ skipped,
334
+ stats["embedded"],
335
  )
 
 
 
 
 
 
336
 
337
  session.commit()
338