Merge pull request 'v26-shadow' (#4) from v26-shadow into main
Deploy Iddaai Backend / build-and-deploy (push) Successful in 27s

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-04-24 01:15:54 +03:00
48 changed files with 1 additions and 434106 deletions
Binary file not shown.
-871
View File
@@ -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 iter Logloss
2 0 0.6924099937
3 1 0.6916660956
4 2 0.691108145
5 3 0.6904585078
6 4 0.689812816
7 5 0.689192261
8 6 0.6886032715
9 7 0.6880706742
10 8 0.6876192378
11 9 0.6870868859
12 10 0.6865493528
13 11 0.686105086
14 12 0.6856345086
15 13 0.6852027185
16 14 0.6848238481
17 15 0.6844045699
18 16 0.6840077621
19 17 0.6836197496
20 18 0.6832475033
21 19 0.6829012069
22 20 0.6825880966
23 21 0.6822424968
24 22 0.6819180513
25 23 0.6816384467
26 24 0.6813262593
27 25 0.6810353411
28 26 0.6808138172
29 27 0.6805550049
30 28 0.680347991
31 29 0.680089679
32 30 0.6798451919
33 31 0.6796090443
34 32 0.6793890865
35 33 0.6791683772
36 34 0.6789766369
37 35 0.6787930242
38 36 0.6786087714
39 37 0.6784161299
40 38 0.6782227897
41 39 0.6780242369
42 40 0.6778499631
43 41 0.6776975784
44 42 0.6775231674
45 43 0.6773582124
46 44 0.6772234666
47 45 0.6770659843
48 46 0.6769049529
49 47 0.6767664194
50 48 0.6766584917
51 49 0.6765507257
52 50 0.6764489911
53 51 0.6763947956
54 52 0.6762778712
55 53 0.6761865366
56 54 0.6760679685
57 55 0.6759774874
58 56 0.6758500622
59 57 0.6757625065
60 58 0.6756876412
61 59 0.6756151069
62 60 0.6755303655
63 61 0.6754565036
64 62 0.6753738983
65 63 0.6752897299
66 64 0.6752115539
67 65 0.6751595431
68 66 0.6750764658
69 67 0.6750179194
70 68 0.6749408803
71 69 0.6748795802
72 70 0.674790372
73 71 0.6747239773
74 72 0.6746701254
75 73 0.6746120937
76 74 0.6745550085
77 75 0.6744855074
78 76 0.6744264172
79 77 0.674381715
80 78 0.6743331681
81 79 0.67428564
82 80 0.6742202413
83 81 0.6741620971
84 82 0.6741109453
85 83 0.6740556003
86 84 0.6740146772
87 85 0.673983295
88 86 0.6739595301
89 87 0.6739336659
90 88 0.673890361
91 89 0.673863586
92 90 0.6738190616
93 91 0.6737799295
94 92 0.6737364374
95 93 0.6737093719
96 94 0.6736630475
97 95 0.67364367
98 96 0.6735998081
99 97 0.6735526984
100 98 0.6735012924
101 99 0.6734818024
102 100 0.6734379341
103 101 0.6734059869
104 102 0.6733740852
105 103 0.6733330971
106 104 0.6733060254
107 105 0.6732755898
108 106 0.6732294722
109 107 0.6732035176
110 108 0.673196437
111 109 0.6731652709
112 110 0.673138808
113 111 0.6731062725
114 112 0.6730726625
115 113 0.6730285927
116 114 0.6729872702
117 115 0.6729721425
118 116 0.6729564624
119 117 0.6729312424
120 118 0.6729354345
121 119 0.6729085401
122 120 0.6728898322
123 121 0.6728773638
124 122 0.6728618874
125 123 0.6728540413
126 124 0.6728441291
127 125 0.672815631
128 126 0.6728082021
129 127 0.6727900064
130 128 0.6727649552
131 129 0.6727467657
132 130 0.6727396032
133 131 0.6727245271
134 132 0.6726955143
135 133 0.67269209
136 134 0.672677932
137 135 0.6726540285
138 136 0.6726288583
139 137 0.6725863431
140 138 0.6725837967
141 139 0.6725772977
142 140 0.6725685594
143 141 0.6725553829
144 142 0.6725484347
145 143 0.6725306172
146 144 0.672543149
147 145 0.6725196247
148 146 0.6725226452
149 147 0.6725056913
150 148 0.6724771476
151 149 0.6724439435
152 150 0.672442532
153 151 0.6724303064
154 152 0.6724235788
155 153 0.6724294499
156 154 0.6724285935
157 155 0.6724172017
158 156 0.6724130745
159 157 0.6723860878
160 158 0.6723707604
161 159 0.6723566111
162 160 0.6723469906
163 161 0.6723287161
164 162 0.6723155898
165 163 0.6722970834
166 164 0.6722872244
167 165 0.6722800481
168 166 0.6722550973
169 167 0.6722394313
170 168 0.6722204135
171 169 0.6721982148
172 170 0.6721971176
173 171 0.6721880705
174 172 0.672179176
175 173 0.6721769709
176 174 0.6721693215
177 175 0.6721581386
178 176 0.6721638661
179 177 0.6721598475
180 178 0.6721433342
181 179 0.6721335599
182 180 0.6721300594
183 181 0.6721153533
184 182 0.6721076397
185 183 0.6721009911
186 184 0.6720999252
187 185 0.6720953028
188 186 0.6720942505
189 187 0.6720856237
190 188 0.6720876136
191 189 0.6720880182
192 190 0.6720743856
193 191 0.6720598415
194 192 0.6720563492
195 193 0.6720389527
196 194 0.6720317324
197 195 0.672000736
198 196 0.6719895017
199 197 0.6719725302
200 198 0.6719770493
201 199 0.6719667172
202 200 0.6719511616
203 201 0.6719427289
204 202 0.6719299116
205 203 0.6719106583
206 204 0.6718967065
207 205 0.671890967
208 206 0.6718896293
209 207 0.6718883534
210 208 0.6718827289
211 209 0.6718763224
212 210 0.67187262
213 211 0.6718590402
214 212 0.6718455115
215 213 0.6718253747
216 214 0.671794877
217 215 0.6717873786
218 216 0.6717765089
219 217 0.6717616726
220 218 0.6717499215
221 219 0.6717326052
222 220 0.6717161937
223 221 0.6717056951
224 222 0.6717021438
225 223 0.6716868488
226 224 0.6716751909
227 225 0.671670116
228 226 0.6716558757
229 227 0.6716559962
230 228 0.6716487875
231 229 0.6716427451
232 230 0.6716323255
233 231 0.6716303547
234 232 0.6716309509
235 233 0.6716215401
236 234 0.6716162103
237 235 0.6716135097
238 236 0.6716156696
239 237 0.6716020054
240 238 0.6715921704
241 239 0.6715804466
242 240 0.6715882966
243 241 0.6715753942
244 242 0.6715752261
245 243 0.6715625509
246 244 0.6715628214
247 245 0.6715601629
248 246 0.6715576255
249 247 0.6715550274
250 248 0.6715448645
251 249 0.6715308166
252 250 0.671519334
253 251 0.6715184071
254 252 0.6715163019
255 253 0.6715096094
256 254 0.6714992963
257 255 0.6714917256
258 256 0.671477406
259 257 0.6714741542
260 258 0.6714576155
261 259 0.6714473645
262 260 0.6714427232
263 261 0.6714364275
264 262 0.6714339587
265 263 0.6714336287
266 264 0.6714283568
267 265 0.6714271895
268 266 0.671413471
269 267 0.6714072396
270 268 0.6714002677
271 269 0.6714001163
272 270 0.6713933952
273 271 0.6713926761
274 272 0.6713836619
275 273 0.6713772112
276 274 0.6713603715
277 275 0.6713560246
278 276 0.6713837913
279 277 0.6713684274
280 278 0.6713619356
281 279 0.6713584836
282 280 0.6713673572
283 281 0.6713625568
284 282 0.6713542652
285 283 0.6713512017
286 284 0.671342038
287 285 0.6713279798
288 286 0.6713123285
289 287 0.6713035326
290 288 0.6713022203
291 289 0.671296041
292 290 0.6712829551
293 291 0.6712769751
294 292 0.6712702915
295 293 0.6712379343
296 294 0.6712192006
297 295 0.6712074061
298 296 0.6711953324
299 297 0.6711891001
300 298 0.6711870526
301 299 0.6711812809
302 300 0.6711768946
303 301 0.6711845012
304 302 0.6711869636
305 303 0.671186884
306 304 0.6711890401
307 305 0.6711868603
308 306 0.6711900892
309 307 0.6711884242
310 308 0.6711837119
311 309 0.6711766645
312 310 0.671172959
313 311 0.6711740433
314 312 0.6711715069
315 313 0.6711589843
316 314 0.6711446402
317 315 0.6711415366
318 316 0.6711359351
319 317 0.671143361
320 318 0.6711353638
321 319 0.6711444387
322 320 0.6711487352
323 321 0.67114436
324 322 0.6711444722
325 323 0.6711325635
326 324 0.6711269403
327 325 0.6711154078
328 326 0.6711203043
329 327 0.6711241333
330 328 0.6711213497
331 329 0.6711231641
332 330 0.6711049215
333 331 0.6711031963
334 332 0.6710996314
335 333 0.6710867309
336 334 0.6710914578
337 335 0.6710929585
338 336 0.6710984779
339 337 0.6710923199
340 338 0.6710893917
341 339 0.6710923306
342 340 0.6710927901
343 341 0.6711092802
344 342 0.6711012995
345 343 0.6711015305
346 344 0.6710975574
347 345 0.6710899474
348 346 0.671085152
349 347 0.6710814533
350 348 0.6710701892
351 349 0.67105503
352 350 0.6710527861
353 351 0.6710508715
354 352 0.6710560803
355 353 0.6710465693
356 354 0.6710440741
357 355 0.6710496913
358 356 0.6710404659
359 357 0.6710293986
360 358 0.6710353817
361 359 0.6710271815
362 360 0.6710288077
363 361 0.6710169894
364 362 0.6710119848
365 363 0.6710114775
366 364 0.6710013614
367 365 0.6709985657
368 366 0.6709948954
369 367 0.6709970591
370 368 0.6709739289
371 369 0.6709754911
372 370 0.6709717066
373 371 0.67096845
374 372 0.6709739445
375 373 0.6709728881
376 374 0.6709694284
377 375 0.6709604166
378 376 0.6709605025
379 377 0.6709603727
380 378 0.670944339
381 379 0.6709447187
382 380 0.6709538679
383 381 0.6709640912
384 382 0.6709534847
385 383 0.6709471555
386 384 0.6709506783
387 385 0.6709546729
388 386 0.670930774
389 387 0.6709287322
390 388 0.6709198643
391 389 0.6709220389
392 390 0.6709230923
393 391 0.670930414
394 392 0.6709354296
395 393 0.6709351544
396 394 0.6709414935
397 395 0.6709445943
398 396 0.6709475685
399 397 0.6709533591
400 398 0.6709592222
401 399 0.6709508704
402 400 0.6709479912
403 401 0.6709417519
404 402 0.6709476082
405 403 0.6709480979
406 404 0.6709448724
407 405 0.6709421934
408 406 0.6709386261
409 407 0.6709461564
410 408 0.670934384
411 409 0.6709312987
412 410 0.670931806
413 411 0.6709286111
414 412 0.6709224729
415 413 0.6709236504
416 414 0.6709245901
417 415 0.6709463437
418 416 0.6709567049
419 417 0.670945606
420 418 0.6709479298
421 419 0.6709464351
422 420 0.6709414048
423 421 0.6709414427
424 422 0.6709296343
425 423 0.670924721
426 424 0.670906284
427 425 0.6708996826
428 426 0.6708987677
429 427 0.670909526
430 428 0.6709033226
431 429 0.6708750209
432 430 0.6708752079
433 431 0.6708776566
434 432 0.6708736133
435 433 0.6708754298
436 434 0.6708751084
437 435 0.6708642042
438 436 0.6708610465
439 437 0.6708574768
440 438 0.6708557953
441 439 0.670871378
442 440 0.6708640187
443 441 0.6708700565
444 442 0.6708667534
445 443 0.6708675383
446 444 0.6708740175
447 445 0.6708774523
448 446 0.6708697231
449 447 0.6708614971
450 448 0.6708607946
451 449 0.6708740865
452 450 0.6708729562
453 451 0.6708674017
454 452 0.6708693088
455 453 0.6708712037
456 454 0.6708703905
457 455 0.6708577595
458 456 0.6708493546
459 457 0.6708523777
460 458 0.6708454134
461 459 0.6708404483
462 460 0.6708274771
463 461 0.6708244992
464 462 0.6708344314
465 463 0.6708279081
466 464 0.6708258106
467 465 0.6708049714
468 466 0.670810989
469 467 0.6708212237
470 468 0.6708221741
471 469 0.6708259658
472 470 0.6708159692
473 471 0.6708136212
474 472 0.6708224942
475 473 0.6708363084
476 474 0.670850875
477 475 0.6708527236
478 476 0.6708453401
479 477 0.6708413844
480 478 0.6708364569
481 479 0.6708251774
482 480 0.6708154393
483 481 0.6708111613
484 482 0.6708102339
485 483 0.6707929623
486 484 0.6707900226
487 485 0.6707832384
488 486 0.6707739118
489 487 0.6707737538
490 488 0.6707730234
491 489 0.6707796291
492 490 0.670791408
493 491 0.6707944906
494 492 0.6707835635
495 493 0.6707908928
496 494 0.670796262
497 495 0.6707877825
498 496 0.6707854132
499 497 0.6707756206
500 498 0.6707707899
501 499 0.6707704386
502 500 0.6707621465
503 501 0.6707661931
504 502 0.6707651988
505 503 0.6707607827
506 504 0.670760242
507 505 0.6707506008
508 506 0.6707452886
509 507 0.6707355189
510 508 0.6707312551
511 509 0.6707199485
512 510 0.6707131947
513 511 0.6707154112
514 512 0.6706982346
515 513 0.6706988941
516 514 0.6706989098
517 515 0.670693306
518 516 0.6706944515
519 517 0.6706899688
520 518 0.6706909374
521 519 0.6706855074
522 520 0.6706787779
523 521 0.6706737082
524 522 0.6706761225
525 523 0.670685455
526 524 0.6706693855
527 525 0.6706647216
528 526 0.6706569188
529 527 0.6706549134
530 528 0.6706547978
531 529 0.6706564214
532 530 0.6706559196
533 531 0.6706515072
534 532 0.6706474616
535 533 0.6706424204
536 534 0.6706520008
537 535 0.6706448306
538 536 0.6706415789
539 537 0.6706305359
540 538 0.6706152774
541 539 0.670616585
542 540 0.6705963243
543 541 0.6706027368
544 542 0.6706003522
545 543 0.6706044301
546 544 0.6706047241
547 545 0.6706038235
548 546 0.6706026913
549 547 0.6705845786
550 548 0.6705873967
551 549 0.6705755426
552 550 0.6705715731
553 551 0.6705757153
554 552 0.6705516814
555 553 0.6705530864
556 554 0.6705552479
557 555 0.6705563336
558 556 0.6705718544
559 557 0.6705688384
560 558 0.6705641528
561 559 0.6705628467
562 560 0.670558488
563 561 0.6705544404
564 562 0.6705617451
565 563 0.6705631717
566 564 0.6705636201
567 565 0.6705537522
568 566 0.670555083
569 567 0.6705524541
570 568 0.6705503132
571 569 0.6705354602
572 570 0.6705387012
573 571 0.6705411923
574 572 0.6705390018
575 573 0.6705354939
576 574 0.670531296
577 575 0.6705377163
578 576 0.6705248875
579 577 0.6705252902
580 578 0.6705181562
581 579 0.6705123446
582 580 0.6705128345
583 581 0.6705173712
584 582 0.670541941
585 583 0.6705463243
586 584 0.6705513215
587 585 0.6705455889
588 586 0.6705408087
589 587 0.6705510193
590 588 0.6705456751
591 589 0.6705402427
592 590 0.6705443402
593 591 0.67054441
594 592 0.6705441955
595 593 0.6705319356
596 594 0.6705358843
597 595 0.6705334396
598 596 0.6705320462
599 597 0.6705332043
600 598 0.6705328363
601 599 0.6705315638
602 600 0.6705274435
603 601 0.670509808
604 602 0.6705077789
605 603 0.6705212132
606 604 0.6705098442
607 605 0.6705061509
608 606 0.6705003071
609 607 0.6705045031
610 608 0.6705083194
611 609 0.6705329997
612 610 0.6705269987
613 611 0.6705315607
614 612 0.6705142835
615 613 0.6705165015
616 614 0.6705001061
617 615 0.6705013916
618 616 0.6705037253
619 617 0.67049647
620 618 0.6705005632
621 619 0.6704957943
622 620 0.6704955333
623 621 0.6704961207
624 622 0.6704921459
625 623 0.6704751713
626 624 0.6704753101
627 625 0.6704620888
628 626 0.6704604282
629 627 0.6704663192
630 628 0.6704680085
631 629 0.670453228
632 630 0.6704577785
633 631 0.67046675
634 632 0.6704731863
635 633 0.6704811116
636 634 0.6704839644
637 635 0.6704854798
638 636 0.6704835837
639 637 0.6704736198
640 638 0.6704640242
641 639 0.670465663
642 640 0.6704646829
643 641 0.6704600961
644 642 0.6704643207
645 643 0.6704600533
646 644 0.6704614691
647 645 0.6704728212
648 646 0.6704758731
649 647 0.6704833026
650 648 0.6704767664
651 649 0.6704702727
652 650 0.6704671372
653 651 0.6704699936
654 652 0.6704587989
655 653 0.6704637668
656 654 0.6704653717
657 655 0.6704598273
658 656 0.6704522865
659 657 0.6704558586
660 658 0.6704466331
661 659 0.6704405886
662 660 0.6704463767
663 661 0.6704475216
664 662 0.6704572386
665 663 0.6704658153
666 664 0.6704600945
667 665 0.6704561998
668 666 0.6704535154
669 667 0.6704413781
670 668 0.6704450013
671 669 0.6704422199
672 670 0.67044342
673 671 0.6704415341
674 672 0.6704439539
675 673 0.6704498197
676 674 0.6704452194
677 675 0.6704366524
678 676 0.6704427124
679 677 0.6704395579
680 678 0.6704401246
681 679 0.6704415621
682 680 0.6704341343
683 681 0.6704369615
684 682 0.6704357425
685 683 0.6704294622
686 684 0.6704289794
687 685 0.6704272409
688 686 0.6704101162
689 687 0.6704069439
690 688 0.6704100747
691 689 0.6704122261
692 690 0.6704137826
693 691 0.6704207952
694 692 0.6704154834
695 693 0.6704253514
696 694 0.6704155636
697 695 0.6704141298
698 696 0.6704207635
699 697 0.6704268341
700 698 0.6704243126
701 699 0.6704235165
702 700 0.6704257736
703 701 0.6704247758
704 702 0.6704331799
705 703 0.6704252722
706 704 0.6704146644
707 705 0.6704164122
708 706 0.6704118954
709 707 0.6704043129
710 708 0.6703978198
711 709 0.6703935976
712 710 0.6703839683
713 711 0.6703843723
714 712 0.6703879502
715 713 0.6703895978
716 714 0.6703894359
717 715 0.6703928777
718 716 0.6703933128
719 717 0.6703844355
720 718 0.6703825151
721 719 0.6703983542
722 720 0.670399556
723 721 0.6703931808
724 722 0.6703886918
725 723 0.6703847574
726 724 0.6703885941
727 725 0.6703788615
728 726 0.6703799906
729 727 0.6703774518
730 728 0.6703783496
731 729 0.6703648854
732 730 0.6703716654
733 731 0.6703550938
734 732 0.6703467057
735 733 0.6703484503
736 734 0.6703549183
737 735 0.6703501504
738 736 0.6703672622
739 737 0.6703560249
740 738 0.6703547155
741 739 0.6703593236
742 740 0.6703606827
743 741 0.6703511404
744 742 0.6703431646
745 743 0.6703475116
746 744 0.6703483634
747 745 0.6703475713
748 746 0.670360457
749 747 0.6703664352
750 748 0.6703617612
751 749 0.6703669926
752 750 0.6703670837
753 751 0.6703706628
754 752 0.670369618
755 753 0.6703692351
756 754 0.6703624433
757 755 0.6703686285
758 756 0.6703598432
759 757 0.6703618766
760 758 0.6703694148
761 759 0.6703683652
762 760 0.6703604855
763 761 0.6703758987
764 762 0.6703773302
765 763 0.6703641028
766 764 0.6703649602
767 765 0.6703567811
768 766 0.6703544688
769 767 0.6703611821
770 768 0.6703527821
771 769 0.6703523616
772 770 0.6703616298
773 771 0.6703603551
774 772 0.6703675655
775 773 0.6703582411
776 774 0.6703581437
777 775 0.6703551885
778 776 0.6703608491
779 777 0.6703674554
780 778 0.6703679619
781 779 0.6703701757
782 780 0.6703603462
783 781 0.670359801
784 782 0.6703523669
785 783 0.6703365674
786 784 0.6703486118
787 785 0.6703450011
788 786 0.6703473135
789 787 0.670350998
790 788 0.6703417767
791 789 0.6703349821
792 790 0.6703457717
793 791 0.6703506266
794 792 0.6703596395
795 793 0.6703799895
796 794 0.6703687687
797 795 0.6703780675
798 796 0.670374835
799 797 0.6703831387
800 798 0.670377656
801 799 0.6703689741
802 800 0.6703709756
803 801 0.6703737517
804 802 0.6703818964
805 803 0.6703812173
806 804 0.6703960068
807 805 0.6703976729
808 806 0.6704024604
809 807 0.6704085008
810 808 0.6704076633
811 809 0.6704111719
812 810 0.6704220803
813 811 0.6704265011
814 812 0.6704251162
815 813 0.6704375472
816 814 0.6704319336
817 815 0.670437614
818 816 0.6704554331
819 817 0.6704603317
820 818 0.6704548042
821 819 0.6704502654
822 820 0.6704512072
823 821 0.6704433481
824 822 0.6704095112
825 823 0.6704086019
826 824 0.6703987131
827 825 0.6704019708
828 826 0.6704046556
829 827 0.6704091961
830 828 0.6704103204
831 829 0.6704074257
832 830 0.6704115335
833 831 0.6704041275
834 832 0.6704004556
835 833 0.6704037097
836 834 0.6704035477
837 835 0.6704025281
838 836 0.6704024549
839 837 0.670405721
840 838 0.6703983189
841 839 0.6704035256
842 840 0.6704047613
843 841 0.6704139617
844 842 0.6704066193
845 843 0.670402892
846 844 0.6704081961
847 845 0.6704029862
848 846 0.6704014281
849 847 0.6704036115
850 848 0.6704016777
851 849 0.6704085392
852 850 0.6704042952
853 851 0.670394789
854 852 0.6703885644
855 853 0.6703946813
856 854 0.6704042137
857 855 0.6704077517
858 856 0.6704118698
859 857 0.6704114259
860 858 0.6704157567
861 859 0.6703837005
862 860 0.6703829482
863 861 0.6703818491
864 862 0.6703826129
865 863 0.6703834487
866 864 0.6703868275
867 865 0.6703853357
868 866 0.670450644
869 867 0.6704556991
870 868 0.6704535742
871 869 0.6704495915
-902
View File
@@ -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,,,,,,,,,,,,
1 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
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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
22 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
23 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
24 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
25 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
26 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
27 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
28 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
29 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
30 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
31 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
32 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
33 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
34 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
35 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
-206
View File
@@ -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()
-240
View File
@@ -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()
-191
View File
@@ -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()
-145
View File
@@ -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()
-215
View File
@@ -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")
-223
View File
@@ -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()
-231
View File
@@ -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()
-164
View File
@@ -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()
-162
View File
@@ -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()
-94
View File
@@ -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()
-230
View File
@@ -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())
-147
View File
@@ -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()
-153
View File
@@ -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()
-136
View File
@@ -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()
-141
View File
@@ -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()
-159
View File
@@ -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()
-182
View File
@@ -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()
-7
View File
@@ -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())
-56
View File
@@ -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()
-282
View File
@@ -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()
-110
View File
@@ -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()
-142
View File
@@ -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)
-286
View File
@@ -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()
-109
View File
@@ -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()
-58
View File
@@ -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')
-95
View File
@@ -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)
-65
View File
@@ -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')
-22
View File
@@ -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!");
});
});
});
+1
View File
@@ -51,6 +51,7 @@ async function bootstrap() {
"https://suggestbet.bilgich.com",
"https://iddaai.com",
"https://www.iddaai.com",
"http://localhost:6195",
]
: 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",
);
});
});
-419
View File
@@ -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();
});
});
-20
View File
@@ -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))
-25
View File
@@ -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!");
});
});
-9
View File
@@ -1,9 +0,0 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}