Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6880eb92f5 | |||
| 9e2edd590c | |||
| b5c2edf346 |
Binary file not shown.
@@ -1,871 +0,0 @@
|
|||||||
iter Logloss
|
|
||||||
0 0.6924099937
|
|
||||||
1 0.6916660956
|
|
||||||
2 0.691108145
|
|
||||||
3 0.6904585078
|
|
||||||
4 0.689812816
|
|
||||||
5 0.689192261
|
|
||||||
6 0.6886032715
|
|
||||||
7 0.6880706742
|
|
||||||
8 0.6876192378
|
|
||||||
9 0.6870868859
|
|
||||||
10 0.6865493528
|
|
||||||
11 0.686105086
|
|
||||||
12 0.6856345086
|
|
||||||
13 0.6852027185
|
|
||||||
14 0.6848238481
|
|
||||||
15 0.6844045699
|
|
||||||
16 0.6840077621
|
|
||||||
17 0.6836197496
|
|
||||||
18 0.6832475033
|
|
||||||
19 0.6829012069
|
|
||||||
20 0.6825880966
|
|
||||||
21 0.6822424968
|
|
||||||
22 0.6819180513
|
|
||||||
23 0.6816384467
|
|
||||||
24 0.6813262593
|
|
||||||
25 0.6810353411
|
|
||||||
26 0.6808138172
|
|
||||||
27 0.6805550049
|
|
||||||
28 0.680347991
|
|
||||||
29 0.680089679
|
|
||||||
30 0.6798451919
|
|
||||||
31 0.6796090443
|
|
||||||
32 0.6793890865
|
|
||||||
33 0.6791683772
|
|
||||||
34 0.6789766369
|
|
||||||
35 0.6787930242
|
|
||||||
36 0.6786087714
|
|
||||||
37 0.6784161299
|
|
||||||
38 0.6782227897
|
|
||||||
39 0.6780242369
|
|
||||||
40 0.6778499631
|
|
||||||
41 0.6776975784
|
|
||||||
42 0.6775231674
|
|
||||||
43 0.6773582124
|
|
||||||
44 0.6772234666
|
|
||||||
45 0.6770659843
|
|
||||||
46 0.6769049529
|
|
||||||
47 0.6767664194
|
|
||||||
48 0.6766584917
|
|
||||||
49 0.6765507257
|
|
||||||
50 0.6764489911
|
|
||||||
51 0.6763947956
|
|
||||||
52 0.6762778712
|
|
||||||
53 0.6761865366
|
|
||||||
54 0.6760679685
|
|
||||||
55 0.6759774874
|
|
||||||
56 0.6758500622
|
|
||||||
57 0.6757625065
|
|
||||||
58 0.6756876412
|
|
||||||
59 0.6756151069
|
|
||||||
60 0.6755303655
|
|
||||||
61 0.6754565036
|
|
||||||
62 0.6753738983
|
|
||||||
63 0.6752897299
|
|
||||||
64 0.6752115539
|
|
||||||
65 0.6751595431
|
|
||||||
66 0.6750764658
|
|
||||||
67 0.6750179194
|
|
||||||
68 0.6749408803
|
|
||||||
69 0.6748795802
|
|
||||||
70 0.674790372
|
|
||||||
71 0.6747239773
|
|
||||||
72 0.6746701254
|
|
||||||
73 0.6746120937
|
|
||||||
74 0.6745550085
|
|
||||||
75 0.6744855074
|
|
||||||
76 0.6744264172
|
|
||||||
77 0.674381715
|
|
||||||
78 0.6743331681
|
|
||||||
79 0.67428564
|
|
||||||
80 0.6742202413
|
|
||||||
81 0.6741620971
|
|
||||||
82 0.6741109453
|
|
||||||
83 0.6740556003
|
|
||||||
84 0.6740146772
|
|
||||||
85 0.673983295
|
|
||||||
86 0.6739595301
|
|
||||||
87 0.6739336659
|
|
||||||
88 0.673890361
|
|
||||||
89 0.673863586
|
|
||||||
90 0.6738190616
|
|
||||||
91 0.6737799295
|
|
||||||
92 0.6737364374
|
|
||||||
93 0.6737093719
|
|
||||||
94 0.6736630475
|
|
||||||
95 0.67364367
|
|
||||||
96 0.6735998081
|
|
||||||
97 0.6735526984
|
|
||||||
98 0.6735012924
|
|
||||||
99 0.6734818024
|
|
||||||
100 0.6734379341
|
|
||||||
101 0.6734059869
|
|
||||||
102 0.6733740852
|
|
||||||
103 0.6733330971
|
|
||||||
104 0.6733060254
|
|
||||||
105 0.6732755898
|
|
||||||
106 0.6732294722
|
|
||||||
107 0.6732035176
|
|
||||||
108 0.673196437
|
|
||||||
109 0.6731652709
|
|
||||||
110 0.673138808
|
|
||||||
111 0.6731062725
|
|
||||||
112 0.6730726625
|
|
||||||
113 0.6730285927
|
|
||||||
114 0.6729872702
|
|
||||||
115 0.6729721425
|
|
||||||
116 0.6729564624
|
|
||||||
117 0.6729312424
|
|
||||||
118 0.6729354345
|
|
||||||
119 0.6729085401
|
|
||||||
120 0.6728898322
|
|
||||||
121 0.6728773638
|
|
||||||
122 0.6728618874
|
|
||||||
123 0.6728540413
|
|
||||||
124 0.6728441291
|
|
||||||
125 0.672815631
|
|
||||||
126 0.6728082021
|
|
||||||
127 0.6727900064
|
|
||||||
128 0.6727649552
|
|
||||||
129 0.6727467657
|
|
||||||
130 0.6727396032
|
|
||||||
131 0.6727245271
|
|
||||||
132 0.6726955143
|
|
||||||
133 0.67269209
|
|
||||||
134 0.672677932
|
|
||||||
135 0.6726540285
|
|
||||||
136 0.6726288583
|
|
||||||
137 0.6725863431
|
|
||||||
138 0.6725837967
|
|
||||||
139 0.6725772977
|
|
||||||
140 0.6725685594
|
|
||||||
141 0.6725553829
|
|
||||||
142 0.6725484347
|
|
||||||
143 0.6725306172
|
|
||||||
144 0.672543149
|
|
||||||
145 0.6725196247
|
|
||||||
146 0.6725226452
|
|
||||||
147 0.6725056913
|
|
||||||
148 0.6724771476
|
|
||||||
149 0.6724439435
|
|
||||||
150 0.672442532
|
|
||||||
151 0.6724303064
|
|
||||||
152 0.6724235788
|
|
||||||
153 0.6724294499
|
|
||||||
154 0.6724285935
|
|
||||||
155 0.6724172017
|
|
||||||
156 0.6724130745
|
|
||||||
157 0.6723860878
|
|
||||||
158 0.6723707604
|
|
||||||
159 0.6723566111
|
|
||||||
160 0.6723469906
|
|
||||||
161 0.6723287161
|
|
||||||
162 0.6723155898
|
|
||||||
163 0.6722970834
|
|
||||||
164 0.6722872244
|
|
||||||
165 0.6722800481
|
|
||||||
166 0.6722550973
|
|
||||||
167 0.6722394313
|
|
||||||
168 0.6722204135
|
|
||||||
169 0.6721982148
|
|
||||||
170 0.6721971176
|
|
||||||
171 0.6721880705
|
|
||||||
172 0.672179176
|
|
||||||
173 0.6721769709
|
|
||||||
174 0.6721693215
|
|
||||||
175 0.6721581386
|
|
||||||
176 0.6721638661
|
|
||||||
177 0.6721598475
|
|
||||||
178 0.6721433342
|
|
||||||
179 0.6721335599
|
|
||||||
180 0.6721300594
|
|
||||||
181 0.6721153533
|
|
||||||
182 0.6721076397
|
|
||||||
183 0.6721009911
|
|
||||||
184 0.6720999252
|
|
||||||
185 0.6720953028
|
|
||||||
186 0.6720942505
|
|
||||||
187 0.6720856237
|
|
||||||
188 0.6720876136
|
|
||||||
189 0.6720880182
|
|
||||||
190 0.6720743856
|
|
||||||
191 0.6720598415
|
|
||||||
192 0.6720563492
|
|
||||||
193 0.6720389527
|
|
||||||
194 0.6720317324
|
|
||||||
195 0.672000736
|
|
||||||
196 0.6719895017
|
|
||||||
197 0.6719725302
|
|
||||||
198 0.6719770493
|
|
||||||
199 0.6719667172
|
|
||||||
200 0.6719511616
|
|
||||||
201 0.6719427289
|
|
||||||
202 0.6719299116
|
|
||||||
203 0.6719106583
|
|
||||||
204 0.6718967065
|
|
||||||
205 0.671890967
|
|
||||||
206 0.6718896293
|
|
||||||
207 0.6718883534
|
|
||||||
208 0.6718827289
|
|
||||||
209 0.6718763224
|
|
||||||
210 0.67187262
|
|
||||||
211 0.6718590402
|
|
||||||
212 0.6718455115
|
|
||||||
213 0.6718253747
|
|
||||||
214 0.671794877
|
|
||||||
215 0.6717873786
|
|
||||||
216 0.6717765089
|
|
||||||
217 0.6717616726
|
|
||||||
218 0.6717499215
|
|
||||||
219 0.6717326052
|
|
||||||
220 0.6717161937
|
|
||||||
221 0.6717056951
|
|
||||||
222 0.6717021438
|
|
||||||
223 0.6716868488
|
|
||||||
224 0.6716751909
|
|
||||||
225 0.671670116
|
|
||||||
226 0.6716558757
|
|
||||||
227 0.6716559962
|
|
||||||
228 0.6716487875
|
|
||||||
229 0.6716427451
|
|
||||||
230 0.6716323255
|
|
||||||
231 0.6716303547
|
|
||||||
232 0.6716309509
|
|
||||||
233 0.6716215401
|
|
||||||
234 0.6716162103
|
|
||||||
235 0.6716135097
|
|
||||||
236 0.6716156696
|
|
||||||
237 0.6716020054
|
|
||||||
238 0.6715921704
|
|
||||||
239 0.6715804466
|
|
||||||
240 0.6715882966
|
|
||||||
241 0.6715753942
|
|
||||||
242 0.6715752261
|
|
||||||
243 0.6715625509
|
|
||||||
244 0.6715628214
|
|
||||||
245 0.6715601629
|
|
||||||
246 0.6715576255
|
|
||||||
247 0.6715550274
|
|
||||||
248 0.6715448645
|
|
||||||
249 0.6715308166
|
|
||||||
250 0.671519334
|
|
||||||
251 0.6715184071
|
|
||||||
252 0.6715163019
|
|
||||||
253 0.6715096094
|
|
||||||
254 0.6714992963
|
|
||||||
255 0.6714917256
|
|
||||||
256 0.671477406
|
|
||||||
257 0.6714741542
|
|
||||||
258 0.6714576155
|
|
||||||
259 0.6714473645
|
|
||||||
260 0.6714427232
|
|
||||||
261 0.6714364275
|
|
||||||
262 0.6714339587
|
|
||||||
263 0.6714336287
|
|
||||||
264 0.6714283568
|
|
||||||
265 0.6714271895
|
|
||||||
266 0.671413471
|
|
||||||
267 0.6714072396
|
|
||||||
268 0.6714002677
|
|
||||||
269 0.6714001163
|
|
||||||
270 0.6713933952
|
|
||||||
271 0.6713926761
|
|
||||||
272 0.6713836619
|
|
||||||
273 0.6713772112
|
|
||||||
274 0.6713603715
|
|
||||||
275 0.6713560246
|
|
||||||
276 0.6713837913
|
|
||||||
277 0.6713684274
|
|
||||||
278 0.6713619356
|
|
||||||
279 0.6713584836
|
|
||||||
280 0.6713673572
|
|
||||||
281 0.6713625568
|
|
||||||
282 0.6713542652
|
|
||||||
283 0.6713512017
|
|
||||||
284 0.671342038
|
|
||||||
285 0.6713279798
|
|
||||||
286 0.6713123285
|
|
||||||
287 0.6713035326
|
|
||||||
288 0.6713022203
|
|
||||||
289 0.671296041
|
|
||||||
290 0.6712829551
|
|
||||||
291 0.6712769751
|
|
||||||
292 0.6712702915
|
|
||||||
293 0.6712379343
|
|
||||||
294 0.6712192006
|
|
||||||
295 0.6712074061
|
|
||||||
296 0.6711953324
|
|
||||||
297 0.6711891001
|
|
||||||
298 0.6711870526
|
|
||||||
299 0.6711812809
|
|
||||||
300 0.6711768946
|
|
||||||
301 0.6711845012
|
|
||||||
302 0.6711869636
|
|
||||||
303 0.671186884
|
|
||||||
304 0.6711890401
|
|
||||||
305 0.6711868603
|
|
||||||
306 0.6711900892
|
|
||||||
307 0.6711884242
|
|
||||||
308 0.6711837119
|
|
||||||
309 0.6711766645
|
|
||||||
310 0.671172959
|
|
||||||
311 0.6711740433
|
|
||||||
312 0.6711715069
|
|
||||||
313 0.6711589843
|
|
||||||
314 0.6711446402
|
|
||||||
315 0.6711415366
|
|
||||||
316 0.6711359351
|
|
||||||
317 0.671143361
|
|
||||||
318 0.6711353638
|
|
||||||
319 0.6711444387
|
|
||||||
320 0.6711487352
|
|
||||||
321 0.67114436
|
|
||||||
322 0.6711444722
|
|
||||||
323 0.6711325635
|
|
||||||
324 0.6711269403
|
|
||||||
325 0.6711154078
|
|
||||||
326 0.6711203043
|
|
||||||
327 0.6711241333
|
|
||||||
328 0.6711213497
|
|
||||||
329 0.6711231641
|
|
||||||
330 0.6711049215
|
|
||||||
331 0.6711031963
|
|
||||||
332 0.6710996314
|
|
||||||
333 0.6710867309
|
|
||||||
334 0.6710914578
|
|
||||||
335 0.6710929585
|
|
||||||
336 0.6710984779
|
|
||||||
337 0.6710923199
|
|
||||||
338 0.6710893917
|
|
||||||
339 0.6710923306
|
|
||||||
340 0.6710927901
|
|
||||||
341 0.6711092802
|
|
||||||
342 0.6711012995
|
|
||||||
343 0.6711015305
|
|
||||||
344 0.6710975574
|
|
||||||
345 0.6710899474
|
|
||||||
346 0.671085152
|
|
||||||
347 0.6710814533
|
|
||||||
348 0.6710701892
|
|
||||||
349 0.67105503
|
|
||||||
350 0.6710527861
|
|
||||||
351 0.6710508715
|
|
||||||
352 0.6710560803
|
|
||||||
353 0.6710465693
|
|
||||||
354 0.6710440741
|
|
||||||
355 0.6710496913
|
|
||||||
356 0.6710404659
|
|
||||||
357 0.6710293986
|
|
||||||
358 0.6710353817
|
|
||||||
359 0.6710271815
|
|
||||||
360 0.6710288077
|
|
||||||
361 0.6710169894
|
|
||||||
362 0.6710119848
|
|
||||||
363 0.6710114775
|
|
||||||
364 0.6710013614
|
|
||||||
365 0.6709985657
|
|
||||||
366 0.6709948954
|
|
||||||
367 0.6709970591
|
|
||||||
368 0.6709739289
|
|
||||||
369 0.6709754911
|
|
||||||
370 0.6709717066
|
|
||||||
371 0.67096845
|
|
||||||
372 0.6709739445
|
|
||||||
373 0.6709728881
|
|
||||||
374 0.6709694284
|
|
||||||
375 0.6709604166
|
|
||||||
376 0.6709605025
|
|
||||||
377 0.6709603727
|
|
||||||
378 0.670944339
|
|
||||||
379 0.6709447187
|
|
||||||
380 0.6709538679
|
|
||||||
381 0.6709640912
|
|
||||||
382 0.6709534847
|
|
||||||
383 0.6709471555
|
|
||||||
384 0.6709506783
|
|
||||||
385 0.6709546729
|
|
||||||
386 0.670930774
|
|
||||||
387 0.6709287322
|
|
||||||
388 0.6709198643
|
|
||||||
389 0.6709220389
|
|
||||||
390 0.6709230923
|
|
||||||
391 0.670930414
|
|
||||||
392 0.6709354296
|
|
||||||
393 0.6709351544
|
|
||||||
394 0.6709414935
|
|
||||||
395 0.6709445943
|
|
||||||
396 0.6709475685
|
|
||||||
397 0.6709533591
|
|
||||||
398 0.6709592222
|
|
||||||
399 0.6709508704
|
|
||||||
400 0.6709479912
|
|
||||||
401 0.6709417519
|
|
||||||
402 0.6709476082
|
|
||||||
403 0.6709480979
|
|
||||||
404 0.6709448724
|
|
||||||
405 0.6709421934
|
|
||||||
406 0.6709386261
|
|
||||||
407 0.6709461564
|
|
||||||
408 0.670934384
|
|
||||||
409 0.6709312987
|
|
||||||
410 0.670931806
|
|
||||||
411 0.6709286111
|
|
||||||
412 0.6709224729
|
|
||||||
413 0.6709236504
|
|
||||||
414 0.6709245901
|
|
||||||
415 0.6709463437
|
|
||||||
416 0.6709567049
|
|
||||||
417 0.670945606
|
|
||||||
418 0.6709479298
|
|
||||||
419 0.6709464351
|
|
||||||
420 0.6709414048
|
|
||||||
421 0.6709414427
|
|
||||||
422 0.6709296343
|
|
||||||
423 0.670924721
|
|
||||||
424 0.670906284
|
|
||||||
425 0.6708996826
|
|
||||||
426 0.6708987677
|
|
||||||
427 0.670909526
|
|
||||||
428 0.6709033226
|
|
||||||
429 0.6708750209
|
|
||||||
430 0.6708752079
|
|
||||||
431 0.6708776566
|
|
||||||
432 0.6708736133
|
|
||||||
433 0.6708754298
|
|
||||||
434 0.6708751084
|
|
||||||
435 0.6708642042
|
|
||||||
436 0.6708610465
|
|
||||||
437 0.6708574768
|
|
||||||
438 0.6708557953
|
|
||||||
439 0.670871378
|
|
||||||
440 0.6708640187
|
|
||||||
441 0.6708700565
|
|
||||||
442 0.6708667534
|
|
||||||
443 0.6708675383
|
|
||||||
444 0.6708740175
|
|
||||||
445 0.6708774523
|
|
||||||
446 0.6708697231
|
|
||||||
447 0.6708614971
|
|
||||||
448 0.6708607946
|
|
||||||
449 0.6708740865
|
|
||||||
450 0.6708729562
|
|
||||||
451 0.6708674017
|
|
||||||
452 0.6708693088
|
|
||||||
453 0.6708712037
|
|
||||||
454 0.6708703905
|
|
||||||
455 0.6708577595
|
|
||||||
456 0.6708493546
|
|
||||||
457 0.6708523777
|
|
||||||
458 0.6708454134
|
|
||||||
459 0.6708404483
|
|
||||||
460 0.6708274771
|
|
||||||
461 0.6708244992
|
|
||||||
462 0.6708344314
|
|
||||||
463 0.6708279081
|
|
||||||
464 0.6708258106
|
|
||||||
465 0.6708049714
|
|
||||||
466 0.670810989
|
|
||||||
467 0.6708212237
|
|
||||||
468 0.6708221741
|
|
||||||
469 0.6708259658
|
|
||||||
470 0.6708159692
|
|
||||||
471 0.6708136212
|
|
||||||
472 0.6708224942
|
|
||||||
473 0.6708363084
|
|
||||||
474 0.670850875
|
|
||||||
475 0.6708527236
|
|
||||||
476 0.6708453401
|
|
||||||
477 0.6708413844
|
|
||||||
478 0.6708364569
|
|
||||||
479 0.6708251774
|
|
||||||
480 0.6708154393
|
|
||||||
481 0.6708111613
|
|
||||||
482 0.6708102339
|
|
||||||
483 0.6707929623
|
|
||||||
484 0.6707900226
|
|
||||||
485 0.6707832384
|
|
||||||
486 0.6707739118
|
|
||||||
487 0.6707737538
|
|
||||||
488 0.6707730234
|
|
||||||
489 0.6707796291
|
|
||||||
490 0.670791408
|
|
||||||
491 0.6707944906
|
|
||||||
492 0.6707835635
|
|
||||||
493 0.6707908928
|
|
||||||
494 0.670796262
|
|
||||||
495 0.6707877825
|
|
||||||
496 0.6707854132
|
|
||||||
497 0.6707756206
|
|
||||||
498 0.6707707899
|
|
||||||
499 0.6707704386
|
|
||||||
500 0.6707621465
|
|
||||||
501 0.6707661931
|
|
||||||
502 0.6707651988
|
|
||||||
503 0.6707607827
|
|
||||||
504 0.670760242
|
|
||||||
505 0.6707506008
|
|
||||||
506 0.6707452886
|
|
||||||
507 0.6707355189
|
|
||||||
508 0.6707312551
|
|
||||||
509 0.6707199485
|
|
||||||
510 0.6707131947
|
|
||||||
511 0.6707154112
|
|
||||||
512 0.6706982346
|
|
||||||
513 0.6706988941
|
|
||||||
514 0.6706989098
|
|
||||||
515 0.670693306
|
|
||||||
516 0.6706944515
|
|
||||||
517 0.6706899688
|
|
||||||
518 0.6706909374
|
|
||||||
519 0.6706855074
|
|
||||||
520 0.6706787779
|
|
||||||
521 0.6706737082
|
|
||||||
522 0.6706761225
|
|
||||||
523 0.670685455
|
|
||||||
524 0.6706693855
|
|
||||||
525 0.6706647216
|
|
||||||
526 0.6706569188
|
|
||||||
527 0.6706549134
|
|
||||||
528 0.6706547978
|
|
||||||
529 0.6706564214
|
|
||||||
530 0.6706559196
|
|
||||||
531 0.6706515072
|
|
||||||
532 0.6706474616
|
|
||||||
533 0.6706424204
|
|
||||||
534 0.6706520008
|
|
||||||
535 0.6706448306
|
|
||||||
536 0.6706415789
|
|
||||||
537 0.6706305359
|
|
||||||
538 0.6706152774
|
|
||||||
539 0.670616585
|
|
||||||
540 0.6705963243
|
|
||||||
541 0.6706027368
|
|
||||||
542 0.6706003522
|
|
||||||
543 0.6706044301
|
|
||||||
544 0.6706047241
|
|
||||||
545 0.6706038235
|
|
||||||
546 0.6706026913
|
|
||||||
547 0.6705845786
|
|
||||||
548 0.6705873967
|
|
||||||
549 0.6705755426
|
|
||||||
550 0.6705715731
|
|
||||||
551 0.6705757153
|
|
||||||
552 0.6705516814
|
|
||||||
553 0.6705530864
|
|
||||||
554 0.6705552479
|
|
||||||
555 0.6705563336
|
|
||||||
556 0.6705718544
|
|
||||||
557 0.6705688384
|
|
||||||
558 0.6705641528
|
|
||||||
559 0.6705628467
|
|
||||||
560 0.670558488
|
|
||||||
561 0.6705544404
|
|
||||||
562 0.6705617451
|
|
||||||
563 0.6705631717
|
|
||||||
564 0.6705636201
|
|
||||||
565 0.6705537522
|
|
||||||
566 0.670555083
|
|
||||||
567 0.6705524541
|
|
||||||
568 0.6705503132
|
|
||||||
569 0.6705354602
|
|
||||||
570 0.6705387012
|
|
||||||
571 0.6705411923
|
|
||||||
572 0.6705390018
|
|
||||||
573 0.6705354939
|
|
||||||
574 0.670531296
|
|
||||||
575 0.6705377163
|
|
||||||
576 0.6705248875
|
|
||||||
577 0.6705252902
|
|
||||||
578 0.6705181562
|
|
||||||
579 0.6705123446
|
|
||||||
580 0.6705128345
|
|
||||||
581 0.6705173712
|
|
||||||
582 0.670541941
|
|
||||||
583 0.6705463243
|
|
||||||
584 0.6705513215
|
|
||||||
585 0.6705455889
|
|
||||||
586 0.6705408087
|
|
||||||
587 0.6705510193
|
|
||||||
588 0.6705456751
|
|
||||||
589 0.6705402427
|
|
||||||
590 0.6705443402
|
|
||||||
591 0.67054441
|
|
||||||
592 0.6705441955
|
|
||||||
593 0.6705319356
|
|
||||||
594 0.6705358843
|
|
||||||
595 0.6705334396
|
|
||||||
596 0.6705320462
|
|
||||||
597 0.6705332043
|
|
||||||
598 0.6705328363
|
|
||||||
599 0.6705315638
|
|
||||||
600 0.6705274435
|
|
||||||
601 0.670509808
|
|
||||||
602 0.6705077789
|
|
||||||
603 0.6705212132
|
|
||||||
604 0.6705098442
|
|
||||||
605 0.6705061509
|
|
||||||
606 0.6705003071
|
|
||||||
607 0.6705045031
|
|
||||||
608 0.6705083194
|
|
||||||
609 0.6705329997
|
|
||||||
610 0.6705269987
|
|
||||||
611 0.6705315607
|
|
||||||
612 0.6705142835
|
|
||||||
613 0.6705165015
|
|
||||||
614 0.6705001061
|
|
||||||
615 0.6705013916
|
|
||||||
616 0.6705037253
|
|
||||||
617 0.67049647
|
|
||||||
618 0.6705005632
|
|
||||||
619 0.6704957943
|
|
||||||
620 0.6704955333
|
|
||||||
621 0.6704961207
|
|
||||||
622 0.6704921459
|
|
||||||
623 0.6704751713
|
|
||||||
624 0.6704753101
|
|
||||||
625 0.6704620888
|
|
||||||
626 0.6704604282
|
|
||||||
627 0.6704663192
|
|
||||||
628 0.6704680085
|
|
||||||
629 0.670453228
|
|
||||||
630 0.6704577785
|
|
||||||
631 0.67046675
|
|
||||||
632 0.6704731863
|
|
||||||
633 0.6704811116
|
|
||||||
634 0.6704839644
|
|
||||||
635 0.6704854798
|
|
||||||
636 0.6704835837
|
|
||||||
637 0.6704736198
|
|
||||||
638 0.6704640242
|
|
||||||
639 0.670465663
|
|
||||||
640 0.6704646829
|
|
||||||
641 0.6704600961
|
|
||||||
642 0.6704643207
|
|
||||||
643 0.6704600533
|
|
||||||
644 0.6704614691
|
|
||||||
645 0.6704728212
|
|
||||||
646 0.6704758731
|
|
||||||
647 0.6704833026
|
|
||||||
648 0.6704767664
|
|
||||||
649 0.6704702727
|
|
||||||
650 0.6704671372
|
|
||||||
651 0.6704699936
|
|
||||||
652 0.6704587989
|
|
||||||
653 0.6704637668
|
|
||||||
654 0.6704653717
|
|
||||||
655 0.6704598273
|
|
||||||
656 0.6704522865
|
|
||||||
657 0.6704558586
|
|
||||||
658 0.6704466331
|
|
||||||
659 0.6704405886
|
|
||||||
660 0.6704463767
|
|
||||||
661 0.6704475216
|
|
||||||
662 0.6704572386
|
|
||||||
663 0.6704658153
|
|
||||||
664 0.6704600945
|
|
||||||
665 0.6704561998
|
|
||||||
666 0.6704535154
|
|
||||||
667 0.6704413781
|
|
||||||
668 0.6704450013
|
|
||||||
669 0.6704422199
|
|
||||||
670 0.67044342
|
|
||||||
671 0.6704415341
|
|
||||||
672 0.6704439539
|
|
||||||
673 0.6704498197
|
|
||||||
674 0.6704452194
|
|
||||||
675 0.6704366524
|
|
||||||
676 0.6704427124
|
|
||||||
677 0.6704395579
|
|
||||||
678 0.6704401246
|
|
||||||
679 0.6704415621
|
|
||||||
680 0.6704341343
|
|
||||||
681 0.6704369615
|
|
||||||
682 0.6704357425
|
|
||||||
683 0.6704294622
|
|
||||||
684 0.6704289794
|
|
||||||
685 0.6704272409
|
|
||||||
686 0.6704101162
|
|
||||||
687 0.6704069439
|
|
||||||
688 0.6704100747
|
|
||||||
689 0.6704122261
|
|
||||||
690 0.6704137826
|
|
||||||
691 0.6704207952
|
|
||||||
692 0.6704154834
|
|
||||||
693 0.6704253514
|
|
||||||
694 0.6704155636
|
|
||||||
695 0.6704141298
|
|
||||||
696 0.6704207635
|
|
||||||
697 0.6704268341
|
|
||||||
698 0.6704243126
|
|
||||||
699 0.6704235165
|
|
||||||
700 0.6704257736
|
|
||||||
701 0.6704247758
|
|
||||||
702 0.6704331799
|
|
||||||
703 0.6704252722
|
|
||||||
704 0.6704146644
|
|
||||||
705 0.6704164122
|
|
||||||
706 0.6704118954
|
|
||||||
707 0.6704043129
|
|
||||||
708 0.6703978198
|
|
||||||
709 0.6703935976
|
|
||||||
710 0.6703839683
|
|
||||||
711 0.6703843723
|
|
||||||
712 0.6703879502
|
|
||||||
713 0.6703895978
|
|
||||||
714 0.6703894359
|
|
||||||
715 0.6703928777
|
|
||||||
716 0.6703933128
|
|
||||||
717 0.6703844355
|
|
||||||
718 0.6703825151
|
|
||||||
719 0.6703983542
|
|
||||||
720 0.670399556
|
|
||||||
721 0.6703931808
|
|
||||||
722 0.6703886918
|
|
||||||
723 0.6703847574
|
|
||||||
724 0.6703885941
|
|
||||||
725 0.6703788615
|
|
||||||
726 0.6703799906
|
|
||||||
727 0.6703774518
|
|
||||||
728 0.6703783496
|
|
||||||
729 0.6703648854
|
|
||||||
730 0.6703716654
|
|
||||||
731 0.6703550938
|
|
||||||
732 0.6703467057
|
|
||||||
733 0.6703484503
|
|
||||||
734 0.6703549183
|
|
||||||
735 0.6703501504
|
|
||||||
736 0.6703672622
|
|
||||||
737 0.6703560249
|
|
||||||
738 0.6703547155
|
|
||||||
739 0.6703593236
|
|
||||||
740 0.6703606827
|
|
||||||
741 0.6703511404
|
|
||||||
742 0.6703431646
|
|
||||||
743 0.6703475116
|
|
||||||
744 0.6703483634
|
|
||||||
745 0.6703475713
|
|
||||||
746 0.670360457
|
|
||||||
747 0.6703664352
|
|
||||||
748 0.6703617612
|
|
||||||
749 0.6703669926
|
|
||||||
750 0.6703670837
|
|
||||||
751 0.6703706628
|
|
||||||
752 0.670369618
|
|
||||||
753 0.6703692351
|
|
||||||
754 0.6703624433
|
|
||||||
755 0.6703686285
|
|
||||||
756 0.6703598432
|
|
||||||
757 0.6703618766
|
|
||||||
758 0.6703694148
|
|
||||||
759 0.6703683652
|
|
||||||
760 0.6703604855
|
|
||||||
761 0.6703758987
|
|
||||||
762 0.6703773302
|
|
||||||
763 0.6703641028
|
|
||||||
764 0.6703649602
|
|
||||||
765 0.6703567811
|
|
||||||
766 0.6703544688
|
|
||||||
767 0.6703611821
|
|
||||||
768 0.6703527821
|
|
||||||
769 0.6703523616
|
|
||||||
770 0.6703616298
|
|
||||||
771 0.6703603551
|
|
||||||
772 0.6703675655
|
|
||||||
773 0.6703582411
|
|
||||||
774 0.6703581437
|
|
||||||
775 0.6703551885
|
|
||||||
776 0.6703608491
|
|
||||||
777 0.6703674554
|
|
||||||
778 0.6703679619
|
|
||||||
779 0.6703701757
|
|
||||||
780 0.6703603462
|
|
||||||
781 0.670359801
|
|
||||||
782 0.6703523669
|
|
||||||
783 0.6703365674
|
|
||||||
784 0.6703486118
|
|
||||||
785 0.6703450011
|
|
||||||
786 0.6703473135
|
|
||||||
787 0.670350998
|
|
||||||
788 0.6703417767
|
|
||||||
789 0.6703349821
|
|
||||||
790 0.6703457717
|
|
||||||
791 0.6703506266
|
|
||||||
792 0.6703596395
|
|
||||||
793 0.6703799895
|
|
||||||
794 0.6703687687
|
|
||||||
795 0.6703780675
|
|
||||||
796 0.670374835
|
|
||||||
797 0.6703831387
|
|
||||||
798 0.670377656
|
|
||||||
799 0.6703689741
|
|
||||||
800 0.6703709756
|
|
||||||
801 0.6703737517
|
|
||||||
802 0.6703818964
|
|
||||||
803 0.6703812173
|
|
||||||
804 0.6703960068
|
|
||||||
805 0.6703976729
|
|
||||||
806 0.6704024604
|
|
||||||
807 0.6704085008
|
|
||||||
808 0.6704076633
|
|
||||||
809 0.6704111719
|
|
||||||
810 0.6704220803
|
|
||||||
811 0.6704265011
|
|
||||||
812 0.6704251162
|
|
||||||
813 0.6704375472
|
|
||||||
814 0.6704319336
|
|
||||||
815 0.670437614
|
|
||||||
816 0.6704554331
|
|
||||||
817 0.6704603317
|
|
||||||
818 0.6704548042
|
|
||||||
819 0.6704502654
|
|
||||||
820 0.6704512072
|
|
||||||
821 0.6704433481
|
|
||||||
822 0.6704095112
|
|
||||||
823 0.6704086019
|
|
||||||
824 0.6703987131
|
|
||||||
825 0.6704019708
|
|
||||||
826 0.6704046556
|
|
||||||
827 0.6704091961
|
|
||||||
828 0.6704103204
|
|
||||||
829 0.6704074257
|
|
||||||
830 0.6704115335
|
|
||||||
831 0.6704041275
|
|
||||||
832 0.6704004556
|
|
||||||
833 0.6704037097
|
|
||||||
834 0.6704035477
|
|
||||||
835 0.6704025281
|
|
||||||
836 0.6704024549
|
|
||||||
837 0.670405721
|
|
||||||
838 0.6703983189
|
|
||||||
839 0.6704035256
|
|
||||||
840 0.6704047613
|
|
||||||
841 0.6704139617
|
|
||||||
842 0.6704066193
|
|
||||||
843 0.670402892
|
|
||||||
844 0.6704081961
|
|
||||||
845 0.6704029862
|
|
||||||
846 0.6704014281
|
|
||||||
847 0.6704036115
|
|
||||||
848 0.6704016777
|
|
||||||
849 0.6704085392
|
|
||||||
850 0.6704042952
|
|
||||||
851 0.670394789
|
|
||||||
852 0.6703885644
|
|
||||||
853 0.6703946813
|
|
||||||
854 0.6704042137
|
|
||||||
855 0.6704077517
|
|
||||||
856 0.6704118698
|
|
||||||
857 0.6704114259
|
|
||||||
858 0.6704157567
|
|
||||||
859 0.6703837005
|
|
||||||
860 0.6703829482
|
|
||||||
861 0.6703818491
|
|
||||||
862 0.6703826129
|
|
||||||
863 0.6703834487
|
|
||||||
864 0.6703868275
|
|
||||||
865 0.6703853357
|
|
||||||
866 0.670450644
|
|
||||||
867 0.6704556991
|
|
||||||
868 0.6704535742
|
|
||||||
869 0.6704495915
|
|
||||||
|
@@ -1,902 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"match_id": "2b1jyd72hogojec5j50fd9gr8",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "1b2chhfsohmulm85sb95y189g",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "X"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "dg84sd1wkmtfrtdm9od7wy7f8",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "Alt",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "dydrdtrxi3dsomph1at54jaxg",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.0264,
|
|
||||||
"avg_confidence": 69.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 2.0,
|
|
||||||
"avg_edge": 0.1559,
|
|
||||||
"avg_confidence": 71.4
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "2.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "b6uzz042mizu0dqpci538z4lw",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.1515,
|
|
||||||
"avg_confidence": 66.3
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 2.0,
|
|
||||||
"avg_edge": 0.1103,
|
|
||||||
"avg_confidence": 64.45
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "dmp0q35bpbb7rt11opg5mwzkk",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.0756,
|
|
||||||
"avg_confidence": 67.5
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "6opr8muwfdoosfpnkm4gbb190",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 2.0,
|
|
||||||
"avg_edge": 0.384,
|
|
||||||
"avg_confidence": 80.8
|
|
||||||
},
|
|
||||||
"v25_main": "Tek",
|
|
||||||
"v26_main": "KG Var"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "a6dxspn0akrnf19mno8z83yms",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 1.001,
|
|
||||||
"avg_confidence": 72.5
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "9qrr3sya0mlfusmqlushjask4",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "2",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "ytsc38rm4j22govgwo3as6j8",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 1.1251,
|
|
||||||
"avg_confidence": 67.6
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "X2",
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "68oi5t06w15a9b8wt8rsl8gk",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "KG Yok",
|
|
||||||
"v26_main": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "bsw4axalza4idsop3qio295hw",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.0113,
|
|
||||||
"avg_confidence": 69.6
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.9893,
|
|
||||||
"avg_confidence": 59.1
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "btsq93obda94w3be5bogr0etw",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "ajgyxy5iqiprjkr2l98np982c",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.0166,
|
|
||||||
"avg_confidence": 69.5
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "4ocwqbbw37kikqj38hf4txes4",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.0755,
|
|
||||||
"avg_confidence": 67.5
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 2.0,
|
|
||||||
"avg_edge": 0.0585,
|
|
||||||
"avg_confidence": 74.65
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "dogkmluzn54lg0q6yuom1l53o",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 1.2566,
|
|
||||||
"avg_confidence": 81.7
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "cfar57gsu6hy770n7e58u8duc",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "cbg4zpl58ahr6r22d6syo0wt0",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "1X",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "5psd7wioj63day6cylflbzf2s",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "3.5 Alt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "570ybzbhwkgvef82h49c27eac",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00c7ift",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "5vaa5dl47mw2t6xf3t3qjhvro",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "9pqea93iqumda82hj1sfxd7v8",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "cjs180a7sqxkurwo18rp9aeqc",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "3j2wsty5be0d432ozvuj5w2l0",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 1.1947,
|
|
||||||
"avg_confidence": 71.4
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.091,
|
|
||||||
"avg_confidence": 68.6
|
|
||||||
},
|
|
||||||
"v25_main": "1X",
|
|
||||||
"v26_main": "3.5 Alt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "3toy2ctfu4kce9ojkngofzf2s",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 2.0,
|
|
||||||
"avg_edge": 0.0557,
|
|
||||||
"avg_confidence": 67.25
|
|
||||||
},
|
|
||||||
"v25_main": "\u00c7ift",
|
|
||||||
"v26_main": "KG Yok"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "5uvgaveseimkwkkj5lf0iv9xw",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "X"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "8i2hjmknzgjkhfhn9pehrhy50",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00c7ift",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "rfhmiaml1h9taxebpvmaijo4",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00c7ift",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "25sjaorwswwaeu3p2hd3p747o",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "X"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "d8e65nskkka2b6s1cr3rks1ec",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.7042,
|
|
||||||
"avg_confidence": 61.7
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "fbxbk21jlzgmf0dc385mu6fo",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.5759,
|
|
||||||
"avg_confidence": 57.1
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "9i8ikoed9f94nt90w8uy6y710",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.8784,
|
|
||||||
"avg_confidence": 56.4
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "X"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "2iw74m76pj2ibdu9igq7u6ql0",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.6571,
|
|
||||||
"avg_confidence": 60.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "3yt7t944i6ftaok5b799fa784",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "X"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "99r0d7ggi1169dhklo4eroopg",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.0266,
|
|
||||||
"avg_confidence": 69.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 4.0,
|
|
||||||
"avg_edge": 0.2509,
|
|
||||||
"avg_confidence": 76.65
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "2.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "63lonant3zu1u7xmpomocetw",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 2.0,
|
|
||||||
"avg_edge": 0.179,
|
|
||||||
"avg_confidence": 66.6
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "790qnaweqoyffb5ndxnb4hlas",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.1067,
|
|
||||||
"avg_confidence": 74.5
|
|
||||||
},
|
|
||||||
"v25_main": "2/2",
|
|
||||||
"v26_main": "2.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "55bckv97w88qeqymhqgwg9fdg",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "1X",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "ir507rn2anyknjvm1xua1c7o",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "Alt",
|
|
||||||
"v26_main": "2.5 Alt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "d9h5hdhwum2s04ma722emljx0",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 1.2061,
|
|
||||||
"avg_confidence": 79.9
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "3riltqclv2llihnklpcw6u784",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "8808y1x2hmz52k3mr598orqc4",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.2221,
|
|
||||||
"avg_confidence": 67.9
|
|
||||||
},
|
|
||||||
"v25_main": "KG Var",
|
|
||||||
"v26_main": "KG Var"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "5qypw3tl5qkx16ihhbifv9jis",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.0979,
|
|
||||||
"avg_confidence": 66.9
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 3.0,
|
|
||||||
"avg_edge": 0.1071,
|
|
||||||
"avg_confidence": 66.3
|
|
||||||
},
|
|
||||||
"v25_main": "\u00dcst",
|
|
||||||
"v26_main": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "710vie8tgz5ccs7sq9xwrjmz8",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 1.2892,
|
|
||||||
"avg_confidence": 82.9
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "2vkbdwkxyjj1invl5877bz0us",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "X"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "7caglruhdiegg9xrc20fzhrtg",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00c7ift",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "gl3nvw4hprtny0aby1z40duc",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00c7ift",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "f87f0jsazmdhlq3wzz7ngp3o",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "X2",
|
|
||||||
"v26_main": "2.5 Alt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "chb58495fc1o6ek9t9kdfbyfo",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.0572,
|
|
||||||
"avg_confidence": 75.5
|
|
||||||
},
|
|
||||||
"v25_main": "1",
|
|
||||||
"v26_main": "KG Var"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "57h5qfr5mr4qoxix7vhiwd2j8",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 1.1927,
|
|
||||||
"avg_confidence": 79.4
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "de76ipnpgaznxvl9tk9ewquqc",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "X"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "5pjdtn5bb9v64cj4ceh4ei1hw",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": "\u00c7ift",
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "6orrk7jpn3ucpybdieh6oin84",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "3shwt6aecvnu9utm2go9diq6s",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.674,
|
|
||||||
"avg_confidence": 60.6
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "5kukkba6u6wq1xj8oi118q1hw",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.8906,
|
|
||||||
"avg_confidence": 56.7
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "X"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "9fzu7f8aybsqy88r3nam79p1w",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "bxcnq91mnr9f6bhicdxgzbims",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1.5 \u00dcst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "dueticp36nbckn2pnpnt6az8",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "X"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "5m9do9pcggnl0tgmeolvnph5g",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 0.6914,
|
|
||||||
"avg_confidence": 61.3
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_id": "avu5vzpdi1wy7mrneodb3gw7o",
|
|
||||||
"v25": {
|
|
||||||
"playable_count": 0.0,
|
|
||||||
"avg_edge": 0.0,
|
|
||||||
"avg_confidence": 0.0
|
|
||||||
},
|
|
||||||
"v26": {
|
|
||||||
"playable_count": 1.0,
|
|
||||||
"avg_edge": 1.1608,
|
|
||||||
"avg_confidence": 78.3
|
|
||||||
},
|
|
||||||
"v25_main": null,
|
|
||||||
"v26_main": "1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
match_id,date,league,match,ht_score,final_score,strategy,market,pick,odds,playable,confidence,result,counted_in_roi,profit_flat,resolution_note,source,reversal_pick,reversal_prob,favorite_gap,favorite_odd,support_score,odds_band_score,odds_band_label,league_reversal_rate,league_strict_rev_rate,referee_strict_rev_rate,surprise_score,reason_codes,pick_reason
|
|
||||||
b6uzz042mizu0dqpci538z4lw,2025-09-06,Süper Kupa,V. Sarsfield vs C. Cordoba,0-0,2-0,v25_aggressive,HTFT,2/1,34.5,True,21.0,LOST,True,-1.0,actual=X/1,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
|
||||||
b6uzz042mizu0dqpci538z4lw,2025-09-06,Süper Kupa,V. Sarsfield vs C. Cordoba,0-0,2-0,v26_aggressive,HTFT,1/1,3.26,True,16.0,LOST,True,-1.0,actual=X/1,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
|
||||||
ytsc38rm4j22govgwo3as6j8,2025-09-06,DK Elemeler,Avusturya vs G. Kıbrıs Rum Kesimi,0-0,1-0,v25_aggressive,HTFT,X/1,4.01,True,11.4,WON,True,3.01,actual=X/1,v25.aggressive_pick,X/1,,,,,,,,,,,,
|
|
||||||
cfar57gsu6hy770n7e58u8duc,2025-09-06,Eerste Divisie,Cambuur vs Willem II,0-1,2-2,v25_aggressive,HTFT,2/1,20.7,True,15.6,LOST,True,-1.0,actual=2/X,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
|
||||||
cfar57gsu6hy770n7e58u8duc,2025-09-06,Eerste Divisie,Cambuur vs Willem II,0-1,2-2,v26_aggressive,HTFT,1/1,2.13,True,14.4,LOST,True,-1.0,actual=2/X,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
|
||||||
99r0d7ggi1169dhklo4eroopg,2025-09-06,2. Lig,Bromley vs Gillingham,2-0,2-2,v25_aggressive,HTFT,2/1,30.5,True,25.6,LOST,True,-1.0,actual=1/X,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
|
||||||
99r0d7ggi1169dhklo4eroopg,2025-09-06,2. Lig,Bromley vs Gillingham,2-0,2-2,v26_surprise,HTFT,1/2,34.5,False,4.9,LOST,False,0.0,actual=1/X,v26.surprise_pick,1/2,0.0679,1.02,1.92,73.5,0.642,,0.04,0.0,0.0,62.7,"favorite_gap_large,favorite_price_supported,reversal_prob_warm,quality_supports_reversal,favorite_odds_band_reversal_window,favorite_streak_break_window",
|
|
||||||
99r0d7ggi1169dhklo4eroopg,2025-09-06,2. Lig,Bromley vs Gillingham,2-0,2-2,v26_aggressive,HTFT,1/1,3.16,True,16.1,LOST,True,-1.0,actual=1/X,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
|
||||||
790qnaweqoyffb5ndxnb4hlas,2025-09-06,Ulusal Lig,Aldershot vs Brackley,1-0,2-2,v25_aggressive,HTFT,X/2,6.07,True,9.0,LOST,True,-1.0,actual=1/X,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
|
||||||
790qnaweqoyffb5ndxnb4hlas,2025-09-06,Ulusal Lig,Aldershot vs Brackley,1-0,2-2,v26_aggressive,HTFT,2/2,3.8,True,37.1,LOST,True,-1.0,actual=1/X,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
|
||||||
8808y1x2hmz52k3mr598orqc4,2025-09-06,DK Elemeler,Ermenistan vs Portekiz,0-3,0-5,v25_aggressive,HTFT,X/2,4.03,True,11.6,LOST,True,-1.0,actual=2/2,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
|
||||||
7gfnwoxqz5o2clin40a85uqdw,2025-09-06,1. Lig,Port Vale vs Leyton Orient,1-2,2-3,v25_aggressive,HTFT,X/1,4.84,True,12.3,LOST,True,-1.0,actual=2/2,v25.aggressive_pick,X/1,,,,,,,,,,,,
|
|
||||||
7gfnwoxqz5o2clin40a85uqdw,2025-09-06,1. Lig,Port Vale vs Leyton Orient,1-2,2-3,v26_surprise,HTFT,1/2,33.0,False,5.4,LOST,False,0.0,actual=2/2,v26.surprise_pick,1/2,0.0749,0.62,1.97,52.1,0.642,,0.044,0.0,0.0,57.0,"favorite_gap_large,favorite_price_supported,reversal_prob_warm,draw_swing_support,quality_supports_reversal,favorite_odds_band_reversal_window",
|
|
||||||
7gfnwoxqz5o2clin40a85uqdw,2025-09-06,1. Lig,Port Vale vs Leyton Orient,1-2,2-3,v26_aggressive,HTFT,2/2,4.51,True,17.3,WON,True,3.51,actual=2/2,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
|
||||||
7fld91ykj1kfuoc8wn4r2frbo,2025-09-06,1. Lig,Lincoln City vs Wigan Ath,2-1,2-2,v25_aggressive,HTFT,X/1,4.7,True,19.2,LOST,True,-1.0,actual=1/X,v25.aggressive_pick,X/1,,,,,,,,,,,,
|
|
||||||
7fld91ykj1kfuoc8wn4r2frbo,2025-09-06,1. Lig,Lincoln City vs Wigan Ath,2-1,2-2,v26_surprise,HTFT,1/2,34.5,False,7.1,LOST,False,0.0,actual=1/X,v26.surprise_pick,1/2,0.0984,0.71,2.0,61.5,0.642,,0.044,0.0,0.0,61.7,"favorite_gap_large,favorite_price_supported,reversal_prob_hot,upset_risk_detected,quality_supports_reversal,favorite_odds_band_reversal_window",
|
|
||||||
7fld91ykj1kfuoc8wn4r2frbo,2025-09-06,1. Lig,Lincoln City vs Wigan Ath,2-1,2-2,v26_aggressive,HTFT,1/1,3.36,True,19.5,LOST,True,-1.0,actual=1/X,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
|
||||||
7i4nhkex1qssyp3x6rsgj03ro,2025-09-06,1. Lig,Wycombe vs Mansfield,1-0,2-0,v25_aggressive,HTFT,X/2,6.92,True,11.1,LOST,True,-1.0,actual=1/1,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
|
||||||
7i4nhkex1qssyp3x6rsgj03ro,2025-09-06,1. Lig,Wycombe vs Mansfield,1-0,2-0,v26_main_htft,HTFT,2/2,5.22,False,39.9,LOST,False,0.0,actual=1/1,v26.main_pick,,,,,,,,,,,0.0,,
|
|
||||||
7hagai8xazmsj5exw7idwhhck,2025-09-06,1. Lig,Rotherham vs Exeter City,1-0,1-0,v25_aggressive,HTFT,X/2,6.28,True,6.7,LOST,True,-1.0,actual=1/1,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
|
||||||
7hagai8xazmsj5exw7idwhhck,2025-09-06,1. Lig,Rotherham vs Exeter City,1-0,1-0,v26_aggressive,HTFT,2/2,4.47,True,29.6,LOST,True,-1.0,actual=1/1,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
|
||||||
6e37x17qvk0snpokd1698lkb8,2025-09-06,Premiership,Crusaders vs Coleraine,0-3,0-4,v25_aggressive,HTFT,X/2,4.24,True,11.5,LOST,True,-1.0,actual=2/2,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
|
||||||
6e37x17qvk0snpokd1698lkb8,2025-09-06,Premiership,Crusaders vs Coleraine,0-3,0-4,v26_aggressive,HTFT,2/2,2.32,True,37.7,WON,True,1.32,actual=2/2,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
|
||||||
6f0fqlafaei9oj8yd9hdi6rdg,2025-09-06,Premiership,Linfield vs Portadown,0-0,3-0,v25_aggressive,HTFT,2/1,23.2,True,18.6,LOST,True,-1.0,actual=X/1,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
|
||||||
6f0fqlafaei9oj8yd9hdi6rdg,2025-09-06,Premiership,Linfield vs Portadown,0-0,3-0,v26_aggressive,HTFT,1/1,1.74,True,20.4,LOST,True,-1.0,actual=X/1,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
|
||||||
6dny1bmxcj6shc5382dno2al0,2025-09-06,Premiership,Carrick vs Cliftonville,0-1,1-2,v25_aggressive,HTFT,2/1,30.5,True,14.4,LOST,True,-1.0,actual=2/2,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
|
||||||
6dny1bmxcj6shc5382dno2al0,2025-09-06,Premiership,Carrick vs Cliftonville,0-1,1-2,v26_surprise,HTFT,2/1,30.5,False,10.1,LOST,False,0.0,actual=2/2,v26.surprise_pick,2/1,0.1401,0.59,1.97,62.8,0.642,,0.0667,0.0,0.0,66.2,"favorite_price_supported,reversal_prob_hot,quality_supports_reversal,favorite_odds_band_reversal_window,league_strict_reversal_prior,draw_pressure_supports_swing",
|
|
||||||
6dny1bmxcj6shc5382dno2al0,2025-09-06,Premiership,Carrick vs Cliftonville,0-1,1-2,v26_aggressive,HTFT,1/1,4.36,True,14.6,LOST,True,-1.0,actual=2/2,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
|
||||||
9d9nwo82prx06riy5odgnr190,2025-09-06,2. Lig,Walsall vs Chesterfield,1-0,1-0,v25_aggressive,HTFT,X/2,5.38,True,10.3,LOST,True,-1.0,actual=1/1,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
|
||||||
9d9nwo82prx06riy5odgnr190,2025-09-06,2. Lig,Walsall vs Chesterfield,1-0,1-0,v26_aggressive,HTFT,2/2,3.78,True,35.1,LOST,True,-1.0,actual=1/1,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
|
||||||
7dwtx5tr7g66bsqkoc1wos9as,2025-09-06,1. Lig,Bolton vs Wimbledon,1-0,3-0,v25_aggressive,HTFT,X/1,3.76,True,12.1,LOST,True,-1.0,actual=1/1,v25.aggressive_pick,X/1,,,,,,,,,,,,
|
|
||||||
7dwtx5tr7g66bsqkoc1wos9as,2025-09-06,1. Lig,Bolton vs Wimbledon,1-0,3-0,v26_aggressive,HTFT,1/1,1.81,True,33.3,WON,True,0.81,actual=1/1,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
|
||||||
7c85gguekhbhadtm7qdbgir6c,2025-09-06,Ulusal Lig,Tamworth vs Eastleigh,0-0,1-0,v25_aggressive,HTFT,1/2,34.5,True,14.1,LOST,True,-1.0,actual=X/1,v25.aggressive_pick,1/2,,,,,,,,,,,,
|
|
||||||
7c85gguekhbhadtm7qdbgir6c,2025-09-06,Ulusal Lig,Tamworth vs Eastleigh,0-0,1-0,v26_aggressive,HTFT,2/2,5.53,True,14.8,LOST,True,-1.0,actual=X/1,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,149 +0,0 @@
|
|||||||
# HT/FT + Upset Backtest
|
|
||||||
|
|
||||||
- Sample: last 120 finished football matches
|
|
||||||
- Scope: only HT/FT reversal and upset-oriented picks
|
|
||||||
- ROI: flat `1 unit` per played pick
|
|
||||||
- Generated at: 2026-04-21T12:32:14.419378+00:00
|
|
||||||
|
|
||||||
## Strategy Summary
|
|
||||||
|
|
||||||
| Strategy | Candidates | Played | Won | Lost | Hit Rate | Profit | ROI |
|
|
||||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
||||||
| v25_aggressive | 16 | 16 | 1 | 15 | 6.25% | -11.99 | -74.94% |
|
|
||||||
| v26_surprise | 4 | 0 | 0 | 0 | 0.0% | +0.00 | +0.00% |
|
|
||||||
| v26_aggressive | 13 | 13 | 3 | 10 | 23.08% | -4.36 | -33.54% |
|
|
||||||
| v26_main_htft | 1 | 0 | 0 | 0 | 0.0% | +0.00 | +0.00% |
|
|
||||||
|
|
||||||
## v26 Surprise By Reversal Type
|
|
||||||
|
|
||||||
| Reversal | Candidates | Played | Won | Lost | Profit | ROI |
|
|
||||||
|---|---:|---:|---:|---:|---:|---:|
|
|
||||||
| 1/2 | 3 | 0 | 0 | 0 | +0.00 | +0.00% |
|
|
||||||
| 2/1 | 1 | 0 | 0 | 0 | +0.00 | +0.00% |
|
|
||||||
| X/1 | 0 | 0 | 0 | 0 | +0.00 | +0.00% |
|
|
||||||
| X/2 | 0 | 0 | 0 | 0 | +0.00 | +0.00% |
|
|
||||||
|
|
||||||
## Match Detail
|
|
||||||
|
|
||||||
| Date | Match | HT | FT | v25 aggressive | v26 surprise | v26 aggressive | v26 main HTFT |
|
|
||||||
|---|---|---|---|---|---|---|---|
|
|
||||||
| 2025-09-06 | Forge FC vs Hfx Wan | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Estoril vs Santa Clara | 0-0 | 0-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Tenerife vs Merida AD | 1-0 | 3-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Ibiza vs Hercules | 2-1 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | V. Sarsfield vs C. Cordoba | 0-0 | 2-0 | 2/1 (LOST, played, -1.00) | - | 1/1 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Botafogo vs Paranaense | 0-1 | 1-3 | - | - | - | - |
|
|
||||||
| 2025-09-06 | San Felipe vs Curico Unido | 2-0 | 4-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Nacional Potosi vs Independiente | 0-1 | 0-3 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Avusturya vs G. Kıbrıs Rum Kesimi | 0-0 | 1-0 | X/1 (WON, played, +3.01) | - | - | - |
|
|
||||||
| 2025-09-06 | CD Estepona FS vs Linares | 1-0 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Pergolettese vs Cittadella | 2-1 | 3-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Union Brescia vs Pro Vercelli | 2-0 | 5-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Casertana vs Potenza | 1-1 | 3-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Montevideo vs Danubio | 0-2 | 0-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Hoogstraten vs W. Beveren | 0-0 | 1-3 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Cambuur vs Willem II | 0-1 | 2-2 | 2/1 (LOST, played, -1.00) | - | 1/1 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Tlaxcala vs A. Oaxaca | 1-0 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | JS Kabylie vs Olympique Akbou | 0-0 | 0-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | CD A. Baleares vs Castellon II | 3-0 | 3-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Karlovac 1919 vs Dugopolje | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | San Sebastian R. vs RSD Alcala | 1-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Hindistan U23 vs Katar U23 | 0-1 | 1-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Estradense vs Boiro | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Yeclano II vs El Palmar | 1-0 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Montlouis vs La Roche | 0-3 | 1-4 | - | - | - | - |
|
|
||||||
| 2025-09-06 | L'Hospitalet vs San Cristobal | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Marchamalo vs Guadalajara II | 1-1 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Real Zaragoza vs R. Valladolid | 0-0 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Bromley vs Gillingham | 2-0 | 2-2 | 2/1 (LOST, played, -1.00) | 1/2 (LOST, not played, +0.00) | 1/1 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Ajman Club vs Al Wahda | 2-1 | 2-4 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Aldershot vs Brackley | 1-0 | 2-2 | X/2 (LOST, played, -1.00) | - | 2/2 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Barbastro vs UE Olot | 1-0 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | CA Atlanta vs Guemes | 1-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Pulpileno vs UCAM Murcia II | 0-0 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Mojados vs Santa Marta | 0-2 | 1-3 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Ermenistan vs Portekiz | 0-3 | 0-5 | X/2 (LOST, played, -1.00) | - | - | - |
|
|
||||||
| 2025-09-06 | USM Khenchela vs CR Belouizdad | 1-0 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Nafta vs Gorica | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | CS Cerrito vs Atenas | 0-0 | 0-3 | - | - | - | - |
|
|
||||||
| 2025-09-06 | VfB Oldenburg vs Hannoverscher | 4-1 | 6-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Lealtad vs Numancia | 1-1 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Astorga vs Real Avila | 0-0 | 1-3 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Grindavik vs IR Reykjavik | 2-1 | 3-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Andratx vs Sant Andreu | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Arenas Getxo vs Cacereno | 2-1 | 2-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Aegir vs Dalvik / Reynir | 1-1 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Laval II vs Cesson | 0-0 | 0-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Palencia CF vs Arandina | 3-0 | 4-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Leioa vs Pasaia | 0-0 | 0-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Vic vs UE Cornella | 0-0 | 0-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Voltigeurs vs Granville | 1-2 | 3-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Bobigny vs Creteil | 0-1 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Beauvais vs Furiani Agliani | 0-0 | 2-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Oissel vs Caen II | 0-1 | 0-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Cartagena LU II vs Un. Molinense | 0-2 | 1-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Hercules II vs Atzeneta | 0-1 | 0-3 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Sassari vs Pianese | 0-0 | 0-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Giugliano vs Foggia | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Sorrento vs Trapani 1905 | 0-0 | 1-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Ascoli vs Juventus U23 | 0-0 | 0-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Arezzo vs Vis Pesaro | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Ath. Carpi vs Campobasso | 0-0 | 2-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Oviedo II vs Sarriana | 2-0 | 3-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Llosetense vs Platges | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Le Havre (K) vs Strasbourg (K) | 2-2 | 2-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Montpellier (K) vs Fleury 91 (K) | 0-1 | 1-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Bistrica vs NK Krsko | 3-0 | 6-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Bilje vs Dravinja | 1-0 | 2-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Nantes (K) vs Saint-Etienne (K) | 2-1 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Jadran Dekani vs NK Ilirija | 3-1 | 3-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Tabor Sezana vs Jesenice | 0-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Alaves II vs Logrones | 0-0 | 2-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Unionistas II vs Tordesillas | 1-1 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Leganes II vs Alcorcon II | 0-0 | 0-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Volna Pinsk vs Bumprom | 0-1 | 0-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | L. Mikulas vs S. Bratislava II | 0-0 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Dubrava vs Hrvace | 0-1 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Plymouth vs Stockport | 2-1 | 4-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Port Vale vs Leyton Orient | 1-2 | 2-3 | X/1 (LOST, played, -1.00) | 1/2 (LOST, not played, +0.00) | 2/2 (WON, played, +3.51) | - |
|
|
||||||
| 2025-09-06 | Huddersfield vs Peterborough | 0-0 | 3-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Lincoln City vs Wigan Ath | 2-1 | 2-2 | X/1 (LOST, played, -1.00) | 1/2 (LOST, not played, +0.00) | 1/1 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Wycombe vs Mansfield | 1-0 | 2-0 | X/2 (LOST, played, -1.00) | - | - | 2/2 (LOST, not played, +0.00) |
|
|
||||||
| 2025-09-06 | Rotherham vs Exeter City | 1-0 | 1-0 | X/2 (LOST, played, -1.00) | - | 2/2 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Ballymena Utd vs Glentoran | 0-2 | 0-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Crusaders vs Coleraine | 0-3 | 0-4 | X/2 (LOST, played, -1.00) | - | 2/2 (WON, played, +1.32) | - |
|
|
||||||
| 2025-09-06 | Glenavon vs Dungannon | 0-2 | 0-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Linfield vs Portadown | 0-0 | 3-0 | 2/1 (LOST, played, -1.00) | - | 1/1 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Colchester vs Crewe | 0-1 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | G. Morton vs Raith Rovers | 0-0 | 0-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Puchov vs Pohronie | 0-1 | 3-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Carrick vs Cliftonville | 0-1 | 1-2 | 2/1 (LOST, played, -1.00) | 2/1 (LOST, not played, +0.00) | 1/1 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Harrogate vs Crawley Town | 0-1 | 0-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Walsall vs Chesterfield | 1-0 | 1-0 | X/2 (LOST, played, -1.00) | - | 2/2 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Banik Lehota vs Samorin | 1-0 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Bolton vs Wimbledon | 1-0 | 3-0 | X/1 (LOST, played, -1.00) | - | 1/1 (WON, played, +0.81) | - |
|
|
||||||
| 2025-09-06 | Edinburgh vs Hearts II | 4-1 | 4-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Kelty Hearts vs Stranraer | 0-0 | 3-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Forfar vs Dundee II | 2-1 | 4-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Spartans vs Hibernian II | 2-0 | 5-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | East Kilbride vs Hamilton | 0-2 | 2-4 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Annan Ath vs St. Mirren II | 0-1 | 2-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | East Fife vs Dumbarton | 1-0 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Clyde vs Motherwell II | 3-0 | 4-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Peterhead vs Dundee United II | 2-0 | 3-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Montrose vs Aberdeen II | 2-1 | 3-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Elgin City vs Cove Rangers | 1-0 | 1-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Queen Of S. vs Rangers II | 0-0 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Boston Utd vs Solihull | 1-1 | 1-2 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Carlisle vs Truro | 2-0 | 3-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Southend vs Halifax | 1-0 | 3-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Woking vs Gateshead | 2-0 | 5-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Tamworth vs Eastleigh | 0-0 | 1-0 | 1/2 (LOST, played, -1.00) | - | 2/2 (LOST, played, -1.00) | - |
|
|
||||||
| 2025-09-06 | Alcorcon vs Teruel | 1-1 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Merthyr vs Worksop | 0-0 | 2-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Chesham Utd vs Bath | 0-0 | 0-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Southport vs South Shields | 0-0 | 0-0 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Kidderminster vs Macclesfield | 0-0 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Throttur Vogar vs Höttur / Huginn | 0-0 | 2-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Alfreton vs Kings Lynn | 0-0 | 1-1 | - | - | - | - |
|
|
||||||
| 2025-09-06 | Buxton vs Oxford City | 1-0 | 2-1 | - | - | - | - |
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,206 +0,0 @@
|
|||||||
"""
|
|
||||||
Backtest for September 13th (Top Leagues Only)
|
|
||||||
==============================================
|
|
||||||
Simulates the NEW 'Skip Logic' on matches from Sept 13, 2025.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Load .env manually to ensure correct DB connection
|
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
sys.path.insert(0, project_root) # Add root to path if needed
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
# ─── Configuration ─────────
|
|
||||||
MIN_CONF_THRESHOLDS = {
|
|
||||||
"MS": 45.0, "DC": 40.0, "OU15": 50.0, "OU25": 45.0,
|
|
||||||
"OU35": 45.0, "BTTS": 45.0, "HT": 40.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
def run_backtest():
|
|
||||||
print("🚀 Backtest: 13 Eylül 2024 - Top Leagues")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# 1. Load Top Leagues
|
|
||||||
leagues_path = os.path.join(project_root, "top_leagues.json")
|
|
||||||
try:
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
# Ensure they are strings for SQL IN clause
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
print(f"📋 Loaded {len(top_leagues)} top leagues.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error loading top_leagues.json: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2. Define Date Range (Sept 13, 2024 UTC)
|
|
||||||
start_dt = datetime(2024, 9, 13, 0, 0, 0)
|
|
||||||
end_dt = datetime(2024, 9, 13, 23, 59, 59)
|
|
||||||
start_ts = int(start_dt.timestamp() * 1000)
|
|
||||||
end_ts = int(end_dt.timestamp() * 1000)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# 3. Fetch Matches & Predictions
|
|
||||||
# We need matches that are FT and have a prediction
|
|
||||||
query = """
|
|
||||||
SELECT p.match_id, p.prediction_json,
|
|
||||||
m.score_home, m.score_away, m.status, m.league_id
|
|
||||||
FROM predictions p
|
|
||||||
JOIN matches m ON p.match_id = m.id
|
|
||||||
WHERE m.mst_utc BETWEEN %s AND %s
|
|
||||||
AND m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND p.prediction_json IS NOT NULL
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
cur.execute(query, (start_ts, end_ts, league_ids))
|
|
||||||
rows = cur.fetchall()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ DB Error: {e}")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"📊 Found {len(rows)} matches with predictions on Sept 13, 2024.")
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print("⚠️ No predictions found for this date. The AI Engine might not have processed these historical matches yet.")
|
|
||||||
print("💡 Tip: Run the feeder or AI engine on this date range to generate predictions first.")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bets = 0
|
|
||||||
winning_bets = 0
|
|
||||||
skipped_bets = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
data = row['prediction_json']
|
|
||||||
if isinstance(data, str):
|
|
||||||
data = json.loads(data)
|
|
||||||
|
|
||||||
home_score = row['score_home'] or 0
|
|
||||||
away_score = row['score_away'] or 0
|
|
||||||
total_goals = home_score + away_score
|
|
||||||
|
|
||||||
# Extract Main Pick
|
|
||||||
main_pick = None
|
|
||||||
main_pick_conf = 0.0
|
|
||||||
main_pick_odds = 0.0
|
|
||||||
|
|
||||||
if "main_pick" in data and isinstance(data["main_pick"], dict):
|
|
||||||
mp = data["main_pick"]
|
|
||||||
main_pick = mp.get("pick")
|
|
||||||
main_pick_conf = mp.get("confidence", 0.0)
|
|
||||||
main_pick_odds = mp.get("odds", 0.0)
|
|
||||||
|
|
||||||
if not main_pick or not main_pick_conf:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Determine Market Type
|
|
||||||
pick_str = str(main_pick).upper()
|
|
||||||
market_type = "MS"
|
|
||||||
if "1X" in pick_str or "X2" in pick_str or "12" in pick_str: market_type = "DC"
|
|
||||||
elif "ÜST" in pick_str or "ALT" in pick_str or "OVER" in pick_str or "UNDER" in pick_str:
|
|
||||||
if "1.5" in pick_str: market_type = "OU15"
|
|
||||||
elif "3.5" in pick_str: market_type = "OU35"
|
|
||||||
else: market_type = "OU25"
|
|
||||||
elif "VAR" in pick_str or "YOK" in pick_str or "BTTS" in pick_str: market_type = "BTTS"
|
|
||||||
|
|
||||||
threshold = MIN_CONF_THRESHOLDS.get(market_type, 45.0)
|
|
||||||
|
|
||||||
# --- SKIP LOGIC ---
|
|
||||||
# 1. Confidence Gate
|
|
||||||
if main_pick_conf < threshold:
|
|
||||||
skipped_bets += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 2. Value Gate
|
|
||||||
if main_pick_odds > 0:
|
|
||||||
implied_prob = 1.0 / main_pick_odds
|
|
||||||
my_prob = main_pick_conf / 100.0
|
|
||||||
edge = my_prob - implied_prob
|
|
||||||
if edge < -0.03:
|
|
||||||
skipped_bets += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# --- BET PLAYED ---
|
|
||||||
total_bets += 1
|
|
||||||
is_won = False
|
|
||||||
|
|
||||||
# Resolve Result
|
|
||||||
if market_type == "MS":
|
|
||||||
if (main_pick == "1" or main_pick == "MS 1") and home_score > away_score: is_won = True
|
|
||||||
elif (main_pick == "X" or main_pick == "MS X") and home_score == away_score: is_won = True
|
|
||||||
elif (main_pick == "2" or main_pick == "MS 2") and away_score > home_score: is_won = True
|
|
||||||
|
|
||||||
elif market_type.startswith("OU"):
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_str: line = 1.5
|
|
||||||
elif "3.5" in pick_str: line = 3.5
|
|
||||||
is_over = total_goals > line
|
|
||||||
is_under = total_goals < line
|
|
||||||
if ("ÜST" in pick_str or "OVER" in pick_str) and is_over: is_won = True
|
|
||||||
elif ("ALT" in pick_str or "UNDER" in pick_str) and is_under: is_won = True
|
|
||||||
|
|
||||||
elif market_type == "BTTS":
|
|
||||||
if home_score > 0 and away_score > 0:
|
|
||||||
if "VAR" in pick_str: is_won = True
|
|
||||||
else:
|
|
||||||
if "YOK" in pick_str: is_won = True
|
|
||||||
|
|
||||||
elif market_type == "DC":
|
|
||||||
if "1X" in pick_str and home_score >= away_score: is_won = True
|
|
||||||
elif "X2" in pick_str and away_score >= home_score: is_won = True
|
|
||||||
elif "12" in pick_str and home_score != away_score: is_won = True
|
|
||||||
|
|
||||||
if is_won:
|
|
||||||
winning_bets += 1
|
|
||||||
profit = main_pick_odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
|
|
||||||
# Report
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📈 BACKTEST RESULTS: 13 EYLÜL 2025 (TOP LEAGUES)")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Total Matches Analyzed: {len(rows)}")
|
|
||||||
print(f"🚫 Bets SKIPPED (Low Conf/Bad Value): {skipped_bets}")
|
|
||||||
print(f"✅ Bets PLAYED: {total_bets}")
|
|
||||||
|
|
||||||
if total_bets > 0:
|
|
||||||
win_rate = (winning_bets / total_bets) * 100
|
|
||||||
roi = (total_profit / total_bets) * 100
|
|
||||||
|
|
||||||
print(f"🏆 Winning Bets: {winning_bets}")
|
|
||||||
print(f"💀 Losing Bets: {total_bets - winning_bets}")
|
|
||||||
print("-" * 40)
|
|
||||||
print(f" Win Rate: {win_rate:.2f}%")
|
|
||||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
|
||||||
print(f"📊 ROI: {roi:.2f}%")
|
|
||||||
|
|
||||||
if roi > 0:
|
|
||||||
print("🟢 STRATEGY IS PROFITABLE!")
|
|
||||||
else:
|
|
||||||
print("🔴 STRATEGY IS LOSING")
|
|
||||||
else:
|
|
||||||
print("⚠️ No bets were played. Thresholds might be too high or no suitable matches found.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_backtest()
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
"""
|
|
||||||
Detailed Backtest with 50 Top League Matches
|
|
||||||
============================================
|
|
||||||
Runs AI Engine predictions on 50 real historical matches and shows
|
|
||||||
exactly which predictions were correct and which were skipped.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python ai-engine/scripts/backtest_50_detailed.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
# Add paths
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
# 50 Match IDs from the query
|
|
||||||
MATCH_IDS = [
|
|
||||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
|
||||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
|
||||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
|
||||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
|
||||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
|
||||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
|
||||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
|
||||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
|
||||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
|
||||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
|
||||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
|
||||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
|
||||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
|
||||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
|
||||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
|
||||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
|
||||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
|
||||||
]
|
|
||||||
|
|
||||||
def run_detailed_backtest():
|
|
||||||
print("🚀 DETAILED BACKTEST: 50 Top League Matches")
|
|
||||||
print("🧠 Engine: V30 Ensemble (V20+V25) + Skip Logic")
|
|
||||||
print("="*80)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# Fetch match details with odds
|
|
||||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
|
||||||
cur.execute(f"""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away, m.league_id,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
l.name as league_name
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
LEFT JOIN leagues l ON m.league_id = l.id
|
|
||||||
WHERE m.id IN ({placeholders})
|
|
||||||
AND m.status = 'FT'
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
""", MATCH_IDS)
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 Found {len(rows)} matches. Starting AI Analysis...")
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print("⚠️ No matches found.")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Initialize AI Engine
|
|
||||||
try:
|
|
||||||
orchestrator = get_single_match_orchestrator()
|
|
||||||
print("✅ AI Engine Loaded.\n")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Failed to load AI Engine: {e}")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# ─── Backtest Loop ───
|
|
||||||
results = []
|
|
||||||
total_skipped = 0
|
|
||||||
total_played = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
MIN_CONF = 45.0
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home_team = row['home_team'] or "Unknown"
|
|
||||||
away_team = row['away_team'] or "Unknown"
|
|
||||||
league = row['league_name'] or "Unknown"
|
|
||||||
home_score = row['score_home'] or 0
|
|
||||||
away_score = row['score_away'] or 0
|
|
||||||
total_goals = home_score + away_score
|
|
||||||
|
|
||||||
print(f"[{i+1}/{len(rows)}] {home_team} vs {away_team} ({league}) ... ", end="", flush=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
prediction = orchestrator.analyze_match(match_id)
|
|
||||||
|
|
||||||
if not prediction:
|
|
||||||
print("⚠️ No prediction")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Extract Main Pick
|
|
||||||
main_pick = prediction.get("main_pick") or {}
|
|
||||||
pick_name = main_pick.get("pick", "")
|
|
||||||
confidence = main_pick.get("confidence", 0)
|
|
||||||
odds = main_pick.get("odds", 0)
|
|
||||||
|
|
||||||
# Apply Skip Logic
|
|
||||||
if confidence < MIN_CONF:
|
|
||||||
print(f"🚫 SKIP (Conf {confidence:.0f}%)")
|
|
||||||
total_skipped += 1
|
|
||||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
|
||||||
"conf": confidence, "odds": odds, "result": "SKIPPED", "profit": 0})
|
|
||||||
continue
|
|
||||||
|
|
||||||
if odds > 0:
|
|
||||||
implied_prob = 1.0 / odds
|
|
||||||
my_prob = confidence / 100.0
|
|
||||||
if my_prob - implied_prob < -0.03:
|
|
||||||
print(f"🚫 SKIP (Bad Value)")
|
|
||||||
total_skipped += 1
|
|
||||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
|
||||||
"conf": confidence, "odds": odds, "result": "SKIPPED", "profit": 0})
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Bet Played
|
|
||||||
total_played += 1
|
|
||||||
won = False
|
|
||||||
|
|
||||||
# Resolve
|
|
||||||
pick_clean = str(pick_name).upper()
|
|
||||||
if pick_clean in ["1", "MS 1", "İY 1"] and home_score > away_score: won = True
|
|
||||||
elif pick_clean in ["X", "MS X", "İY X"] and home_score == away_score: won = True
|
|
||||||
elif pick_clean in ["2", "MS 2", "İY 2"] and away_score > home_score: won = True
|
|
||||||
elif pick_clean in ["1X", "X2"] or ("1X" in pick_clean or "X2" in pick_clean):
|
|
||||||
if "1X" in pick_clean and home_score >= away_score: won = True
|
|
||||||
elif "X2" in pick_clean and away_score >= home_score: won = True
|
|
||||||
elif pick_clean in ["12"] and home_score != away_score: won = True
|
|
||||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
elif "3.5" in pick_clean: line = 3.5
|
|
||||||
if total_goals > line: won = True
|
|
||||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
elif "3.5" in pick_clean: line = 3.5
|
|
||||||
if total_goals < line: won = True
|
|
||||||
elif "VAR" in pick_clean and home_score > 0 and away_score > 0: won = True
|
|
||||||
elif "YOK" in pick_clean and (home_score == 0 or away_score == 0): won = True
|
|
||||||
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
print(f"✅ WON ({pick_name} @ {odds:.2f}, +{profit:.2f})")
|
|
||||||
else:
|
|
||||||
profit = -1.0
|
|
||||||
print(f"❌ LOST ({pick_name} @ {odds:.2f})")
|
|
||||||
|
|
||||||
total_profit += profit
|
|
||||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
|
||||||
"conf": confidence, "odds": odds,
|
|
||||||
"result": "WON" if won else "LOST", "profit": profit,
|
|
||||||
"score": f"{home_score}-{away_score}"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"💥 Error: {e}")
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
|
|
||||||
# ─── DETAILED REPORT ───
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print("📈 DETAILED BACKTEST RESULTS")
|
|
||||||
print(f"⏱️ Time: {elapsed:.1f}s")
|
|
||||||
print("="*80)
|
|
||||||
print(f"📊 Total Matches: {len(rows)}")
|
|
||||||
print(f"🚫 Skipped: {total_skipped}")
|
|
||||||
print(f"🎲 Played: {total_played}")
|
|
||||||
print(f"✅ Won: {total_won}")
|
|
||||||
print(f"💀 Lost: {total_played - total_won}")
|
|
||||||
print(f"💰 Profit: {total_profit:+.2f} units")
|
|
||||||
|
|
||||||
if total_played > 0:
|
|
||||||
win_rate = (total_won / total_played) * 100
|
|
||||||
roi = (total_profit / total_played) * 100
|
|
||||||
print(f"📊 Win Rate: {win_rate:.1f}%")
|
|
||||||
print(f"📊 ROI: {roi:.1f}%")
|
|
||||||
if roi > 0:
|
|
||||||
print("🟢 STRATEGY IS PROFITABLE!")
|
|
||||||
else:
|
|
||||||
print("🔴 STRATEGY IS LOSING")
|
|
||||||
|
|
||||||
# ─── TABLE OF ALL RESULTS ───
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print("📋 DETAILED MATCH RESULTS")
|
|
||||||
print("="*80)
|
|
||||||
print(f"{'Match':<40} {'Pick':<15} {'Conf':<6} {'Odds':<6} {'Result':<8} {'Score':<6}")
|
|
||||||
print("-"*80)
|
|
||||||
for r in results:
|
|
||||||
match_str = r['match'][:38]
|
|
||||||
pick_str = str(r['pick'])[:13]
|
|
||||||
conf_str = f"{r['conf']:.0f}%"
|
|
||||||
odds_str = f"{r['odds']:.2f}" if r['odds'] > 0 else "N/A"
|
|
||||||
res_str = r['result']
|
|
||||||
score_str = r.get('score', '')
|
|
||||||
|
|
||||||
# Color coding
|
|
||||||
if res_str == "WON": res_display = f"✅ {res_str}"
|
|
||||||
elif res_str == "LOST": res_display = f"❌ {res_str}"
|
|
||||||
else: res_display = f"🚫 {res_str}"
|
|
||||||
|
|
||||||
print(f"{match_str:<40} {pick_str:<15} {conf_str:<6} {odds_str:<6} {res_display:<12} {score_str:<6}")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_detailed_backtest()
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
"""
|
|
||||||
Adaptive 500 Match Backtest
|
|
||||||
=============================
|
|
||||||
Skips NO match unless NO odds exist.
|
|
||||||
Evaluates ALL available markets (MS, OU, BTTS) and picks the BEST value bet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_adaptive_backtest():
|
|
||||||
print("🔄 ADAPTIVE 500 MATCH BACKTEST")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# 1. Load Top Leagues
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# 2. Fetch 500 Finished Matches with Odds
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away, m.league_id,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 500
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 Found {len(rows)} matches. Analyzing...\n")
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print("⚠️ No matches found.")
|
|
||||||
return
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Error: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Stats
|
|
||||||
total_evaluated = 0
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
skipped_count = 0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
total_evaluated += 1
|
|
||||||
# print(f"[{i+1}] {home} vs {away} ... ", end="", flush=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred:
|
|
||||||
# print("⚠️ No Data")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ─── ADAPTIVE PICKING ───
|
|
||||||
# Check ALL recommendations (Expert or Standard) to find the BEST option
|
|
||||||
candidates = []
|
|
||||||
|
|
||||||
# Add main picks
|
|
||||||
if pred.get("expert_recommendation"):
|
|
||||||
rec = pred["expert_recommendation"]
|
|
||||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
|
||||||
if rec.get("safe_alternative"): candidates.append(rec["safe_alternative"])
|
|
||||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
|
||||||
elif pred.get("main_pick"):
|
|
||||||
candidates.append(pred["main_pick"])
|
|
||||||
|
|
||||||
best_bet = None
|
|
||||||
for c in candidates:
|
|
||||||
if not c: continue
|
|
||||||
conf = c.get("confidence", 0)
|
|
||||||
odds = c.get("odds", 0)
|
|
||||||
pick = c.get("pick")
|
|
||||||
|
|
||||||
# Flexible Criteria:
|
|
||||||
# 1. Confidence > 60%
|
|
||||||
# 2. Odds > 1.10 (Not "free" odds like 1.00)
|
|
||||||
# 3. Edge > -2% (Slightly tolerant)
|
|
||||||
if conf >= 60 and odds > 1.10:
|
|
||||||
implied = 1.0 / odds
|
|
||||||
edge = ((conf/100) - implied) * 100
|
|
||||||
|
|
||||||
# Prioritize positive edge, but accept small negative if confidence is high
|
|
||||||
if edge > -2.0:
|
|
||||||
if best_bet is None or (conf > best_bet.get("confidence", 0)):
|
|
||||||
best_bet = c
|
|
||||||
|
|
||||||
if best_bet:
|
|
||||||
pick = str(best_bet.get("pick")).upper()
|
|
||||||
conf = best_bet.get("confidence")
|
|
||||||
odds = best_bet.get("odds")
|
|
||||||
|
|
||||||
# Resolution Logic
|
|
||||||
won = False
|
|
||||||
if pick in ["1", "MS 1", "İY 1"] and h_score > a_score: won = True
|
|
||||||
elif pick in ["X", "MS X", "İY X"] and h_score == a_score: won = True
|
|
||||||
elif pick in ["2", "MS 2", "İY 2"] and a_score > h_score: won = True
|
|
||||||
elif pick in ["1X", "X2"]:
|
|
||||||
if "1X" in pick and h_score >= a_score: won = True
|
|
||||||
elif "X2" in pick and a_score >= h_score: won = True
|
|
||||||
elif pick == "12" and h_score != a_score: won = True
|
|
||||||
elif "ÜST" in pick or "OVER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick or "UNDER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
total_bet += 1
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
# print(f"✅ WON (+{profit:.2f}) | {pick}")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
# print(f"❌ LOST ({pick} @ {odds:.2f})")
|
|
||||||
else:
|
|
||||||
skipped_count += 1
|
|
||||||
# print(f"🚫 SKIP (No Value)")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# print(f"💥 Error: {e}")
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("🔄 ADAPTIVE BACKTEST RESULTS (500 Matches)")
|
|
||||||
print("="*60)
|
|
||||||
print(f"📊 Evaluated: {total_evaluated}")
|
|
||||||
print(f"🎲 Played: {total_bet}")
|
|
||||||
print(f"🚫 Skipped: {skipped_count}")
|
|
||||||
print(f"✅ Won: {total_won}")
|
|
||||||
|
|
||||||
if total_bet > 0:
|
|
||||||
win_rate = (total_won / total_bet) * 100
|
|
||||||
roi = (total_profit / total_bet) * 100
|
|
||||||
print(f"📈 Win Rate: {win_rate:.2f}%")
|
|
||||||
print(f"💰 Total Profit: {total_profit:.2f} Units")
|
|
||||||
print(f"📊 ROI: {roi:.2f}%")
|
|
||||||
if total_profit > 0: print("🟢 KARLI STRATEJİ")
|
|
||||||
else: print("🔴 ZARARDA")
|
|
||||||
else:
|
|
||||||
print("⚠️ Hiç bahis oynanmadı. Veri kalitesi çok düşük.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_adaptive_backtest()
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
"""
|
|
||||||
Diagnostic Backtest - Hangi Pazar Kanıyor?
|
|
||||||
===========================================
|
|
||||||
Analyses the 500 matches to see WHICH markets are losing money.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_diagnostic():
|
|
||||||
print("🔍 TANI BACKTESTİ: NEREDE KAYBETTİK?")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away, m.league_id,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 500
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç analiz ediliyor...\n")
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Market Stats: { "MS": {"won": 10, "lost": 20, "profit": -5.0}, ... }
|
|
||||||
market_stats = defaultdict(lambda: {"won": 0, "lost": 0, "profit": 0.0, "total": 0})
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred: continue
|
|
||||||
|
|
||||||
candidates = []
|
|
||||||
if pred.get("expert_recommendation"):
|
|
||||||
rec = pred["expert_recommendation"]
|
|
||||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
|
||||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
|
||||||
elif pred.get("main_pick"):
|
|
||||||
candidates.append(pred["main_pick"])
|
|
||||||
|
|
||||||
played_this = False
|
|
||||||
for c in candidates:
|
|
||||||
if not c: continue
|
|
||||||
conf = c.get("confidence", 0)
|
|
||||||
odds = c.get("odds", 0)
|
|
||||||
pick = str(c.get("pick")).upper()
|
|
||||||
market_type = c.get("market_type", "Unknown")
|
|
||||||
|
|
||||||
# Criteria
|
|
||||||
if conf >= 60 and odds > 1.10:
|
|
||||||
implied = 1.0 / odds
|
|
||||||
edge = ((conf/100) - implied) * 100
|
|
||||||
if edge > -2.0:
|
|
||||||
# Resolve
|
|
||||||
won = False
|
|
||||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif pick in ["1X", "X2"]:
|
|
||||||
if "1X" in pick and h_score >= a_score: won = True
|
|
||||||
elif "X2" in pick and a_score >= h_score: won = True
|
|
||||||
elif pick == "12" and h_score != a_score: won = True
|
|
||||||
elif "ÜST" in pick or "OVER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick or "UNDER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
market_stats[market_type]["total"] += 1
|
|
||||||
if won:
|
|
||||||
market_stats[market_type]["won"] += 1
|
|
||||||
market_stats[market_type]["profit"] += (odds - 1.0)
|
|
||||||
else:
|
|
||||||
market_stats[market_type]["lost"] += 1
|
|
||||||
market_stats[market_type]["profit"] -= 1.0
|
|
||||||
|
|
||||||
played_this = True
|
|
||||||
break # Only one bet per match
|
|
||||||
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
# Print Results
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📊 PAZAR BAZLI KAR/ZARAR TABLOSU")
|
|
||||||
print("="*60)
|
|
||||||
print(f"{'Market':<15} {'Oynanan':<10} {'Kazanılan':<10} {'Win%':<8} {'Kâr':<10}")
|
|
||||||
print("-" * 60)
|
|
||||||
|
|
||||||
for mkt, stats in sorted(market_stats.items(), key=lambda x: x[1]["profit"], reverse=True):
|
|
||||||
wr = (stats["won"] / stats["total"] * 100) if stats["total"] > 0 else 0
|
|
||||||
print(f"{mkt:<15} {stats['total']:<10} {stats['won']:<10} {wr:.1f}% {stats['profit']:+.2f} Units")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_diagnostic()
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
"""
|
|
||||||
V27 FINAL BACKTEST — Conservative Flat Bet
|
|
||||||
Only the strongest validated edges. No Kelly compounding.
|
|
||||||
"""
|
|
||||||
import pandas as pd, numpy as np
|
|
||||||
|
|
||||||
df = pd.read_csv('data/training_data_v27.csv', low_memory=False)
|
|
||||||
for c in df.columns:
|
|
||||||
if c not in ['match_id','league_name','home_team','away_team']:
|
|
||||||
df[c] = pd.to_numeric(df[c], errors='coerce')
|
|
||||||
df = df.dropna(subset=['odds_ms_h','odds_ms_d','odds_ms_a'])
|
|
||||||
df = df[(df.odds_ms_h>1.01)&(df.odds_ms_d>1.01)&(df.odds_ms_a>1.01)]
|
|
||||||
|
|
||||||
n = len(df)
|
|
||||||
# 5-fold walk-forward: train on 60%, validate patterns, test on remaining
|
|
||||||
folds = 5
|
|
||||||
fold_size = n // folds
|
|
||||||
all_results = []
|
|
||||||
|
|
||||||
print("="*65)
|
|
||||||
print(" V27 WALK-FORWARD FLAT-BET BACKTEST")
|
|
||||||
print("="*65)
|
|
||||||
|
|
||||||
for fold in range(2, folds): # start from fold 2 so we have enough training data
|
|
||||||
train_end = fold * fold_size
|
|
||||||
test_start = train_end
|
|
||||||
test_end = (fold+1)*fold_size if fold < folds-1 else n
|
|
||||||
|
|
||||||
train_df = df.iloc[:train_end]
|
|
||||||
test_df = df.iloc[test_start:test_end]
|
|
||||||
|
|
||||||
print(f"\n --- Fold {fold}: train={len(train_df)}, test={len(test_df)} ---")
|
|
||||||
|
|
||||||
# Discover REST edges from training data
|
|
||||||
strategies = []
|
|
||||||
|
|
||||||
for hr in [5, 7, 10, 14]:
|
|
||||||
for ar in [3, 4, 5]:
|
|
||||||
for cls, col in [(0,'odds_ms_h'), (2,'odds_ms_a')]:
|
|
||||||
idx = (train_df.home_days_rest > hr) & (train_df.away_days_rest < ar)
|
|
||||||
sub = train_df[idx]
|
|
||||||
if len(sub) < 50:
|
|
||||||
continue
|
|
||||||
rate = (sub.label_ms == cls).mean()
|
|
||||||
avg_odds = sub[col].mean()
|
|
||||||
ev = rate * avg_odds
|
|
||||||
if ev > 1.02: # only strong edges (>2% edge)
|
|
||||||
strategies.append((hr, ar, cls, rate, avg_odds, ev, len(sub)))
|
|
||||||
|
|
||||||
if not strategies:
|
|
||||||
print(" No strong edges found in training data")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Apply best strategies to test
|
|
||||||
strategies.sort(key=lambda x: x[5], reverse=True)
|
|
||||||
best = strategies[:3] # top 3 only
|
|
||||||
|
|
||||||
fold_bets = 0
|
|
||||||
fold_wins = 0
|
|
||||||
fold_pnl = 0
|
|
||||||
stake = 10 # flat 10 units
|
|
||||||
|
|
||||||
for _, row in test_df.iterrows():
|
|
||||||
for hr, ar, cls, est_p, _, _, _ in best:
|
|
||||||
if pd.isna(row.home_days_rest) or pd.isna(row.away_days_rest):
|
|
||||||
continue
|
|
||||||
if row.home_days_rest <= hr or row.away_days_rest >= ar:
|
|
||||||
continue
|
|
||||||
odds_col = ['odds_ms_h','odds_ms_d','odds_ms_a'][cls]
|
|
||||||
odds_val = row[odds_col]
|
|
||||||
if pd.isna(odds_val) or odds_val < 1.50 or odds_val > 5.0:
|
|
||||||
continue
|
|
||||||
# Additional filter: only bet when odds give reasonable EV
|
|
||||||
if est_p * odds_val < 1.0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
won = (row.label_ms == cls)
|
|
||||||
pnl = stake * (odds_val - 1) if won else -stake
|
|
||||||
fold_bets += 1
|
|
||||||
if won:
|
|
||||||
fold_wins += 1
|
|
||||||
fold_pnl += pnl
|
|
||||||
all_results.append({'fold': fold, 'won': won, 'pnl': pnl,
|
|
||||||
'odds': odds_val, 'stake': stake,
|
|
||||||
'cls': ['H','D','A'][cls]})
|
|
||||||
|
|
||||||
if fold_bets > 0:
|
|
||||||
roi = fold_pnl / (fold_bets * stake) * 100
|
|
||||||
print(f" Best strategies: {[(h,a,['H','D','A'][c],f'EV={e:.3f}') for h,a,c,_,_,e,_ in best]}")
|
|
||||||
print(f" Bets: {fold_bets}, Wins: {fold_wins} ({fold_wins/fold_bets*100:.1f}%), "
|
|
||||||
f"ROI: {roi:+.1f}%, PnL: {fold_pnl:+.0f}")
|
|
||||||
|
|
||||||
# Overall
|
|
||||||
print("\n" + "="*65)
|
|
||||||
print(" OVERALL RESULTS")
|
|
||||||
print("="*65)
|
|
||||||
if all_results:
|
|
||||||
total = len(all_results)
|
|
||||||
wins = sum(1 for r in all_results if r['won'])
|
|
||||||
total_pnl = sum(r['pnl'] for r in all_results)
|
|
||||||
total_staked = sum(r['stake'] for r in all_results)
|
|
||||||
roi = total_pnl / total_staked * 100
|
|
||||||
|
|
||||||
print(f" Total bets: {total}")
|
|
||||||
print(f" Wins: {wins} ({wins/total*100:.1f}%)")
|
|
||||||
print(f" Total staked: {total_staked:.0f}")
|
|
||||||
print(f" PnL: {total_pnl:+.0f}")
|
|
||||||
print(f" ROI: {roi:+.1f}%")
|
|
||||||
print(f" Avg odds: {np.mean([r['odds'] for r in all_results]):.2f}")
|
|
||||||
|
|
||||||
# By class
|
|
||||||
print("\n --- By Bet Type ---")
|
|
||||||
for cls in ['H','A']:
|
|
||||||
cb = [r for r in all_results if r['cls'] == cls]
|
|
||||||
if cb:
|
|
||||||
cw = sum(1 for r in cb if r['won'])
|
|
||||||
cp = sum(r['pnl'] for r in cb)
|
|
||||||
cs = sum(r['stake'] for r in cb)
|
|
||||||
print(f" {cls}: {len(cb)} bets, hit={cw/len(cb)*100:.1f}%, ROI={cp/cs*100:+.1f}%")
|
|
||||||
|
|
||||||
# Cumulative PnL curve
|
|
||||||
print("\n --- Cumulative PnL ---")
|
|
||||||
cum = 0
|
|
||||||
step = max(1, total // 15)
|
|
||||||
for j in range(0, total, step):
|
|
||||||
cum = sum(r['pnl'] for r in all_results[:j+1])
|
|
||||||
print(f" After bet {j+1:4d}: PnL={cum:+.0f}")
|
|
||||||
cum = sum(r['pnl'] for r in all_results)
|
|
||||||
print(f" After bet {total:4d}: PnL={cum:+.0f} (FINAL)")
|
|
||||||
else:
|
|
||||||
print(" No bets placed!")
|
|
||||||
|
|
||||||
# ── Now combine with MODEL for smarter filtering ──
|
|
||||||
print("\n" + "="*65)
|
|
||||||
print(" COMBINED: Rest Rules + Fundamentals Model")
|
|
||||||
print("="*65)
|
|
||||||
|
|
||||||
import pickle, json
|
|
||||||
from pathlib import Path
|
|
||||||
MODELS_DIR = Path("models/v27")
|
|
||||||
|
|
||||||
feat_cols = json.load(open(MODELS_DIR / "v27_feature_cols.json"))
|
|
||||||
ms_models = {}
|
|
||||||
for name in ['xgb','lgb','cb']:
|
|
||||||
p = MODELS_DIR / f"v27_ms_{name}.pkl"
|
|
||||||
if p.exists():
|
|
||||||
with open(p,'rb') as f:
|
|
||||||
ms_models[name] = pickle.load(f)
|
|
||||||
|
|
||||||
if ms_models:
|
|
||||||
test_df = df.iloc[int(n*0.8):].copy()
|
|
||||||
X_test = test_df[feat_cols].values
|
|
||||||
|
|
||||||
# Get model predictions
|
|
||||||
preds = []
|
|
||||||
for name, m in ms_models.items():
|
|
||||||
if name == 'xgb':
|
|
||||||
import xgboost as xgb
|
|
||||||
dm = xgb.DMatrix(X_test, feature_names=feat_cols)
|
|
||||||
preds.append(m.predict(dm))
|
|
||||||
elif name == 'lgb':
|
|
||||||
preds.append(m.predict(X_test))
|
|
||||||
elif name == 'cb':
|
|
||||||
preds.append(m.predict_proba(X_test))
|
|
||||||
model_probs = np.mean(preds, axis=0) # (n, 3)
|
|
||||||
|
|
||||||
# Now apply rest rules + model agreement
|
|
||||||
margin = 1/test_df.odds_ms_h.values + 1/test_df.odds_ms_d.values + 1/test_df.odds_ms_a.values
|
|
||||||
impl = np.column_stack([
|
|
||||||
(1/test_df.odds_ms_h.values)/margin,
|
|
||||||
(1/test_df.odds_ms_d.values)/margin,
|
|
||||||
(1/test_df.odds_ms_a.values)/margin,
|
|
||||||
])
|
|
||||||
|
|
||||||
combo_bets = 0
|
|
||||||
combo_wins = 0
|
|
||||||
combo_pnl = 0
|
|
||||||
|
|
||||||
for j in range(len(test_df)):
|
|
||||||
row = test_df.iloc[j]
|
|
||||||
for hr, ar in [(14,5),(10,5),(7,5),(5,5)]:
|
|
||||||
if pd.isna(row.home_days_rest) or pd.isna(row.away_days_rest):
|
|
||||||
continue
|
|
||||||
if row.home_days_rest <= hr or row.away_days_rest >= ar:
|
|
||||||
continue
|
|
||||||
for cls in [0, 2]:
|
|
||||||
odds_val = [row.odds_ms_h, row.odds_ms_d, row.odds_ms_a][cls]
|
|
||||||
if pd.isna(odds_val) or odds_val < 1.50 or odds_val > 5.0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
model_p = model_probs[j, cls]
|
|
||||||
impl_p = impl[j, cls]
|
|
||||||
|
|
||||||
# DOUBLE FILTER: rest rule + model agrees (model_prob > implied)
|
|
||||||
if model_p <= impl_p:
|
|
||||||
continue # model disagrees, skip
|
|
||||||
edge = model_p - impl_p
|
|
||||||
if edge < 0.03:
|
|
||||||
continue # too small
|
|
||||||
|
|
||||||
won = (row.label_ms == cls)
|
|
||||||
pnl = 10 * (odds_val - 1) if won else -10
|
|
||||||
combo_bets += 1
|
|
||||||
if won:
|
|
||||||
combo_wins += 1
|
|
||||||
combo_pnl += pnl
|
|
||||||
|
|
||||||
if combo_bets > 0:
|
|
||||||
roi = combo_pnl / (combo_bets * 10) * 100
|
|
||||||
print(f" Bets: {combo_bets}")
|
|
||||||
print(f" Wins: {combo_wins} ({combo_wins/combo_bets*100:.1f}%)")
|
|
||||||
print(f" PnL: {combo_pnl:+.0f}")
|
|
||||||
print(f" ROI: {roi:+.1f}%")
|
|
||||||
else:
|
|
||||||
print(" No combined bets triggered")
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
"""
|
|
||||||
Real AI Engine Backtest Script
|
|
||||||
==============================
|
|
||||||
Uses the ACTUAL models (V20/V25 Ensemble) to predict historical matches.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python ai-engine/scripts/backtest_real.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Add paths
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
|
|
||||||
# Fix for Windows path issues in scripts
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR) # One level up if inside scripts folder
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator, MatchData
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_backtest():
|
|
||||||
print("🚀 REAL AI BACKTEST: Sept 13, 2024 - Top Leagues")
|
|
||||||
print("🧠 Engine: V30 Ensemble (V20+V25)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# Load Top Leagues
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
try:
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
print(f"📋 Loaded {len(top_leagues)} top leagues.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error loading top_leagues.json: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Date Range (Sept 13, 2024)
|
|
||||||
start_dt = datetime(2024, 9, 13, 0, 0, 0)
|
|
||||||
end_dt = datetime(2024, 9, 13, 23, 59, 59)
|
|
||||||
start_ts = int(start_dt.timestamp() * 1000)
|
|
||||||
end_ts = int(end_dt.timestamp() * 1000)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# Fetch Matches
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.mst_utc, m.league_id, m.status, m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
l.name as league_name
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
LEFT JOIN leagues l ON m.league_id = l.id
|
|
||||||
WHERE m.mst_utc BETWEEN %s AND %s
|
|
||||||
AND m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
ORDER BY m.mst_utc ASC
|
|
||||||
LIMIT 20 -- Limit to 20 matches to avoid running for hours on a single backtest
|
|
||||||
""", (start_ts, end_ts, league_ids))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 Found {len(rows)} finished matches. Starting AI Analysis...")
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print("⚠️ No matches found for this date.")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Initialize AI Engine
|
|
||||||
try:
|
|
||||||
orchestrator = get_single_match_orchestrator()
|
|
||||||
print("✅ AI Engine (SingleMatchOrchestrator) Loaded.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Failed to load AI Engine: {e}")
|
|
||||||
print("💡 Make sure models are trained/present in ai-engine/models/")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# ─── Backtest Loop ───
|
|
||||||
total_matches_analyzed = 0
|
|
||||||
bets_skipped = 0
|
|
||||||
bets_played = 0
|
|
||||||
bets_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
# Thresholds matching the NEW Skip Logic
|
|
||||||
MIN_CONF = 45.0
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home_team = row['home_team']
|
|
||||||
away_team = row['away_team']
|
|
||||||
home_score = row['score_home']
|
|
||||||
away_score = row['score_away']
|
|
||||||
|
|
||||||
print(f"\n[{i+1}/{len(rows)}] Analyzing: {home_team} vs {away_team} ...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. AI PREDICTION (Actual Model Call)
|
|
||||||
prediction = orchestrator.analyze_match(match_id)
|
|
||||||
|
|
||||||
if not prediction:
|
|
||||||
print(f" ⚠️ AI returned no prediction.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
total_matches_analyzed += 1
|
|
||||||
|
|
||||||
# 2. Extract Main Pick
|
|
||||||
main_pick = prediction.get("main_pick") or {}
|
|
||||||
pick_name = main_pick.get("pick")
|
|
||||||
confidence = main_pick.get("confidence", 0)
|
|
||||||
odds = main_pick.get("odds", 0)
|
|
||||||
|
|
||||||
if not pick_name or not confidence:
|
|
||||||
print(f" ⚠️ No main pick found in prediction.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f" 🤖 Pick: {pick_name} | Conf: {confidence}% | Odds: {odds}")
|
|
||||||
|
|
||||||
# 3. Apply Skip Logic (New Backtest Logic)
|
|
||||||
if confidence < MIN_CONF:
|
|
||||||
print(f" 🚫 SKIPPED (Confidence {confidence}% < {MIN_CONF}%)")
|
|
||||||
bets_skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if odds > 0:
|
|
||||||
implied_prob = 1.0 / odds
|
|
||||||
my_prob = confidence / 100.0
|
|
||||||
if my_prob - implied_prob < -0.03: # Negative edge
|
|
||||||
print(f" 🚫 SKIPPED (Negative Edge)")
|
|
||||||
bets_skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 4. Bet Played
|
|
||||||
bets_played += 1
|
|
||||||
print(f" 🎲 BET PLAYED: {pick_name} @ {odds}")
|
|
||||||
|
|
||||||
# 5. Resolve Bet
|
|
||||||
won = False
|
|
||||||
# Basic resolution logic (Need to parse pick_name like "1", "X", "2", "2.5 Üst", etc.)
|
|
||||||
pick_clean = str(pick_name).upper()
|
|
||||||
|
|
||||||
# MS
|
|
||||||
if pick_clean in ["1", "MS 1"] and home_score > away_score: won = True
|
|
||||||
elif pick_clean in ["X", "MS X"] and home_score == away_score: won = True
|
|
||||||
elif pick_clean in ["2", "MS 2"] and away_score > home_score: won = True
|
|
||||||
|
|
||||||
# OU25
|
|
||||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
|
||||||
if (home_score + away_score) > 2.5: won = True
|
|
||||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
|
||||||
if (home_score + away_score) < 2.5: won = True
|
|
||||||
|
|
||||||
# BTTS
|
|
||||||
elif "VAR" in pick_clean and home_score > 0 and away_score > 0: won = True
|
|
||||||
elif "YOK" in pick_clean and (home_score == 0 or away_score == 0): won = True
|
|
||||||
|
|
||||||
if won:
|
|
||||||
bets_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
print(f" ✅ WON! (+{profit:.2f} units)")
|
|
||||||
else:
|
|
||||||
profit = -1.0
|
|
||||||
print(f" ❌ LOST! (-1.00 units)")
|
|
||||||
|
|
||||||
total_profit += profit
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" 💥 Error during analysis: {e}")
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
|
|
||||||
# ─── FINAL REPORT ───
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📈 REAL AI BACKTEST RESULTS")
|
|
||||||
print(f"🕒 Time taken: {elapsed:.1f} seconds")
|
|
||||||
print("="*60)
|
|
||||||
print(f"📊 Matches Analyzed: {total_matches_analyzed}")
|
|
||||||
print(f"🚫 Bets SKIPPED: {bets_skipped}")
|
|
||||||
print(f"✅ Bets PLAYED: {bets_played}")
|
|
||||||
|
|
||||||
if bets_played > 0:
|
|
||||||
win_rate = (bets_won / bets_played) * 100
|
|
||||||
roi = (total_profit / bets_played) * 100
|
|
||||||
yield_val = total_profit # Net Units
|
|
||||||
|
|
||||||
print(f"🏆 Bets Won: {bets_won}")
|
|
||||||
print(f"💀 Bets Lost: {bets_played - bets_won}")
|
|
||||||
print("-" * 40)
|
|
||||||
print(f" Win Rate: {win_rate:.2f}%")
|
|
||||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
|
||||||
print(f"📊 ROI: {roi:.2f}%")
|
|
||||||
|
|
||||||
if roi > 0:
|
|
||||||
print("🟢 STRATEGY IS PROFITABLE!")
|
|
||||||
else:
|
|
||||||
print("🔴 STRATEGY IS LOSING")
|
|
||||||
else:
|
|
||||||
print("⚠️ No bets were played. All were skipped or failed.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_backtest()
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
"""
|
|
||||||
Backtest ROI Engine
|
|
||||||
===================
|
|
||||||
Simulates the NEW "Skip Logic" on historical predictions.
|
|
||||||
Answers: "What if we only played the bets the model was confident about?"
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python ai-engine/scripts/backtest_roi.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from typing import Dict, List, Any
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
# Load .env from project root (2 levels up from this script)
|
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
load_dotenv(os.path.join(project_root, ".env"))
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
"""Return a psycopg2-compatible DSN from DATABASE_URL."""
|
|
||||||
# HARDCODED FOR BACKTEST (Bypassing dotenv issues)
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
# ─── Configuration (Matching the NEW BetRecommender Logic) ─────────
|
|
||||||
# Minimum confidence to even consider a bet (Hard Gate)
|
|
||||||
MIN_CONF_THRESHOLDS = {
|
|
||||||
"MS": 45.0,
|
|
||||||
"DC": 40.0,
|
|
||||||
"OU15": 50.0,
|
|
||||||
"OU25": 45.0,
|
|
||||||
"OU35": 45.0,
|
|
||||||
"BTTS": 45.0,
|
|
||||||
"HT": 40.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_market_type_from_key(key: str) -> str:
|
|
||||||
"""Map prediction keys to market types for thresholding."""
|
|
||||||
if key.startswith("ms_") or key in ["1", "X", "2"]: return "MS"
|
|
||||||
if key.startswith("dc_") or key in ["1X", "X2", "12"]: return "DC"
|
|
||||||
if key.startswith("ou15_") or key.startswith("1.5"): return "OU15"
|
|
||||||
if key.startswith("ou25_") or key.startswith("2.5"): return "OU25"
|
|
||||||
if key.startswith("ou35_") or key.startswith("3.5"): return "OU35"
|
|
||||||
if key.startswith("btts_") or key in ["Var", "Yok"]: return "BTTS"
|
|
||||||
if key.startswith("ht_") or key.startswith("İY"): return "HT"
|
|
||||||
return "MS"
|
|
||||||
|
|
||||||
def simulate_backtest():
|
|
||||||
print("🚀 Starting Backtest with NEW 'Skip Logic'...")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# 1. Fetch PREDICTIONS that have a confidence score
|
|
||||||
# We limit to last 1000 finished matches to keep it fast but representative
|
|
||||||
cur.execute("""
|
|
||||||
SELECT p.match_id, p.prediction_json,
|
|
||||||
m.score_home, m.score_away, m.status
|
|
||||||
FROM predictions p
|
|
||||||
JOIN matches m ON p.match_id = m.id
|
|
||||||
WHERE m.status = 'FT'
|
|
||||||
AND p.prediction_json IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 2000
|
|
||||||
""")
|
|
||||||
predictions = cur.fetchall()
|
|
||||||
|
|
||||||
print(f"📊 Loaded {len(predictions)} historical predictions.")
|
|
||||||
|
|
||||||
total_bets = 0
|
|
||||||
winning_bets = 0
|
|
||||||
skipped_bets = 0
|
|
||||||
total_profit = 0.0 # Assuming unit stake of 1.0
|
|
||||||
|
|
||||||
# 2. Process each prediction
|
|
||||||
for pred_row in predictions:
|
|
||||||
match_id = pred_row['match_id']
|
|
||||||
data = pred_row['prediction_json']
|
|
||||||
if isinstance(data, str):
|
|
||||||
data = json.loads(data)
|
|
||||||
|
|
||||||
# Real result
|
|
||||||
home_score = pred_row['score_home'] or 0
|
|
||||||
away_score = pred_row['score_away'] or 0
|
|
||||||
total_goals = home_score + away_score
|
|
||||||
|
|
||||||
# Extract prediction details from the JSON structure
|
|
||||||
# The structure varies, but usually contains 'main_pick', 'bet_summary', or 'market_board'
|
|
||||||
|
|
||||||
# Try to get the main pick recommendation
|
|
||||||
main_pick = None
|
|
||||||
main_pick_conf = 0.0
|
|
||||||
main_pick_odds = 0.0
|
|
||||||
|
|
||||||
# Navigate the V20+ JSON structure
|
|
||||||
market_board = data.get("market_board", {})
|
|
||||||
|
|
||||||
# Check Main Pick
|
|
||||||
if "main_pick" in data:
|
|
||||||
mp = data["main_pick"]
|
|
||||||
if isinstance(mp, dict):
|
|
||||||
main_pick = mp.get("pick")
|
|
||||||
main_pick_conf = mp.get("confidence", 0.0)
|
|
||||||
main_pick_odds = mp.get("odds", 0.0)
|
|
||||||
|
|
||||||
# If no main pick, try bet_summary
|
|
||||||
if not main_pick and "bet_summary" in data:
|
|
||||||
summary = data["bet_summary"]
|
|
||||||
if isinstance(summary, list) and len(summary) > 0:
|
|
||||||
# Take the highest confidence one
|
|
||||||
best = max(summary, key=lambda x: x.get("confidence", 0))
|
|
||||||
main_pick = best.get("pick")
|
|
||||||
main_pick_conf = best.get("confidence", 0.0)
|
|
||||||
main_pick_odds = best.get("odds", 0.0)
|
|
||||||
|
|
||||||
if not main_pick or not main_pick_conf:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ─── NEW LOGIC: APPLY FILTERS ───
|
|
||||||
# 1. Determine Market Type
|
|
||||||
# Simple heuristic based on pick string
|
|
||||||
pick_str = str(main_pick).upper()
|
|
||||||
market_type = "MS"
|
|
||||||
if "1X" in pick_str or "X2" in pick_str or "12" in pick_str: market_type = "DC"
|
|
||||||
elif "ÜST" in pick_str or "ALT" in pick_str or "OVER" in pick_str or "UNDER" in pick_str:
|
|
||||||
if "1.5" in pick_str: market_type = "OU15"
|
|
||||||
elif "3.5" in pick_str: market_type = "OU35"
|
|
||||||
else: market_type = "OU25"
|
|
||||||
elif "VAR" in pick_str or "YOK" in pick_str or "BTTS" in pick_str: market_type = "BTTS"
|
|
||||||
|
|
||||||
threshold = MIN_CONF_THRESHOLDS.get(market_type, 45.0)
|
|
||||||
|
|
||||||
# 2. Check Confidence Gate
|
|
||||||
if main_pick_conf < threshold:
|
|
||||||
skipped_bets += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 3. Check Value Gate (Edge)
|
|
||||||
if main_pick_odds > 0:
|
|
||||||
implied_prob = 1.0 / main_pick_odds
|
|
||||||
my_prob = main_pick_conf / 100.0
|
|
||||||
edge = my_prob - implied_prob
|
|
||||||
if edge < -0.03: # Negative value
|
|
||||||
skipped_bets += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ─── BET IS PLAYED ───
|
|
||||||
total_bets += 1
|
|
||||||
|
|
||||||
# Determine if WON
|
|
||||||
is_won = False
|
|
||||||
|
|
||||||
# Resolve MS (1, X, 2)
|
|
||||||
if market_type == "MS":
|
|
||||||
if main_pick == "1" and home_score > away_score: is_won = True
|
|
||||||
elif main_pick == "X" and home_score == away_score: is_won = True
|
|
||||||
elif main_pick == "2" and away_score > home_score: is_won = True
|
|
||||||
elif main_pick == "MS 1" and home_score > away_score: is_won = True
|
|
||||||
elif main_pick == "MS X" and home_score == away_score: is_won = True
|
|
||||||
elif main_pick == "MS 2" and away_score > home_score: is_won = True
|
|
||||||
|
|
||||||
# Resolve OU (Over/Under)
|
|
||||||
elif market_type.startswith("OU"):
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_str: line = 1.5
|
|
||||||
elif "3.5" in pick_str: line = 3.5
|
|
||||||
|
|
||||||
is_over = total_goals > line
|
|
||||||
is_under = total_goals < line # Simplification (usually line is X.5 so no draw)
|
|
||||||
|
|
||||||
if "ÜST" in pick_str or "OVER" in pick_str:
|
|
||||||
if is_over: is_won = True
|
|
||||||
elif "ALT" in pick_str or "UNDER" in pick_str:
|
|
||||||
if is_under: is_won = True
|
|
||||||
|
|
||||||
# Resolve BTTS
|
|
||||||
elif market_type == "BTTS":
|
|
||||||
if home_score > 0 and away_score > 0:
|
|
||||||
if "VAR" in pick_str: is_won = True
|
|
||||||
else:
|
|
||||||
if "YOK" in pick_str: is_won = True
|
|
||||||
|
|
||||||
# Resolve DC (Double Chance) - Simplified
|
|
||||||
elif market_type == "DC":
|
|
||||||
if "1X" in pick_str and (home_score >= away_score): is_won = True
|
|
||||||
elif "X2" in pick_str and (away_score >= home_score): is_won = True
|
|
||||||
elif "12" in pick_str and (home_score != away_score): is_won = True
|
|
||||||
|
|
||||||
if is_won:
|
|
||||||
winning_bets += 1
|
|
||||||
profit = main_pick_odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
|
|
||||||
# ─── REPORT ───
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📈 BACKTEST RESULTS (With NEW Skip Logic)")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Total Historical Matches Analyzed: {len(predictions)}")
|
|
||||||
print(f"🚫 Bets SKIPPED (Low Conf/Bad Value): {skipped_bets}")
|
|
||||||
print(f"✅ Bets PLAYED: {total_bets}")
|
|
||||||
|
|
||||||
if total_bets > 0:
|
|
||||||
win_rate = (winning_bets / total_bets) * 100
|
|
||||||
roi = (total_profit / total_bets) * 100
|
|
||||||
|
|
||||||
print(f"🏆 Winning Bets: {winning_bets}")
|
|
||||||
print(f"💀 Losing Bets: {total_bets - winning_bets}")
|
|
||||||
print("-" * 40)
|
|
||||||
print(f" Win Rate: {win_rate:.2f}%")
|
|
||||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
|
||||||
print(f"📊 ROI: {roi:.2f}%")
|
|
||||||
|
|
||||||
if roi > 0:
|
|
||||||
print("🟢 STRATEGY IS PROFITABLE!")
|
|
||||||
else:
|
|
||||||
print("🔴 STRATEGY IS LOSING (Adjust thresholds!)")
|
|
||||||
else:
|
|
||||||
print("⚠️ No bets were played. Thresholds might be too high.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
simulate_backtest()
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
"""
|
|
||||||
SNIPER Backtest
|
|
||||||
===============
|
|
||||||
Sadece en yüksek güvenilirlik ve değere sahip bahisleri oynar.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
MATCH_IDS = [
|
|
||||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
|
||||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
|
||||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
|
||||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
|
||||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
|
||||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
|
||||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
|
||||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
|
||||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
|
||||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
|
||||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
|
||||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
|
||||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
|
||||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
|
||||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
|
||||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
|
||||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
|
||||||
]
|
|
||||||
|
|
||||||
def run_sniper_backtest():
|
|
||||||
print("🎯 SNIPER BACKTEST: SADECE NET OLANLAR")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
|
||||||
cur.execute(f"""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
l.name as league_name
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
LEFT JOIN leagues l ON m.league_id = l.id
|
|
||||||
WHERE m.id IN ({placeholders}) AND m.status = 'FT'
|
|
||||||
""", MATCH_IDS)
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 Analiz edilecek {len(rows)} maç var.\n")
|
|
||||||
|
|
||||||
try:
|
|
||||||
orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
print(f"[{i+1}/{len(rows)}] {home} vs {away} ... ", end="", flush=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred:
|
|
||||||
print("⚠️ Veri Yok")
|
|
||||||
continue
|
|
||||||
|
|
||||||
pick_data = pred.get("expert_recommendation", {}).get("main_pick") or pred.get("main_pick", {})
|
|
||||||
pick = pick_data.get("pick") or pick_data.get("market_type")
|
|
||||||
conf = pick_data.get("confidence", 0)
|
|
||||||
odds = pick_data.get("odds", 0)
|
|
||||||
|
|
||||||
# SNIPER FİLTRELERİ
|
|
||||||
if conf < 75:
|
|
||||||
print(f"🚫 PASS (Conf: {conf:.0f}%)")
|
|
||||||
continue
|
|
||||||
if odds < 1.35:
|
|
||||||
print(f"🚫 PASS (Odds: {odds:.2f} çok düşük)")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Value Control
|
|
||||||
implied = 1.0 / odds
|
|
||||||
if (conf/100) < implied:
|
|
||||||
print(f"🚫 PASS (Negatif Value)")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# OYNA
|
|
||||||
total_bet += 1
|
|
||||||
won = False
|
|
||||||
pick_clean = str(pick).upper()
|
|
||||||
|
|
||||||
if pick_clean in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick_clean in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick_clean in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
elif "3.5" in pick_clean: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
elif "3.5" in pick_clean: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick_clean and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick_clean and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
print(f"✅ WON! (+{profit:.2f})")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
print(f"❌ LOST! ({pick} @ {odds:.2f})")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"💥 Hata: {e}")
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("🎯 SNIPER SONUÇLARI")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Oynanan: {total_bet}")
|
|
||||||
print(f"Kazanılan: {total_won}")
|
|
||||||
print(f"Kazanma Oranı: %{(total_won/total_bet)*100:.1f}" if total_bet > 0 else "Kazanma Oranı: N/A")
|
|
||||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
|
||||||
|
|
||||||
if total_profit > 0:
|
|
||||||
print("🟢 PARA KAZANDIK!")
|
|
||||||
else:
|
|
||||||
print("🔴 PARA KAYBETTİK!")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_sniper_backtest()
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
"""
|
|
||||||
Strict Sniper Backtest (Calibrated)
|
|
||||||
===================================
|
|
||||||
Sadece Güven > %75 ve Oran > 1.30 olan bahisleri oynar.
|
|
||||||
Modelin şişirilmiş özgüvenini elemek için yapıldı.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_strict_backtest():
|
|
||||||
print("🎯 STRICT SNIPER BACKTEST (Conf > 75%)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 500
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç taranıyor. Sadece NET OLANLAR oynanacak...\n")
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred: continue
|
|
||||||
|
|
||||||
# Check all picks for a HIGH CONFIDENCE bet
|
|
||||||
candidates = []
|
|
||||||
if pred.get("expert_recommendation"):
|
|
||||||
rec = pred["expert_recommendation"]
|
|
||||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
|
||||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
|
||||||
elif pred.get("main_pick"):
|
|
||||||
candidates.append(pred["main_pick"])
|
|
||||||
|
|
||||||
best_bet = None
|
|
||||||
for c in candidates:
|
|
||||||
if not c: continue
|
|
||||||
# Access attributes safely (Dict or Object)
|
|
||||||
conf = c.get("confidence", 0) if isinstance(c, dict) else getattr(c, 'confidence', 0)
|
|
||||||
odds = c.get("odds", 0) if isinstance(c, dict) else getattr(c, 'odds', 0)
|
|
||||||
pick = c.get("pick", "") if isinstance(c, dict) else getattr(c, 'pick', "")
|
|
||||||
|
|
||||||
# STRICT CRITERIA
|
|
||||||
if conf >= 75.0 and odds >= 1.30:
|
|
||||||
# Check Value (Edge)
|
|
||||||
implied = 1.0 / odds
|
|
||||||
edge = ((conf/100) - implied) * 100
|
|
||||||
if edge > -5.0: # Tolerant edge
|
|
||||||
if best_bet is None or (conf > (best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0))):
|
|
||||||
best_bet = c
|
|
||||||
|
|
||||||
if best_bet:
|
|
||||||
pick = str(best_bet.get("pick") if isinstance(best_bet, dict) else getattr(best_bet, 'pick', "")).upper()
|
|
||||||
conf = best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0)
|
|
||||||
odds = best_bet.get("odds", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'odds', 0)
|
|
||||||
|
|
||||||
# Resolution
|
|
||||||
won = False
|
|
||||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif pick in ["1X", "X2"]:
|
|
||||||
if "1X" in pick and h_score >= a_score: won = True
|
|
||||||
elif "X2" in pick and a_score >= h_score: won = True
|
|
||||||
elif "ÜST" in pick or "OVER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick or "UNDER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
total_bet += 1
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({conf:.0f}%) -> WON (+{profit:.2f})")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({conf:.0f}%) -> LOST")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("🎯 STRICT SNIPER SONUÇLARI")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Oynanan Bahis: {total_bet}")
|
|
||||||
print(f"Kazanılan: {total_won}")
|
|
||||||
|
|
||||||
if total_bet > 0:
|
|
||||||
win_rate = (total_won / total_bet) * 100
|
|
||||||
roi = (total_profit / total_bet) * 100
|
|
||||||
print(f"Kazanma Oranı: %{win_rate:.2f}")
|
|
||||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
|
||||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
|
||||||
else: print("🔴 PARA KAYBETTİK!")
|
|
||||||
else:
|
|
||||||
print("⚠️ Yeteri kadar NET maç bulunamadı.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_strict_backtest()
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
|
|
||||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_DIR))
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_dsn() -> str:
|
|
||||||
env_path = AI_ENGINE_DIR / ".env"
|
|
||||||
if env_path.exists():
|
|
||||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
||||||
if line.startswith("DATABASE_URL="):
|
|
||||||
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
|
||||||
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
|
||||||
|
|
||||||
|
|
||||||
def _fetch_matches(dsn: str, limit: int = 60) -> list[str]:
|
|
||||||
query = """
|
|
||||||
SELECT m.id
|
|
||||||
FROM matches m
|
|
||||||
WHERE m.status = 'FT'
|
|
||||||
AND m.sport = 'football'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND m.score_away IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT %s
|
|
||||||
"""
|
|
||||||
with psycopg2.connect(dsn) as conn:
|
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
||||||
cur.execute(query, (limit,))
|
|
||||||
return [str(row["id"]) for row in cur.fetchall()]
|
|
||||||
|
|
||||||
|
|
||||||
def _score_prediction(package: dict) -> dict[str, float]:
|
|
||||||
rows = package.get("bet_summary", []) or []
|
|
||||||
playable = [row for row in rows if row.get("playable")]
|
|
||||||
return {
|
|
||||||
"playable_count": float(len(playable)),
|
|
||||||
"avg_edge": round(
|
|
||||||
sum(float(row.get("ev_edge", 0.0)) for row in playable) / len(playable),
|
|
||||||
4,
|
|
||||||
)
|
|
||||||
if playable
|
|
||||||
else 0.0,
|
|
||||||
"avg_confidence": round(
|
|
||||||
sum(float(row.get("calibrated_confidence", 0.0)) for row in playable)
|
|
||||||
/ len(playable),
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
if playable
|
|
||||||
else 0.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
dsn = _resolve_dsn()
|
|
||||||
match_ids = _fetch_matches(dsn)
|
|
||||||
orchestrator = SingleMatchOrchestrator()
|
|
||||||
|
|
||||||
results: list[dict[str, object]] = []
|
|
||||||
for match_id in match_ids:
|
|
||||||
orchestrator.engine_mode = "v25"
|
|
||||||
v25 = orchestrator.analyze_match(match_id)
|
|
||||||
orchestrator.engine_mode = "v26"
|
|
||||||
v26 = orchestrator.analyze_match(match_id)
|
|
||||||
if not v25 or not v26:
|
|
||||||
continue
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"match_id": match_id,
|
|
||||||
"v25": _score_prediction(v25),
|
|
||||||
"v26": _score_prediction(v26),
|
|
||||||
"v25_main": (v25.get("main_pick") or {}).get("pick"),
|
|
||||||
"v26_main": (v26.get("main_pick") or {}).get("pick"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
out_path = AI_ENGINE_DIR / "reports" / "backtest_v26_shadow.json"
|
|
||||||
out_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
|
|
||||||
print(f"[OK] Shadow backtest summary written to {out_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import csv
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
|
|
||||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_DIR))
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
|
||||||
|
|
||||||
|
|
||||||
STRATEGIES = ("v25_aggressive", "v26_surprise", "v26_aggressive", "v26_main_htft")
|
|
||||||
REVERSAL_LABELS = ("1/2", "2/1", "X/1", "X/2")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MatchContext:
|
|
||||||
match_id: str
|
|
||||||
match_date_ms: int
|
|
||||||
league: str
|
|
||||||
home_team: str
|
|
||||||
away_team: str
|
|
||||||
final_home: int
|
|
||||||
final_away: int
|
|
||||||
ht_home: Optional[int]
|
|
||||||
ht_away: Optional[int]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def match_name(self) -> str:
|
|
||||||
return f"{self.home_team} vs {self.away_team}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def final_score(self) -> str:
|
|
||||||
return f"{self.final_home}-{self.final_away}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ht_score(self) -> str:
|
|
||||||
if self.ht_home is None or self.ht_away is None:
|
|
||||||
return "-"
|
|
||||||
return f"{self.ht_home}-{self.ht_away}"
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_dsn() -> str:
|
|
||||||
env_path = AI_ENGINE_DIR / ".env"
|
|
||||||
if env_path.exists():
|
|
||||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
||||||
if line.startswith("DATABASE_URL="):
|
|
||||||
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
|
||||||
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
|
||||||
|
|
||||||
|
|
||||||
def _fetch_matches(dsn: str, limit: int) -> list[MatchContext]:
|
|
||||||
query = """
|
|
||||||
SELECT
|
|
||||||
m.id,
|
|
||||||
m.mst_utc,
|
|
||||||
COALESCE(l.name, 'Unknown League') AS league,
|
|
||||||
COALESCE(ht.name, 'Home') AS home_team,
|
|
||||||
COALESCE(at.name, 'Away') AS away_team,
|
|
||||||
COALESCE(m.score_home, 0) AS score_home,
|
|
||||||
COALESCE(m.score_away, 0) AS score_away,
|
|
||||||
m.ht_score_home,
|
|
||||||
m.ht_score_away
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN leagues l ON l.id = m.league_id
|
|
||||||
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
|
||||||
LEFT JOIN teams at ON at.id = m.away_team_id
|
|
||||||
WHERE m.status = 'FT'
|
|
||||||
AND m.sport = 'football'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND m.score_away IS NOT NULL
|
|
||||||
AND m.ht_score_home IS NOT NULL
|
|
||||||
AND m.ht_score_away IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT %s
|
|
||||||
"""
|
|
||||||
with psycopg2.connect(dsn) as conn:
|
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
||||||
cur.execute(query, (limit,))
|
|
||||||
rows = cur.fetchall()
|
|
||||||
return [
|
|
||||||
MatchContext(
|
|
||||||
match_id=str(row["id"]),
|
|
||||||
match_date_ms=int(row["mst_utc"] or 0),
|
|
||||||
league=str(row["league"] or "Unknown League"),
|
|
||||||
home_team=str(row["home_team"] or "Home"),
|
|
||||||
away_team=str(row["away_team"] or "Away"),
|
|
||||||
final_home=int(row["score_home"] or 0),
|
|
||||||
final_away=int(row["score_away"] or 0),
|
|
||||||
ht_home=int(row["ht_score_home"]) if row.get("ht_score_home") is not None else None,
|
|
||||||
ht_away=int(row["ht_score_away"]) if row.get("ht_score_away") is not None else None,
|
|
||||||
)
|
|
||||||
for row in rows
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_float(value: Any) -> float:
|
|
||||||
try:
|
|
||||||
return float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def _outcome_symbol(home: int, away: int) -> str:
|
|
||||||
if home > away:
|
|
||||||
return "1"
|
|
||||||
if home < away:
|
|
||||||
return "2"
|
|
||||||
return "X"
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_htft(pick: str, context: MatchContext) -> Dict[str, Any]:
|
|
||||||
if not pick or "/" not in str(pick):
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "htft_pick_invalid"}
|
|
||||||
actual = f"{_outcome_symbol(context.ht_home or 0, context.ht_away or 0)}/{_outcome_symbol(context.final_home, context.final_away)}"
|
|
||||||
won = str(pick).strip().upper() == actual
|
|
||||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
|
||||||
|
|
||||||
|
|
||||||
def _market_odds(odds: Dict[str, Any], market: str, pick: str) -> float:
|
|
||||||
mapping = {
|
|
||||||
"HTFT": {
|
|
||||||
"1/1": "htft_11",
|
|
||||||
"1/X": "htft_1x",
|
|
||||||
"1/2": "htft_12",
|
|
||||||
"X/1": "htft_x1",
|
|
||||||
"X/X": "htft_xx",
|
|
||||||
"X/2": "htft_x2",
|
|
||||||
"2/1": "htft_21",
|
|
||||||
"2/X": "htft_2x",
|
|
||||||
"2/2": "htft_22",
|
|
||||||
},
|
|
||||||
"MS": {"1": "ms_h", "X": "ms_d", "2": "ms_a"},
|
|
||||||
}
|
|
||||||
key = mapping.get(market, {}).get(str(pick))
|
|
||||||
if not key:
|
|
||||||
return 0.0
|
|
||||||
value = _safe_float((odds or {}).get(key))
|
|
||||||
return value if value > 1.0 else 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def _evaluate_pick(
|
|
||||||
*,
|
|
||||||
strategy: str,
|
|
||||||
market: str,
|
|
||||||
pick: str,
|
|
||||||
odds: Any,
|
|
||||||
playable: bool,
|
|
||||||
confidence: Any,
|
|
||||||
extra: Optional[Dict[str, Any]],
|
|
||||||
context: MatchContext,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
odds_value = _safe_float(odds)
|
|
||||||
if market == "HT/FT":
|
|
||||||
market = "HTFT"
|
|
||||||
resolution = _resolve_htft(pick, context) if market == "HTFT" else {
|
|
||||||
"result": "UNRESOLVED",
|
|
||||||
"won": None,
|
|
||||||
"note": "non_htft_market",
|
|
||||||
}
|
|
||||||
counted = bool(playable and market == "HTFT" and odds_value > 1.01 and resolution["result"] in {"WON", "LOST"})
|
|
||||||
profit = 0.0
|
|
||||||
if counted:
|
|
||||||
profit = (odds_value - 1.0) if resolution["result"] == "WON" else -1.0
|
|
||||||
row = {
|
|
||||||
"strategy": strategy,
|
|
||||||
"market": market,
|
|
||||||
"pick": pick,
|
|
||||||
"odds": round(odds_value, 2),
|
|
||||||
"playable": playable,
|
|
||||||
"confidence": round(_safe_float(confidence), 1),
|
|
||||||
"result": resolution["result"],
|
|
||||||
"counted_in_roi": counted,
|
|
||||||
"profit_flat": round(profit, 4),
|
|
||||||
"resolution_note": resolution["note"],
|
|
||||||
}
|
|
||||||
if extra:
|
|
||||||
row.update(extra)
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_strategy_rows(
|
|
||||||
*,
|
|
||||||
context: MatchContext,
|
|
||||||
odds_data: Dict[str, Any],
|
|
||||||
v25: Dict[str, Any],
|
|
||||||
v26: Dict[str, Any],
|
|
||||||
) -> Dict[str, Optional[Dict[str, Any]]]:
|
|
||||||
strategies: Dict[str, Optional[Dict[str, Any]]] = {name: None for name in STRATEGIES}
|
|
||||||
|
|
||||||
v25_aggressive = v25.get("aggressive_pick") or {}
|
|
||||||
if v25_aggressive.get("pick"):
|
|
||||||
pick = str(v25_aggressive.get("pick"))
|
|
||||||
strategies["v25_aggressive"] = _evaluate_pick(
|
|
||||||
strategy="v25_aggressive",
|
|
||||||
market=str(v25_aggressive.get("market") or "HTFT"),
|
|
||||||
pick=pick,
|
|
||||||
odds=_market_odds(odds_data, "HTFT", pick),
|
|
||||||
playable=True,
|
|
||||||
confidence=v25_aggressive.get("confidence"),
|
|
||||||
extra={
|
|
||||||
"source": "v25.aggressive_pick",
|
|
||||||
"reversal_pick": pick,
|
|
||||||
},
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
|
|
||||||
v26_surprise = v26.get("surprise_pick") or {}
|
|
||||||
v26_hunter = v26.get("surprise_hunter") or {}
|
|
||||||
if v26_surprise.get("pick"):
|
|
||||||
pick = str(v26_surprise.get("raw_pick") or v26_surprise.get("pick"))
|
|
||||||
strategies["v26_surprise"] = _evaluate_pick(
|
|
||||||
strategy="v26_surprise",
|
|
||||||
market=str(v26_surprise.get("market") or "HTFT"),
|
|
||||||
pick=pick,
|
|
||||||
odds=v26_surprise.get("odds") or _market_odds(odds_data, "HTFT", pick),
|
|
||||||
playable=bool(v26_surprise.get("playable")),
|
|
||||||
confidence=v26_surprise.get("calibrated_confidence", v26_surprise.get("confidence")),
|
|
||||||
extra={
|
|
||||||
"source": "v26.surprise_pick",
|
|
||||||
"surprise_score": round(_safe_float(v26_surprise.get("surprise_score")), 1),
|
|
||||||
"support_score": round(_safe_float(v26_surprise.get("support_score")), 1),
|
|
||||||
"reversal_pick": v26_hunter.get("reversal_pick"),
|
|
||||||
"reversal_prob": round(_safe_float(v26_hunter.get("reversal_prob")), 4),
|
|
||||||
"favorite_gap": round(_safe_float(v26_hunter.get("favorite_gap")), 3),
|
|
||||||
"favorite_odd": round(_safe_float(v26_hunter.get("favorite_odd")), 2),
|
|
||||||
"odds_band_score": round(_safe_float(v26_hunter.get("odds_band_score")), 3),
|
|
||||||
"odds_band_label": str(v26_hunter.get("odds_band_label") or ""),
|
|
||||||
"league_reversal_rate": round(_safe_float(v26_hunter.get("league_reversal_rate")), 4),
|
|
||||||
"league_strict_rev_rate": round(_safe_float(v26_hunter.get("league_strict_rev_rate")), 4),
|
|
||||||
"referee_strict_rev_rate": round(_safe_float(v26_hunter.get("referee_strict_rev_rate")), 4),
|
|
||||||
"reason_codes": ",".join(v26_hunter.get("reason_codes", [])),
|
|
||||||
},
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
|
|
||||||
v26_aggressive = v26.get("aggressive_pick") or {}
|
|
||||||
if v26_aggressive.get("pick"):
|
|
||||||
pick = str(v26_aggressive.get("pick"))
|
|
||||||
strategies["v26_aggressive"] = _evaluate_pick(
|
|
||||||
strategy="v26_aggressive",
|
|
||||||
market=str(v26_aggressive.get("market") or "HTFT"),
|
|
||||||
pick=pick,
|
|
||||||
odds=v26_aggressive.get("odds") or _market_odds(odds_data, "HTFT", pick),
|
|
||||||
playable=True,
|
|
||||||
confidence=v26_aggressive.get("confidence"),
|
|
||||||
extra={
|
|
||||||
"source": "v26.aggressive_pick",
|
|
||||||
"reversal_pick": pick,
|
|
||||||
},
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
|
|
||||||
v26_main = v26.get("main_pick") or {}
|
|
||||||
if str(v26_main.get("market") or "") == "HTFT" and v26_main.get("pick"):
|
|
||||||
pick = str(v26_main.get("raw_pick") or v26_main.get("pick"))
|
|
||||||
strategies["v26_main_htft"] = _evaluate_pick(
|
|
||||||
strategy="v26_main_htft",
|
|
||||||
market="HTFT",
|
|
||||||
pick=pick,
|
|
||||||
odds=v26_main.get("odds") or _market_odds(odds_data, "HTFT", pick),
|
|
||||||
playable=bool(v26_main.get("playable")),
|
|
||||||
confidence=v26_main.get("calibrated_confidence", v26_main.get("confidence")),
|
|
||||||
extra={
|
|
||||||
"source": "v26.main_pick",
|
|
||||||
"pick_reason": v26_main.get("pick_reason"),
|
|
||||||
"surprise_score": round(_safe_float(v26_main.get("surprise_score")), 1),
|
|
||||||
},
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
|
|
||||||
return strategies
|
|
||||||
|
|
||||||
|
|
||||||
def _summarize_bucket(bucket: Dict[str, float]) -> Dict[str, Any]:
|
|
||||||
played = int(bucket["played"])
|
|
||||||
won = int(bucket["won"])
|
|
||||||
lost = int(bucket["lost"])
|
|
||||||
candidate = int(bucket["candidate"])
|
|
||||||
profit = round(bucket["profit"], 4)
|
|
||||||
roi = round((profit / played) * 100.0, 2) if played else 0.0
|
|
||||||
hit = round((won / played) * 100.0, 2) if played else 0.0
|
|
||||||
return {
|
|
||||||
"candidates": candidate,
|
|
||||||
"played": played,
|
|
||||||
"won": won,
|
|
||||||
"lost": lost,
|
|
||||||
"profit_flat": profit,
|
|
||||||
"roi_flat_pct": roi,
|
|
||||||
"hit_rate_pct": hit,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _format_date(ms: int) -> str:
|
|
||||||
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
|
|
||||||
def _build_markdown(report: Dict[str, Any]) -> str:
|
|
||||||
lines: list[str] = []
|
|
||||||
lines.append("# HT/FT + Upset Backtest")
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"- Sample: last {report['sample_size']} finished football matches")
|
|
||||||
lines.append("- Scope: only HT/FT reversal and upset-oriented picks")
|
|
||||||
lines.append("- ROI: flat `1 unit` per played pick")
|
|
||||||
lines.append(f"- Generated at: {report['generated_at']}")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Strategy Summary")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("| Strategy | Candidates | Played | Won | Lost | Hit Rate | Profit | ROI |")
|
|
||||||
lines.append("|---|---:|---:|---:|---:|---:|---:|---:|")
|
|
||||||
for strategy in STRATEGIES:
|
|
||||||
payload = report["summary"]["strategies"][strategy]
|
|
||||||
lines.append(
|
|
||||||
f"| {strategy} | {payload['candidates']} | {payload['played']} | {payload['won']} | "
|
|
||||||
f"{payload['lost']} | {payload['hit_rate_pct']}% | {payload['profit_flat']:+.2f} | {payload['roi_flat_pct']:+.2f}% |"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## v26 Surprise By Reversal Type")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("| Reversal | Candidates | Played | Won | Lost | Profit | ROI |")
|
|
||||||
lines.append("|---|---:|---:|---:|---:|---:|---:|")
|
|
||||||
for reversal, payload in report["summary"]["v26_surprise_by_pick"].items():
|
|
||||||
lines.append(
|
|
||||||
f"| {reversal} | {payload['candidates']} | {payload['played']} | {payload['won']} | "
|
|
||||||
f"{payload['lost']} | {payload['profit_flat']:+.2f} | {payload['roi_flat_pct']:+.2f}% |"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Match Detail")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("| Date | Match | HT | FT | v25 aggressive | v26 surprise | v26 aggressive | v26 main HTFT |")
|
|
||||||
lines.append("|---|---|---|---|---|---|---|---|")
|
|
||||||
for match in report["matches"]:
|
|
||||||
lines.append(
|
|
||||||
f"| {_format_date(match['match_date_ms'])} | {match['match_name']} | {match['ht_score']} | {match['final_score']} | "
|
|
||||||
f"{match['v25_aggressive']} | {match['v26_surprise']} | {match['v26_aggressive']} | {match['v26_main_htft']} |"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description="HT/FT + upset focused backtest.")
|
|
||||||
parser.add_argument("--limit", type=int, default=120, help="Number of finished matches to analyze.")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
dsn = _resolve_dsn()
|
|
||||||
orchestrator = SingleMatchOrchestrator()
|
|
||||||
matches = _fetch_matches(dsn, max(1, args.limit))
|
|
||||||
|
|
||||||
strategy_buckets: Dict[str, Dict[str, float]] = {name: defaultdict(float) for name in STRATEGIES}
|
|
||||||
v26_reversal_buckets: Dict[str, Dict[str, float]] = {label: defaultdict(float) for label in REVERSAL_LABELS}
|
|
||||||
report_matches: list[Dict[str, Any]] = []
|
|
||||||
csv_rows: list[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
for context in matches:
|
|
||||||
data = orchestrator._load_match_data(context.match_id) # noqa: SLF001
|
|
||||||
if data is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
orchestrator.engine_mode = "v25"
|
|
||||||
v25 = orchestrator.analyze_match(context.match_id) or {}
|
|
||||||
orchestrator.engine_mode = "v26"
|
|
||||||
v26 = orchestrator.analyze_match(context.match_id) or {}
|
|
||||||
|
|
||||||
extracted = _extract_strategy_rows(
|
|
||||||
context=context,
|
|
||||||
odds_data=data.odds_data or {},
|
|
||||||
v25=v25,
|
|
||||||
v26=v26,
|
|
||||||
)
|
|
||||||
|
|
||||||
match_row: Dict[str, Any] = {
|
|
||||||
"match_id": context.match_id,
|
|
||||||
"match_name": context.match_name,
|
|
||||||
"league": context.league,
|
|
||||||
"match_date_ms": context.match_date_ms,
|
|
||||||
"ht_score": context.ht_score,
|
|
||||||
"final_score": context.final_score,
|
|
||||||
}
|
|
||||||
|
|
||||||
for strategy, payload in extracted.items():
|
|
||||||
if payload:
|
|
||||||
strategy_buckets[strategy]["candidate"] += 1
|
|
||||||
if payload["counted_in_roi"]:
|
|
||||||
strategy_buckets[strategy]["played"] += 1
|
|
||||||
if payload["result"] == "WON":
|
|
||||||
strategy_buckets[strategy]["won"] += 1
|
|
||||||
else:
|
|
||||||
strategy_buckets[strategy]["lost"] += 1
|
|
||||||
strategy_buckets[strategy]["profit"] += payload["profit_flat"]
|
|
||||||
|
|
||||||
if strategy == "v26_surprise":
|
|
||||||
reversal_label = str(payload.get("reversal_pick") or "")
|
|
||||||
if reversal_label in v26_reversal_buckets:
|
|
||||||
v26_reversal_buckets[reversal_label]["candidate"] += 1
|
|
||||||
if payload["counted_in_roi"]:
|
|
||||||
v26_reversal_buckets[reversal_label]["played"] += 1
|
|
||||||
if payload["result"] == "WON":
|
|
||||||
v26_reversal_buckets[reversal_label]["won"] += 1
|
|
||||||
else:
|
|
||||||
v26_reversal_buckets[reversal_label]["lost"] += 1
|
|
||||||
v26_reversal_buckets[reversal_label]["profit"] += payload["profit_flat"]
|
|
||||||
|
|
||||||
summary = (
|
|
||||||
f"{payload['pick']} ({payload['result']}, {'played' if payload['counted_in_roi'] else 'not played'}, {payload['profit_flat']:+.2f})"
|
|
||||||
)
|
|
||||||
match_row[strategy] = summary
|
|
||||||
|
|
||||||
csv_rows.append(
|
|
||||||
{
|
|
||||||
"match_id": context.match_id,
|
|
||||||
"date": _format_date(context.match_date_ms),
|
|
||||||
"league": context.league,
|
|
||||||
"match": context.match_name,
|
|
||||||
"ht_score": context.ht_score,
|
|
||||||
"final_score": context.final_score,
|
|
||||||
**payload,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
match_row[strategy] = "-"
|
|
||||||
|
|
||||||
report_matches.append(match_row)
|
|
||||||
|
|
||||||
report = {
|
|
||||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
||||||
"sample_size": len(report_matches),
|
|
||||||
"summary": {
|
|
||||||
"strategies": {
|
|
||||||
strategy: _summarize_bucket(bucket)
|
|
||||||
for strategy, bucket in strategy_buckets.items()
|
|
||||||
},
|
|
||||||
"v26_surprise_by_pick": {
|
|
||||||
label: _summarize_bucket(bucket)
|
|
||||||
for label, bucket in v26_reversal_buckets.items()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"matches": report_matches,
|
|
||||||
}
|
|
||||||
|
|
||||||
report_dir = AI_ENGINE_DIR / "reports"
|
|
||||||
json_path = report_dir / "backtest_v26_shadow_htft_upset.json"
|
|
||||||
csv_path = report_dir / "backtest_v26_shadow_htft_upset.csv"
|
|
||||||
md_path = report_dir / "backtest_v26_shadow_htft_upset.md"
|
|
||||||
|
|
||||||
json_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
||||||
with csv_path.open("w", encoding="utf-8", newline="") as handle:
|
|
||||||
writer = csv.DictWriter(
|
|
||||||
handle,
|
|
||||||
fieldnames=[
|
|
||||||
"match_id",
|
|
||||||
"date",
|
|
||||||
"league",
|
|
||||||
"match",
|
|
||||||
"ht_score",
|
|
||||||
"final_score",
|
|
||||||
"strategy",
|
|
||||||
"market",
|
|
||||||
"pick",
|
|
||||||
"odds",
|
|
||||||
"playable",
|
|
||||||
"confidence",
|
|
||||||
"result",
|
|
||||||
"counted_in_roi",
|
|
||||||
"profit_flat",
|
|
||||||
"resolution_note",
|
|
||||||
"source",
|
|
||||||
"reversal_pick",
|
|
||||||
"reversal_prob",
|
|
||||||
"favorite_gap",
|
|
||||||
"favorite_odd",
|
|
||||||
"support_score",
|
|
||||||
"odds_band_score",
|
|
||||||
"odds_band_label",
|
|
||||||
"league_reversal_rate",
|
|
||||||
"league_strict_rev_rate",
|
|
||||||
"referee_strict_rev_rate",
|
|
||||||
"surprise_score",
|
|
||||||
"reason_codes",
|
|
||||||
"pick_reason",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerows(csv_rows)
|
|
||||||
md_path.write_text(_build_markdown(report), encoding="utf-8")
|
|
||||||
|
|
||||||
print(f"[OK] JSON report written to {json_path}")
|
|
||||||
print(f"[OK] CSV report written to {csv_path}")
|
|
||||||
print(f"[OK] Markdown report written to {md_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,810 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import csv
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, Iterable, Optional
|
|
||||||
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
|
|
||||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_DIR))
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
|
||||||
from utils.top_leagues import load_top_league_ids
|
|
||||||
|
|
||||||
|
|
||||||
MARKET_ORDER = [
|
|
||||||
"MS",
|
|
||||||
"DC",
|
|
||||||
"OU15",
|
|
||||||
"OU25",
|
|
||||||
"OU35",
|
|
||||||
"BTTS",
|
|
||||||
"HT",
|
|
||||||
"HT_OU05",
|
|
||||||
"HT_OU15",
|
|
||||||
"HTFT",
|
|
||||||
"OE",
|
|
||||||
"CARDS",
|
|
||||||
"HCAP",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MatchContext:
|
|
||||||
match_id: str
|
|
||||||
match_date_ms: int
|
|
||||||
league_id: Optional[str]
|
|
||||||
league: str
|
|
||||||
home_team: str
|
|
||||||
away_team: str
|
|
||||||
final_home: int
|
|
||||||
final_away: int
|
|
||||||
ht_home: Optional[int]
|
|
||||||
ht_away: Optional[int]
|
|
||||||
total_cards: Optional[float]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def match_name(self) -> str:
|
|
||||||
return f"{self.home_team} vs {self.away_team}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def final_score(self) -> str:
|
|
||||||
return f"{self.final_home}-{self.final_away}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ht_score(self) -> Optional[str]:
|
|
||||||
if self.ht_home is None or self.ht_away is None:
|
|
||||||
return None
|
|
||||||
return f"{self.ht_home}-{self.ht_away}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_goals(self) -> int:
|
|
||||||
return self.final_home + self.final_away
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_ht_goals(self) -> Optional[int]:
|
|
||||||
if self.ht_home is None or self.ht_away is None:
|
|
||||||
return None
|
|
||||||
return self.ht_home + self.ht_away
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_dsn() -> str:
|
|
||||||
env_path = AI_ENGINE_DIR / ".env"
|
|
||||||
if env_path.exists():
|
|
||||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
||||||
if line.startswith("DATABASE_URL="):
|
|
||||||
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
|
||||||
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
|
||||||
|
|
||||||
|
|
||||||
def _fetch_matches(
|
|
||||||
dsn: str,
|
|
||||||
limit: int,
|
|
||||||
top_league_ids: Optional[list[str]] = None,
|
|
||||||
) -> list[MatchContext]:
|
|
||||||
query = """
|
|
||||||
SELECT
|
|
||||||
m.id,
|
|
||||||
m.mst_utc,
|
|
||||||
m.league_id,
|
|
||||||
COALESCE(l.name, 'Unknown League') AS league,
|
|
||||||
COALESCE(ht.name, 'Home') AS home_team,
|
|
||||||
COALESCE(at.name, 'Away') AS away_team,
|
|
||||||
COALESCE(m.score_home, 0) AS score_home,
|
|
||||||
COALESCE(m.score_away, 0) AS score_away,
|
|
||||||
m.ht_score_home,
|
|
||||||
m.ht_score_away,
|
|
||||||
cards.total_cards
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN leagues l ON l.id = m.league_id
|
|
||||||
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
|
||||||
LEFT JOIN teams at ON at.id = m.away_team_id
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT
|
|
||||||
mpe.match_id,
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN mpe.event_type::text LIKE '%%yellow_card%%' THEN 1
|
|
||||||
WHEN mpe.event_type::text LIKE '%%red_card%%' THEN 2
|
|
||||||
ELSE 1
|
|
||||||
END
|
|
||||||
)::float AS total_cards
|
|
||||||
FROM match_player_events mpe
|
|
||||||
WHERE mpe.event_type::text LIKE '%%card%%'
|
|
||||||
GROUP BY mpe.match_id
|
|
||||||
) cards ON cards.match_id = m.id
|
|
||||||
WHERE m.status = 'FT'
|
|
||||||
AND m.sport = 'football'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND m.score_away IS NOT NULL
|
|
||||||
"""
|
|
||||||
params: list[Any] = []
|
|
||||||
if top_league_ids:
|
|
||||||
query += " AND m.league_id = ANY(%s)"
|
|
||||||
params.append(top_league_ids)
|
|
||||||
query += """
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT %s
|
|
||||||
"""
|
|
||||||
params.append(limit)
|
|
||||||
with psycopg2.connect(dsn) as conn:
|
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
||||||
cur.execute(query, params)
|
|
||||||
rows = cur.fetchall()
|
|
||||||
|
|
||||||
results: list[MatchContext] = []
|
|
||||||
for row in rows:
|
|
||||||
results.append(
|
|
||||||
MatchContext(
|
|
||||||
match_id=str(row["id"]),
|
|
||||||
match_date_ms=int(row["mst_utc"] or 0),
|
|
||||||
league_id=str(row["league_id"]) if row.get("league_id") else None,
|
|
||||||
league=str(row["league"] or "Unknown League"),
|
|
||||||
home_team=str(row["home_team"] or "Home"),
|
|
||||||
away_team=str(row["away_team"] or "Away"),
|
|
||||||
final_home=int(row["score_home"] or 0),
|
|
||||||
final_away=int(row["score_away"] or 0),
|
|
||||||
ht_home=(
|
|
||||||
int(row["ht_score_home"])
|
|
||||||
if row.get("ht_score_home") is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
ht_away=(
|
|
||||||
int(row["ht_score_away"])
|
|
||||||
if row.get("ht_score_away") is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
total_cards=(
|
|
||||||
float(row["total_cards"])
|
|
||||||
if row.get("total_cards") is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def _odds_band(odds: float) -> str:
|
|
||||||
if odds < 1.5:
|
|
||||||
return "<1.50"
|
|
||||||
if odds < 1.8:
|
|
||||||
return "1.50-1.79"
|
|
||||||
if odds < 2.1:
|
|
||||||
return "1.80-2.09"
|
|
||||||
if odds < 2.5:
|
|
||||||
return "2.10-2.49"
|
|
||||||
return "2.50+"
|
|
||||||
|
|
||||||
|
|
||||||
def _confidence_band(confidence: float) -> str:
|
|
||||||
if confidence < 55.0:
|
|
||||||
return "<55"
|
|
||||||
if confidence < 65.0:
|
|
||||||
return "55-64.9"
|
|
||||||
if confidence < 75.0:
|
|
||||||
return "65-74.9"
|
|
||||||
return "75+"
|
|
||||||
|
|
||||||
|
|
||||||
def _edge_band(edge: float) -> str:
|
|
||||||
if edge < 0.03:
|
|
||||||
return "<0.03"
|
|
||||||
if edge < 0.06:
|
|
||||||
return "0.03-0.059"
|
|
||||||
if edge < 0.10:
|
|
||||||
return "0.06-0.099"
|
|
||||||
return "0.10+"
|
|
||||||
|
|
||||||
|
|
||||||
def _top_n_buckets(rows: Iterable[tuple[str, float]], limit: int = 10) -> list[dict[str, Any]]:
|
|
||||||
ranked = sorted(rows, key=lambda item: (-item[1], item[0]))
|
|
||||||
return [
|
|
||||||
{"label": label, "count": int(count)}
|
|
||||||
for label, count in ranked[:limit]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _summarize_v26_losses(csv_rows: list[Dict[str, Any]]) -> Dict[str, Any]:
|
|
||||||
losses = [
|
|
||||||
row for row in csv_rows
|
|
||||||
if row.get("model") == "v26.shadow"
|
|
||||||
and bool(row.get("counted_in_roi"))
|
|
||||||
and row.get("result") == "LOST"
|
|
||||||
]
|
|
||||||
by_market: Dict[str, float] = defaultdict(float)
|
|
||||||
by_league: Dict[str, float] = defaultdict(float)
|
|
||||||
by_pick: Dict[str, float] = defaultdict(float)
|
|
||||||
by_odds_band: Dict[str, float] = defaultdict(float)
|
|
||||||
by_conf_band: Dict[str, float] = defaultdict(float)
|
|
||||||
by_edge_band: Dict[str, float] = defaultdict(float)
|
|
||||||
|
|
||||||
for row in losses:
|
|
||||||
market = str(row.get("market") or "UNKNOWN")
|
|
||||||
league = str(row.get("league") or "Unknown League")
|
|
||||||
pick = str(row.get("pick") or "")
|
|
||||||
odds = _safe_float(row.get("odds"))
|
|
||||||
confidence = _safe_float(row.get("confidence"))
|
|
||||||
edge = _safe_float(row.get("edge"))
|
|
||||||
|
|
||||||
by_market[market] += 1
|
|
||||||
by_league[league] += 1
|
|
||||||
by_pick[f"{market} {pick}".strip()] += 1
|
|
||||||
by_odds_band[_odds_band(odds)] += 1
|
|
||||||
by_conf_band[_confidence_band(confidence)] += 1
|
|
||||||
by_edge_band[_edge_band(edge)] += 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
"lost_bets": len(losses),
|
|
||||||
"by_market": _top_n_buckets(by_market.items(), limit=20),
|
|
||||||
"by_league": _top_n_buckets(by_league.items(), limit=15),
|
|
||||||
"by_pick": _top_n_buckets(by_pick.items(), limit=15),
|
|
||||||
"by_odds_band": _top_n_buckets(by_odds_band.items(), limit=10),
|
|
||||||
"by_confidence_band": _top_n_buckets(by_conf_band.items(), limit=10),
|
|
||||||
"by_edge_band": _top_n_buckets(by_edge_band.items(), limit=10),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_float(value: Any) -> float:
|
|
||||||
try:
|
|
||||||
return float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_text(value: Any) -> str:
|
|
||||||
text = str(value or "").strip().upper()
|
|
||||||
return (
|
|
||||||
text.replace("İ", "I")
|
|
||||||
.replace("İ", "I")
|
|
||||||
.replace("Ş", "S")
|
|
||||||
.replace("Ğ", "G")
|
|
||||||
.replace("Ü", "U")
|
|
||||||
.replace("Ö", "O")
|
|
||||||
.replace("Ç", "C")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _outcome_symbol(home: int, away: int) -> str:
|
|
||||||
if home > away:
|
|
||||||
return "1"
|
|
||||||
if home < away:
|
|
||||||
return "2"
|
|
||||||
return "X"
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_pick(
|
|
||||||
market: str,
|
|
||||||
pick: str,
|
|
||||||
context: MatchContext,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
market_code = _normalize_text(market).replace("/", "")
|
|
||||||
pick_text = str(pick or "").strip()
|
|
||||||
pick_norm = _normalize_text(pick_text)
|
|
||||||
|
|
||||||
if not market_code or not pick_norm:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "pick_missing"}
|
|
||||||
|
|
||||||
if market_code == "HTFT":
|
|
||||||
market_code = "HTFT"
|
|
||||||
if market_code == "HTFT" or market_code == "HTFT":
|
|
||||||
if context.ht_home is None or context.ht_away is None:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "ht_score_missing"}
|
|
||||||
if "/" not in pick_text:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "htft_pick_invalid"}
|
|
||||||
ht_pick, ft_pick = pick_text.split("/", 1)
|
|
||||||
actual = f"{_outcome_symbol(context.ht_home, context.ht_away)}/{_outcome_symbol(context.final_home, context.final_away)}"
|
|
||||||
won = f"{_normalize_text(ht_pick)}/{_normalize_text(ft_pick)}" == actual
|
|
||||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
|
||||||
|
|
||||||
if market_code == "MS":
|
|
||||||
actual = _outcome_symbol(context.final_home, context.final_away)
|
|
||||||
won = pick_norm in {actual, f"MS {actual}"}
|
|
||||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
|
||||||
|
|
||||||
if market_code == "DC":
|
|
||||||
actual = _outcome_symbol(context.final_home, context.final_away)
|
|
||||||
winning = {
|
|
||||||
"1X": {"1", "X"},
|
|
||||||
"X2": {"X", "2"},
|
|
||||||
"12": {"1", "2"},
|
|
||||||
}
|
|
||||||
won = actual in winning.get(pick_norm, set())
|
|
||||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
|
||||||
|
|
||||||
if market_code in {"OU15", "OU25", "OU35", "HTOU05", "HTOU15", "HT_OU05", "HT_OU15"}:
|
|
||||||
if market_code in {"HTOU05", "HTOU15", "HT_OU05", "HT_OU15"}:
|
|
||||||
if context.total_ht_goals is None:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "ht_score_missing"}
|
|
||||||
total = context.total_ht_goals
|
|
||||||
line = 0.5 if "05" in market_code else 1.5
|
|
||||||
else:
|
|
||||||
total = context.total_goals
|
|
||||||
line = {"OU15": 1.5, "OU25": 2.5, "OU35": 3.5}[market_code]
|
|
||||||
|
|
||||||
if "UST" in pick_norm or "OVER" in pick_norm:
|
|
||||||
won = total > line
|
|
||||||
side = "OVER"
|
|
||||||
elif "ALT" in pick_norm or "UNDER" in pick_norm:
|
|
||||||
won = total < line
|
|
||||||
side = "UNDER"
|
|
||||||
else:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "ou_side_unknown"}
|
|
||||||
return {
|
|
||||||
"result": "WON" if won else "LOST",
|
|
||||||
"won": won,
|
|
||||||
"note": f"actual_total={total} side={side} line={line}",
|
|
||||||
}
|
|
||||||
|
|
||||||
if market_code == "BTTS":
|
|
||||||
both_scored = context.final_home > 0 and context.final_away > 0
|
|
||||||
if "VAR" in pick_norm or "YES" in pick_norm:
|
|
||||||
won = both_scored
|
|
||||||
side = "YES"
|
|
||||||
elif "YOK" in pick_norm or pick_norm.endswith("NO") or pick_norm == "NO":
|
|
||||||
won = not both_scored
|
|
||||||
side = "NO"
|
|
||||||
else:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "btts_side_unknown"}
|
|
||||||
return {
|
|
||||||
"result": "WON" if won else "LOST",
|
|
||||||
"won": won,
|
|
||||||
"note": f"actual_btts={'YES' if both_scored else 'NO'} side={side}",
|
|
||||||
}
|
|
||||||
|
|
||||||
if market_code == "HT":
|
|
||||||
if context.ht_home is None or context.ht_away is None:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "ht_score_missing"}
|
|
||||||
actual = _outcome_symbol(context.ht_home, context.ht_away)
|
|
||||||
won = pick_norm == actual
|
|
||||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
|
||||||
|
|
||||||
if market_code == "OE":
|
|
||||||
actual = "EVEN" if context.total_goals % 2 == 0 else "ODD"
|
|
||||||
if pick_norm in {"CIFT", "EVEN"}:
|
|
||||||
wanted = "EVEN"
|
|
||||||
elif pick_norm in {"TEK", "ODD"}:
|
|
||||||
wanted = "ODD"
|
|
||||||
else:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "oe_pick_unknown"}
|
|
||||||
won = actual == wanted
|
|
||||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
|
||||||
|
|
||||||
if market_code == "CARDS":
|
|
||||||
if context.total_cards is None:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "cards_missing"}
|
|
||||||
if "UST" in pick_norm or "OVER" in pick_norm:
|
|
||||||
won = context.total_cards > 4.5
|
|
||||||
side = "OVER"
|
|
||||||
elif "ALT" in pick_norm or "UNDER" in pick_norm:
|
|
||||||
won = context.total_cards < 4.5
|
|
||||||
side = "UNDER"
|
|
||||||
else:
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "cards_side_unknown"}
|
|
||||||
return {
|
|
||||||
"result": "WON" if won else "LOST",
|
|
||||||
"won": won,
|
|
||||||
"note": f"actual_cards={context.total_cards:.1f} side={side} line=4.5",
|
|
||||||
}
|
|
||||||
|
|
||||||
if market_code == "HCAP":
|
|
||||||
adjusted_home = context.final_home - 1.0
|
|
||||||
adjusted_away = float(context.final_away)
|
|
||||||
if adjusted_home > adjusted_away:
|
|
||||||
actual = "1"
|
|
||||||
elif adjusted_home < adjusted_away:
|
|
||||||
actual = "2"
|
|
||||||
else:
|
|
||||||
actual = "X"
|
|
||||||
won = pick_norm == actual
|
|
||||||
return {
|
|
||||||
"result": "WON" if won else "LOST",
|
|
||||||
"won": won,
|
|
||||||
"note": f"actual={actual} line_home=-1.0",
|
|
||||||
}
|
|
||||||
|
|
||||||
return {"result": "UNRESOLVED", "won": None, "note": "market_not_supported"}
|
|
||||||
|
|
||||||
|
|
||||||
def _evaluate_row(
|
|
||||||
market: str,
|
|
||||||
pick: str,
|
|
||||||
odds: Any,
|
|
||||||
playable: bool,
|
|
||||||
stake_units: Any,
|
|
||||||
context: MatchContext,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
resolution = _resolve_pick(market, pick, context)
|
|
||||||
odds_value = _safe_float(odds)
|
|
||||||
stake_value = _safe_float(stake_units)
|
|
||||||
counted = bool(playable and odds_value > 1.01 and resolution["result"] in {"WON", "LOST"})
|
|
||||||
|
|
||||||
flat_profit = 0.0
|
|
||||||
stake_profit = 0.0
|
|
||||||
if counted:
|
|
||||||
flat_profit = (odds_value - 1.0) if resolution["result"] == "WON" else -1.0
|
|
||||||
stake_profit = flat_profit * (stake_value if stake_value > 0 else 1.0)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"result": resolution["result"],
|
|
||||||
"won": resolution["won"],
|
|
||||||
"resolution_note": resolution["note"],
|
|
||||||
"counted_in_roi": counted,
|
|
||||||
"profit_flat": round(flat_profit, 4),
|
|
||||||
"profit_stake": round(stake_profit, 4),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _summarize_bucket(bucket: Dict[str, float]) -> Dict[str, Any]:
|
|
||||||
played = int(bucket["played"])
|
|
||||||
won = int(bucket["won"])
|
|
||||||
lost = int(bucket["lost"])
|
|
||||||
unresolved = int(bucket["unresolved"])
|
|
||||||
profit = round(bucket["profit"], 4)
|
|
||||||
roi = round((profit / played) * 100.0, 2) if played else 0.0
|
|
||||||
win_rate = round((won / played) * 100.0, 2) if played else 0.0
|
|
||||||
return {
|
|
||||||
"played": played,
|
|
||||||
"won": won,
|
|
||||||
"lost": lost,
|
|
||||||
"unresolved": unresolved,
|
|
||||||
"profit_flat": profit,
|
|
||||||
"roi_flat_pct": roi,
|
|
||||||
"win_rate_pct": win_rate,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _format_date(ms: int) -> str:
|
|
||||||
if ms <= 0:
|
|
||||||
return "-"
|
|
||||||
dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
|
||||||
return dt.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
|
|
||||||
def _build_markdown_report(report: Dict[str, Any]) -> str:
|
|
||||||
lines: list[str] = []
|
|
||||||
lines.append("# v25 vs v26.shadow ROI Report")
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"- Sample: last {report['sample_size']} finished football matches")
|
|
||||||
if report.get("top_leagues_only"):
|
|
||||||
lines.append("- Filter: top leagues only")
|
|
||||||
lines.append("- ROI calculation: flat `1 unit` per playable and resolvable bet")
|
|
||||||
lines.append(f"- Generated at: {report['generated_at']}")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Overall Summary")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("| Model | Played | Won | Lost | Win Rate | Profit | ROI | Main Pick ROI | Main Pick W/L |")
|
|
||||||
lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---|")
|
|
||||||
for model_name, payload in report["summary"]["models"].items():
|
|
||||||
main = payload["main_pick"]
|
|
||||||
lines.append(
|
|
||||||
f"| {model_name} | {payload['all_playable']['played']} | {payload['all_playable']['won']} | "
|
|
||||||
f"{payload['all_playable']['lost']} | {payload['all_playable']['win_rate_pct']}% | "
|
|
||||||
f"{payload['all_playable']['profit_flat']:+.2f} | {payload['all_playable']['roi_flat_pct']:+.2f}% | "
|
|
||||||
f"{main['roi_flat_pct']:+.2f}% | {main['won']}/{main['played']} |"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Market Summary")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("| Model | Market | Played | Won | Lost | Profit | ROI |")
|
|
||||||
lines.append("|---|---|---:|---:|---:|---:|---:|")
|
|
||||||
for model_name, markets in report["summary"]["markets"].items():
|
|
||||||
for market_name in MARKET_ORDER:
|
|
||||||
payload = markets.get(market_name)
|
|
||||||
if not payload or payload["played"] == 0:
|
|
||||||
continue
|
|
||||||
lines.append(
|
|
||||||
f"| {model_name} | {market_name} | {payload['played']} | {payload['won']} | {payload['lost']} | "
|
|
||||||
f"{payload['profit_flat']:+.2f} | {payload['roi_flat_pct']:+.2f}% |"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
loss_summary = report["summary"].get("v26_loss_analysis", {})
|
|
||||||
if loss_summary:
|
|
||||||
lines.append("## v26 Loss Analysis")
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"- Lost bets: {loss_summary.get('lost_bets', 0)}")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("| Bucket | Top Items |")
|
|
||||||
lines.append("|---|---|")
|
|
||||||
for label, key in (
|
|
||||||
("By market", "by_market"),
|
|
||||||
("By league", "by_league"),
|
|
||||||
("By pick", "by_pick"),
|
|
||||||
("By odds band", "by_odds_band"),
|
|
||||||
("By confidence band", "by_confidence_band"),
|
|
||||||
("By edge band", "by_edge_band"),
|
|
||||||
):
|
|
||||||
items = loss_summary.get(key) or []
|
|
||||||
rendered = ", ".join(f"{item['label']} ({item['count']})" for item in items[:6]) or "-"
|
|
||||||
lines.append(f"| {label} | {rendered} |")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Match By Match")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("| Date | Match | Score | v25 Main | v25 Played Picks | v25 Profit | v26 Main | v26 Played Picks | v26 Profit |")
|
|
||||||
lines.append("|---|---|---|---|---|---:|---|---|---:|")
|
|
||||||
for match in report["matches"]:
|
|
||||||
v25 = match["models"]["v25"]
|
|
||||||
v26 = match["models"]["v26.shadow"]
|
|
||||||
lines.append(
|
|
||||||
f"| {_format_date(match['match_date_ms'])} | {match['match_name']} | {match['final_score']} | "
|
|
||||||
f"{v25['main_pick']['summary']} | {v25['played_picks_summary']} | {v25['profit_flat']:+.2f} | "
|
|
||||||
f"{v26['main_pick']['summary']} | {v26['played_picks_summary']} | {v26['profit_flat']:+.2f} |"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Detailed ROI backtest for v25 vs v26.shadow.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--limit", type=int, default=60, help="Number of finished matches to analyze.")
|
|
||||||
parser.add_argument(
|
|
||||||
"--top-leagues-only",
|
|
||||||
action="store_true",
|
|
||||||
help="Only analyze matches whose league_id exists in top_leagues.json.",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
dsn = _resolve_dsn()
|
|
||||||
top_league_ids = sorted(load_top_league_ids()) if args.top_leagues_only else None
|
|
||||||
matches = _fetch_matches(dsn, max(1, args.limit), top_league_ids=top_league_ids)
|
|
||||||
orchestrator = SingleMatchOrchestrator()
|
|
||||||
|
|
||||||
report_matches: list[Dict[str, Any]] = []
|
|
||||||
model_aggregate: Dict[str, Dict[str, float]] = {
|
|
||||||
"v25": defaultdict(float),
|
|
||||||
"v26.shadow": defaultdict(float),
|
|
||||||
}
|
|
||||||
main_pick_aggregate: Dict[str, Dict[str, float]] = {
|
|
||||||
"v25": defaultdict(float),
|
|
||||||
"v26.shadow": defaultdict(float),
|
|
||||||
}
|
|
||||||
market_aggregate: Dict[str, Dict[str, Dict[str, float]]] = {
|
|
||||||
"v25": defaultdict(lambda: defaultdict(float)),
|
|
||||||
"v26.shadow": defaultdict(lambda: defaultdict(float)),
|
|
||||||
}
|
|
||||||
csv_rows: list[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
for context in matches:
|
|
||||||
match_payload = {
|
|
||||||
"match_id": context.match_id,
|
|
||||||
"match_name": context.match_name,
|
|
||||||
"league": context.league,
|
|
||||||
"match_date_ms": context.match_date_ms,
|
|
||||||
"final_score": context.final_score,
|
|
||||||
"ht_score": context.ht_score,
|
|
||||||
"total_cards": context.total_cards,
|
|
||||||
"models": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
for model_name, mode in (("v25", "v25"), ("v26.shadow", "v26")):
|
|
||||||
orchestrator.engine_mode = mode
|
|
||||||
package = orchestrator.analyze_match(context.match_id) or {}
|
|
||||||
rows = package.get("bet_summary") or []
|
|
||||||
evaluated_rows: list[Dict[str, Any]] = []
|
|
||||||
match_profit = 0.0
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
market = str(row.get("market") or "")
|
|
||||||
pick = str(row.get("pick") or "")
|
|
||||||
evaluation = _evaluate_row(
|
|
||||||
market=market,
|
|
||||||
pick=pick,
|
|
||||||
odds=row.get("odds"),
|
|
||||||
playable=bool(row.get("playable")),
|
|
||||||
stake_units=row.get("stake_units"),
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
combined = {
|
|
||||||
"market": market,
|
|
||||||
"pick": pick,
|
|
||||||
"playable": bool(row.get("playable")),
|
|
||||||
"bet_grade": row.get("bet_grade"),
|
|
||||||
"odds": round(_safe_float(row.get("odds")), 2),
|
|
||||||
"calibrated_confidence": round(_safe_float(row.get("calibrated_confidence")), 1),
|
|
||||||
"edge": round(_safe_float(row.get("ev_edge", row.get("edge"))), 4),
|
|
||||||
"stake_units": round(_safe_float(row.get("stake_units")), 2),
|
|
||||||
**evaluation,
|
|
||||||
}
|
|
||||||
evaluated_rows.append(combined)
|
|
||||||
|
|
||||||
if combined["counted_in_roi"]:
|
|
||||||
bucket = market_aggregate[model_name][market]
|
|
||||||
bucket["played"] += 1
|
|
||||||
if combined["result"] == "WON":
|
|
||||||
bucket["won"] += 1
|
|
||||||
else:
|
|
||||||
bucket["lost"] += 1
|
|
||||||
bucket["profit"] += combined["profit_flat"]
|
|
||||||
|
|
||||||
model_bucket = model_aggregate[model_name]
|
|
||||||
model_bucket["played"] += 1
|
|
||||||
if combined["result"] == "WON":
|
|
||||||
model_bucket["won"] += 1
|
|
||||||
else:
|
|
||||||
model_bucket["lost"] += 1
|
|
||||||
model_bucket["profit"] += combined["profit_flat"]
|
|
||||||
match_profit += combined["profit_flat"]
|
|
||||||
elif combined["playable"]:
|
|
||||||
model_aggregate[model_name]["unresolved"] += 1
|
|
||||||
market_aggregate[model_name][market]["unresolved"] += 1
|
|
||||||
|
|
||||||
csv_rows.append(
|
|
||||||
{
|
|
||||||
"match_id": context.match_id,
|
|
||||||
"date": _format_date(context.match_date_ms),
|
|
||||||
"league": context.league,
|
|
||||||
"match": context.match_name,
|
|
||||||
"final_score": context.final_score,
|
|
||||||
"ht_score": context.ht_score or "",
|
|
||||||
"model": model_name,
|
|
||||||
"market": market,
|
|
||||||
"pick": pick,
|
|
||||||
"playable": combined["playable"],
|
|
||||||
"bet_grade": combined["bet_grade"],
|
|
||||||
"odds": combined["odds"],
|
|
||||||
"confidence": combined["calibrated_confidence"],
|
|
||||||
"edge": combined["edge"],
|
|
||||||
"result": combined["result"],
|
|
||||||
"counted_in_roi": combined["counted_in_roi"],
|
|
||||||
"profit_flat": combined["profit_flat"],
|
|
||||||
"resolution_note": combined["resolution_note"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
main_pick = package.get("main_pick") or {}
|
|
||||||
main_eval = _evaluate_row(
|
|
||||||
market=str(main_pick.get("market") or ""),
|
|
||||||
pick=str(main_pick.get("pick") or ""),
|
|
||||||
odds=main_pick.get("odds"),
|
|
||||||
playable=bool(main_pick.get("playable")),
|
|
||||||
stake_units=main_pick.get("stake_units"),
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
main_pick_summary = {
|
|
||||||
"market": main_pick.get("market"),
|
|
||||||
"pick": main_pick.get("pick"),
|
|
||||||
"playable": bool(main_pick.get("playable")),
|
|
||||||
"odds": round(_safe_float(main_pick.get("odds")), 2),
|
|
||||||
"confidence": round(
|
|
||||||
_safe_float(
|
|
||||||
main_pick.get("calibrated_confidence", main_pick.get("confidence"))
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
"edge": round(_safe_float(main_pick.get("ev_edge", main_pick.get("edge"))), 4),
|
|
||||||
**main_eval,
|
|
||||||
}
|
|
||||||
|
|
||||||
if main_pick_summary["counted_in_roi"]:
|
|
||||||
summary_suffix = (
|
|
||||||
f"{main_pick_summary['result']}, played, {main_pick_summary['profit_flat']:+.2f}"
|
|
||||||
)
|
|
||||||
elif main_pick_summary.get("market") and main_pick_summary.get("pick"):
|
|
||||||
summary_suffix = f"{main_pick_summary['result']}, not played"
|
|
||||||
else:
|
|
||||||
summary_suffix = ""
|
|
||||||
|
|
||||||
if main_pick_summary["counted_in_roi"]:
|
|
||||||
bucket = main_pick_aggregate[model_name]
|
|
||||||
bucket["played"] += 1
|
|
||||||
if main_pick_summary["result"] == "WON":
|
|
||||||
bucket["won"] += 1
|
|
||||||
else:
|
|
||||||
bucket["lost"] += 1
|
|
||||||
bucket["profit"] += main_pick_summary["profit_flat"]
|
|
||||||
elif main_pick_summary["playable"]:
|
|
||||||
main_pick_aggregate[model_name]["unresolved"] += 1
|
|
||||||
|
|
||||||
main_pick_summary["summary"] = (
|
|
||||||
f"{main_pick_summary['market']} {main_pick_summary['pick']} "
|
|
||||||
f"({summary_suffix})"
|
|
||||||
if main_pick_summary.get("market") and main_pick_summary.get("pick")
|
|
||||||
else "No main pick"
|
|
||||||
)
|
|
||||||
|
|
||||||
played_rows = [row for row in evaluated_rows if row["counted_in_roi"]]
|
|
||||||
played_picks_summary = (
|
|
||||||
"; ".join(
|
|
||||||
f"{row['market']} {row['pick']}={row['result']} ({row['profit_flat']:+.2f})"
|
|
||||||
for row in played_rows
|
|
||||||
)
|
|
||||||
if played_rows
|
|
||||||
else "-"
|
|
||||||
)
|
|
||||||
|
|
||||||
match_payload["models"][model_name] = {
|
|
||||||
"main_pick": main_pick_summary,
|
|
||||||
"profit_flat": round(match_profit, 4),
|
|
||||||
"played_picks_summary": played_picks_summary,
|
|
||||||
"played_picks": played_rows,
|
|
||||||
"all_picks": evaluated_rows,
|
|
||||||
}
|
|
||||||
|
|
||||||
report_matches.append(match_payload)
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"models": {
|
|
||||||
model_name: {
|
|
||||||
"all_playable": _summarize_bucket(model_aggregate[model_name]),
|
|
||||||
"main_pick": _summarize_bucket(main_pick_aggregate[model_name]),
|
|
||||||
}
|
|
||||||
for model_name in ("v25", "v26.shadow")
|
|
||||||
},
|
|
||||||
"markets": {
|
|
||||||
model_name: {
|
|
||||||
market_name: _summarize_bucket(bucket)
|
|
||||||
for market_name, bucket in sorted(
|
|
||||||
market_aggregate[model_name].items(),
|
|
||||||
key=lambda item: (
|
|
||||||
MARKET_ORDER.index(item[0]) if item[0] in MARKET_ORDER else 999,
|
|
||||||
item[0],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for model_name in ("v25", "v26.shadow")
|
|
||||||
},
|
|
||||||
"v26_loss_analysis": _summarize_v26_losses(csv_rows),
|
|
||||||
}
|
|
||||||
|
|
||||||
report = {
|
|
||||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
||||||
"sample_size": len(report_matches),
|
|
||||||
"top_leagues_only": bool(args.top_leagues_only),
|
|
||||||
"summary": summary,
|
|
||||||
"matches": report_matches,
|
|
||||||
}
|
|
||||||
|
|
||||||
report_dir = AI_ENGINE_DIR / "reports"
|
|
||||||
json_path = report_dir / "backtest_v26_shadow_roi_detail.json"
|
|
||||||
csv_path = report_dir / "backtest_v26_shadow_roi_picks.csv"
|
|
||||||
md_path = report_dir / "backtest_v26_shadow_roi_report.md"
|
|
||||||
|
|
||||||
json_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
||||||
|
|
||||||
with csv_path.open("w", encoding="utf-8", newline="") as handle:
|
|
||||||
writer = csv.DictWriter(
|
|
||||||
handle,
|
|
||||||
fieldnames=[
|
|
||||||
"match_id",
|
|
||||||
"date",
|
|
||||||
"league",
|
|
||||||
"match",
|
|
||||||
"final_score",
|
|
||||||
"ht_score",
|
|
||||||
"model",
|
|
||||||
"market",
|
|
||||||
"pick",
|
|
||||||
"playable",
|
|
||||||
"bet_grade",
|
|
||||||
"odds",
|
|
||||||
"confidence",
|
|
||||||
"edge",
|
|
||||||
"result",
|
|
||||||
"counted_in_roi",
|
|
||||||
"profit_flat",
|
|
||||||
"resolution_note",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerows(csv_rows)
|
|
||||||
|
|
||||||
md_path.write_text(_build_markdown_report(report), encoding="utf-8")
|
|
||||||
|
|
||||||
print(f"[OK] JSON report written to {json_path}")
|
|
||||||
print(f"[OK] CSV report written to {csv_path}")
|
|
||||||
print(f"[OK] Markdown report written to {md_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
"""
|
|
||||||
Backtest the live V2 predictor stack against recent finished football matches.
|
|
||||||
|
|
||||||
This script uses the same path as production:
|
|
||||||
database -> feature extractor -> betting predictor -> quant ranking.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from sqlalchemy import text
|
|
||||||
|
|
||||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
|
||||||
if str(ROOT_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(ROOT_DIR))
|
|
||||||
|
|
||||||
from core.quant import MarketPick, analyze_market
|
|
||||||
from data.database import dispose_engine, get_session
|
|
||||||
from features.extractor import extract_features
|
|
||||||
from models.betting_engine import get_predictor
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BacktestStats:
|
|
||||||
sampled_matches: int = 0
|
|
||||||
analyzed_matches: int = 0
|
|
||||||
skipped_matches: int = 0
|
|
||||||
ms_correct: int = 0
|
|
||||||
ou25_correct: int = 0
|
|
||||||
btts_correct: int = 0
|
|
||||||
main_pick_count: int = 0
|
|
||||||
main_pick_correct: int = 0
|
|
||||||
playable_pick_count: int = 0
|
|
||||||
playable_pick_correct: int = 0
|
|
||||||
playable_units_staked: float = 0.0
|
|
||||||
playable_units_profit: float = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--limit", type=int, default=50)
|
|
||||||
parser.add_argument("--days", type=int, default=45)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def _actual_ms(score_home: int, score_away: int) -> str:
|
|
||||||
if score_home > score_away:
|
|
||||||
return "1"
|
|
||||||
if score_home < score_away:
|
|
||||||
return "2"
|
|
||||||
return "X"
|
|
||||||
|
|
||||||
|
|
||||||
def _actual_ou25(score_home: int, score_away: int) -> str:
|
|
||||||
return "Over" if (score_home + score_away) > 2 else "Under"
|
|
||||||
|
|
||||||
|
|
||||||
def _actual_btts(score_home: int, score_away: int) -> str:
|
|
||||||
return "Yes" if score_home > 0 and score_away > 0 else "No"
|
|
||||||
|
|
||||||
|
|
||||||
def _odds_map_from_features(feats) -> dict[str, dict[str, float]]:
|
|
||||||
return {
|
|
||||||
"MS": {"1": feats.odds_home, "X": feats.odds_draw, "2": feats.odds_away},
|
|
||||||
"OU25": {"Under": feats.odds_under25, "Over": feats.odds_over25},
|
|
||||||
"BTTS": {"No": feats.odds_btts_no, "Yes": feats.odds_btts_yes},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _best_pick(feats, all_probs: dict[str, dict[str, float]]) -> MarketPick | None:
|
|
||||||
odds_map = _odds_map_from_features(feats)
|
|
||||||
picks = [
|
|
||||||
analyze_market("MS", all_probs["MS"], odds_map["MS"], feats.data_quality_score),
|
|
||||||
analyze_market("OU25", all_probs["OU25"], odds_map["OU25"], feats.data_quality_score),
|
|
||||||
analyze_market("BTTS", all_probs["BTTS"], odds_map["BTTS"], feats.data_quality_score),
|
|
||||||
]
|
|
||||||
ranked = sorted(
|
|
||||||
[pick for pick in picks if pick.pick],
|
|
||||||
key=lambda pick: pick.play_score,
|
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
return ranked[0] if ranked else None
|
|
||||||
|
|
||||||
|
|
||||||
def _pick_won(pick: MarketPick, actuals: dict[str, str]) -> bool:
|
|
||||||
return actuals.get(pick.market) == pick.pick
|
|
||||||
|
|
||||||
|
|
||||||
async def _load_match_rows(limit: int, days: int) -> list[dict[str, object]]:
|
|
||||||
min_mst_utc = days * 86400000
|
|
||||||
query = text("""
|
|
||||||
SELECT
|
|
||||||
m.id,
|
|
||||||
m.match_name,
|
|
||||||
m.score_home,
|
|
||||||
m.score_away,
|
|
||||||
m.mst_utc
|
|
||||||
FROM matches m
|
|
||||||
WHERE m.sport = 'football'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND m.score_away IS NOT NULL
|
|
||||||
AND m.mst_utc >= (
|
|
||||||
EXTRACT(EPOCH FROM NOW()) * 1000 - :min_mst_utc
|
|
||||||
)
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM odd_categories oc
|
|
||||||
WHERE oc.match_id = m.id
|
|
||||||
AND oc.name IN ('Maç Sonucu', '2,5 Alt/Üst', 'Karşılıklı Gol')
|
|
||||||
)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT :limit
|
|
||||||
""")
|
|
||||||
async with get_session() as session:
|
|
||||||
result = await session.execute(
|
|
||||||
query,
|
|
||||||
{"limit": limit, "min_mst_utc": min_mst_utc},
|
|
||||||
)
|
|
||||||
rows = result.mappings().all()
|
|
||||||
return [dict(row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def _run(limit: int, days: int) -> BacktestStats:
|
|
||||||
stats = BacktestStats()
|
|
||||||
predictor = get_predictor()
|
|
||||||
rows = await _load_match_rows(limit, days)
|
|
||||||
stats.sampled_matches = len(rows)
|
|
||||||
|
|
||||||
async with get_session() as session:
|
|
||||||
for row in rows:
|
|
||||||
match_id = str(row["id"])
|
|
||||||
score_home = int(row["score_home"])
|
|
||||||
score_away = int(row["score_away"])
|
|
||||||
feats = await extract_features(session, match_id)
|
|
||||||
|
|
||||||
if feats is None:
|
|
||||||
stats.skipped_matches += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if feats.data_quality_score <= 0.0:
|
|
||||||
stats.skipped_matches += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
all_probs = predictor.predict_all(feats.to_model_array(), feats)
|
|
||||||
stats.analyzed_matches += 1
|
|
||||||
|
|
||||||
actuals = {
|
|
||||||
"MS": _actual_ms(score_home, score_away),
|
|
||||||
"OU25": _actual_ou25(score_home, score_away),
|
|
||||||
"BTTS": _actual_btts(score_home, score_away),
|
|
||||||
}
|
|
||||||
|
|
||||||
if max(all_probs["MS"], key=all_probs["MS"].get) == actuals["MS"]:
|
|
||||||
stats.ms_correct += 1
|
|
||||||
if max(all_probs["OU25"], key=all_probs["OU25"].get) == actuals["OU25"]:
|
|
||||||
stats.ou25_correct += 1
|
|
||||||
if max(all_probs["BTTS"], key=all_probs["BTTS"].get) == actuals["BTTS"]:
|
|
||||||
stats.btts_correct += 1
|
|
||||||
|
|
||||||
best_pick = _best_pick(feats, all_probs)
|
|
||||||
if best_pick is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
stats.main_pick_count += 1
|
|
||||||
if _pick_won(best_pick, actuals):
|
|
||||||
stats.main_pick_correct += 1
|
|
||||||
|
|
||||||
if best_pick.playable:
|
|
||||||
stats.playable_pick_count += 1
|
|
||||||
stats.playable_units_staked += best_pick.stake_units
|
|
||||||
if _pick_won(best_pick, actuals):
|
|
||||||
stats.playable_pick_correct += 1
|
|
||||||
stats.playable_units_profit += best_pick.stake_units * (best_pick.odds - 1.0)
|
|
||||||
else:
|
|
||||||
stats.playable_units_profit -= best_pick.stake_units
|
|
||||||
|
|
||||||
return stats
|
|
||||||
|
|
||||||
|
|
||||||
def _pct(numerator: int, denominator: int) -> float:
|
|
||||||
if denominator <= 0:
|
|
||||||
return 0.0
|
|
||||||
return round((numerator / denominator) * 100.0, 2)
|
|
||||||
|
|
||||||
|
|
||||||
def _roi(profit: float, staked: float) -> float:
|
|
||||||
if staked <= 0:
|
|
||||||
return 0.0
|
|
||||||
return round((profit / staked) * 100.0, 2)
|
|
||||||
|
|
||||||
|
|
||||||
def _print_summary(stats: BacktestStats) -> None:
|
|
||||||
print("=== V2 Runtime Backtest ===")
|
|
||||||
print(f"Sampled matches : {stats.sampled_matches}")
|
|
||||||
print(f"Analyzed matches : {stats.analyzed_matches}")
|
|
||||||
print(f"Skipped matches : {stats.skipped_matches}")
|
|
||||||
print(f"MS accuracy : {_pct(stats.ms_correct, stats.analyzed_matches)}%")
|
|
||||||
print(f"OU2.5 accuracy : {_pct(stats.ou25_correct, stats.analyzed_matches)}%")
|
|
||||||
print(f"BTTS accuracy : {_pct(stats.btts_correct, stats.analyzed_matches)}%")
|
|
||||||
print(
|
|
||||||
"Main pick accuracy : "
|
|
||||||
f"{_pct(stats.main_pick_correct, stats.main_pick_count)}% "
|
|
||||||
f"({stats.main_pick_correct}/{stats.main_pick_count})"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
"Playable accuracy : "
|
|
||||||
f"{_pct(stats.playable_pick_correct, stats.playable_pick_count)}% "
|
|
||||||
f"({stats.playable_pick_correct}/{stats.playable_pick_count})"
|
|
||||||
)
|
|
||||||
print(f"Units staked : {stats.playable_units_staked:.2f}")
|
|
||||||
print(f"Units profit : {stats.playable_units_profit:.2f}")
|
|
||||||
print(f"ROI : {_roi(stats.playable_units_profit, stats.playable_units_staked)}%")
|
|
||||||
|
|
||||||
|
|
||||||
async def _main() -> None:
|
|
||||||
args = _parse_args()
|
|
||||||
try:
|
|
||||||
stats = await _run(args.limit, args.days)
|
|
||||||
_print_summary(stats)
|
|
||||||
finally:
|
|
||||||
await dispose_engine()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(_main())
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
"""
|
|
||||||
Value Hunter Backtest
|
|
||||||
=====================
|
|
||||||
Sadece modelin büroyu yendiği (Pozitif Edge) maçları oynar.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os, sys, json, time, psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR): ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
MATCH_IDS = [
|
|
||||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
|
||||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
|
||||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
|
||||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
|
||||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
|
||||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
|
||||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
|
||||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
|
||||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
|
||||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
|
||||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
|
||||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
|
||||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
|
||||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
|
||||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
|
||||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
|
||||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
|
||||||
]
|
|
||||||
|
|
||||||
def run_value_hunter():
|
|
||||||
print("💎 VALUE HUNTER: SADECE HATALI ORANLARI YAKALA")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
|
||||||
cur.execute(f"""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.id IN ({placeholders}) AND m.status = 'FT'
|
|
||||||
""", MATCH_IDS)
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç taranıyor...\n")
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
total_edge_found = 0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred: continue
|
|
||||||
|
|
||||||
# Tüm önerileri kontrol et
|
|
||||||
picks = pred.get("expert_recommendation", {}).get("value_picks", [])
|
|
||||||
if not picks: picks = [pred.get("expert_recommendation", {}).get("main_pick")]
|
|
||||||
|
|
||||||
played_this_match = False
|
|
||||||
|
|
||||||
for pick_data in picks:
|
|
||||||
if not pick_data: continue
|
|
||||||
pick = pick_data.get("pick")
|
|
||||||
conf = pick_data.get("confidence", 0)
|
|
||||||
odds = pick_data.get("odds", 0)
|
|
||||||
edge = pick_data.get("edge", 0)
|
|
||||||
|
|
||||||
# VALUE KURALI: Model bürodan en az %10 daha iyi olmalı
|
|
||||||
if edge < 10: continue
|
|
||||||
if odds < 1.20: continue
|
|
||||||
|
|
||||||
total_bet += 1
|
|
||||||
total_edge_found += edge
|
|
||||||
won = False
|
|
||||||
pick_clean = str(pick).upper()
|
|
||||||
|
|
||||||
if pick_clean in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick_clean in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick_clean in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick_clean and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick_clean and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({edge:.0f}% Edge) -> WON! (+{profit:.2f})")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({edge:.0f}% Edge) -> LOST")
|
|
||||||
|
|
||||||
played_this_match = True
|
|
||||||
break # Maç başına tek bahis
|
|
||||||
|
|
||||||
except Exception: pass
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("💎 VALUE HUNTER SONUÇLARI")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Toplam Value Bulunan Bahis: {total_bet}")
|
|
||||||
print(f"Ortalama Edge: {total_edge_found/total_bet:.1f}%" if total_bet > 0 else "N/A")
|
|
||||||
print(f"Kazanılan: {total_won}")
|
|
||||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
|
||||||
|
|
||||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
|
||||||
else: print("🔴 PARA KAYBETTİK!")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_value_hunter()
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
"""
|
|
||||||
Value Sniper Backtest (High Odds)
|
|
||||||
=================================
|
|
||||||
Sadece Oran > 1.50 ve Güven > %70 olan bahisleri oynar.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_value_sniper():
|
|
||||||
print("💰 VALUE SNIPER BACKTEST (Odds > 1.50)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 500
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç taranıyor...\n")
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred: continue
|
|
||||||
|
|
||||||
candidates = []
|
|
||||||
if pred.get("expert_recommendation"):
|
|
||||||
rec = pred["expert_recommendation"]
|
|
||||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
|
||||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
|
||||||
elif pred.get("main_pick"):
|
|
||||||
candidates.append(pred["main_pick"])
|
|
||||||
|
|
||||||
best_bet = None
|
|
||||||
for c in candidates:
|
|
||||||
if not c: continue
|
|
||||||
conf = c.get("confidence", 0) if isinstance(c, dict) else getattr(c, 'confidence', 0)
|
|
||||||
odds = c.get("odds", 0) if isinstance(c, dict) else getattr(c, 'odds', 0)
|
|
||||||
|
|
||||||
# VALUE CRITERIA: Odds > 1.50 AND Conf > 70%
|
|
||||||
if conf >= 70.0 and odds >= 1.50:
|
|
||||||
# Check Edge
|
|
||||||
implied = 1.0 / odds
|
|
||||||
edge = ((conf/100) - implied) * 100
|
|
||||||
if edge > 0: # Must be positive value
|
|
||||||
if best_bet is None or (conf > (best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0))):
|
|
||||||
best_bet = c
|
|
||||||
|
|
||||||
if best_bet:
|
|
||||||
pick = str(best_bet.get("pick") if isinstance(best_bet, dict) else getattr(best_bet, 'pick', "")).upper()
|
|
||||||
conf = best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0)
|
|
||||||
odds = best_bet.get("odds", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'odds', 0)
|
|
||||||
|
|
||||||
won = False
|
|
||||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif "ÜST" in pick or "OVER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick or "UNDER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
total_bet += 1
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({odds:.2f}) -> WON (+{profit:.2f})")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({odds:.2f}) -> LOST")
|
|
||||||
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("💰 VALUE SNIPER SONUÇLARI")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Oynanan Bahis: {total_bet}")
|
|
||||||
print(f"Kazanılan: {total_won}")
|
|
||||||
|
|
||||||
if total_bet > 0:
|
|
||||||
win_rate = (total_won / total_bet) * 100
|
|
||||||
roi = (total_profit / total_bet) * 100
|
|
||||||
print(f"Kazanma Oranı: %{win_rate:.2f}")
|
|
||||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
|
||||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
|
||||||
else: print("🔴 PARA KAYBETTİK!")
|
|
||||||
else:
|
|
||||||
print("⚠️ Yeterli VALUE bulunamadı.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_value_sniper()
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN Full Backtest
|
|
||||||
===================
|
|
||||||
Tests all 3 VQWEN models (MS, OU25, BTTS) on 1000 historical matches.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import pickle
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_vqwen_backtest():
|
|
||||||
print("🧠 VQWEN FULL BACKTEST")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# Load Models
|
|
||||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
|
||||||
try:
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
|
||||||
print("✅ VQWEN MS, OU25, BTTS modelleri yüklendi.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Model hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
|
||||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa,
|
|
||||||
COALESCE((SELECT AVG(CASE WHEN m2.home_team_id = m.home_team_id AND m2.score_home > m2.score_away THEN 3 WHEN m2.home_team_id = m.home_team_id AND m2.score_home = m2.score_away THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as h_form,
|
|
||||||
COALESCE((SELECT AVG(CASE WHEN m2.away_team_id = m.away_team_id AND m2.score_away > m2.score_home THEN 3 WHEN m2.away_team_id = m.away_team_id AND m2.score_away = m2.score_home THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as a_form,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_sc,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_co,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_sc,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_co
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 1000
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
|
||||||
|
|
||||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
oh, od, oa = float(row['oh'] or 0), float(row['od'] or 0), float(row['oa'] or 0)
|
|
||||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
|
||||||
|
|
||||||
h_xg = (float(row['h_sc'] or 1.2) + float(row['a_co'] or 1.2)) / 2
|
|
||||||
a_xg = (float(row['a_sc'] or 1.2) + float(row['h_co'] or 1.2)) / 2
|
|
||||||
h_p = (float(row['h_form'] or 0)*10) + (float(row['h_sc'] or 1.2)*5) - (float(row['h_co'] or 1.2)*5)
|
|
||||||
a_p = (float(row['a_form'] or 0)*10) + (float(row['a_sc'] or 1.2)*5) - (float(row['a_co'] or 1.2)*5)
|
|
||||||
|
|
||||||
margin = (1/oh) + (1/od) + (1/oa)
|
|
||||||
|
|
||||||
# MS Prediction
|
|
||||||
f_ms = pd.DataFrame([{'h_form': float(row['h_form']), 'a_form': float(row['a_form']), 'h_xg': h_xg, 'a_xg': a_xg,
|
|
||||||
'pow_diff': h_p - a_p, 'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
|
||||||
'h_sot': 4.0, 'a_sot': 3.0}])
|
|
||||||
ms_probs = model_ms.predict(f_ms)[0]
|
|
||||||
|
|
||||||
# MS Value Bet
|
|
||||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
|
||||||
if odd <= 1.0: continue
|
|
||||||
edge = prob - (1/odd)
|
|
||||||
if edge > 0.05 and prob > 0.50: # Value ve Güven
|
|
||||||
results['ms']['bet'] += 1
|
|
||||||
h, a = row['score_home'], row['score_away']
|
|
||||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
|
||||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
|
||||||
else: results['ms']['profit'] -= 1.0
|
|
||||||
break
|
|
||||||
|
|
||||||
# OU2.5 Prediction
|
|
||||||
f_ou = pd.DataFrame([{'h_xg': h_xg, 'a_xg': a_xg, 'total_xg': h_xg+a_xg, 'h_sot': 4.0, 'a_sot': 3.0}])
|
|
||||||
p_over = model_ou.predict(f_ou)[0]
|
|
||||||
|
|
||||||
# OU2.5 Value Bet
|
|
||||||
if p_over > 0.55 and oh > 1.0: # Sadece örnek olarak over > %55 ise
|
|
||||||
results['ou25']['bet'] += 1
|
|
||||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85 # Ortalama oran
|
|
||||||
else: results['ou25']['profit'] -= 1.0
|
|
||||||
|
|
||||||
# BTTS Prediction
|
|
||||||
f_btts = pd.DataFrame([{'h_xg': h_xg, 'a_xg': a_xg, 'h_sc': float(row['h_sc']), 'a_sc': float(row['a_sc'])}])
|
|
||||||
p_btts = model_btts.predict(f_btts)[0]
|
|
||||||
|
|
||||||
# BTTS Value Bet
|
|
||||||
if p_btts > 0.55:
|
|
||||||
results['btts']['bet'] += 1
|
|
||||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
|
||||||
else: results['btts']['profit'] -= 1.0
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📊 VQWEN PAZAR BAZLI SONUÇLAR")
|
|
||||||
print("="*60)
|
|
||||||
for mkt in ['ms', 'ou25', 'btts']:
|
|
||||||
r = results[mkt]
|
|
||||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
|
||||||
print(f"{mkt.upper():<10} Oynanan: {r['bet']:<5} Kazanılan: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f} Units")
|
|
||||||
|
|
||||||
total_profit = sum(r['profit'] for r in results.values())
|
|
||||||
print(f"\n💰 TOPLAM KÂR: {total_profit:+.2f} Units")
|
|
||||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
|
||||||
else: print("🔴 ZARARDA")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_vqwen_backtest()
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN Deep Backtest
|
|
||||||
===================
|
|
||||||
Tests the NEW Deep model with player & card data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import pickle
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_vqwen_deep_backtest():
|
|
||||||
print("🧠 VQWEN DEEP BACKTEST")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# Load Models
|
|
||||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
|
||||||
try:
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
|
||||||
print("✅ VQWEN Deep modelleri yüklendi.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Model hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
|
||||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa,
|
|
||||||
COALESCE((SELECT AVG(CASE WHEN m2.home_team_id = m.home_team_id AND m2.score_home > m2.score_away THEN 3 WHEN m2.home_team_id = m.home_team_id AND m2.score_home = m2.score_away THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as h_form,
|
|
||||||
COALESCE((SELECT AVG(CASE WHEN m2.away_team_id = m.away_team_id AND m2.score_away > m2.score_home THEN 3 WHEN m2.away_team_id = m.away_team_id AND m2.score_away = m2.score_home THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as a_form,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_sc,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_co,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_sc,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_co,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.home_team_id AND mp.is_starting = true), 0) as h_xi,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.away_team_id AND mp.is_starting = true), 0) as a_xi,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_events mpe WHERE mpe.match_id = m.id AND mpe.event_type = 'card'), 0) as cards
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 1000
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
|
||||||
|
|
||||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
oh = float(row['oh'] or 0)
|
|
||||||
od = float(row['od'] or 0)
|
|
||||||
oa = float(row['oa'] or 0)
|
|
||||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
|
||||||
|
|
||||||
h_xg = (float(row['h_sc'] or 1.2) + float(row['a_co'] or 1.2)) / 2
|
|
||||||
a_xg = (float(row['a_sc'] or 1.2) + float(row['h_co'] or 1.2)) / 2
|
|
||||||
h_p = (float(row['h_form'] or 0)*10) + (float(row['h_sc'] or 1.2)*5) - (float(row['h_co'] or 1.2)*5)
|
|
||||||
a_p = (float(row['a_form'] or 0)*10) + (float(row['a_sc'] or 1.2)*5) - (float(row['a_co'] or 1.2)*5)
|
|
||||||
|
|
||||||
margin = (1/oh) + (1/od) + (1/oa)
|
|
||||||
h_sot, a_sot = 4.0, 3.0
|
|
||||||
|
|
||||||
# Features
|
|
||||||
f = pd.DataFrame([{
|
|
||||||
'h_form': float(row['h_form']), 'a_form': float(row['a_form']),
|
|
||||||
'h_xg': h_xg, 'a_xg': a_xg, 'pow_diff': h_p - a_p,
|
|
||||||
'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
|
||||||
'h_sot': h_sot, 'a_sot': a_sot,
|
|
||||||
'h_xi': float(row['h_xi']), 'a_xi': float(row['a_xi']),
|
|
||||||
'xi_diff': float(row['h_xi'] - row['a_xi']),
|
|
||||||
'cards': float(row['cards'])
|
|
||||||
}])
|
|
||||||
|
|
||||||
# MS
|
|
||||||
ms_probs = model_ms.predict(f)[0]
|
|
||||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
|
||||||
if odd <= 1.0: continue
|
|
||||||
edge = prob - (1/odd)
|
|
||||||
if edge > 0.05 and prob > 0.50:
|
|
||||||
results['ms']['bet'] += 1
|
|
||||||
h, a = row['score_home'], row['score_away']
|
|
||||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
|
||||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
|
||||||
else: results['ms']['profit'] -= 1.0
|
|
||||||
break
|
|
||||||
|
|
||||||
# OU2.5
|
|
||||||
p_over = float(model_ou.predict(f)[0])
|
|
||||||
if p_over > 0.55:
|
|
||||||
results['ou25']['bet'] += 1
|
|
||||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85
|
|
||||||
else: results['ou25']['profit'] -= 1.0
|
|
||||||
|
|
||||||
# BTTS
|
|
||||||
p_btts = float(model_btts.predict(f)[0])
|
|
||||||
if p_btts > 0.55:
|
|
||||||
results['btts']['bet'] += 1
|
|
||||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
|
||||||
else: results['btts']['profit'] -= 1.0
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📊 VQWEN DEEP SONUÇLAR")
|
|
||||||
print("="*60)
|
|
||||||
for mkt in ['ms', 'ou25', 'btts']:
|
|
||||||
r = results[mkt]
|
|
||||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
|
||||||
print(f"{mkt.upper():<10} Oyn: {r['bet']:<5} Kaz: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f}")
|
|
||||||
|
|
||||||
total = sum(r['profit'] for r in results.values())
|
|
||||||
print(f"\n💰 TOPLAM: {total:+.2f} Units")
|
|
||||||
print("🟢 PARA KAZANDIK!" if total > 0 else "🔴 ZARARDA")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_vqwen_deep_backtest()
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN Final Backtest
|
|
||||||
====================
|
|
||||||
Tests the Final Model (ELO + Rest + Context).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import pickle
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_final_backtest():
|
|
||||||
print("🧠 VQWEN FINAL BACKTEST (ELO + REST)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# Load Models
|
|
||||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
|
||||||
try:
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
|
||||||
print("✅ VQWEN Final modelleri yüklendi.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Model hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
|
||||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
|
||||||
m.mst_utc,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
maf.home_elo, maf.away_elo,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc), 1.2) as h_home_goals,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc), 1.2) as a_away_goals,
|
|
||||||
COALESCE(EXTRACT(EPOCH FROM (to_timestamp(m.mst_utc/1000) - (SELECT MAX(to_timestamp(m2.mst_utc/1000)) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc)) / 86400), 7) as h_rest,
|
|
||||||
COALESCE(EXTRACT(EPOCH FROM (to_timestamp(m.mst_utc/1000) - (SELECT MAX(to_timestamp(m2.mst_utc/1000)) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc)) / 86400), 7) as a_rest,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.home_team_id AND mp.is_starting = true), 11) as h_xi,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.away_team_id AND mp.is_starting = true), 11) as a_xi,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_events mpe WHERE mpe.match_id = m.id AND mpe.event_type = 'card'), 4) as cards,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
LEFT JOIN football_ai_features maf ON maf.match_id = m.id
|
|
||||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 1000
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
|
||||||
|
|
||||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
oh = float(row['oh'] or 0)
|
|
||||||
od = float(row['od'] or 0)
|
|
||||||
oa = float(row['oa'] or 0)
|
|
||||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
|
||||||
|
|
||||||
# Features
|
|
||||||
h_elo = float(row['home_elo'] or 1500)
|
|
||||||
a_elo = float(row['away_elo'] or 1500)
|
|
||||||
h_home_goals = float(row['h_home_goals'] or 1.2)
|
|
||||||
a_away_goals = float(row['a_away_goals'] or 1.2)
|
|
||||||
h_rest = float(row['h_rest'] or 7)
|
|
||||||
a_rest = float(row['a_rest'] or 7)
|
|
||||||
h_xi = float(row['h_xi'] or 11)
|
|
||||||
a_xi = float(row['a_xi'] or 11)
|
|
||||||
cards = float(row['cards'] or 4)
|
|
||||||
|
|
||||||
def fatigue(rest):
|
|
||||||
if rest < 3: return 0.85
|
|
||||||
if rest < 5: return 0.95
|
|
||||||
return 1.0
|
|
||||||
|
|
||||||
h_fat = fatigue(h_rest)
|
|
||||||
a_fat = fatigue(a_rest)
|
|
||||||
|
|
||||||
h_xg = h_home_goals * h_fat
|
|
||||||
a_xg = a_away_goals * a_fat
|
|
||||||
total_xg = h_xg + a_xg
|
|
||||||
|
|
||||||
margin = (1/oh) + (1/od) + (1/oa)
|
|
||||||
f = pd.DataFrame([{
|
|
||||||
'elo_diff': h_elo - a_elo,
|
|
||||||
'h_xg': h_xg, 'a_xg': a_xg,
|
|
||||||
'total_xg': total_xg,
|
|
||||||
'pow_diff': (h_elo/100)*h_fat - (a_elo/100)*a_fat,
|
|
||||||
'rest_diff': h_rest - a_rest,
|
|
||||||
'h_fatigue': h_fat, 'a_fatigue': a_fat,
|
|
||||||
'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
|
||||||
'h_xi': h_xi, 'a_xi': a_xi,
|
|
||||||
'cards': cards
|
|
||||||
}])
|
|
||||||
|
|
||||||
# MS
|
|
||||||
ms_probs = model_ms.predict(f)[0]
|
|
||||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
|
||||||
if odd <= 1.0: continue
|
|
||||||
edge = prob - (1/odd)
|
|
||||||
if edge > 0.05 and prob > 0.45:
|
|
||||||
results['ms']['bet'] += 1
|
|
||||||
h, a = row['score_home'], row['score_away']
|
|
||||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
|
||||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
|
||||||
else: results['ms']['profit'] -= 1.0
|
|
||||||
break
|
|
||||||
|
|
||||||
# OU2.5
|
|
||||||
p_over = float(model_ou.predict(f)[0])
|
|
||||||
if p_over > 0.55:
|
|
||||||
results['ou25']['bet'] += 1
|
|
||||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85
|
|
||||||
else: results['ou25']['profit'] -= 1.0
|
|
||||||
|
|
||||||
# BTTS
|
|
||||||
p_btts = float(model_btts.predict(f)[0])
|
|
||||||
if p_btts > 0.55:
|
|
||||||
results['btts']['bet'] += 1
|
|
||||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
|
||||||
else: results['btts']['profit'] -= 1.0
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📊 VQWEN FINAL SONUÇLAR")
|
|
||||||
print("="*60)
|
|
||||||
for mkt in ['ms', 'ou25', 'btts']:
|
|
||||||
r = results[mkt]
|
|
||||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
|
||||||
print(f"{mkt.upper():<10} Oyn: {r['bet']:<5} Kaz: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f}")
|
|
||||||
|
|
||||||
total = sum(r['profit'] for r in results.values())
|
|
||||||
print(f"\n💰 TOPLAM: {total:+.2f} Units")
|
|
||||||
print("🟢 PARA KAZANDIK!" if total > 0 else "🔴 ZARARDA")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_final_backtest()
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN v3 Shared-Contract Backtest
|
|
||||||
=================================
|
|
||||||
|
|
||||||
Evaluates the retrained VQWEN models on the temporal validation slice using
|
|
||||||
the exact same pre-match feature contract as training/runtime.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import pickle
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import psycopg2
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
AI_DIR = Path(__file__).resolve().parent
|
|
||||||
ENGINE_DIR = AI_DIR.parent
|
|
||||||
REPO_DIR = ENGINE_DIR.parent
|
|
||||||
MODELS_DIR = ENGINE_DIR / "models" / "vqwen"
|
|
||||||
|
|
||||||
if str(ENGINE_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(ENGINE_DIR))
|
|
||||||
|
|
||||||
from features.vqwen_contract import FEATURE_COLUMNS # noqa: E402
|
|
||||||
from train_vqwen_v3 import ( # noqa: E402
|
|
||||||
_enrich_pre_match_context,
|
|
||||||
_fetch_dataframe,
|
|
||||||
_prepare_features,
|
|
||||||
_temporal_split,
|
|
||||||
load_top_league_ids,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_env() -> None:
|
|
||||||
load_dotenv(REPO_DIR / ".env", override=False)
|
|
||||||
load_dotenv(ENGINE_DIR / ".env", override=False)
|
|
||||||
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
_load_env()
|
|
||||||
raw = os.getenv("DATABASE_URL", "").strip().strip('"').strip("'")
|
|
||||||
if not raw:
|
|
||||||
raise RuntimeError("DATABASE_URL is missing.")
|
|
||||||
return raw.split("?", 1)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
||||||
if len(y_true) == 0:
|
|
||||||
return 0.0
|
|
||||||
return float((y_true == y_pred).mean())
|
|
||||||
|
|
||||||
|
|
||||||
def _binary_metrics(prob: np.ndarray, y_true: np.ndarray) -> tuple[float, float]:
|
|
||||||
pred = (prob >= 0.5).astype(int)
|
|
||||||
acc = _accuracy(y_true, pred)
|
|
||||||
brier = float(np.mean((prob - y_true) ** 2)) if len(y_true) else 1.0
|
|
||||||
return acc, brier
|
|
||||||
|
|
||||||
|
|
||||||
def _multiclass_brier(prob: np.ndarray, y_true: np.ndarray, n_classes: int = 3) -> float:
|
|
||||||
if len(y_true) == 0:
|
|
||||||
return 1.0
|
|
||||||
target = np.zeros((len(y_true), n_classes), dtype=np.float64)
|
|
||||||
target[np.arange(len(y_true)), y_true.astype(int)] = 1.0
|
|
||||||
return float(np.mean(np.sum((prob - target) ** 2, axis=1)))
|
|
||||||
|
|
||||||
|
|
||||||
def _band_label(probability: float) -> str:
|
|
||||||
if probability >= 0.70:
|
|
||||||
return "HIGH"
|
|
||||||
if probability >= 0.60:
|
|
||||||
return "MEDIUM"
|
|
||||||
if probability >= 0.50:
|
|
||||||
return "LOW"
|
|
||||||
return "NO_BET"
|
|
||||||
|
|
||||||
|
|
||||||
def _summarize_bands(
|
|
||||||
name: str,
|
|
||||||
confidence: np.ndarray,
|
|
||||||
is_correct: np.ndarray,
|
|
||||||
) -> list[str]:
|
|
||||||
lines: list[str] = []
|
|
||||||
for band in ("HIGH", "MEDIUM", "LOW"):
|
|
||||||
mask = np.array([_band_label(float(p)) == band for p in confidence], dtype=bool)
|
|
||||||
count = int(mask.sum())
|
|
||||||
accuracy = float(is_correct[mask].mean()) if count else 0.0
|
|
||||||
avg_conf = float(confidence[mask].mean()) if count else 0.0
|
|
||||||
lines.append(
|
|
||||||
f"{name} {band:<6} count={count:<4} accuracy={accuracy*100:5.1f}% avg_conf={avg_conf*100:5.1f}%"
|
|
||||||
)
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
def run_v3_backtest() -> None:
|
|
||||||
print("VQWEN v3 SHARED-CONTRACT BACKTEST")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
league_ids = load_top_league_ids()
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
|
|
||||||
with psycopg2.connect(dsn) as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
df = _fetch_dataframe(cur, league_ids)
|
|
||||||
df = _enrich_pre_match_context(cur, df)
|
|
||||||
df = _prepare_features(df)
|
|
||||||
|
|
||||||
train_df, valid_df = _temporal_split(df)
|
|
||||||
print(f"Toplam ornek: {len(df)} | Train: {len(train_df)} | Valid: {len(valid_df)}")
|
|
||||||
|
|
||||||
with (MODELS_DIR / "vqwen_ms.pkl").open("rb") as handle:
|
|
||||||
model_ms = pickle.load(handle)
|
|
||||||
with (MODELS_DIR / "vqwen_ou25.pkl").open("rb") as handle:
|
|
||||||
model_ou25 = pickle.load(handle)
|
|
||||||
with (MODELS_DIR / "vqwen_btts.pkl").open("rb") as handle:
|
|
||||||
model_btts = pickle.load(handle)
|
|
||||||
|
|
||||||
X_valid = valid_df[FEATURE_COLUMNS]
|
|
||||||
y_ms = valid_df["t_ms"].to_numpy(dtype=np.int64)
|
|
||||||
y_ou25 = valid_df["t_ou"].to_numpy(dtype=np.int64)
|
|
||||||
y_btts = valid_df["t_btts"].to_numpy(dtype=np.int64)
|
|
||||||
|
|
||||||
ms_prob = np.asarray(model_ms.predict(X_valid), dtype=np.float64)
|
|
||||||
ou25_prob = np.asarray(model_ou25.predict(X_valid), dtype=np.float64).reshape(-1)
|
|
||||||
btts_prob = np.asarray(model_btts.predict(X_valid), dtype=np.float64).reshape(-1)
|
|
||||||
|
|
||||||
ms_pred = np.argmax(ms_prob, axis=1)
|
|
||||||
ms_conf = np.max(ms_prob, axis=1)
|
|
||||||
ms_correct = (ms_pred == y_ms).astype(np.int64)
|
|
||||||
|
|
||||||
ou25_pred = (ou25_prob >= 0.5).astype(np.int64)
|
|
||||||
ou25_conf = np.where(ou25_prob >= 0.5, ou25_prob, 1.0 - ou25_prob)
|
|
||||||
ou25_correct = (ou25_pred == y_ou25).astype(np.int64)
|
|
||||||
|
|
||||||
btts_pred = (btts_prob >= 0.5).astype(np.int64)
|
|
||||||
btts_conf = np.where(btts_prob >= 0.5, btts_prob, 1.0 - btts_prob)
|
|
||||||
btts_correct = (btts_pred == y_btts).astype(np.int64)
|
|
||||||
|
|
||||||
ms_acc = _accuracy(y_ms, ms_pred)
|
|
||||||
ou25_acc, ou25_brier = _binary_metrics(ou25_prob, y_ou25)
|
|
||||||
btts_acc, btts_brier = _binary_metrics(btts_prob, y_btts)
|
|
||||||
ms_brier = _multiclass_brier(ms_prob, y_ms)
|
|
||||||
|
|
||||||
print("\nGenel metrikler")
|
|
||||||
print(f"MS accuracy : {ms_acc*100:.2f}% | multiclass_brier={ms_brier:.4f}")
|
|
||||||
print(f"OU25 accuracy : {ou25_acc*100:.2f}% | brier={ou25_brier:.4f}")
|
|
||||||
print(f"BTTS accuracy : {btts_acc*100:.2f}% | brier={btts_brier:.4f}")
|
|
||||||
|
|
||||||
print("\nConfidence band")
|
|
||||||
for line in _summarize_bands("MS", ms_conf, ms_correct):
|
|
||||||
print(line)
|
|
||||||
for line in _summarize_bands("OU25", ou25_conf, ou25_correct):
|
|
||||||
print(line)
|
|
||||||
for line in _summarize_bands("BTTS", btts_conf, btts_correct):
|
|
||||||
print(line)
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"validation_samples": int(len(valid_df)),
|
|
||||||
"metrics": {
|
|
||||||
"ms_accuracy": round(ms_acc, 4),
|
|
||||||
"ms_brier": round(ms_brier, 4),
|
|
||||||
"ou25_accuracy": round(ou25_acc, 4),
|
|
||||||
"ou25_brier": round(ou25_brier, 4),
|
|
||||||
"btts_accuracy": round(btts_acc, 4),
|
|
||||||
"btts_brier": round(btts_brier, 4),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
(MODELS_DIR / "vqwen_backtest_v3_summary.json").write_text(
|
|
||||||
json.dumps(summary, indent=2),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
print("\nKaydedildi: vqwen_backtest_v3_summary.json")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_v3_backtest()
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import os, psycopg2
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
load_dotenv('/Users/piton/Documents/Suggest-Bet-BE/.env')
|
|
||||||
conn = psycopg2.connect(os.getenv('DATABASE_URL').split('?')[0])
|
|
||||||
cur = conn.cursor()
|
|
||||||
cur.execute('SELECT mpe.match_id, SUM(CASE WHEN event_type::text LIKE \'%yellow_card%\' THEN 1 WHEN event_type::text LIKE \'%red_card%\' THEN 2 ELSE 1 END) as cards FROM match_player_events mpe WHERE event_type::text LIKE \'%card%\' GROUP BY mpe.match_id LIMIT 5')
|
|
||||||
print(cur.fetchall())
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
"""Quick test: V20+Quant integration — EV Edge, Kelly staking, edge-based grading."""
|
|
||||||
import json
|
|
||||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
|
||||||
|
|
||||||
MATCH_IDS = [
|
|
||||||
"er7n8hqndkhvdsg6an72r7h90", # Def. Justicia vs Atl Lanus
|
|
||||||
"etpay8k4qr3gts3jjidfebaxg", # CA Tigre vs Gymnasia
|
|
||||||
]
|
|
||||||
|
|
||||||
o = SingleMatchOrchestrator()
|
|
||||||
|
|
||||||
for mid in MATCH_IDS:
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"MATCH: {mid}")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
r = o.analyze_match(mid)
|
|
||||||
if not r:
|
|
||||||
print(" Match not found")
|
|
||||||
continue
|
|
||||||
|
|
||||||
info = r.get("match_info", {})
|
|
||||||
print(f" {info.get('match_name', '?')} | {info.get('league', '?')}")
|
|
||||||
|
|
||||||
mp = r.get("main_pick", {})
|
|
||||||
print(f"\n MAIN PICK: {mp.get('market')} {mp.get('pick')}")
|
|
||||||
print(f" probability: {mp.get('probability', 0):.4f}")
|
|
||||||
print(f" odds: {mp.get('odds', 0):.2f}")
|
|
||||||
print(f" ev_edge: {mp.get('ev_edge', mp.get('edge', 0)):+.4f}")
|
|
||||||
print(f" implied_prob: {mp.get('implied_prob', 0):.4f}")
|
|
||||||
print(f" bet_grade: {mp.get('bet_grade', 'N/A')}")
|
|
||||||
print(f" stake_units: {mp.get('stake_units', 0)}")
|
|
||||||
print(f" playable: {mp.get('playable', False)}")
|
|
||||||
print(f" reasons: {mp.get('decision_reasons', [])}")
|
|
||||||
|
|
||||||
print(f"\n ALL MARKETS (with EV Edge + Kelly):")
|
|
||||||
for b in r.get("bet_summary", []):
|
|
||||||
ev = b.get("ev_edge", 0)
|
|
||||||
imp = b.get("implied_prob", 0)
|
|
||||||
flag = ">>>" if b.get("playable") else " "
|
|
||||||
mkt = b["market"]
|
|
||||||
pick = b["pick"]
|
|
||||||
odds = b.get("odds", 0)
|
|
||||||
grade = b["bet_grade"]
|
|
||||||
stake = b["stake_units"]
|
|
||||||
conf = b.get("calibrated_confidence", 0)
|
|
||||||
print(
|
|
||||||
f" {flag} {mkt:8s} {pick:12s} "
|
|
||||||
f"ev_edge={ev:+.3f} "
|
|
||||||
f"odds={odds:.2f} "
|
|
||||||
f"stake={stake:.1f} "
|
|
||||||
f"grade={grade:4s} "
|
|
||||||
f"conf={conf:.1f}% "
|
|
||||||
f"implied={imp:.3f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
print()
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from decimal import Decimal
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
|
||||||
|
|
||||||
from core.engines.odds_predictor import OddsPredictorEngine
|
|
||||||
from features.sidelined_analyzer import SidelinedAnalyzer
|
|
||||||
|
|
||||||
|
|
||||||
class EngineNullSafetyTests(unittest.TestCase):
|
|
||||||
def test_odds_predictor_accepts_decimal_inputs_without_crashing(self):
|
|
||||||
engine = OddsPredictorEngine()
|
|
||||||
|
|
||||||
prediction = engine.predict(
|
|
||||||
odds_data={
|
|
||||||
"ms_h": Decimal("2.10"),
|
|
||||||
"ms_d": Decimal("3.25"),
|
|
||||||
"ms_a": Decimal("3.60"),
|
|
||||||
"ou25_o": Decimal("1.90"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertGreater(prediction.market_home_prob, 0.0)
|
|
||||||
self.assertGreater(prediction.market_draw_prob, 0.0)
|
|
||||||
self.assertGreater(prediction.market_away_prob, 0.0)
|
|
||||||
|
|
||||||
def test_sidelined_analyzer_handles_non_numeric_fields(self):
|
|
||||||
analyzer = SidelinedAnalyzer.__new__(SidelinedAnalyzer)
|
|
||||||
analyzer.position_weights = {"K": 0.35, "D": 0.20, "O": 0.25, "F": 0.30}
|
|
||||||
analyzer.max_rating = 10
|
|
||||||
analyzer.adaptation_threshold = 10
|
|
||||||
analyzer.adaptation_discount = 0.5
|
|
||||||
analyzer.goalkeeper_penalty = 0.15
|
|
||||||
analyzer.confidence_boost = 10
|
|
||||||
analyzer.max_impact = 0.85
|
|
||||||
analyzer.key_player_threshold = 3
|
|
||||||
analyzer.recent_matches_lookback = 15
|
|
||||||
analyzer._fetch_player_stats = MagicMock(return_value={})
|
|
||||||
|
|
||||||
result = analyzer.analyze(
|
|
||||||
{
|
|
||||||
"totalSidelined": 2,
|
|
||||||
"players": [
|
|
||||||
{
|
|
||||||
"playerId": "p1",
|
|
||||||
"playerName": "Player One",
|
|
||||||
"positionShort": "O",
|
|
||||||
"matchesMissed": "N/A",
|
|
||||||
"average": "?",
|
|
||||||
"type": "injury",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"playerId": "p2",
|
|
||||||
"playerName": "Player Two",
|
|
||||||
"positionShort": "K",
|
|
||||||
"matchesMissed": "12",
|
|
||||||
"average": "6.7",
|
|
||||||
"type": "suspension",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(result.total_sidelined, 2)
|
|
||||||
self.assertGreaterEqual(result.impact_score, 0.0)
|
|
||||||
self.assertTrue(len(result.player_details) >= 2)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
"""
|
|
||||||
Unit tests for FeatureEnrichmentService
|
|
||||||
========================================
|
|
||||||
Tests all 6 enrichment methods with mocked DB cursor:
|
|
||||||
1. compute_team_stats
|
|
||||||
2. compute_h2h
|
|
||||||
3. compute_form_streaks
|
|
||||||
4. compute_referee_stats
|
|
||||||
5. compute_league_averages
|
|
||||||
6. compute_momentum
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
|
||||||
|
|
||||||
from services.feature_enrichment import FeatureEnrichmentService, _safe_avg
|
|
||||||
|
|
||||||
|
|
||||||
def _make_cursor(rows=None, side_effect=None):
|
|
||||||
"""Create a mock RealDictCursor."""
|
|
||||||
cur = MagicMock()
|
|
||||||
if side_effect:
|
|
||||||
cur.execute.side_effect = side_effect
|
|
||||||
else:
|
|
||||||
cur.fetchall.return_value = rows or []
|
|
||||||
cur.fetchone.return_value = rows[0] if rows else None
|
|
||||||
return cur
|
|
||||||
|
|
||||||
|
|
||||||
class TestSafeAvg(unittest.TestCase):
|
|
||||||
def test_returns_average(self):
|
|
||||||
self.assertAlmostEqual(_safe_avg([2.0, 4.0, 6.0], 0.0), 4.0)
|
|
||||||
|
|
||||||
def test_returns_default_on_empty(self):
|
|
||||||
self.assertEqual(_safe_avg([], 99.0), 99.0)
|
|
||||||
|
|
||||||
def test_single_value(self):
|
|
||||||
self.assertAlmostEqual(_safe_avg([7.5], 0.0), 7.5)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeTeamStats(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_team_id(self):
|
|
||||||
result = self.svc.compute_team_stats(MagicMock(), '', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_rows(self):
|
|
||||||
cur = _make_cursor(rows=[])
|
|
||||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
|
|
||||||
|
|
||||||
def test_returns_defaults_on_db_error(self):
|
|
||||||
cur = _make_cursor(side_effect=Exception('DB down'))
|
|
||||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
|
|
||||||
|
|
||||||
def test_calculates_averages_correctly(self):
|
|
||||||
rows = [
|
|
||||||
{'possession_percentage': 60.0, 'shots_on_target': 5, 'total_shots': 10, 'corners': 7},
|
|
||||||
{'possession_percentage': 40.0, 'shots_on_target': 3, 'total_shots': 12, 'corners': 3},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
self.assertAlmostEqual(result['avg_possession'], 50.0)
|
|
||||||
self.assertAlmostEqual(result['avg_shots_on_target'], 4.0)
|
|
||||||
self.assertAlmostEqual(result['shot_conversion'], (5 / 10 + 3 / 12) / 2, places=4)
|
|
||||||
self.assertAlmostEqual(result['avg_corners'], 5.0)
|
|
||||||
|
|
||||||
def test_handles_none_subfields_gracefully(self):
|
|
||||||
"""Rows with None values should be skipped, not crash."""
|
|
||||||
rows = [
|
|
||||||
{'possession_percentage': 55.0, 'shots_on_target': None, 'total_shots': None, 'corners': 4},
|
|
||||||
{'possession_percentage': None, 'shots_on_target': 2, 'total_shots': 8, 'corners': None},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
self.assertAlmostEqual(result['avg_possession'], 55.0)
|
|
||||||
self.assertAlmostEqual(result['avg_shots_on_target'], 2.0)
|
|
||||||
self.assertAlmostEqual(result['avg_corners'], 4.0)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeH2H(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_ids(self):
|
|
||||||
result = self.svc.compute_h2h(MagicMock(), '', 'away1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_rows(self):
|
|
||||||
cur = _make_cursor(rows=[])
|
|
||||||
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
|
|
||||||
|
|
||||||
def test_calculates_h2h_stats(self):
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'home1', 'away_team_id': 'away1', 'score_home': 2, 'score_away': 1}, # home win, btts, over25
|
|
||||||
{'home_team_id': 'home1', 'away_team_id': 'away1', 'score_home': 0, 'score_away': 0}, # draw, no btts, no over25
|
|
||||||
{'home_team_id': 'away1', 'away_team_id': 'home1', 'score_home': 1, 'score_away': 3}, # reversed: home wins again, btts, over25
|
|
||||||
{'home_team_id': 'away1', 'away_team_id': 'home1', 'score_home': 2, 'score_away': 0}, # reversed: away(=home1) lost
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
|
|
||||||
|
|
||||||
self.assertEqual(result['total_matches'], 4)
|
|
||||||
self.assertAlmostEqual(result['home_win_rate'], 2 / 4)
|
|
||||||
self.assertAlmostEqual(result['draw_rate'], 1 / 4)
|
|
||||||
self.assertAlmostEqual(result['btts_rate'], 2 / 4)
|
|
||||||
self.assertAlmostEqual(result['over25_rate'], 2 / 4)
|
|
||||||
|
|
||||||
def test_returns_defaults_on_db_error(self):
|
|
||||||
cur = _make_cursor(side_effect=Exception('connection lost'))
|
|
||||||
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeFormStreaks(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_team_id(self):
|
|
||||||
result = self.svc.compute_form_streaks(MagicMock(), '', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_FORM)
|
|
||||||
|
|
||||||
def test_calculates_streaks_correctly(self):
|
|
||||||
"""Most recent first: W, W, D, L → winning_streak=2, unbeaten_streak=3."""
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 2, 'score_away': 0}, # W (clean sheet, scored)
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 1, 'score_away': 0}, # W (clean sheet, scored)
|
|
||||||
{'home_team_id': 'x', 'away_team_id': 'team1', 'score_home': 1, 'score_away': 1}, # D (scored, conceded)
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 2}, # L (not scored, conceded)
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_form_streaks(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
self.assertEqual(result['winning_streak'], 2)
|
|
||||||
self.assertEqual(result['unbeaten_streak'], 3)
|
|
||||||
self.assertAlmostEqual(result['clean_sheet_rate'], 2 / 4)
|
|
||||||
self.assertAlmostEqual(result['scoring_rate'], 3 / 4)
|
|
||||||
|
|
||||||
def test_all_losses(self):
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 1},
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 3},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_form_streaks(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
self.assertEqual(result['winning_streak'], 0)
|
|
||||||
self.assertEqual(result['unbeaten_streak'], 0)
|
|
||||||
self.assertAlmostEqual(result['scoring_rate'], 0.0)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeRefereeStats(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_name(self):
|
|
||||||
result = self.svc.compute_referee_stats(MagicMock(), None, self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_REFEREE)
|
|
||||||
|
|
||||||
def test_calculates_referee_tendencies(self):
|
|
||||||
match_rows = [
|
|
||||||
{'home_team_id': 'h1', 'score_home': 2, 'score_away': 0, 'match_id': 'm1'}, # home win
|
|
||||||
{'home_team_id': 'h2', 'score_home': 1, 'score_away': 1, 'match_id': 'm2'}, # draw
|
|
||||||
]
|
|
||||||
card_row = {'yellows': 6, 'total_cards': 8}
|
|
||||||
|
|
||||||
cur = MagicMock()
|
|
||||||
# First execute (match query) → match_rows
|
|
||||||
# Second execute (card query) → card_row
|
|
||||||
cur.fetchall.return_value = match_rows
|
|
||||||
cur.fetchone.return_value = card_row
|
|
||||||
|
|
||||||
result = self.svc.compute_referee_stats(cur, 'Ref Name', self.ts)
|
|
||||||
|
|
||||||
self.assertEqual(result['experience'], 2)
|
|
||||||
self.assertAlmostEqual(result['avg_goals'], (2 + 0 + 1 + 1) / 2)
|
|
||||||
# home_bias = (1/2) - 0.46 = 0.04
|
|
||||||
self.assertAlmostEqual(result['home_bias'], 0.04, places=4)
|
|
||||||
self.assertAlmostEqual(result['avg_yellow'], 6 / 2)
|
|
||||||
self.assertAlmostEqual(result['cards_total'], 8 / 2)
|
|
||||||
|
|
||||||
def test_returns_defaults_on_db_error(self):
|
|
||||||
cur = _make_cursor(side_effect=Exception('timeout'))
|
|
||||||
result = self.svc.compute_referee_stats(cur, 'Some Ref', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_REFEREE)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeLeagueAverages(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_league_id(self):
|
|
||||||
result = self.svc.compute_league_averages(MagicMock(), None, self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_LEAGUE)
|
|
||||||
|
|
||||||
def test_calculates_league_averages(self):
|
|
||||||
rows = [
|
|
||||||
{'score_home': 1, 'score_away': 1}, # 2 goals
|
|
||||||
{'score_home': 0, 'score_away': 0}, # 0 goals (zero-goal match)
|
|
||||||
{'score_home': 3, 'score_away': 2}, # 5 goals
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_league_averages(cur, 'league1', self.ts)
|
|
||||||
|
|
||||||
self.assertAlmostEqual(result['avg_goals'], 7 / 3, places=4)
|
|
||||||
self.assertAlmostEqual(result['zero_goal_rate'], 1 / 3, places=4)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeMomentum(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_zero_when_no_team_id(self):
|
|
||||||
result = self.svc.compute_momentum(MagicMock(), '', self.ts)
|
|
||||||
self.assertEqual(result, 0.0)
|
|
||||||
|
|
||||||
def test_returns_zero_when_no_rows(self):
|
|
||||||
cur = _make_cursor(rows=[])
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
self.assertEqual(result, 0.0)
|
|
||||||
|
|
||||||
def test_all_wins_returns_one(self):
|
|
||||||
"""All wins → momentum = 1.0 (max possible)."""
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'score_home': 3, 'score_away': 0},
|
|
||||||
{'home_team_id': 'team1', 'score_home': 2, 'score_away': 1},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
self.assertAlmostEqual(result, 1.0, places=4)
|
|
||||||
|
|
||||||
def test_all_losses_returns_negative(self):
|
|
||||||
"""All losses → negative momentum."""
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'score_home': 0, 'score_away': 2},
|
|
||||||
{'home_team_id': 'team1', 'score_home': 1, 'score_away': 3},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
self.assertLess(result, 0.0)
|
|
||||||
|
|
||||||
def test_mixed_results(self):
|
|
||||||
"""W, D, L → weighted score between -1 and 1."""
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'score_home': 1, 'score_away': 0}, # W (weight=3)
|
|
||||||
{'home_team_id': 'x', 'away_team_id': 'team1', 'score_home': 0, 'score_away': 0}, # D (weight=2)
|
|
||||||
{'home_team_id': 'team1', 'score_home': 0, 'score_away': 1}, # L (weight=1)
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
# weighted = 3*3 + 1*2 + (-1)*1 = 9+2-1 = 10
|
|
||||||
# max_possible = 3*3 + 3*2 + 3*1 = 18
|
|
||||||
# normalised = 10/18 ≈ 0.5556
|
|
||||||
self.assertAlmostEqual(result, round(10 / 18, 4), places=4)
|
|
||||||
|
|
||||||
def test_returns_zero_on_db_error(self):
|
|
||||||
cur = _make_cursor(side_effect=Exception('broken pipe'))
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
self.assertEqual(result, 0.0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
|
||||||
|
|
||||||
import main as ai_main
|
|
||||||
|
|
||||||
|
|
||||||
def _run(coro):
|
|
||||||
return asyncio.run(coro)
|
|
||||||
|
|
||||||
|
|
||||||
class MainApiFunctionTests(unittest.TestCase):
|
|
||||||
def test_analyze_match_v20plus_returns_payload(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.analyze_match.return_value = {"match_info": {"match_id": "m1"}}
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
result = _run(ai_main.analyze_match_v20plus("m1"))
|
|
||||||
|
|
||||||
self.assertEqual(result["match_info"]["match_id"], "m1")
|
|
||||||
|
|
||||||
def test_analyze_match_v20plus_raises_404(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.analyze_match.return_value = None
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
with self.assertRaises(HTTPException) as ctx:
|
|
||||||
_run(ai_main.analyze_match_v20plus("missing"))
|
|
||||||
|
|
||||||
self.assertEqual(ctx.exception.status_code, 404)
|
|
||||||
|
|
||||||
def test_analyze_match_htms_v20plus_returns_payload(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.analyze_match_htms.return_value = {
|
|
||||||
"status": "ok",
|
|
||||||
"engine_used": "v20plus_top_htms",
|
|
||||||
}
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
result = _run(ai_main.analyze_match_htms_v20plus("m1"))
|
|
||||||
|
|
||||||
self.assertEqual(result["status"], "ok")
|
|
||||||
self.assertEqual(result["engine_used"], "v20plus_top_htms")
|
|
||||||
|
|
||||||
def test_analyze_match_htft_timeout_validation(self):
|
|
||||||
with self.assertRaises(HTTPException) as ctx:
|
|
||||||
_run(ai_main.analyze_match_htft_v20plus("m1", timeout_sec=2))
|
|
||||||
|
|
||||||
self.assertEqual(ctx.exception.status_code, 400)
|
|
||||||
|
|
||||||
def test_generate_coupon_v20plus_forwards_payload(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.build_coupon.return_value = {"bets": []}
|
|
||||||
|
|
||||||
request = ai_main.CouponRequest(
|
|
||||||
match_ids=["m1", "m2"],
|
|
||||||
strategy="SAFE",
|
|
||||||
max_matches=3,
|
|
||||||
min_confidence=70,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
result = _run(ai_main.generate_coupon_v20plus(request))
|
|
||||||
|
|
||||||
self.assertEqual(result, {"bets": []})
|
|
||||||
orchestrator.build_coupon.assert_called_once_with(
|
|
||||||
match_ids=["m1", "m2"],
|
|
||||||
strategy="SAFE",
|
|
||||||
max_matches=3,
|
|
||||||
min_confidence=70.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_reversal_watchlist_validation(self):
|
|
||||||
with self.assertRaises(HTTPException) as ctx:
|
|
||||||
_run(ai_main.get_reversal_watchlist_v20plus(count=0))
|
|
||||||
self.assertEqual(ctx.exception.status_code, 400)
|
|
||||||
|
|
||||||
def test_reversal_watchlist_forwards_payload(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.get_reversal_watchlist.return_value = {"watchlist": []}
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
result = _run(
|
|
||||||
ai_main.get_reversal_watchlist_v20plus(
|
|
||||||
count=12,
|
|
||||||
horizon_hours=48,
|
|
||||||
min_score=50.5,
|
|
||||||
top_leagues_only=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(result, {"watchlist": []})
|
|
||||||
orchestrator.get_reversal_watchlist.assert_called_once_with(
|
|
||||||
count=12,
|
|
||||||
horizon_hours=48,
|
|
||||||
min_score=50.5,
|
|
||||||
top_leagues_only=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,768 +0,0 @@
|
|||||||
import json
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
|
||||||
|
|
||||||
from models.v20_ensemble import FullMatchPrediction
|
|
||||||
from models.basketball_v25 import BasketballMatchPrediction
|
|
||||||
from services.single_match_orchestrator import MatchData, SingleMatchOrchestrator
|
|
||||||
|
|
||||||
|
|
||||||
class _CursorContext:
|
|
||||||
def __init__(self, cursor):
|
|
||||||
self._cursor = cursor
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self._cursor
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc, tb):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class _ConnContext:
|
|
||||||
def __init__(self, cursor):
|
|
||||||
self._cursor = cursor
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc, tb):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def cursor(self, cursor_factory=None):
|
|
||||||
return _CursorContext(self._cursor)
|
|
||||||
|
|
||||||
|
|
||||||
class _StaticFetchAllCursor:
|
|
||||||
def __init__(self, rows):
|
|
||||||
self.rows = rows
|
|
||||||
self.executed = []
|
|
||||||
|
|
||||||
def execute(self, query, params=None):
|
|
||||||
self.executed.append((query, params))
|
|
||||||
|
|
||||||
def fetchall(self):
|
|
||||||
return list(self.rows)
|
|
||||||
|
|
||||||
|
|
||||||
class _RouterCursor:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
live_row=None,
|
|
||||||
hist_row=None,
|
|
||||||
relational_rows=None,
|
|
||||||
participation_rows=None,
|
|
||||||
probable_rows=None,
|
|
||||||
):
|
|
||||||
self.live_row = live_row
|
|
||||||
self.hist_row = hist_row
|
|
||||||
self.relational_rows = relational_rows or []
|
|
||||||
self.participation_rows = participation_rows or []
|
|
||||||
self.probable_rows = probable_rows or []
|
|
||||||
self.last_query = ""
|
|
||||||
|
|
||||||
def execute(self, query, params=None):
|
|
||||||
self.last_query = query
|
|
||||||
|
|
||||||
def fetchone(self):
|
|
||||||
if "FROM live_matches" in self.last_query:
|
|
||||||
return self.live_row
|
|
||||||
if "FROM matches m" in self.last_query:
|
|
||||||
return self.hist_row
|
|
||||||
return None
|
|
||||||
|
|
||||||
def fetchall(self):
|
|
||||||
if "FROM odd_categories" in self.last_query:
|
|
||||||
return list(self.relational_rows)
|
|
||||||
if "FROM match_player_participation" in self.last_query and "GROUP BY" not in self.last_query:
|
|
||||||
return list(self.participation_rows)
|
|
||||||
if "GROUP BY mpp.player_id" in self.last_query:
|
|
||||||
return list(self.probable_rows)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def _build_orchestrator() -> SingleMatchOrchestrator:
|
|
||||||
orchestrator = SingleMatchOrchestrator.__new__(SingleMatchOrchestrator)
|
|
||||||
orchestrator.v25_predictor = MagicMock()
|
|
||||||
orchestrator.v26_shadow_engine = None
|
|
||||||
orchestrator.basketball_predictor = MagicMock()
|
|
||||||
orchestrator.dsn = "postgresql://unit-test"
|
|
||||||
orchestrator.engine_mode = "v25"
|
|
||||||
orchestrator.league_reliability = {}
|
|
||||||
orchestrator.market_calibration = {
|
|
||||||
"MS": 0.82,
|
|
||||||
"DC": 0.93,
|
|
||||||
"OU15": 0.90,
|
|
||||||
"OU25": 0.85,
|
|
||||||
"OU35": 0.88,
|
|
||||||
"BTTS": 0.83,
|
|
||||||
"HT": 0.80,
|
|
||||||
"HT_OU05": 0.88,
|
|
||||||
}
|
|
||||||
orchestrator.market_min_conf = {
|
|
||||||
"MS": 52.0,
|
|
||||||
"DC": 56.0,
|
|
||||||
"OU15": 60.0,
|
|
||||||
"OU25": 58.0,
|
|
||||||
"OU35": 54.0,
|
|
||||||
"BTTS": 57.0,
|
|
||||||
"HT": 53.0,
|
|
||||||
"HT_OU05": 55.0,
|
|
||||||
}
|
|
||||||
orchestrator.market_min_play_score = {
|
|
||||||
"MS": 72.0,
|
|
||||||
"DC": 62.0,
|
|
||||||
"OU15": 64.0,
|
|
||||||
"OU25": 70.0,
|
|
||||||
"OU35": 76.0,
|
|
||||||
"BTTS": 70.0,
|
|
||||||
"HT": 74.0,
|
|
||||||
"HT_OU05": 64.0,
|
|
||||||
}
|
|
||||||
orchestrator.market_min_edge = {
|
|
||||||
"MS": 0.03,
|
|
||||||
"DC": 0.01,
|
|
||||||
"OU15": 0.01,
|
|
||||||
"OU25": 0.02,
|
|
||||||
"OU35": 0.04,
|
|
||||||
"BTTS": 0.03,
|
|
||||||
"HT": 0.04,
|
|
||||||
"HT_OU05": 0.01,
|
|
||||||
}
|
|
||||||
return orchestrator
|
|
||||||
|
|
||||||
|
|
||||||
class SingleMatchOrchestratorTests(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.orchestrator = _build_orchestrator()
|
|
||||||
|
|
||||||
def test_parse_odds_json_uses_exact_market_match_and_ignores_collisions(self):
|
|
||||||
odds_json = {
|
|
||||||
"Maç Sonucu": {"1": "2.15", "X": "3.20", "2": "3.30"},
|
|
||||||
"İlk Yarı/Maç Sonucu": {"1/1": "4.30"},
|
|
||||||
"2,5 Alt/Üst": {"Üst": "1.85", "Alt": "1.95"},
|
|
||||||
"İY 0,5 Alt/Üst": {"Üst": "1.49", "Alt": "2.20"},
|
|
||||||
"1. Yarı Ev Sahibi 0,5 Alt/Üst": {"Üst": "1.99", "Alt": "1.45"},
|
|
||||||
"2,5 Kart Puanı Alt/Üst": {"Üst": "1.33", "Alt": "2.95"},
|
|
||||||
"Karşılıklı Gol": {"Var": "1.75", "Yok": "2.05"},
|
|
||||||
"1. Yarı Karşılıklı Gol": {"Var": "2.10", "Yok": "1.60"},
|
|
||||||
"Çifte Şans": {"1-X": "1.33", "X-2": "1.62", "1-2": "1.30"},
|
|
||||||
"1. Yarı Sonucu": {"1": "2.45", "X": "2.00", "2": "3.80"},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ms_h"], 2.15)
|
|
||||||
self.assertEqual(parsed["ms_d"], 3.20)
|
|
||||||
self.assertEqual(parsed["ms_a"], 3.30)
|
|
||||||
self.assertEqual(parsed["ou25_o"], 1.85)
|
|
||||||
self.assertEqual(parsed["ou25_u"], 1.95)
|
|
||||||
self.assertEqual(parsed["btts_y"], 1.75)
|
|
||||||
self.assertEqual(parsed["btts_n"], 2.05)
|
|
||||||
self.assertEqual(parsed["dc_1x"], 1.33)
|
|
||||||
self.assertEqual(parsed["dc_x2"], 1.62)
|
|
||||||
self.assertEqual(parsed["dc_12"], 1.30)
|
|
||||||
self.assertEqual(parsed["ht_h"], 2.45)
|
|
||||||
self.assertEqual(parsed["ht_d"], 2.00)
|
|
||||||
self.assertEqual(parsed["ht_a"], 3.80)
|
|
||||||
self.assertEqual(parsed["ht_ou05_o"], 1.49)
|
|
||||||
self.assertEqual(parsed["ht_ou05_u"], 2.20)
|
|
||||||
self.assertEqual(parsed["htft_11"], 4.30)
|
|
||||||
|
|
||||||
def test_parse_odds_json_accepts_selection_variants(self):
|
|
||||||
odds_json = {
|
|
||||||
"2,5 Alt/Üst": {"2,5 Üst": "1.91", "2,5 Alt": "1.86"},
|
|
||||||
"Karşılıklı Gol": {"YES": "1.82", "NO": "1.96"},
|
|
||||||
"Çifte Şans": {"1X": "1.28", "X2": "1.44", "12": "1.32"},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ou25_o"], 1.91)
|
|
||||||
self.assertEqual(parsed["ou25_u"], 1.86)
|
|
||||||
self.assertEqual(parsed["btts_y"], 1.82)
|
|
||||||
self.assertEqual(parsed["btts_n"], 1.96)
|
|
||||||
self.assertEqual(parsed["dc_1x"], 1.28)
|
|
||||||
self.assertEqual(parsed["dc_x2"], 1.44)
|
|
||||||
self.assertEqual(parsed["dc_12"], 1.32)
|
|
||||||
|
|
||||||
def test_parse_odds_json_maps_all_football_markets_with_noise(self):
|
|
||||||
odds_json = {
|
|
||||||
"Maç Sonucu": {"1": "2.31", "X": "3.22", "2": "3.05"},
|
|
||||||
"Çifte Şans": {"1-X": "1.34", "X-2": "1.52", "1-2": "1.28"},
|
|
||||||
"1,5 Alt/Üst": {"Üst": "1.29", "Alt": "3.45"},
|
|
||||||
"2,5 Alt/Üst": {"Üst": "1.71", "Alt": "2.05"},
|
|
||||||
"3,5 Alt/Üst": {"Üst": "2.62", "Alt": "1.41"},
|
|
||||||
"Karşılıklı Gol": {"Var": "1.66", "Yok": "2.11"},
|
|
||||||
"1. Yarı Sonucu": {"1": "3.10", "X": "1.95", "2": "4.60"},
|
|
||||||
"1. Yarı 0,5 Alt/Üst": {"Üst": "1.21", "Alt": "2.72"},
|
|
||||||
# noise categories that must not overwrite football main markets
|
|
||||||
"1. Yarı Ev Sahibi 0,5 Alt/Üst": {"Üst": "1.99", "Alt": "1.45"},
|
|
||||||
"1. Yarı Deplasman 0,5 Alt/Üst": {"Üst": "1.73", "Alt": "1.63"},
|
|
||||||
"1.Yarı 3,5 Korner Alt/Üst": {"Üst": "1.26", "Alt": "2.30"},
|
|
||||||
"2,5 Kart Puanı Alt/Üst": {"Üst": "1.40", "Alt": "2.60"},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ms_h"], 2.31)
|
|
||||||
self.assertEqual(parsed["ms_d"], 3.22)
|
|
||||||
self.assertEqual(parsed["ms_a"], 3.05)
|
|
||||||
self.assertEqual(parsed["dc_1x"], 1.34)
|
|
||||||
self.assertEqual(parsed["dc_x2"], 1.52)
|
|
||||||
self.assertEqual(parsed["dc_12"], 1.28)
|
|
||||||
self.assertEqual(parsed["ou15_o"], 1.29)
|
|
||||||
self.assertEqual(parsed["ou15_u"], 3.45)
|
|
||||||
self.assertEqual(parsed["ou25_o"], 1.71)
|
|
||||||
self.assertEqual(parsed["ou25_u"], 2.05)
|
|
||||||
self.assertEqual(parsed["ou35_o"], 2.62)
|
|
||||||
self.assertEqual(parsed["ou35_u"], 1.41)
|
|
||||||
self.assertEqual(parsed["btts_y"], 1.66)
|
|
||||||
self.assertEqual(parsed["btts_n"], 2.11)
|
|
||||||
self.assertEqual(parsed["ht_h"], 3.10)
|
|
||||||
self.assertEqual(parsed["ht_d"], 1.95)
|
|
||||||
self.assertEqual(parsed["ht_a"], 4.60)
|
|
||||||
self.assertEqual(parsed["ht_ou05_o"], 1.21)
|
|
||||||
self.assertEqual(parsed["ht_ou05_u"], 2.72)
|
|
||||||
|
|
||||||
def test_v25_market_odds_ignores_synthetic_default_when_selection_missing(self):
|
|
||||||
odds_json = {
|
|
||||||
"1,5 Alt/Üst": {"Alt": 5.70},
|
|
||||||
"Çifte Şans": {"1-X": 1.30, "X-2": 1.38, "1-2": 1.09},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ou15_o"], 0.0)
|
|
||||||
self.assertEqual(
|
|
||||||
self.orchestrator._v25_market_odds(parsed, "OU15", "Over"),
|
|
||||||
1.0,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.orchestrator._v25_market_odds(parsed, "OU15", "Under"),
|
|
||||||
5.7,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.orchestrator._v25_market_odds(parsed, "DC", "X2"),
|
|
||||||
1.38,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_parse_odds_json_extracts_basketball_ml_total_spread(self):
|
|
||||||
odds_json = {
|
|
||||||
"Maç Sonucu (Uzt. Dahil)": {"1": "1.74", "2": "2.08"},
|
|
||||||
"Alt/Üst (163,5)": {"Üst": "1.86", "Alt": "1.94"},
|
|
||||||
"1. Yarı Alt/Üst (81,5)": {"Üst": "1.89", "Alt": "1.91"},
|
|
||||||
"1. Yarı Alt/Üst (100,5)": {"Üst": "1.83", "Alt": "1.97"},
|
|
||||||
"Hnd. MS (0:5,5)": {"1": "1.91", "+5.5h": "1.87"},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ml_h"], 1.74)
|
|
||||||
self.assertEqual(parsed["ml_a"], 2.08)
|
|
||||||
self.assertEqual(parsed["tot_line"], 163.5)
|
|
||||||
self.assertEqual(parsed["tot_o"], 1.86)
|
|
||||||
self.assertEqual(parsed["tot_u"], 1.94)
|
|
||||||
self.assertEqual(parsed["spread_home_line"], -5.5)
|
|
||||||
self.assertEqual(parsed["spread_h"], 1.91)
|
|
||||||
self.assertEqual(parsed["spread_a"], 1.87)
|
|
||||||
self.assertNotIn("ht_ou05_o", parsed)
|
|
||||||
self.assertNotIn("ht_ou05_u", parsed)
|
|
||||||
|
|
||||||
def test_extract_odds_merges_relational_when_live_json_is_incomplete(self):
|
|
||||||
row = {
|
|
||||||
"match_id": "m-1",
|
|
||||||
"odds": {"Maç Sonucu": {"1": 2.10, "X": 3.20, "2": 3.35}},
|
|
||||||
}
|
|
||||||
relational_rows = [
|
|
||||||
{"category_name": "Çifte Şans", "selection_name": "1-X", "odd_value": 1.28},
|
|
||||||
{"category_name": "Çifte Şans", "selection_name": "X-2", "odd_value": 1.44},
|
|
||||||
{"category_name": "Çifte Şans", "selection_name": "1-2", "odd_value": 1.31},
|
|
||||||
{"category_name": "2,5 Alt/Üst", "selection_name": "Üst", "odd_value": 1.89},
|
|
||||||
{"category_name": "2,5 Alt/Üst", "selection_name": "Alt", "odd_value": 1.94},
|
|
||||||
{"category_name": "Karşılıklı Gol", "selection_name": "Var", "odd_value": 1.77},
|
|
||||||
{"category_name": "Karşılıklı Gol", "selection_name": "Yok", "odd_value": 2.02},
|
|
||||||
{"category_name": "1. Yarı Sonucu", "selection_name": "1", "odd_value": 2.55},
|
|
||||||
{"category_name": "1. Yarı Sonucu", "selection_name": "X", "odd_value": 1.98},
|
|
||||||
{"category_name": "1. Yarı Sonucu", "selection_name": "2", "odd_value": 3.40},
|
|
||||||
]
|
|
||||||
cur = _StaticFetchAllCursor(relational_rows)
|
|
||||||
|
|
||||||
odds = self.orchestrator._extract_odds(cur, row)
|
|
||||||
|
|
||||||
self.assertEqual(odds["ms_h"], 2.10)
|
|
||||||
self.assertEqual(odds["ms_d"], 3.20)
|
|
||||||
self.assertEqual(odds["ms_a"], 3.35)
|
|
||||||
self.assertEqual(odds["dc_x2"], 1.44)
|
|
||||||
self.assertEqual(odds["ou25_o"], 1.89)
|
|
||||||
self.assertEqual(odds["btts_y"], 1.77)
|
|
||||||
self.assertEqual(odds["ht_d"], 1.98)
|
|
||||||
self.assertEqual(len(cur.executed), 1)
|
|
||||||
|
|
||||||
def test_extract_odds_fills_default_ms_when_no_source_available(self):
|
|
||||||
row = {"match_id": "m-2", "odds": None}
|
|
||||||
cur = _StaticFetchAllCursor([])
|
|
||||||
|
|
||||||
odds = self.orchestrator._extract_odds(cur, row)
|
|
||||||
|
|
||||||
self.assertEqual(odds["ms_h"], SingleMatchOrchestrator.DEFAULT_MS_H)
|
|
||||||
self.assertEqual(odds["ms_d"], SingleMatchOrchestrator.DEFAULT_MS_D)
|
|
||||||
self.assertEqual(odds["ms_a"], SingleMatchOrchestrator.DEFAULT_MS_A)
|
|
||||||
|
|
||||||
def test_parse_lineups_json_supports_id_playerid_personid(self):
|
|
||||||
lineups = {
|
|
||||||
"home": {
|
|
||||||
"xi": [
|
|
||||||
{"id": "11"},
|
|
||||||
{"playerId": "12"},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"away": {
|
|
||||||
"starting": [
|
|
||||||
{"personId": "21"},
|
|
||||||
"22",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
home, away = self.orchestrator._parse_lineups_json(lineups)
|
|
||||||
|
|
||||||
self.assertEqual(home, ["11", "12"])
|
|
||||||
self.assertEqual(away, ["21", "22"])
|
|
||||||
|
|
||||||
def test_extract_lineups_uses_participation_and_probable_xi_fallbacks(self):
|
|
||||||
row = {
|
|
||||||
"match_id": "m-3",
|
|
||||||
"home_team_id": "h1",
|
|
||||||
"away_team_id": "a1",
|
|
||||||
"match_date_ms": 1700000000000,
|
|
||||||
"lineups": {
|
|
||||||
"home": {"xi": [{"personId": "h-live-1"}]},
|
|
||||||
"away": {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
participation = [
|
|
||||||
{"team_id": "a1", "player_id": "a-db-1"},
|
|
||||||
{"team_id": "a1", "player_id": "a-db-2"},
|
|
||||||
]
|
|
||||||
cur = _StaticFetchAllCursor(participation)
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
self.orchestrator,
|
|
||||||
"_build_probable_xi",
|
|
||||||
side_effect=[["h-prob-1"], ["a-prob-1"]],
|
|
||||||
) as probable_xi:
|
|
||||||
home, away, source = self.orchestrator._extract_lineups(cur, row)
|
|
||||||
|
|
||||||
self.assertEqual(home, ["h-live-1"])
|
|
||||||
self.assertEqual(away, ["a-db-1", "a-db-2"])
|
|
||||||
self.assertEqual(source, "none")
|
|
||||||
probable_xi.assert_not_called()
|
|
||||||
|
|
||||||
def test_extract_lineups_falls_back_to_probable_xi_when_live_and_participation_missing(self):
|
|
||||||
row = {
|
|
||||||
"match_id": "m-4",
|
|
||||||
"home_team_id": "h2",
|
|
||||||
"away_team_id": "a2",
|
|
||||||
"match_date_ms": 1700000000000,
|
|
||||||
"lineups": None,
|
|
||||||
}
|
|
||||||
cur = _StaticFetchAllCursor([])
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
self.orchestrator,
|
|
||||||
"_build_probable_xi",
|
|
||||||
side_effect=[["h-prob-1", "h-prob-2"], ["a-prob-1"]],
|
|
||||||
) as probable_xi:
|
|
||||||
home, away, source = self.orchestrator._extract_lineups(cur, row)
|
|
||||||
|
|
||||||
self.assertEqual(home, ["h-prob-1", "h-prob-2"])
|
|
||||||
self.assertEqual(away, ["a-prob-1"])
|
|
||||||
self.assertEqual(source, "probable_xi")
|
|
||||||
self.assertEqual(probable_xi.call_count, 2)
|
|
||||||
|
|
||||||
def test_load_match_data_parses_live_row_json_and_sidelined(self):
|
|
||||||
odds_payload = {
|
|
||||||
"Maç Sonucu": {"1": 2.10, "X": 3.30, "2": 3.50},
|
|
||||||
"Çifte Şans": {"1-X": 1.30, "X-2": 1.52, "1-2": 1.34},
|
|
||||||
"1,5 Alt/Üst": {"Üst": 1.33, "Alt": 2.90},
|
|
||||||
"2,5 Alt/Üst": {"Üst": 1.91, "Alt": 1.85},
|
|
||||||
"3,5 Alt/Üst": {"Üst": 2.95, "Alt": 1.38},
|
|
||||||
"Karşılıklı Gol": {"Var": 1.84, "Yok": 1.92},
|
|
||||||
"1. Yarı Sonucu": {"1": 2.55, "X": 1.97, "2": 3.45},
|
|
||||||
}
|
|
||||||
lineups_payload = {
|
|
||||||
"home": {"xi": [{"personId": "101"}, {"personId": "102"}]},
|
|
||||||
"away": {"xi": [{"personId": "201"}, {"personId": "202"}]},
|
|
||||||
}
|
|
||||||
live_row = {
|
|
||||||
"match_id": "live-101",
|
|
||||||
"home_team_id": "h-101",
|
|
||||||
"away_team_id": "a-101",
|
|
||||||
"league_id": "l-101",
|
|
||||||
"sport": "FOOTBALL",
|
|
||||||
"match_date_ms": 1760000000000,
|
|
||||||
"odds": json.dumps(odds_payload),
|
|
||||||
"lineups": json.dumps(lineups_payload),
|
|
||||||
"sidelined": json.dumps(
|
|
||||||
{
|
|
||||||
"homeTeam": {"totalSidelined": 1, "players": []},
|
|
||||||
"awayTeam": {"totalSidelined": 0, "players": []},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
"referee_name": "John Ref",
|
|
||||||
"home_team_name": "Home FC",
|
|
||||||
"away_team_name": "Away FC",
|
|
||||||
"league_name": "League Name",
|
|
||||||
}
|
|
||||||
cursor = _RouterCursor(live_row=live_row)
|
|
||||||
|
|
||||||
with patch("services.single_match_orchestrator.psycopg2.connect", return_value=_ConnContext(cursor)):
|
|
||||||
data = self.orchestrator._load_match_data("live-101")
|
|
||||||
|
|
||||||
self.assertIsNotNone(data)
|
|
||||||
self.assertEqual(data.match_id, "live-101")
|
|
||||||
self.assertEqual(data.home_team_id, "h-101")
|
|
||||||
self.assertEqual(data.away_team_id, "a-101")
|
|
||||||
self.assertEqual(data.sport, "football")
|
|
||||||
self.assertEqual(data.referee_name, "John Ref")
|
|
||||||
self.assertEqual(data.home_lineup, ["101", "102"])
|
|
||||||
self.assertEqual(data.away_lineup, ["201", "202"])
|
|
||||||
self.assertEqual(data.lineup_source, "none")
|
|
||||||
self.assertEqual(data.sidelined_data["homeTeam"]["totalSidelined"], 1)
|
|
||||||
self.assertEqual(data.odds_data["dc_x2"], 1.52)
|
|
||||||
self.assertEqual(data.odds_data["ht_h"], 2.55)
|
|
||||||
|
|
||||||
def test_analyze_match_forwards_all_core_fields_to_predictor(self):
|
|
||||||
match_data = MatchData(
|
|
||||||
match_id="live-55",
|
|
||||||
home_team_id="home-55",
|
|
||||||
away_team_id="away-55",
|
|
||||||
home_team_name="Home 55",
|
|
||||||
away_team_name="Away 55",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="football",
|
|
||||||
league_id="league-55",
|
|
||||||
league_name="League 55",
|
|
||||||
referee_name="Ref 55",
|
|
||||||
odds_data={"ms_h": 2.4, "ms_d": 3.1, "ms_a": 2.9},
|
|
||||||
home_lineup=["h1", "h2"],
|
|
||||||
away_lineup=["a1", "a2"],
|
|
||||||
sidelined_data={
|
|
||||||
"homeTeam": {"totalSidelined": 2, "players": []},
|
|
||||||
"awayTeam": {"totalSidelined": 1, "players": []},
|
|
||||||
},
|
|
||||||
home_goals_avg=1.6,
|
|
||||||
home_conceded_avg=1.1,
|
|
||||||
away_goals_avg=1.2,
|
|
||||||
away_conceded_avg=1.4,
|
|
||||||
home_position=5,
|
|
||||||
away_position=8,
|
|
||||||
lineup_source="confirmed_live",
|
|
||||||
)
|
|
||||||
prediction = FullMatchPrediction(match_id="live-55", home_team="Home 55", away_team="Away 55")
|
|
||||||
|
|
||||||
self.orchestrator._load_match_data = MagicMock(return_value=match_data)
|
|
||||||
self.orchestrator.v25_predictor.predict_market_bundle = MagicMock(return_value={"MS": {"pick": "1"}})
|
|
||||||
self.orchestrator._build_v25_features = MagicMock(return_value={})
|
|
||||||
self.orchestrator._get_v25_signal = MagicMock(return_value={"MS": {"pick": "1"}})
|
|
||||||
self.orchestrator._build_v25_prediction = MagicMock(return_value=prediction)
|
|
||||||
self.orchestrator._build_prediction_package = MagicMock(return_value={"ok": True})
|
|
||||||
|
|
||||||
result = self.orchestrator.analyze_match("live-55")
|
|
||||||
|
|
||||||
self.assertEqual(result, {"ok": True})
|
|
||||||
self.orchestrator._build_v25_features.assert_called_once_with(match_data)
|
|
||||||
self.orchestrator._get_v25_signal.assert_called_once_with(match_data, {})
|
|
||||||
self.orchestrator._build_v25_prediction.assert_called_once_with(
|
|
||||||
match_data,
|
|
||||||
{},
|
|
||||||
{"MS": {"pick": "1"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_analyze_match_routes_basketball_to_basketball_predictor(self):
|
|
||||||
match_data = MatchData(
|
|
||||||
match_id="b-live-1",
|
|
||||||
home_team_id="bh",
|
|
||||||
away_team_id="ba",
|
|
||||||
home_team_name="Home B",
|
|
||||||
away_team_name="Away B",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="basketball",
|
|
||||||
league_id="bleague",
|
|
||||||
league_name="B League",
|
|
||||||
referee_name=None,
|
|
||||||
odds_data={"ml_h": 1.75, "ml_a": 2.05, "tot_line": 161.5, "tot_o": 1.88, "tot_u": 1.92},
|
|
||||||
home_lineup=None,
|
|
||||||
away_lineup=None,
|
|
||||||
sidelined_data={"homeTeam": {"totalSidelined": 1}, "awayTeam": {"totalSidelined": 0}},
|
|
||||||
home_goals_avg=85.0,
|
|
||||||
home_conceded_avg=79.0,
|
|
||||||
away_goals_avg=82.0,
|
|
||||||
away_conceded_avg=81.0,
|
|
||||||
home_position=4,
|
|
||||||
away_position=7,
|
|
||||||
lineup_source="none",
|
|
||||||
)
|
|
||||||
prediction = BasketballMatchPrediction(
|
|
||||||
match_id="b-live-1",
|
|
||||||
home_team_name="Home B",
|
|
||||||
away_team_name="Away B",
|
|
||||||
league_name="B League",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.orchestrator._load_match_data = MagicMock(return_value=match_data)
|
|
||||||
self.orchestrator.basketball_predictor.predict = MagicMock(return_value=prediction)
|
|
||||||
self.orchestrator._build_basketball_prediction_package = MagicMock(
|
|
||||||
return_value={"sport": "basketball", "ok": True}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = self.orchestrator.analyze_match("b-live-1")
|
|
||||||
|
|
||||||
self.assertEqual(result, {"sport": "basketball", "ok": True})
|
|
||||||
self.orchestrator.basketball_predictor.predict.assert_called_once()
|
|
||||||
kwargs = self.orchestrator.basketball_predictor.predict.call_args.kwargs
|
|
||||||
self.assertEqual(kwargs["match_id"], "b-live-1")
|
|
||||||
self.assertEqual(kwargs["home_team_id"], "bh")
|
|
||||||
self.assertEqual(kwargs["away_team_id"], "ba")
|
|
||||||
self.assertEqual(kwargs["league_id"], "bleague")
|
|
||||||
self.assertEqual(kwargs["odds_data"]["ml_h"], 1.75)
|
|
||||||
self.orchestrator.v25_predictor.predict_market_bundle.assert_not_called()
|
|
||||||
|
|
||||||
def test_build_market_rows_maps_odds_keys_correctly(self):
|
|
||||||
data = MatchData(
|
|
||||||
match_id="m-rows",
|
|
||||||
home_team_id="h",
|
|
||||||
away_team_id="a",
|
|
||||||
home_team_name="Home",
|
|
||||||
away_team_name="Away",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="football",
|
|
||||||
league_id=None,
|
|
||||||
league_name="",
|
|
||||||
referee_name=None,
|
|
||||||
odds_data={
|
|
||||||
"ms_h": 2.3,
|
|
||||||
"ms_d": 3.2,
|
|
||||||
"ms_a": 3.1,
|
|
||||||
"dc_x2": 1.45,
|
|
||||||
"ou15_o": 1.36,
|
|
||||||
"ou25_u": 1.92,
|
|
||||||
"ou35_o": 2.85,
|
|
||||||
"btts_y": 1.88,
|
|
||||||
"ht_h": 2.55,
|
|
||||||
"ht_ou05_o": 1.47,
|
|
||||||
},
|
|
||||||
home_lineup=None,
|
|
||||||
away_lineup=None,
|
|
||||||
sidelined_data=None,
|
|
||||||
home_goals_avg=1.5,
|
|
||||||
home_conceded_avg=1.2,
|
|
||||||
away_goals_avg=1.2,
|
|
||||||
away_conceded_avg=1.4,
|
|
||||||
home_position=10,
|
|
||||||
away_position=10,
|
|
||||||
lineup_source="none",
|
|
||||||
)
|
|
||||||
pred = FullMatchPrediction(
|
|
||||||
match_id="m-rows",
|
|
||||||
home_team="Home",
|
|
||||||
away_team="Away",
|
|
||||||
ms_home_prob=0.25,
|
|
||||||
ms_draw_prob=0.30,
|
|
||||||
ms_away_prob=0.45,
|
|
||||||
ms_pick="2",
|
|
||||||
ms_confidence=69.0,
|
|
||||||
dc_1x_prob=0.60,
|
|
||||||
dc_x2_prob=0.72,
|
|
||||||
dc_12_prob=0.68,
|
|
||||||
dc_pick="X2",
|
|
||||||
dc_confidence=67.0,
|
|
||||||
over_15_prob=0.74,
|
|
||||||
under_15_prob=0.26,
|
|
||||||
ou15_pick="1.5 Üst",
|
|
||||||
ou15_confidence=72.0,
|
|
||||||
over_25_prob=0.44,
|
|
||||||
under_25_prob=0.56,
|
|
||||||
ou25_pick="2.5 Alt",
|
|
||||||
ou25_confidence=61.0,
|
|
||||||
over_35_prob=0.39,
|
|
||||||
under_35_prob=0.61,
|
|
||||||
ou35_pick="3.5 Over",
|
|
||||||
ou35_confidence=58.0,
|
|
||||||
btts_yes_prob=0.57,
|
|
||||||
btts_no_prob=0.43,
|
|
||||||
btts_pick="Yes",
|
|
||||||
btts_confidence=63.0,
|
|
||||||
ht_home_prob=0.41,
|
|
||||||
ht_draw_prob=0.39,
|
|
||||||
ht_away_prob=0.20,
|
|
||||||
ht_pick="1",
|
|
||||||
ht_confidence=60.0,
|
|
||||||
ht_over_05_prob=0.64,
|
|
||||||
ht_under_05_prob=0.36,
|
|
||||||
ht_ou_pick="Over 0.5",
|
|
||||||
)
|
|
||||||
|
|
||||||
rows = self.orchestrator._build_market_rows(data, pred)
|
|
||||||
by_market = {row["market"]: row for row in rows}
|
|
||||||
|
|
||||||
self.assertEqual(by_market["MS"]["odds"], 3.1)
|
|
||||||
self.assertEqual(by_market["DC"]["odds"], 1.45)
|
|
||||||
self.assertEqual(by_market["OU15"]["odds"], 1.36)
|
|
||||||
self.assertEqual(by_market["OU25"]["odds"], 1.92)
|
|
||||||
self.assertEqual(by_market["OU35"]["odds"], 2.85)
|
|
||||||
self.assertEqual(by_market["BTTS"]["odds"], 1.88)
|
|
||||||
self.assertEqual(by_market["HT"]["odds"], 2.55)
|
|
||||||
self.assertEqual(by_market["HT_OU05"]["odds"], 1.47)
|
|
||||||
|
|
||||||
def test_build_basketball_market_rows_maps_odds_keys_correctly(self):
|
|
||||||
data = MatchData(
|
|
||||||
match_id="b-rows",
|
|
||||||
home_team_id="bh",
|
|
||||||
away_team_id="ba",
|
|
||||||
home_team_name="Home B",
|
|
||||||
away_team_name="Away B",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="basketball",
|
|
||||||
league_id="bl",
|
|
||||||
league_name="Basketball League",
|
|
||||||
referee_name=None,
|
|
||||||
odds_data={
|
|
||||||
"ml_h": 1.73,
|
|
||||||
"ml_a": 2.10,
|
|
||||||
"tot_line": 162.5,
|
|
||||||
"tot_o": 1.89,
|
|
||||||
"tot_u": 1.93,
|
|
||||||
"spread_home_line": -4.5,
|
|
||||||
"spread_h": 1.91,
|
|
||||||
"spread_a": 1.88,
|
|
||||||
},
|
|
||||||
home_lineup=None,
|
|
||||||
away_lineup=None,
|
|
||||||
sidelined_data=None,
|
|
||||||
home_goals_avg=84.0,
|
|
||||||
home_conceded_avg=80.0,
|
|
||||||
away_goals_avg=82.0,
|
|
||||||
away_conceded_avg=81.0,
|
|
||||||
home_position=5,
|
|
||||||
away_position=8,
|
|
||||||
lineup_source="none",
|
|
||||||
)
|
|
||||||
pred = {
|
|
||||||
"match_id": "b-rows",
|
|
||||||
"market_board": {
|
|
||||||
"ML": {"1": "62%", "2": "38%"},
|
|
||||||
"Totals": {"Under 162.5": "43%", "Over 162.5": "57%"},
|
|
||||||
"Spread": {"Away +4.5": "46%", "Home -4.5": "54%"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows = self.orchestrator._build_basketball_market_rows(data, pred)
|
|
||||||
by_market = {row["market"]: row for row in rows}
|
|
||||||
|
|
||||||
self.assertEqual(by_market["ML"]["odds"], 1.73)
|
|
||||||
self.assertEqual(by_market["TOTAL"]["odds"], 1.89)
|
|
||||||
self.assertEqual(by_market["SPREAD"]["odds"], 1.91)
|
|
||||||
|
|
||||||
def test_compute_data_quality_flags_missing_referee_and_lineup(self):
|
|
||||||
data = MatchData(
|
|
||||||
match_id="dq-1",
|
|
||||||
home_team_id="h",
|
|
||||||
away_team_id="a",
|
|
||||||
home_team_name="Home",
|
|
||||||
away_team_name="Away",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="football",
|
|
||||||
league_id=None,
|
|
||||||
league_name="",
|
|
||||||
referee_name=None,
|
|
||||||
odds_data={"ms_h": 2.5, "ms_d": 3.2, "ms_a": 2.9},
|
|
||||||
home_lineup=["h1", "h2"],
|
|
||||||
away_lineup=["a1"],
|
|
||||||
sidelined_data=None,
|
|
||||||
home_goals_avg=1.5,
|
|
||||||
home_conceded_avg=1.2,
|
|
||||||
away_goals_avg=1.2,
|
|
||||||
away_conceded_avg=1.4,
|
|
||||||
home_position=10,
|
|
||||||
away_position=10,
|
|
||||||
lineup_source="none",
|
|
||||||
)
|
|
||||||
|
|
||||||
quality = self.orchestrator._compute_data_quality(data)
|
|
||||||
|
|
||||||
self.assertIn("lineup_incomplete", quality["flags"])
|
|
||||||
self.assertIn("missing_referee", quality["flags"])
|
|
||||||
self.assertEqual(quality["label"], "MEDIUM")
|
|
||||||
|
|
||||||
def test_load_match_data_returns_none_when_team_ids_missing(self):
|
|
||||||
live_row = {
|
|
||||||
"match_id": "live-missing-ids",
|
|
||||||
"home_team_id": None,
|
|
||||||
"away_team_id": None,
|
|
||||||
"league_id": "l-1",
|
|
||||||
"sport": "football",
|
|
||||||
"match_date_ms": 1760000000000,
|
|
||||||
"odds": None,
|
|
||||||
"lineups": None,
|
|
||||||
"sidelined": None,
|
|
||||||
"referee_name": None,
|
|
||||||
"home_team_name": "Home",
|
|
||||||
"away_team_name": "Away",
|
|
||||||
"league_name": "League",
|
|
||||||
}
|
|
||||||
cursor = _RouterCursor(live_row=live_row)
|
|
||||||
|
|
||||||
with patch("services.single_match_orchestrator.psycopg2.connect", return_value=_ConnContext(cursor)):
|
|
||||||
data = self.orchestrator._load_match_data("live-missing-ids")
|
|
||||||
|
|
||||||
self.assertIsNone(data)
|
|
||||||
|
|
||||||
def test_decorate_market_row_blocks_required_market_when_odds_missing(self):
|
|
||||||
data = MatchData(
|
|
||||||
match_id="dq-odds",
|
|
||||||
home_team_id="h",
|
|
||||||
away_team_id="a",
|
|
||||||
home_team_name="Home",
|
|
||||||
away_team_name="Away",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="football",
|
|
||||||
league_id="l1",
|
|
||||||
league_name="League",
|
|
||||||
referee_name="Ref",
|
|
||||||
odds_data={"ms_h": 2.2, "ms_d": 3.2, "ms_a": 3.0},
|
|
||||||
home_lineup=["h"] * 11,
|
|
||||||
away_lineup=["a"] * 11,
|
|
||||||
sidelined_data=None,
|
|
||||||
home_goals_avg=1.5,
|
|
||||||
home_conceded_avg=1.2,
|
|
||||||
away_goals_avg=1.2,
|
|
||||||
away_conceded_avg=1.4,
|
|
||||||
home_position=7,
|
|
||||||
away_position=9,
|
|
||||||
lineup_source="confirmed_live",
|
|
||||||
)
|
|
||||||
prediction = FullMatchPrediction(match_id="dq-odds", home_team="Home", away_team="Away")
|
|
||||||
quality = self.orchestrator._compute_data_quality(data)
|
|
||||||
row = {
|
|
||||||
"market": "HT_OU05",
|
|
||||||
"pick": "İY 0.5 Üst",
|
|
||||||
"probability": 0.65,
|
|
||||||
"confidence": 66.0,
|
|
||||||
"odds": 0.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
out = self.orchestrator._decorate_market_row(data, prediction, quality, row)
|
|
||||||
self.assertFalse(out["playable"])
|
|
||||||
self.assertIn("market_odds_missing", out["decision_reasons"])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
"""
|
|
||||||
Unit Test for NEW Skip Logic in BetRecommender
|
|
||||||
==============================================
|
|
||||||
Run with: python ai-engine/tests/test_skip_logic.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
# Add paths
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
|
||||||
|
|
||||||
from core.calculators.bet_recommender import BetRecommender, RecommendationResult, MarketPredictionDTO
|
|
||||||
from core.calculators.risk_assessor import RiskAnalysis
|
|
||||||
from core.calculators.match_result_calculator import MatchResultPrediction
|
|
||||||
from core.calculators.over_under_calculator import OverUnderPrediction
|
|
||||||
from config.config_loader import get_config
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DummyContext:
|
|
||||||
"""Minimal mock for CalculationContext"""
|
|
||||||
odds_data: dict
|
|
||||||
|
|
||||||
class TestSkipLogic(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# Mock config to pass into BetRecommender
|
|
||||||
self.mock_config = {
|
|
||||||
"recommendations.market_weights": {"MS": 1.0, "ÇŞ": 0.9, "BTTS": 0.9, "2.5 Üst/Alt": 0.9},
|
|
||||||
"recommendations.safe_markets": ["ÇŞ", "1.5 Üst/Alt"],
|
|
||||||
"recommendations.market_accuracy": {"MS": 65, "ÇŞ": 75, "BTTS": 60, "2.5 Üst/Alt": 65},
|
|
||||||
"recommendations.baseline_accuracy": 65.0,
|
|
||||||
"recommendations.confidence_threshold": 60,
|
|
||||||
"recommendations.value_confidence_min": 45,
|
|
||||||
"recommendations.value_confidence_max": 60,
|
|
||||||
"recommendations.value_edge_margin": 0.03,
|
|
||||||
"recommendations.value_upgrade_edge": 5.0,
|
|
||||||
"recommendations.risk_safe_boost": 1.2,
|
|
||||||
"recommendations.risk_ms_penalty_high": 0.5,
|
|
||||||
"recommendations.risk_other_penalty": 0.7,
|
|
||||||
"recommendations.risk_ms_penalty_medium": 0.8,
|
|
||||||
}
|
|
||||||
self.recommender = BetRecommender(self.mock_config)
|
|
||||||
|
|
||||||
def _make_risk(self, level="MEDIUM", is_surprise=False):
|
|
||||||
return RiskAnalysis(risk_level=level, is_surprise_risk=is_surprise, risk_score=0.5)
|
|
||||||
|
|
||||||
def _make_ms_pred(self, pick, conf):
|
|
||||||
# pick: "1", "X", "2"
|
|
||||||
probs = {"1": {"ms_home_prob": 0.5, "ms_draw_prob": 0.3, "ms_away_prob": 0.2},
|
|
||||||
"X": {"ms_home_prob": 0.2, "ms_draw_prob": 0.5, "ms_away_prob": 0.3},
|
|
||||||
"2": {"ms_home_prob": 0.2, "ms_draw_prob": 0.3, "ms_away_prob": 0.5}}
|
|
||||||
p = probs.get(pick, probs["1"])
|
|
||||||
return MatchResultPrediction(
|
|
||||||
ms_pick=pick, ms_confidence=conf,
|
|
||||||
dc_pick="1X", dc_confidence=0,
|
|
||||||
dc_1x_prob=0.7, dc_x2_prob=0.7, dc_12_prob=0.7,
|
|
||||||
**p
|
|
||||||
)
|
|
||||||
|
|
||||||
def _make_ou_pred(self):
|
|
||||||
return OverUnderPrediction(
|
|
||||||
ou25_pick="2.5 Üst", ou25_confidence=50.0,
|
|
||||||
over_25_prob=0.55, under_25_prob=0.45,
|
|
||||||
|
|
||||||
btts_pick="Var", btts_confidence=50.0,
|
|
||||||
btts_yes_prob=0.55, btts_no_prob=0.45,
|
|
||||||
|
|
||||||
ou15_pick="1.5 Üst", ou15_confidence=60.0, over_15_prob=0.7, under_15_prob=0.3,
|
|
||||||
ou35_pick="3.5 Alt", ou35_confidence=50.0, over_35_prob=0.3, under_35_prob=0.7
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_low_confidence_should_skip(self):
|
|
||||||
"""Confidence < 45% should be SKIPPED"""
|
|
||||||
ms_pred = self._make_ms_pred(pick="2", conf=40.0)
|
|
||||||
ou_pred = self._make_ou_pred()
|
|
||||||
risk = self._make_risk("MEDIUM")
|
|
||||||
ctx = DummyContext(odds_data={"ms_2": 2.5})
|
|
||||||
|
|
||||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
|
||||||
|
|
||||||
# Check if MS bet is skipped
|
|
||||||
ms_bet = next((b for b in res.skipped_bets if b.market_type == "MS"), None)
|
|
||||||
self.assertIsNotNone(ms_bet, "MS bet with 40% conf should be skipped!")
|
|
||||||
self.assertTrue(ms_bet.is_skip)
|
|
||||||
|
|
||||||
def test_good_confidence_should_recommend(self):
|
|
||||||
"""Confidence > 60% and Good Odds should be RECOMMENDED"""
|
|
||||||
ms_pred = self._make_ms_pred(pick="1", conf=70.0)
|
|
||||||
ou_pred = self._make_ou_pred()
|
|
||||||
risk = self._make_risk("MEDIUM")
|
|
||||||
# Odds 1.80 for 70% prob = Good Value (Need real odds for MS to pass)
|
|
||||||
ctx = DummyContext(odds_data={"ms_1": 1.80, "ou15_o": 1.50}) # Added ou15 odds
|
|
||||||
|
|
||||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
|
||||||
|
|
||||||
# Check if ANY bet is recommended (doesn't have to be MS, but usually is)
|
|
||||||
self.assertGreater(len(res.recommended_bets), 0, "At least one bet should be recommended!")
|
|
||||||
# Check that MS bet is NOT skipped
|
|
||||||
ms_bet = next((b for b in res.recommended_bets if b.market_type == "MS"), None)
|
|
||||||
if ms_bet:
|
|
||||||
self.assertFalse(ms_bet.is_skip)
|
|
||||||
|
|
||||||
def test_negative_edge_should_skip(self):
|
|
||||||
"""Even with high confidence, if Odds are too low (Bad Value), SKIP"""
|
|
||||||
ms_pred = self._make_ms_pred(pick="1", conf=70.0) # 70% prob
|
|
||||||
ou_pred = self._make_ou_pred()
|
|
||||||
risk = self._make_risk("MEDIUM")
|
|
||||||
# Odds 1.10 -> Implied 90%. Our prob is 70%. Edge is -20% -> SKIP
|
|
||||||
ctx = DummyContext(odds_data={"ms_1": 1.10})
|
|
||||||
|
|
||||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
|
||||||
|
|
||||||
ms_bet = next((b for b in res.skipped_bets if b.market_type == "MS"), None)
|
|
||||||
self.assertIsNotNone(ms_bet, "MS bet with terrible odds (Negative Edge) should be skipped!")
|
|
||||||
self.assertTrue(ms_bet.is_skip)
|
|
||||||
|
|
||||||
def test_no_bets_recommendation(self):
|
|
||||||
"""If all bets are low confidence, best_bet should be None"""
|
|
||||||
ms_pred = self._make_ms_pred(pick="1", conf=30.0) # Very low conf
|
|
||||||
ou_pred = self._make_ou_pred()
|
|
||||||
# Reset ALL OU confs to low
|
|
||||||
ou_pred.ou25_confidence = 30.0
|
|
||||||
ou_pred.btts_confidence = 30.0
|
|
||||||
ou_pred.ou15_confidence = 30.0 # This was 60 in setUp, causing the fail!
|
|
||||||
ou_pred.ou35_confidence = 30.0
|
|
||||||
|
|
||||||
risk = self._make_risk("MEDIUM")
|
|
||||||
ctx = DummyContext(odds_data={"ms_1": 2.0})
|
|
||||||
|
|
||||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
|
||||||
|
|
||||||
self.assertIsNone(res.best_bet, "If everything is skipped, there should be no best_bet.")
|
|
||||||
self.assertEqual(len(res.recommended_bets), 0, "No bets should be recommended!")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print("🧪 Running Skip Logic Unit Tests...")
|
|
||||||
print("="*50)
|
|
||||||
unittest.main(verbosity=2)
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
|
||||||
|
|
||||||
from services.v26_shadow_engine import V26ShadowEngine
|
|
||||||
|
|
||||||
|
|
||||||
def _build_prediction():
|
|
||||||
return SimpleNamespace(
|
|
||||||
risk_level="MEDIUM",
|
|
||||||
risk_score=42.0,
|
|
||||||
is_surprise_risk=False,
|
|
||||||
surprise_type="",
|
|
||||||
surprise_score=0.0,
|
|
||||||
surprise_comment="",
|
|
||||||
surprise_reasons=[],
|
|
||||||
risk_warnings=[],
|
|
||||||
team_confidence=71.0,
|
|
||||||
player_confidence=64.0,
|
|
||||||
odds_confidence=75.0,
|
|
||||||
referee_confidence=58.0,
|
|
||||||
predicted_ft_score="2-1",
|
|
||||||
predicted_ht_score="1-0",
|
|
||||||
home_xg=1.72,
|
|
||||||
away_xg=1.08,
|
|
||||||
total_xg=2.8,
|
|
||||||
ft_scores_top5=[
|
|
||||||
{"score": "2-1", "prob": 0.093},
|
|
||||||
{"score": "1-1", "prob": 0.086},
|
|
||||||
],
|
|
||||||
ms_home_prob=0.52,
|
|
||||||
ms_draw_prob=0.24,
|
|
||||||
ms_away_prob=0.24,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_data(referee_name="Ref A", lineup_source="confirmed_live", league_id="league1"):
|
|
||||||
return SimpleNamespace(
|
|
||||||
match_id="m1",
|
|
||||||
home_team_name="Home",
|
|
||||||
away_team_name="Away",
|
|
||||||
league_id=league_id,
|
|
||||||
league_name="League",
|
|
||||||
match_date_ms=1710000000000,
|
|
||||||
sport="football",
|
|
||||||
home_lineup=["h"] * 11,
|
|
||||||
away_lineup=["a"] * 11,
|
|
||||||
lineup_source=lineup_source,
|
|
||||||
referee_name=referee_name,
|
|
||||||
odds_data={
|
|
||||||
"ms_h": 2.1,
|
|
||||||
"ms_d": 3.4,
|
|
||||||
"ms_a": 3.7,
|
|
||||||
"dc_1x": 1.28,
|
|
||||||
"dc_x2": 1.68,
|
|
||||||
"dc_12": 1.34,
|
|
||||||
"ou15_o": 1.24,
|
|
||||||
"ou15_u": 4.1,
|
|
||||||
"ou25_o": 1.77,
|
|
||||||
"ou25_u": 2.05,
|
|
||||||
"ou35_o": 2.95,
|
|
||||||
"ou35_u": 1.4,
|
|
||||||
"btts_y": 1.74,
|
|
||||||
"btts_n": 2.04,
|
|
||||||
"ht_h": 2.72,
|
|
||||||
"ht_d": 2.05,
|
|
||||||
"ht_a": 4.8,
|
|
||||||
"ht_ou05_o": 1.38,
|
|
||||||
"ht_ou05_u": 2.85,
|
|
||||||
"ht_ou15_o": 2.48,
|
|
||||||
"ht_ou15_u": 1.48,
|
|
||||||
"oe_odd": 1.92,
|
|
||||||
"oe_even": 1.9,
|
|
||||||
"cards_o": 1.98,
|
|
||||||
"cards_u": 1.84,
|
|
||||||
"hcap_h": 3.3,
|
|
||||||
"hcap_d": 3.7,
|
|
||||||
"hcap_a": 1.93,
|
|
||||||
"htft_11": 3.8,
|
|
||||||
"htft_1x": 5.1,
|
|
||||||
"htft_12": 16.5,
|
|
||||||
"htft_x1": 5.6,
|
|
||||||
"htft_xx": 4.8,
|
|
||||||
"htft_x2": 7.4,
|
|
||||||
"htft_21": 22.0,
|
|
||||||
"htft_2x": 12.0,
|
|
||||||
"htft_22": 6.2,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class V26ShadowEngineTests(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.engine = V26ShadowEngine()
|
|
||||||
self.engine.top_league_ids = {"top1"}
|
|
||||||
self.prediction = _build_prediction()
|
|
||||||
self.quality = {
|
|
||||||
"label": "HIGH",
|
|
||||||
"score": 0.88,
|
|
||||||
"home_lineup_count": 11,
|
|
||||||
"away_lineup_count": 11,
|
|
||||||
"lineup_source": "confirmed_live",
|
|
||||||
"flags": [],
|
|
||||||
}
|
|
||||||
self.v25_signal = {
|
|
||||||
"MS": {"probs": {"1": 0.46, "X": 0.27, "2": 0.27}},
|
|
||||||
"HT": {"probs": {"1": 0.39, "X": 0.41, "2": 0.20}},
|
|
||||||
"HTFT": {"probs": {"1/1": 0.22, "X/X": 0.18, "2/2": 0.14}},
|
|
||||||
"HCAP": {"probs": {"1": 0.21, "X": 0.19, "2": 0.60}},
|
|
||||||
"CARDS": {"probs": {"Under": 0.53, "Over": 0.47}},
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_build_package_exposes_shadow_metadata(self):
|
|
||||||
package = self.engine.build_package(
|
|
||||||
data=_build_data(),
|
|
||||||
prediction=self.prediction,
|
|
||||||
v25_signal=self.v25_signal,
|
|
||||||
quality=self.quality,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(package["model_version"], "v26.shadow.2")
|
|
||||||
self.assertIn("calibration_version", package)
|
|
||||||
self.assertIn("decision_trace_id", package)
|
|
||||||
self.assertIn("market_reliability", package)
|
|
||||||
self.assertTrue(package["bet_summary"])
|
|
||||||
|
|
||||||
def test_cards_defaults_to_pass_when_referee_missing(self):
|
|
||||||
package = self.engine.build_package(
|
|
||||||
data=_build_data(referee_name=None),
|
|
||||||
prediction=self.prediction,
|
|
||||||
v25_signal=self.v25_signal,
|
|
||||||
quality=self.quality,
|
|
||||||
)
|
|
||||||
|
|
||||||
cards = next(item for item in package["bet_summary"] if item["market"] == "CARDS")
|
|
||||||
self.assertFalse(cards["playable"])
|
|
||||||
self.assertEqual(cards["bet_grade"], "PASS")
|
|
||||||
|
|
||||||
def test_select_main_pick_prioritizes_ms_when_playable(self):
|
|
||||||
rows = [
|
|
||||||
{
|
|
||||||
"market": "OU25",
|
|
||||||
"pick": "2.5 Üst",
|
|
||||||
"playable": True,
|
|
||||||
"selection_score": 86.0,
|
|
||||||
"play_score": 83.0,
|
|
||||||
"edge": 0.15,
|
|
||||||
"calibrated_confidence": 72.0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"market": "MS",
|
|
||||||
"pick": "1",
|
|
||||||
"playable": True,
|
|
||||||
"selection_score": 81.0,
|
|
||||||
"play_score": 82.0,
|
|
||||||
"edge": 0.08,
|
|
||||||
"calibrated_confidence": 64.0,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
main_pick = self.engine._select_main_pick(rows)
|
|
||||||
|
|
||||||
self.assertIsNotNone(main_pick)
|
|
||||||
self.assertEqual(main_pick["market"], "MS")
|
|
||||||
self.assertEqual(main_pick["pick_reason"], "ms_priority_market")
|
|
||||||
|
|
||||||
def test_build_package_exposes_surprise_pick_when_reversal_is_hot(self):
|
|
||||||
prediction = _build_prediction()
|
|
||||||
prediction.is_surprise_risk = True
|
|
||||||
prediction.surprise_score = 82.0
|
|
||||||
prediction.surprise_type = "favorite_reversal"
|
|
||||||
v25_signal = dict(self.v25_signal)
|
|
||||||
v25_signal["HTFT"] = {
|
|
||||||
"probs": {
|
|
||||||
"1/2": 0.24,
|
|
||||||
"X/2": 0.14,
|
|
||||||
"1/1": 0.12,
|
|
||||||
"X/X": 0.10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
package = self.engine.build_package(
|
|
||||||
data=_build_data(),
|
|
||||||
prediction=prediction,
|
|
||||||
v25_signal=v25_signal,
|
|
||||||
quality=self.quality,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertIn("surprise_hunter", package)
|
|
||||||
self.assertIn("surprise_pick", package)
|
|
||||||
self.assertTrue(package["surprise_hunter"]["playable"])
|
|
||||||
self.assertEqual(package["surprise_pick"]["market"], "HTFT")
|
|
||||||
self.assertEqual(package["surprise_pick"]["strategy_channel"], "surprise_sidecar")
|
|
||||||
self.assertEqual(package["surprise_hunter"]["strategy_channel"], "surprise_sidecar")
|
|
||||||
self.assertGreaterEqual(package["surprise_pick"]["surprise_score"], 66.0)
|
|
||||||
self.assertEqual(package["main_pick"]["strategy_channel"], "standard")
|
|
||||||
self.assertNotEqual(package["main_pick"].get("strategy_channel"), package["surprise_pick"].get("strategy_channel"))
|
|
||||||
self.assertNotEqual(package["main_pick"].get("pick_reason"), "favorite_reversal_signal")
|
|
||||||
|
|
||||||
def test_top_league_policy_suppresses_early_and_extra_goal_markets(self):
|
|
||||||
package = self.engine.build_package(
|
|
||||||
data=_build_data(league_id="top1"),
|
|
||||||
prediction=self.prediction,
|
|
||||||
v25_signal=self.v25_signal,
|
|
||||||
quality=self.quality,
|
|
||||||
)
|
|
||||||
|
|
||||||
summary = {item["market"]: item for item in package["bet_summary"]}
|
|
||||||
self.assertFalse(summary["HT_OU05"]["playable"])
|
|
||||||
self.assertTrue(
|
|
||||||
"top_league_early_market_suppressed" in summary["HT_OU05"]["reasons"]
|
|
||||||
or "top_league_ht_ou05_over_disabled" in summary["HT_OU05"]["reasons"]
|
|
||||||
)
|
|
||||||
|
|
||||||
playable_goal_cluster = [
|
|
||||||
item for item in package["bet_summary"]
|
|
||||||
if item["market"] in {"OU15", "OU25", "OU35", "BTTS"} and item["playable"]
|
|
||||||
]
|
|
||||||
self.assertLessEqual(len(playable_goal_cluster), 1)
|
|
||||||
|
|
||||||
def test_scoreline_consistency_blocks_conflicting_markets(self):
|
|
||||||
rows = [
|
|
||||||
{
|
|
||||||
"market": "MS",
|
|
||||||
"raw_pick": "1",
|
|
||||||
"pick": "1",
|
|
||||||
"playable": True,
|
|
||||||
"bet_grade": "A",
|
|
||||||
"stake_units": 1.0,
|
|
||||||
"decision_reasons": [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"market": "BTTS",
|
|
||||||
"raw_pick": "Yes",
|
|
||||||
"pick": "KG Var",
|
|
||||||
"playable": True,
|
|
||||||
"bet_grade": "A",
|
|
||||||
"stake_units": 1.0,
|
|
||||||
"decision_reasons": [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"market": "OU25",
|
|
||||||
"raw_pick": "Over",
|
|
||||||
"pick": "2.5 Üst",
|
|
||||||
"playable": True,
|
|
||||||
"bet_grade": "A",
|
|
||||||
"stake_units": 1.0,
|
|
||||||
"decision_reasons": [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"market": "OU25",
|
|
||||||
"raw_pick": "Under",
|
|
||||||
"pick": "2.5 Alt",
|
|
||||||
"playable": True,
|
|
||||||
"bet_grade": "A",
|
|
||||||
"stake_units": 1.0,
|
|
||||||
"decision_reasons": [],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
prediction = _build_prediction()
|
|
||||||
prediction.predicted_ft_score = "1-0"
|
|
||||||
prediction.predicted_ht_score = "1-0"
|
|
||||||
|
|
||||||
controlled = self.engine._apply_scoreline_consistency_controls(rows, prediction)
|
|
||||||
by_market_pick = {(row["market"], row["raw_pick"]): row for row in controlled}
|
|
||||||
|
|
||||||
self.assertTrue(by_market_pick[("MS", "1")]["playable"])
|
|
||||||
self.assertIn(
|
|
||||||
"scoreline_scenario_aligned",
|
|
||||||
by_market_pick[("MS", "1")]["decision_reasons"],
|
|
||||||
)
|
|
||||||
self.assertFalse(by_market_pick[("BTTS", "Yes")]["playable"])
|
|
||||||
self.assertFalse(by_market_pick[("OU25", "Over")]["playable"])
|
|
||||||
self.assertTrue(by_market_pick[("OU25", "Under")]["playable"])
|
|
||||||
self.assertIn(
|
|
||||||
"scoreline_scenario_conflict",
|
|
||||||
by_market_pick[("BTTS", "Yes")]["decision_reasons"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import torch
|
|
||||||
import torch.nn.functional as F
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# Path alignment
|
|
||||||
sys.path.append(os.getcwd())
|
|
||||||
sys.path.append(os.path.join(os.getcwd(), 'ai-engine'))
|
|
||||||
|
|
||||||
from pipeline.tiered_loader import TieredDataLoader
|
|
||||||
from pipeline.sequence_builder import SequenceBuilder
|
|
||||||
from models.hybrid_v11 import HybridDeepModel
|
|
||||||
from features.odds_history import OddsHistoryEngine
|
|
||||||
from features.synthetic_xg import SyntheticXGModel
|
|
||||||
|
|
||||||
DEVICE = 'cpu'
|
|
||||||
MODEL_PATH = 'ai-engine/models/v11_hybrid_model.pth'
|
|
||||||
TARGET_ID = 'en78ih6ec7exnpxcku3xc3das'
|
|
||||||
|
|
||||||
def audit():
|
|
||||||
print(f"🕵️ Auditing Match: {TARGET_ID}")
|
|
||||||
|
|
||||||
# 1. Pipeline Data
|
|
||||||
builder = SequenceBuilder()
|
|
||||||
X, y, meta = builder.build_sequences()
|
|
||||||
|
|
||||||
# Check if target is in dataset
|
|
||||||
idx_list = meta.index[meta['match_id'] == TARGET_ID].tolist()
|
|
||||||
if not idx_list:
|
|
||||||
print("❌ Match not found in generated sequences. Is it too old or too new?")
|
|
||||||
return
|
|
||||||
|
|
||||||
idx = idx_list[0]
|
|
||||||
row_meta = meta.iloc[idx]
|
|
||||||
|
|
||||||
# 2. Features
|
|
||||||
loader = TieredDataLoader()
|
|
||||||
odds_df = loader.load_gold_data([TARGET_ID])
|
|
||||||
eng = OddsHistoryEngine()
|
|
||||||
xg_model = SyntheticXGModel()
|
|
||||||
|
|
||||||
# Team Mapping
|
|
||||||
unique_teams = meta['team_id'].unique()
|
|
||||||
team_map = {tid: i for i, tid in enumerate(unique_teams)}
|
|
||||||
|
|
||||||
# 3. Predict exactly like Backtest
|
|
||||||
state = torch.load(MODEL_PATH, map_location=DEVICE)
|
|
||||||
emb_key = 'entity_emb.weight' if 'entity_emb.weight' in state else 'team_embedding.weight'
|
|
||||||
saved_vocab_size = state[emb_key].shape[0]
|
|
||||||
|
|
||||||
model = HybridDeepModel(num_teams=saved_vocab_size)
|
|
||||||
new_state = {k.replace('team_embedding', 'entity_emb'): v for k, v in state.items()}
|
|
||||||
model.load_state_dict(new_state, strict=False)
|
|
||||||
model.eval()
|
|
||||||
|
|
||||||
# Data components
|
|
||||||
team_idx = team_map.get(row_meta['team_id'], 0)
|
|
||||||
entities = torch.LongTensor([team_idx, 0]).unsqueeze(0)
|
|
||||||
seq = torch.FloatTensor(X[idx]).unsqueeze(0)
|
|
||||||
|
|
||||||
# Context (Odds + xG)
|
|
||||||
odds_lookup = {}
|
|
||||||
for _, r in odds_df.iterrows():
|
|
||||||
mid = r['match_id']
|
|
||||||
if mid not in odds_lookup: odds_lookup[mid] = {}
|
|
||||||
if r['category'] == 'Maç Sonucu': odds_lookup[mid][r['selection']] = r['odd_value']
|
|
||||||
elif r['category'] == '2,5 Alt/Üst':
|
|
||||||
if 'Üst' in r['selection']: odds_lookup[mid]['Over'] = r['odd_value']
|
|
||||||
else: odds_lookup[mid]['Under'] = r['odd_value']
|
|
||||||
|
|
||||||
odds = odds_lookup.get(TARGET_ID, {'1': 1.0, 'X': 1.0, '2': 1.0, 'Over': 1.0, 'Under': 1.0})
|
|
||||||
syn_xg = 1.35 # Placeholder in trainer for xG component if used
|
|
||||||
hist_win_rate = eng.get_feature(row_meta['team_id'], float(odds.get('1', 1.0)))
|
|
||||||
|
|
||||||
ctx = torch.FloatTensor([
|
|
||||||
float(odds.get('1', 1.0)), float(odds.get('X', 1.0)), float(odds.get('2', 1.0)),
|
|
||||||
float(odds.get('Over', 1.0)), float(odds.get('Under', 1.0)),
|
|
||||||
syn_xg, syn_xg,
|
|
||||||
hist_win_rate
|
|
||||||
]).unsqueeze(0)
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
logits_res, pred_goals, logits_btts, logits_ht_ft = model(entities, seq, ctx)
|
|
||||||
probs = F.softmax(logits_res, dim=1).numpy()[0]
|
|
||||||
prob_btts = torch.sigmoid(logits_btts).item()
|
|
||||||
probs_ht = F.softmax(logits_ht_ft, dim=1).numpy()[0]
|
|
||||||
|
|
||||||
print("\n📊 INTERNAL PIPELINE PREDICTION:")
|
|
||||||
print(f"Target Team: {row_meta['team_id']}")
|
|
||||||
print(f"1X2 Probs: Home:{probs[0]:.4f} Draw:{probs[1]:.4f} Away:{probs[2]:.4f}")
|
|
||||||
print(f"BTTS Prob: {prob_btts:.4f}")
|
|
||||||
|
|
||||||
ht_map = ["1/1", "1/X", "1/2", "X/1", "X/X", "X/2", "2/1", "2/X", "2/2"]
|
|
||||||
top3_ht = np.argsort(probs_ht)[-3:][::-1]
|
|
||||||
print("Top 3 HT/FT:")
|
|
||||||
for idx_ht in top3_ht:
|
|
||||||
print(f" {ht_map[idx_ht]}: {probs_ht[idx_ht]:.4f}")
|
|
||||||
|
|
||||||
actual_res = y[idx][0]
|
|
||||||
actual_ht_idx = int(y[idx][3])
|
|
||||||
print(f"\n✅ ACTUAL REALITY:")
|
|
||||||
print(f"Result (Y): {actual_res} (0.0=Away)")
|
|
||||||
print(f"HT/FT Class: {actual_ht_idx} ({ht_map[actual_ht_idx]})")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
audit()
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Test surprise detection on known surprise matches."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, 'ai-engine')
|
|
||||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Test Bayern vs Augsburg (24 Jan 2026) - 1/2 Reversal
|
|
||||||
match_id = 'en78ih6ec7exnpxcku3xc3das'
|
|
||||||
|
|
||||||
orch = SingleMatchOrchestrator()
|
|
||||||
result = orch.analyze_match(match_id)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
print('=== Bayern Munch vs Augsburg (24 Jan 2026) ===')
|
|
||||||
print('Actual: HT 1-0, FT 1-2 (1/2 Reversal!)')
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check risk
|
|
||||||
risk = result.get('risk', {})
|
|
||||||
print(f"Risk Level: {risk.get('level', 'N/A')}")
|
|
||||||
print(f"Is Surprise Risk: {risk.get('is_surprise_risk', False)}")
|
|
||||||
print(f"Surprise Type: {risk.get('surprise_type', 'N/A')}")
|
|
||||||
print(f"Risk Score: {risk.get('score', 'N/A')}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check HT/FT probabilities from market_board
|
|
||||||
htft = result.get('market_board', {}).get('HTFT', {}).get('probs', {})
|
|
||||||
print('HT/FT Probabilities:')
|
|
||||||
if htft:
|
|
||||||
for k, v in sorted(htft.items(), key=lambda x: x[1], reverse=True):
|
|
||||||
print(f" {k}: {v*100:.1f}%")
|
|
||||||
else:
|
|
||||||
print(" EMPTY!")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check main pick
|
|
||||||
main = result.get('main_pick', {})
|
|
||||||
print(f"Main Pick: {main.get('market', 'N/A')} - {main.get('pick', 'N/A')}")
|
|
||||||
print(f"Confidence: {main.get('calibrated_confidence', 'N/A')}%")
|
|
||||||
print(f"Is Guaranteed: {main.get('is_guaranteed', False)}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check aggressive pick
|
|
||||||
agg = result.get('aggressive_pick', {})
|
|
||||||
if agg:
|
|
||||||
print(f"Aggressive Pick: {agg.get('market', 'N/A')} - {agg.get('pick', 'N/A')}")
|
|
||||||
print(f"Odds: {agg.get('odds', 'N/A')}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check bet_summary for HTFT
|
|
||||||
bet_summary = result.get('bet_summary', [])
|
|
||||||
for bet in bet_summary:
|
|
||||||
if bet.get('market') == 'HTFT':
|
|
||||||
print(f"HTFT Bet: {bet}")
|
|
||||||
else:
|
|
||||||
print('Match not found')
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Test the improved surprise detection logic"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, 'ai-engine')
|
|
||||||
|
|
||||||
from core.calculators.risk_assessor import RiskAssessor
|
|
||||||
from config.config_loader import get_config
|
|
||||||
|
|
||||||
def test_surprise_detection():
|
|
||||||
config = get_config()
|
|
||||||
assessor = RiskAssessor(config)
|
|
||||||
|
|
||||||
# Test cases based on real scenarios
|
|
||||||
test_cases = [
|
|
||||||
{
|
|
||||||
'name': 'Bayern vs Augsburg (1.30 odds, 2% 1/2 prob)',
|
|
||||||
'odds': {'ms_h': 1.30, 'ms_d': 5.00, 'ms_a': 8.00},
|
|
||||||
'ht_ft': {'1/1': 0.30, '1/X': 0.07, '1/2': 0.02, 'X/1': 0.15, 'X/X': 0.16, 'X/2': 0.09, '2/1': 0.03, '2/X': 0.04, '2/2': 0.14},
|
|
||||||
'expected_surprise': True,
|
|
||||||
'expected_type': '1/2 Potential Upset'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Strong favorite (1.20 odds, 1.5% 1/2 prob)',
|
|
||||||
'odds': {'ms_h': 1.20, 'ms_d': 6.00, 'ms_a': 12.00},
|
|
||||||
'ht_ft': {'1/1': 0.35, '1/X': 0.05, '1/2': 0.015, 'X/1': 0.20, 'X/X': 0.15, 'X/2': 0.05, '2/1': 0.02, '2/X': 0.03, '2/2': 0.10},
|
|
||||||
'expected_surprise': True,
|
|
||||||
'expected_type': '1/2 Potential Upset'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Moderate favorite (1.50 odds, 3% 1/2 prob)',
|
|
||||||
'odds': {'ms_h': 1.50, 'ms_d': 4.00, 'ms_a': 6.00},
|
|
||||||
'ht_ft': {'1/1': 0.28, '1/X': 0.08, '1/2': 0.03, 'X/1': 0.18, 'X/X': 0.15, 'X/2': 0.08, '2/1': 0.04, '2/X': 0.05, '2/2': 0.11},
|
|
||||||
'expected_surprise': True,
|
|
||||||
'expected_type': '1/2 Potential Upset'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Even match (2.00 odds, 5% 1/2 prob)',
|
|
||||||
'odds': {'ms_h': 2.00, 'ms_d': 3.30, 'ms_a': 3.30},
|
|
||||||
'ht_ft': {'1/1': 0.20, '1/X': 0.10, '1/2': 0.05, 'X/1': 0.15, 'X/X': 0.15, 'X/2': 0.10, '2/1': 0.05, '2/X': 0.10, '2/2': 0.10},
|
|
||||||
'expected_surprise': False, # No clear favorite
|
|
||||||
'expected_type': None
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Away favorite (1.40 away odds, 2% 2/1 prob)',
|
|
||||||
'odds': {'ms_h': 6.00, 'ms_d': 4.00, 'ms_a': 1.40},
|
|
||||||
'ht_ft': {'1/1': 0.10, '1/X': 0.05, '1/2': 0.04, 'X/1': 0.08, 'X/X': 0.15, 'X/2': 0.20, '2/1': 0.02, '2/X': 0.06, '2/2': 0.30},
|
|
||||||
'expected_surprise': True,
|
|
||||||
'expected_type': '2/1 Potential Upset'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
print("=" * 70)
|
|
||||||
print("SURPRISE DETECTION TEST RESULTS")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
passed = 0
|
|
||||||
failed = 0
|
|
||||||
|
|
||||||
for tc in test_cases:
|
|
||||||
class MockCtx:
|
|
||||||
is_surprise = False
|
|
||||||
is_top_league = True
|
|
||||||
sport = 'football'
|
|
||||||
xgboost_preds = {'ht_ft': tc['ht_ft']}
|
|
||||||
odds_data = tc['odds']
|
|
||||||
|
|
||||||
result = assessor.assess_risk(MockCtx())
|
|
||||||
|
|
||||||
# Check if result matches expectation
|
|
||||||
is_correct = result.is_surprise_risk == tc['expected_surprise']
|
|
||||||
if tc['expected_type'] and result.surprise_type != tc['expected_type']:
|
|
||||||
is_correct = False
|
|
||||||
|
|
||||||
status = "✅ PASS" if is_correct else "❌ FAIL"
|
|
||||||
if is_correct:
|
|
||||||
passed += 1
|
|
||||||
else:
|
|
||||||
failed += 1
|
|
||||||
|
|
||||||
print(f"\n{status} - {tc['name']}")
|
|
||||||
print(f" Expected: surprise={tc['expected_surprise']}, type={tc['expected_type']}")
|
|
||||||
print(f" Got: surprise={result.is_surprise_risk}, type={result.surprise_type}")
|
|
||||||
if result.reasons:
|
|
||||||
print(f" Reasons: {result.reasons}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
print(f"SUMMARY: {passed} passed, {failed} failed")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
return failed == 0
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
success = test_surprise_detection()
|
|
||||||
sys.exit(0 if success else 1)
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Test UpsetEngine on Bayern vs Augsburg match."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, 'ai-engine')
|
|
||||||
from features.upset_engine import get_upset_engine
|
|
||||||
from data.db import get_clean_dsn
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Get match data
|
|
||||||
conn = psycopg2.connect(get_clean_dsn())
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
|
||||||
m.ht_score_home, m.ht_score_away, m.mst_utc,
|
|
||||||
th.name as home_name, ta.name as away_name, l.name as league
|
|
||||||
FROM matches m
|
|
||||||
JOIN teams th ON m.home_team_id = th.id
|
|
||||||
JOIN teams ta ON m.away_team_id = ta.id
|
|
||||||
JOIN leagues l ON m.league_id = l.id
|
|
||||||
WHERE m.id = 'en78ih6ec7exnpxcku3xc3das'
|
|
||||||
""")
|
|
||||||
match = cur.fetchone()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if match:
|
|
||||||
print('=== Bayern Munch vs Augsburg (24 Jan 2026) ===')
|
|
||||||
print(f"Actual: HT {match['ht_score_home']}-{match['ht_score_away']}, FT {match['score_home']}-{match['score_away']} (1/2 Reversal!)")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Test UpsetEngine
|
|
||||||
engine = get_upset_engine()
|
|
||||||
|
|
||||||
# Calculate upset potential using get_features
|
|
||||||
result = engine.get_features(
|
|
||||||
home_team_name=match['home_name'],
|
|
||||||
home_team_id=match['home_team_id'],
|
|
||||||
away_team_name=match['away_name'],
|
|
||||||
league_name=match['league'],
|
|
||||||
home_position=1, # Bayern is typically top
|
|
||||||
away_position=15, # Augsburg is typically lower
|
|
||||||
match_date_ms=match['mst_utc'],
|
|
||||||
total_teams=18,
|
|
||||||
)
|
|
||||||
|
|
||||||
print('UpsetEngine Results:')
|
|
||||||
print(f" Atmosphere Score: {result.get('upset_atmosphere', 0):.2f}")
|
|
||||||
print(f" Motivation Score: {result.get('upset_motivation', 0):.2f}")
|
|
||||||
print(f" Fatigue Score: {result.get('upset_fatigue', 0):.2f}")
|
|
||||||
print(f" Historical Score: {result.get('upset_historical', 0):.2f}")
|
|
||||||
print(f" TOTAL UPSET POTENTIAL: {result.get('upset_potential', 0):.2f}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check if upset was detected
|
|
||||||
if result.get('upset_potential', 0) > 0.5:
|
|
||||||
print("🔥 HIGH UPSET POTENTIAL DETECTED!")
|
|
||||||
elif result.get('upset_potential', 0) > 0.3:
|
|
||||||
print("⚠️ MEDIUM UPSET POTENTIAL")
|
|
||||||
else:
|
|
||||||
print("❌ LOW UPSET POTENTIAL - Model did not detect this as upset")
|
|
||||||
else:
|
|
||||||
print('Match not found')
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import { AppController } from "./app.controller";
|
|
||||||
import { AppService } from "./app.service";
|
|
||||||
|
|
||||||
describe("AppController", () => {
|
|
||||||
let appController: AppController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const app: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [AppController],
|
|
||||||
providers: [AppService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
appController = app.get<AppController>(AppController);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("root", () => {
|
|
||||||
it('should return "Hello World!"', () => {
|
|
||||||
expect(appController.getHello()).toBe("Hello World!");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -51,6 +51,7 @@ async function bootstrap() {
|
|||||||
"https://suggestbet.bilgich.com",
|
"https://suggestbet.bilgich.com",
|
||||||
"https://iddaai.com",
|
"https://iddaai.com",
|
||||||
"https://www.iddaai.com",
|
"https://www.iddaai.com",
|
||||||
|
"http://localhost:6195",
|
||||||
]
|
]
|
||||||
: true,
|
: true,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
|
||||||
import axios from "axios";
|
|
||||||
import { PredictionJobType } from "./predictions.types";
|
|
||||||
import { PredictionsProcessor } from "./predictions.processor";
|
|
||||||
|
|
||||||
jest.mock("axios");
|
|
||||||
|
|
||||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
||||||
|
|
||||||
describe("PredictionsProcessor", () => {
|
|
||||||
let processor: PredictionsProcessor;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
process.env.AI_ENGINE_URL = "http://unit-ai:8000";
|
|
||||||
processor = new PredictionsProcessor();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
delete process.env.AI_ENGINE_URL;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("posts to analyze endpoint for predict-match jobs", async () => {
|
|
||||||
mockedAxios.post.mockResolvedValueOnce({ data: { ok: true } } as any);
|
|
||||||
|
|
||||||
const job = {
|
|
||||||
id: "j1",
|
|
||||||
name: PredictionJobType.PREDICT_MATCH,
|
|
||||||
data: { matchId: "match-123" },
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const result = await processor.process(job);
|
|
||||||
|
|
||||||
expect(result).toEqual({ ok: true });
|
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
|
||||||
"http://unit-ai:8000/v20plus/analyze/match-123",
|
|
||||||
{},
|
|
||||||
{ timeout: 30000 },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("posts mapped payload to coupon endpoint for smart-coupon jobs", async () => {
|
|
||||||
mockedAxios.post.mockResolvedValueOnce({ data: { bets: [] } } as any);
|
|
||||||
|
|
||||||
const job = {
|
|
||||||
id: "j2",
|
|
||||||
name: PredictionJobType.SMART_COUPON,
|
|
||||||
data: {
|
|
||||||
matchIds: ["m1", "m2"],
|
|
||||||
strategy: "BALANCED",
|
|
||||||
options: { maxMatches: 4, minConfidence: 65 },
|
|
||||||
},
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const result = await processor.process(job);
|
|
||||||
|
|
||||||
expect(result).toEqual({ bets: [] });
|
|
||||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
|
||||||
"http://unit-ai:8000/v20plus/coupon",
|
|
||||||
{
|
|
||||||
match_ids: ["m1", "m2"],
|
|
||||||
strategy: "BALANCED",
|
|
||||||
max_matches: 4,
|
|
||||||
min_confidence: 65,
|
|
||||||
},
|
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws for unknown job type", async () => {
|
|
||||||
const job = {
|
|
||||||
id: "j3",
|
|
||||||
name: "unknown-job",
|
|
||||||
data: {},
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
await expect(processor.process(job)).rejects.toThrow(
|
|
||||||
"Unknown job type: unknown-job",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
/**
|
|
||||||
* ===================================================
|
|
||||||
* BACKTEST ACCURACY — V30 Prediction System
|
|
||||||
* ===================================================
|
|
||||||
* Tests historical predictions against actual outcomes.
|
|
||||||
* Uses the running AI Engine's /v20plus/analyze/{match_id}
|
|
||||||
* endpoint which extracts features from DB internally.
|
|
||||||
*
|
|
||||||
* Usage: npx ts-node --transpile-only -r tsconfig-paths/register src/scripts/backtest-accuracy.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
// Configuration
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
const AI_ENGINE_URL = process.env.AI_ENGINE_URL || "http://127.0.0.1:3005";
|
|
||||||
const CONCURRENT_REQUESTS = 5;
|
|
||||||
const MAX_MATCHES = 1000;
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
// Types
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
interface TestMatch {
|
|
||||||
id: string;
|
|
||||||
scoreHome: number;
|
|
||||||
scoreAway: number;
|
|
||||||
htScoreHome: number | null;
|
|
||||||
htScoreAway: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BacktestResult {
|
|
||||||
matchId: string;
|
|
||||||
actual: { ms: string; ou25: string; btts: string; htft: string };
|
|
||||||
predicted: { ms: string; ou25: string; btts: string };
|
|
||||||
probabilities: {
|
|
||||||
home: number;
|
|
||||||
draw: number;
|
|
||||||
away: number;
|
|
||||||
over: number;
|
|
||||||
under: number;
|
|
||||||
bttsYes: number;
|
|
||||||
bttsNo: number;
|
|
||||||
};
|
|
||||||
mainPickCorrect: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
// Helpers
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
function determineActualOutcome(
|
|
||||||
scoreHome: number,
|
|
||||||
scoreAway: number,
|
|
||||||
htScoreHome: number | null,
|
|
||||||
htScoreAway: number | null,
|
|
||||||
): { ms: string; ou25: string; btts: string; htft: string } {
|
|
||||||
const ms = scoreHome > scoreAway ? "1" : scoreHome < scoreAway ? "2" : "X";
|
|
||||||
const ou25 = scoreHome + scoreAway > 2.5 ? "Over" : "Under";
|
|
||||||
const btts = scoreHome > 0 && scoreAway > 0 ? "Yes" : "No";
|
|
||||||
|
|
||||||
let htft = "unknown";
|
|
||||||
if (htScoreHome !== null && htScoreAway !== null) {
|
|
||||||
const htResult =
|
|
||||||
htScoreHome > htScoreAway ? "1" : htScoreHome < htScoreAway ? "2" : "X";
|
|
||||||
htft = `${htResult}/${ms}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ms, ou25, btts, htft };
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractPrediction(response: unknown): {
|
|
||||||
ms: string;
|
|
||||||
ou25: string;
|
|
||||||
btts: string;
|
|
||||||
probs: BacktestResult["probabilities"];
|
|
||||||
mainPick: string;
|
|
||||||
mainMarket: string;
|
|
||||||
} {
|
|
||||||
const data = response as Record<string, unknown>;
|
|
||||||
const predictions = data?.predictions as Record<string, unknown> | undefined;
|
|
||||||
|
|
||||||
const mainPickObj = data?.main_pick as Record<string, unknown> | undefined;
|
|
||||||
const mainPick =
|
|
||||||
typeof mainPickObj?.pick === "string" ? mainPickObj.pick : "";
|
|
||||||
const mainMarket =
|
|
||||||
typeof mainPickObj?.market === "string" ? mainPickObj.market : "";
|
|
||||||
|
|
||||||
// Extract MS from probabilities or main pick
|
|
||||||
const msProbs = (predictions?.ms || data?.ms || {}) as Record<
|
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
const homeProb =
|
|
||||||
typeof msProbs["1"] === "number"
|
|
||||||
? msProbs["1"]
|
|
||||||
: typeof msProbs.home_prob === "number"
|
|
||||||
? msProbs.home_prob
|
|
||||||
: 0;
|
|
||||||
const drawProb =
|
|
||||||
typeof msProbs["X"] === "number"
|
|
||||||
? msProbs["X"]
|
|
||||||
: typeof msProbs.draw_prob === "number"
|
|
||||||
? msProbs.draw_prob
|
|
||||||
: 0;
|
|
||||||
const awayProb =
|
|
||||||
typeof msProbs["2"] === "number"
|
|
||||||
? msProbs["2"]
|
|
||||||
: typeof msProbs.away_prob === "number"
|
|
||||||
? msProbs.away_prob
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
let ms = "1";
|
|
||||||
if (drawProb > homeProb && drawProb > awayProb) ms = "X";
|
|
||||||
else if (awayProb > homeProb) ms = "2";
|
|
||||||
|
|
||||||
// Extract OU25
|
|
||||||
const ou25Probs = (predictions?.ou25 || data?.ou25 || {}) as Record<
|
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
const overProb =
|
|
||||||
typeof ou25Probs.Over === "number"
|
|
||||||
? ou25Probs.Over
|
|
||||||
: typeof ou25Probs.over_prob === "number"
|
|
||||||
? ou25Probs.over_prob
|
|
||||||
: 0;
|
|
||||||
const underProb =
|
|
||||||
typeof ou25Probs.Under === "number"
|
|
||||||
? ou25Probs.Under
|
|
||||||
: typeof ou25Probs.under_prob === "number"
|
|
||||||
? ou25Probs.under_prob
|
|
||||||
: 0;
|
|
||||||
const ou25 = overProb > underProb ? "Over" : "Under";
|
|
||||||
|
|
||||||
// Extract BTTS
|
|
||||||
const bttsProbs = (predictions?.btts || data?.btts || {}) as Record<
|
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
const bttsYes =
|
|
||||||
typeof bttsProbs.Yes === "number"
|
|
||||||
? bttsProbs.Yes
|
|
||||||
: typeof bttsProbs.yes_prob === "number"
|
|
||||||
? bttsProbs.yes_prob
|
|
||||||
: 0;
|
|
||||||
const bttsNo =
|
|
||||||
typeof bttsProbs.No === "number"
|
|
||||||
? bttsProbs.No
|
|
||||||
: typeof bttsProbs.no_prob === "number"
|
|
||||||
? bttsProbs.no_prob
|
|
||||||
: 0;
|
|
||||||
const btts = bttsYes > bttsNo ? "Yes" : "No";
|
|
||||||
|
|
||||||
return {
|
|
||||||
ms,
|
|
||||||
ou25,
|
|
||||||
btts,
|
|
||||||
probs: {
|
|
||||||
home: homeProb,
|
|
||||||
draw: drawProb,
|
|
||||||
away: awayProb,
|
|
||||||
over: overProb,
|
|
||||||
under: underProb,
|
|
||||||
bttsYes,
|
|
||||||
bttsNo,
|
|
||||||
},
|
|
||||||
mainPick,
|
|
||||||
mainMarket,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processBatch(batch: TestMatch[]): Promise<BacktestResult[]> {
|
|
||||||
const results: BacktestResult[] = [];
|
|
||||||
|
|
||||||
const promises = batch.map(async (match) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(
|
|
||||||
`${AI_ENGINE_URL}/v20plus/analyze/${match.id}`,
|
|
||||||
{},
|
|
||||||
{ timeout: 15000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const actual = determineActualOutcome(
|
|
||||||
match.scoreHome,
|
|
||||||
match.scoreAway,
|
|
||||||
match.htScoreHome,
|
|
||||||
match.htScoreAway,
|
|
||||||
);
|
|
||||||
|
|
||||||
const pred = extractPrediction(response.data);
|
|
||||||
|
|
||||||
// Check main pick
|
|
||||||
let mainPickCorrect = false;
|
|
||||||
if (pred.mainMarket === "MS") {
|
|
||||||
mainPickCorrect = pred.mainPick === actual.ms;
|
|
||||||
} else if (pred.mainMarket === "OU25") {
|
|
||||||
mainPickCorrect = pred.mainPick === actual.ou25;
|
|
||||||
} else if (pred.mainMarket === "BTTS") {
|
|
||||||
mainPickCorrect = pred.mainPick === actual.btts;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
matchId: match.id,
|
|
||||||
actual,
|
|
||||||
predicted: { ms: pred.ms, ou25: pred.ou25, btts: pred.btts },
|
|
||||||
probabilities: pred.probs,
|
|
||||||
mainPickCorrect,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Skip failed matches silently
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
// Main Backtest
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
async function runBacktest(): Promise<void> {
|
|
||||||
console.log("🎯 BACKTEST ACCURACY — V30 Betting Engine");
|
|
||||||
console.log("════════════════════════════════════════════════════════");
|
|
||||||
|
|
||||||
// 1. Health check
|
|
||||||
try {
|
|
||||||
const health = await axios.get(`${AI_ENGINE_URL}/health`, {
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
console.log(`✅ AI Engine: ${JSON.stringify(health.data)}`);
|
|
||||||
} catch {
|
|
||||||
console.error("❌ AI Engine not reachable at", AI_ENGINE_URL);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Load finished matches with features
|
|
||||||
console.log("\n📥 Loading test matches...");
|
|
||||||
const matches = await prisma.$queryRaw<TestMatch[]>`
|
|
||||||
SELECT m.id, m.score_home AS "scoreHome", m.score_away AS "scoreAway",
|
|
||||||
m.ht_score_home AS "htScoreHome", m.ht_score_away AS "htScoreAway"
|
|
||||||
FROM matches m
|
|
||||||
JOIN match_ai_features maf ON maf.match_id = m.id
|
|
||||||
WHERE m.status = 'FT'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND m.score_away IS NOT NULL
|
|
||||||
AND m.sport = 'football'
|
|
||||||
AND maf.home_elo != 1500
|
|
||||||
AND maf.implied_home != 0.33
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT ${MAX_MATCHES}
|
|
||||||
`;
|
|
||||||
console.log(` 📊 Test matches: ${matches.length}`);
|
|
||||||
|
|
||||||
// 3. Run predictions in batches
|
|
||||||
console.log("\n🤖 Running predictions...");
|
|
||||||
const allResults: BacktestResult[] = [];
|
|
||||||
let processed = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < matches.length; i += CONCURRENT_REQUESTS) {
|
|
||||||
const batch = matches.slice(i, i + CONCURRENT_REQUESTS);
|
|
||||||
const batchResults = await processBatch(batch);
|
|
||||||
allResults.push(...batchResults);
|
|
||||||
processed += batch.length;
|
|
||||||
|
|
||||||
if (processed % 50 === 0 || processed === matches.length) {
|
|
||||||
const currentMsAcc =
|
|
||||||
allResults.length > 0
|
|
||||||
? (
|
|
||||||
(allResults.filter((r) => r.predicted.ms === r.actual.ms).length /
|
|
||||||
allResults.length) *
|
|
||||||
100
|
|
||||||
).toFixed(1)
|
|
||||||
: "0";
|
|
||||||
console.log(
|
|
||||||
` 📊 ${processed}/${matches.length} — Success: ${allResults.length} — MS Acc: ${currentMsAcc}%`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Calculate metrics
|
|
||||||
const total = allResults.length;
|
|
||||||
if (total === 0) {
|
|
||||||
console.error("❌ No results to analyze");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const msCorrect = allResults.filter(
|
|
||||||
(r) => r.predicted.ms === r.actual.ms,
|
|
||||||
).length;
|
|
||||||
const ou25Correct = allResults.filter(
|
|
||||||
(r) => r.predicted.ou25 === r.actual.ou25,
|
|
||||||
).length;
|
|
||||||
const bttsCorrect = allResults.filter(
|
|
||||||
(r) => r.predicted.btts === r.actual.btts,
|
|
||||||
).length;
|
|
||||||
const mainPickCorrect = allResults.filter((r) => r.mainPickCorrect).length;
|
|
||||||
|
|
||||||
// Actual distribution
|
|
||||||
const actHome = allResults.filter((r) => r.actual.ms === "1").length;
|
|
||||||
const actDraw = allResults.filter((r) => r.actual.ms === "X").length;
|
|
||||||
const actAway = allResults.filter((r) => r.actual.ms === "2").length;
|
|
||||||
|
|
||||||
// Predicted distribution
|
|
||||||
const predHome = allResults.filter((r) => r.predicted.ms === "1").length;
|
|
||||||
const predDraw = allResults.filter((r) => r.predicted.ms === "X").length;
|
|
||||||
const predAway = allResults.filter((r) => r.predicted.ms === "2").length;
|
|
||||||
|
|
||||||
// Confidence calibration (based on max probability)
|
|
||||||
const buckets: Record<string, { correct: number; total: number }> = {
|
|
||||||
"33-40%": { correct: 0, total: 0 },
|
|
||||||
"40-50%": { correct: 0, total: 0 },
|
|
||||||
"50-60%": { correct: 0, total: 0 },
|
|
||||||
"60-70%": { correct: 0, total: 0 },
|
|
||||||
"70%+": { correct: 0, total: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const r of allResults) {
|
|
||||||
const maxProb = Math.max(
|
|
||||||
r.probabilities.home,
|
|
||||||
r.probabilities.draw,
|
|
||||||
r.probabilities.away,
|
|
||||||
);
|
|
||||||
const key =
|
|
||||||
maxProb >= 0.7
|
|
||||||
? "70%+"
|
|
||||||
: maxProb >= 0.6
|
|
||||||
? "60-70%"
|
|
||||||
: maxProb >= 0.5
|
|
||||||
? "50-60%"
|
|
||||||
: maxProb >= 0.4
|
|
||||||
? "40-50%"
|
|
||||||
: "33-40%";
|
|
||||||
buckets[key].total++;
|
|
||||||
if (r.predicted.ms === r.actual.ms) buckets[key].correct++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Print Report
|
|
||||||
console.log("\n════════════════════════════════════════════════════════");
|
|
||||||
console.log("📊 BACKTEST ACCURACY REPORT");
|
|
||||||
console.log("════════════════════════════════════════════════════════");
|
|
||||||
console.log(` Total Matches Analyzed: ${total}`);
|
|
||||||
console.log("");
|
|
||||||
console.log(" 🎯 Market Accuracy:");
|
|
||||||
console.log(
|
|
||||||
` ⚽ Match Result (MS): ${((msCorrect / total) * 100).toFixed(2)}% (${msCorrect}/${total})`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` 📈 Over/Under 2.5: ${((ou25Correct / total) * 100).toFixed(2)}% (${ou25Correct}/${total})`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` 🤝 Both Teams Score: ${((bttsCorrect / total) * 100).toFixed(2)}% (${bttsCorrect}/${total})`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` 🏆 Main Pick Success: ${((mainPickCorrect / total) * 100).toFixed(2)}% (${mainPickCorrect}/${total})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("\n 📊 MS Distribution:");
|
|
||||||
console.log(
|
|
||||||
` Actual: 1: ${actHome} (${((actHome / total) * 100).toFixed(1)}%) | X: ${actDraw} (${((actDraw / total) * 100).toFixed(1)}%) | 2: ${actAway} (${((actAway / total) * 100).toFixed(1)}%)`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Predicted: 1: ${predHome} (${((predHome / total) * 100).toFixed(1)}%) | X: ${predDraw} (${((predDraw / total) * 100).toFixed(1)}%) | 2: ${predAway} (${((predAway / total) * 100).toFixed(1)}%)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("\n 📊 Confidence Calibration:");
|
|
||||||
for (const [range, bucket] of Object.entries(buckets)) {
|
|
||||||
if (bucket.total === 0) continue;
|
|
||||||
const acc = (bucket.correct / bucket.total) * 100;
|
|
||||||
const bar = "█".repeat(Math.round(acc / 3));
|
|
||||||
console.log(
|
|
||||||
` ${range.padEnd(8)} : ${acc.toFixed(1)}% acc (n=${bucket.total}) ${bar}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Per-market deep dive
|
|
||||||
console.log("\n 📊 OU25 Breakdown:");
|
|
||||||
const actOver = allResults.filter((r) => r.actual.ou25 === "Over").length;
|
|
||||||
const actUnder = total - actOver;
|
|
||||||
const predOver = allResults.filter((r) => r.predicted.ou25 === "Over").length;
|
|
||||||
const predUnder = total - predOver;
|
|
||||||
console.log(
|
|
||||||
` Actual: Over: ${actOver} (${((actOver / total) * 100).toFixed(1)}%) | Under: ${actUnder} (${((actUnder / total) * 100).toFixed(1)}%)`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Predicted: Over: ${predOver} (${((predOver / total) * 100).toFixed(1)}%) | Under: ${predUnder} (${((predUnder / total) * 100).toFixed(1)}%)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("\n 📊 BTTS Breakdown:");
|
|
||||||
const actBttsYes = allResults.filter((r) => r.actual.btts === "Yes").length;
|
|
||||||
const actBttsNo = total - actBttsYes;
|
|
||||||
const predBttsYes = allResults.filter(
|
|
||||||
(r) => r.predicted.btts === "Yes",
|
|
||||||
).length;
|
|
||||||
const predBttsNo = total - predBttsYes;
|
|
||||||
console.log(
|
|
||||||
` Actual: Yes: ${actBttsYes} (${((actBttsYes / total) * 100).toFixed(1)}%) | No: ${actBttsNo} (${((actBttsNo / total) * 100).toFixed(1)}%)`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Predicted: Yes: ${predBttsYes} (${((predBttsYes / total) * 100).toFixed(1)}%) | No: ${predBttsNo} (${((predBttsNo / total) * 100).toFixed(1)}%)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("════════════════════════════════════════════════════════");
|
|
||||||
console.log("✅ Backtest complete!");
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
runBacktest().catch((err: unknown) => {
|
|
||||||
console.error("❌ Backtest failed:", err);
|
|
||||||
void prisma.$disconnect();
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { FeederService } from "../modules/feeder/feeder.service";
|
|
||||||
import { HistoricalResultsSyncTask } from "./historical-results-sync.task";
|
|
||||||
|
|
||||||
describe("HistoricalResultsSyncTask", () => {
|
|
||||||
const runPreviousDayCompletedMatchesScan = jest.fn();
|
|
||||||
let task: HistoricalResultsSyncTask;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
delete process.env.FEEDER_MODE;
|
|
||||||
|
|
||||||
task = new HistoricalResultsSyncTask({
|
|
||||||
runPreviousDayCompletedMatchesScan,
|
|
||||||
} as unknown as FeederService);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
delete process.env.FEEDER_MODE;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls feeder service in normal mode", async () => {
|
|
||||||
await task.syncPreviousDayCompletedMatches();
|
|
||||||
|
|
||||||
expect(runPreviousDayCompletedMatchesScan).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips execution in historical feeder mode", async () => {
|
|
||||||
process.env.FEEDER_MODE = "historical";
|
|
||||||
|
|
||||||
await task.syncPreviousDayCompletedMatches();
|
|
||||||
|
|
||||||
expect(runPreviousDayCompletedMatchesScan).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
match_id = '7cnm7h7qbsq2bbaxngusojh90'
|
|
||||||
url = f'http://localhost:8007/v20plus/analyze/{match_id}'
|
|
||||||
|
|
||||||
print(f"🔮 Sending prediction request for: {match_id}")
|
|
||||||
print(f"URL: {url}\n")
|
|
||||||
|
|
||||||
response = requests.post(url)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
print("📊 DATA QUALITY:")
|
|
||||||
print(json.dumps(data.get('data_quality', {}), indent=2))
|
|
||||||
|
|
||||||
print("\n🎯 MAIN PICK:")
|
|
||||||
print(json.dumps(data.get('main_pick', {}), indent=2))
|
|
||||||
|
|
||||||
print("\n⚽ SCORE PREDICTION:")
|
|
||||||
print(json.dumps(data.get('score_prediction', {}), indent=2))
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import { INestApplication } from "@nestjs/common";
|
|
||||||
import request from "supertest";
|
|
||||||
import { App } from "supertest/types";
|
|
||||||
import { AppModule } from "./../src/app.module";
|
|
||||||
|
|
||||||
describe("AppController (e2e)", () => {
|
|
||||||
let app: INestApplication<App>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AppModule],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
|
||||||
await app.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("/ (GET)", () => {
|
|
||||||
return request(app.getHttpServer())
|
|
||||||
.get("/")
|
|
||||||
.expect(200)
|
|
||||||
.expect("Hello World!");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
|
||||||
"rootDir": ".",
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testRegex": ".e2e-spec.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user