Compare commits
10 Commits
cron
..
9e2edd590c
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e2edd590c | |||
| b5c2edf346 | |||
| bf7473c1e7 | |||
| 1f26a5bf2f | |||
| fb53fdf1df | |||
| 634204acf0 | |||
| df428ed1e8 | |||
| 2ccd6831eb | |||
| 1346924387 | |||
| e4c74025e5 |
@@ -0,0 +1,874 @@
|
|||||||
|
{
|
||||||
|
"meta":{"test_sets":["test"],"test_metrics":[{"best_value":"Min","name":"Logloss"}],"learn_metrics":[{"best_value":"Min","name":"Logloss"}],"launch_mode":"Train","parameters":"","iteration_count":2000,"learn_sets":["learn"],"name":"experiment"},
|
||||||
|
"iterations":[
|
||||||
|
{"learn":[0.692389481],"iteration":0,"passed_time":0.04679785798,"remaining_time":93.54891809,"test":[0.6924099937]},
|
||||||
|
{"learn":[0.6916338586],"iteration":1,"passed_time":0.08350330552,"remaining_time":83.41980222,"test":[0.6916660956]},
|
||||||
|
{"learn":[0.6910159214],"iteration":2,"passed_time":0.132821758,"remaining_time":88.41501689,"test":[0.691108145]},
|
||||||
|
{"learn":[0.6903417151],"iteration":3,"passed_time":0.162826233,"remaining_time":81.25029026,"test":[0.6904585078]},
|
||||||
|
{"learn":[0.6896961461],"iteration":4,"passed_time":0.1969265393,"remaining_time":78.57368918,"test":[0.689812816]},
|
||||||
|
{"learn":[0.6890979366],"iteration":5,"passed_time":0.2309352918,"remaining_time":76.74749531,"test":[0.689192261]},
|
||||||
|
{"learn":[0.6884946167],"iteration":6,"passed_time":0.2693987513,"remaining_time":76.70167304,"test":[0.6886032715]},
|
||||||
|
{"learn":[0.6879503686],"iteration":7,"passed_time":0.3199759681,"remaining_time":79.67401607,"test":[0.6880706742]},
|
||||||
|
{"learn":[0.6874528094],"iteration":8,"passed_time":0.3645802206,"remaining_time":80.65324659,"test":[0.6876192378]},
|
||||||
|
{"learn":[0.6869036785],"iteration":9,"passed_time":0.4116507506,"remaining_time":81.91849936,"test":[0.6870868859]},
|
||||||
|
{"learn":[0.6863761921],"iteration":10,"passed_time":0.4562469316,"remaining_time":82.49774064,"test":[0.6865493528]},
|
||||||
|
{"learn":[0.6859038678],"iteration":11,"passed_time":0.491541699,"remaining_time":81.43207481,"test":[0.686105086]},
|
||||||
|
{"learn":[0.685410175],"iteration":12,"passed_time":0.5221556769,"remaining_time":79.80948692,"test":[0.6856345086]},
|
||||||
|
{"learn":[0.6849483392],"iteration":13,"passed_time":0.5553110353,"remaining_time":78.77483686,"test":[0.6852027185]},
|
||||||
|
{"learn":[0.6845417792],"iteration":14,"passed_time":0.5952927147,"remaining_time":78.77706925,"test":[0.6848238481]},
|
||||||
|
{"learn":[0.6841038875],"iteration":15,"passed_time":0.6300274185,"remaining_time":78.12339989,"test":[0.6844045699]},
|
||||||
|
{"learn":[0.6836957422],"iteration":16,"passed_time":0.662600544,"remaining_time":77.29040464,"test":[0.6840077621]},
|
||||||
|
{"learn":[0.6832947461],"iteration":17,"passed_time":0.7004221698,"remaining_time":77.12426337,"test":[0.6836197496]},
|
||||||
|
{"learn":[0.6829014105],"iteration":18,"passed_time":0.7300844347,"remaining_time":76.12090869,"test":[0.6832475033]},
|
||||||
|
{"learn":[0.6825264546],"iteration":19,"passed_time":0.7641559459,"remaining_time":75.65143865,"test":[0.6829012069]},
|
||||||
|
{"learn":[0.6822106577],"iteration":20,"passed_time":0.8040792063,"remaining_time":75.77489282,"test":[0.6825880966]},
|
||||||
|
{"learn":[0.6818649349],"iteration":21,"passed_time":0.8356039756,"remaining_time":75.12839381,"test":[0.6822424968]},
|
||||||
|
{"learn":[0.6815467855],"iteration":22,"passed_time":0.8861440327,"remaining_time":76.16985881,"test":[0.6819180513]},
|
||||||
|
{"learn":[0.6812293319],"iteration":23,"passed_time":0.920219319,"remaining_time":75.76472393,"test":[0.6816384467]},
|
||||||
|
{"learn":[0.6808837443],"iteration":24,"passed_time":0.960164738,"remaining_time":75.8530143,"test":[0.6813262593]},
|
||||||
|
{"learn":[0.6805816494],"iteration":25,"passed_time":0.9895547925,"remaining_time":75.13004463,"test":[0.6810353411]},
|
||||||
|
{"learn":[0.6803209634],"iteration":26,"passed_time":1.025550161,"remaining_time":74.94112844,"test":[0.6808138172]},
|
||||||
|
{"learn":[0.6800350862],"iteration":27,"passed_time":1.060852064,"remaining_time":74.71429535,"test":[0.6805550049]},
|
||||||
|
{"learn":[0.6797703947],"iteration":28,"passed_time":1.10467538,"remaining_time":75.07983357,"test":[0.680347991]},
|
||||||
|
{"learn":[0.6794926675],"iteration":29,"passed_time":1.141766834,"remaining_time":74.97602208,"test":[0.680089679]},
|
||||||
|
{"learn":[0.6792251865],"iteration":30,"passed_time":1.180421588,"remaining_time":74.9758099,"test":[0.6798451919]},
|
||||||
|
{"learn":[0.6789670166],"iteration":31,"passed_time":1.213674604,"remaining_time":74.64098814,"test":[0.6796090443]},
|
||||||
|
{"learn":[0.678722402],"iteration":32,"passed_time":1.245848393,"remaining_time":74.26011482,"test":[0.6793890865]},
|
||||||
|
{"learn":[0.678476935],"iteration":33,"passed_time":1.287262512,"remaining_time":74.43406171,"test":[0.6791683772]},
|
||||||
|
{"learn":[0.6782297335],"iteration":34,"passed_time":1.327473991,"remaining_time":74.52818262,"test":[0.6789766369]},
|
||||||
|
{"learn":[0.6780226701],"iteration":35,"passed_time":1.3760549,"remaining_time":75.07143955,"test":[0.6787930242]},
|
||||||
|
{"learn":[0.6778291026],"iteration":36,"passed_time":1.427620019,"remaining_time":75.74102965,"test":[0.6786087714]},
|
||||||
|
{"learn":[0.6776045324],"iteration":37,"passed_time":1.468182407,"remaining_time":75.80457587,"test":[0.6784161299]},
|
||||||
|
{"learn":[0.6773969079],"iteration":38,"passed_time":1.508647379,"remaining_time":75.85788487,"test":[0.6782227897]},
|
||||||
|
{"learn":[0.6771819602],"iteration":39,"passed_time":1.549435187,"remaining_time":75.92232419,"test":[0.6780242369]},
|
||||||
|
{"learn":[0.6769816736],"iteration":40,"passed_time":1.586036608,"remaining_time":75.78160282,"test":[0.6778499631]},
|
||||||
|
{"learn":[0.6767984027],"iteration":41,"passed_time":1.621458864,"remaining_time":75.59086802,"test":[0.6776975784]},
|
||||||
|
{"learn":[0.6766201184],"iteration":42,"passed_time":1.663424818,"remaining_time":75.70517136,"test":[0.6775231674]},
|
||||||
|
{"learn":[0.6764394377],"iteration":43,"passed_time":1.70110089,"remaining_time":75.62166686,"test":[0.6773582124]},
|
||||||
|
{"learn":[0.6762698797],"iteration":44,"passed_time":1.739954496,"remaining_time":75.59135644,"test":[0.6772234666]},
|
||||||
|
{"learn":[0.6760974263],"iteration":45,"passed_time":1.776461223,"remaining_time":75.46098325,"test":[0.6770659843]},
|
||||||
|
{"learn":[0.6759245179],"iteration":46,"passed_time":1.819761638,"remaining_time":75.61690381,"test":[0.6769049529]},
|
||||||
|
{"learn":[0.6757673909],"iteration":47,"passed_time":1.869479807,"remaining_time":76.02551217,"test":[0.6767664194]},
|
||||||
|
{"learn":[0.6756172628],"iteration":48,"passed_time":1.916010121,"remaining_time":76.28848462,"test":[0.6766584917]},
|
||||||
|
{"learn":[0.675474531],"iteration":49,"passed_time":1.953635244,"remaining_time":76.19177452,"test":[0.6765507257]},
|
||||||
|
{"learn":[0.6753286933],"iteration":50,"passed_time":1.993876686,"remaining_time":76.19736591,"test":[0.6764489911]},
|
||||||
|
{"learn":[0.6751900513],"iteration":51,"passed_time":2.038943041,"remaining_time":76.38194316,"test":[0.6763947956]},
|
||||||
|
{"learn":[0.6750574835],"iteration":52,"passed_time":2.080276765,"remaining_time":76.42073325,"test":[0.6762778712]},
|
||||||
|
{"learn":[0.6749329567],"iteration":53,"passed_time":2.158576742,"remaining_time":77.78871001,"test":[0.6761865366]},
|
||||||
|
{"learn":[0.6748033265],"iteration":54,"passed_time":2.220619687,"remaining_time":78.52918711,"test":[0.6760679685]},
|
||||||
|
{"learn":[0.6746797823],"iteration":55,"passed_time":2.286959228,"remaining_time":79.39015604,"test":[0.6759774874]},
|
||||||
|
{"learn":[0.674535525],"iteration":56,"passed_time":2.328472096,"remaining_time":79.3723032,"test":[0.6758500622]},
|
||||||
|
{"learn":[0.6744256514],"iteration":57,"passed_time":2.367031568,"remaining_time":79.25474665,"test":[0.6757625065]},
|
||||||
|
{"learn":[0.674310819],"iteration":58,"passed_time":2.409161286,"remaining_time":79.25732298,"test":[0.6756876412]},
|
||||||
|
{"learn":[0.6741967947],"iteration":59,"passed_time":2.444825903,"remaining_time":79.04937087,"test":[0.6756151069]},
|
||||||
|
{"learn":[0.6740879654],"iteration":60,"passed_time":2.48484996,"remaining_time":78.98564055,"test":[0.6755303655]},
|
||||||
|
{"learn":[0.6739772476],"iteration":61,"passed_time":2.521603395,"remaining_time":78.8204416,"test":[0.6754565036]},
|
||||||
|
{"learn":[0.67388281],"iteration":62,"passed_time":2.554102332,"remaining_time":78.5285114,"test":[0.6753738983]},
|
||||||
|
{"learn":[0.6737789726],"iteration":63,"passed_time":2.593937938,"remaining_time":78.46662263,"test":[0.6752897299]},
|
||||||
|
{"learn":[0.6736812332],"iteration":64,"passed_time":2.623889155,"remaining_time":78.11116175,"test":[0.6752115539]},
|
||||||
|
{"learn":[0.6735930009],"iteration":65,"passed_time":2.660795108,"remaining_time":77.96935967,"test":[0.6751595431]},
|
||||||
|
{"learn":[0.6734947116],"iteration":66,"passed_time":2.695822592,"remaining_time":77.77649358,"test":[0.6750764658]},
|
||||||
|
{"learn":[0.6733961481],"iteration":67,"passed_time":2.725876686,"remaining_time":77.44696703,"test":[0.6750179194]},
|
||||||
|
{"learn":[0.6732990195],"iteration":68,"passed_time":2.761848366,"remaining_time":77.29172746,"test":[0.6749408803]},
|
||||||
|
{"learn":[0.6732133575],"iteration":69,"passed_time":2.791847449,"remaining_time":76.97522253,"test":[0.6748795802]},
|
||||||
|
{"learn":[0.673111539],"iteration":70,"passed_time":2.824541003,"remaining_time":76.73999429,"test":[0.674790372]},
|
||||||
|
{"learn":[0.6730080451],"iteration":71,"passed_time":2.861023716,"remaining_time":76.61185729,"test":[0.6747239773]},
|
||||||
|
{"learn":[0.6729157861],"iteration":72,"passed_time":2.897136588,"remaining_time":76.47646857,"test":[0.6746701254]},
|
||||||
|
{"learn":[0.6728347949],"iteration":73,"passed_time":2.935718661,"remaining_time":76.40802894,"test":[0.6746120937]},
|
||||||
|
{"learn":[0.6727640693],"iteration":74,"passed_time":3.040023476,"remaining_time":78.02726921,"test":[0.6745550085]},
|
||||||
|
{"learn":[0.6726808811],"iteration":75,"passed_time":3.097341794,"remaining_time":78.41165279,"test":[0.6744855074]},
|
||||||
|
{"learn":[0.6726029645],"iteration":76,"passed_time":3.152948955,"remaining_time":78.74182909,"test":[0.6744264172]},
|
||||||
|
{"learn":[0.6725356026],"iteration":77,"passed_time":3.216126808,"remaining_time":79.24866314,"test":[0.674381715]},
|
||||||
|
{"learn":[0.6724606887],"iteration":78,"passed_time":3.256861302,"remaining_time":79.19532355,"test":[0.6743331681]},
|
||||||
|
{"learn":[0.6723849561],"iteration":79,"passed_time":3.305679851,"remaining_time":79.33631641,"test":[0.67428564]},
|
||||||
|
{"learn":[0.6723050519],"iteration":80,"passed_time":3.348083566,"remaining_time":79.32064647,"test":[0.6742202413]},
|
||||||
|
{"learn":[0.6722508802],"iteration":81,"passed_time":3.38129387,"remaining_time":79.08928832,"test":[0.6741620971]},
|
||||||
|
{"learn":[0.6721773904],"iteration":82,"passed_time":3.41660066,"remaining_time":78.91112609,"test":[0.6741109453]},
|
||||||
|
{"learn":[0.6721007598],"iteration":83,"passed_time":3.48099347,"remaining_time":79.39980344,"test":[0.6740556003]},
|
||||||
|
{"learn":[0.6720353564],"iteration":84,"passed_time":3.535359896,"remaining_time":79.64957884,"test":[0.6740146772]},
|
||||||
|
{"learn":[0.6719790902],"iteration":85,"passed_time":3.581806996,"remaining_time":79.71603012,"test":[0.673983295]},
|
||||||
|
{"learn":[0.6719140024],"iteration":86,"passed_time":3.612293661,"remaining_time":79.42893993,"test":[0.6739595301]},
|
||||||
|
{"learn":[0.6718573633],"iteration":87,"passed_time":3.644530261,"remaining_time":79.18570293,"test":[0.6739336659]},
|
||||||
|
{"learn":[0.671795602],"iteration":88,"passed_time":3.67809653,"remaining_time":78.97575809,"test":[0.673890361]},
|
||||||
|
{"learn":[0.6717369134],"iteration":89,"passed_time":3.712417516,"remaining_time":78.78574951,"test":[0.673863586]},
|
||||||
|
{"learn":[0.6716711079],"iteration":90,"passed_time":3.743502971,"remaining_time":78.53128759,"test":[0.6738190616]},
|
||||||
|
{"learn":[0.6716070843],"iteration":91,"passed_time":3.775351679,"remaining_time":78.2975109,"test":[0.6737799295]},
|
||||||
|
{"learn":[0.6715517232],"iteration":92,"passed_time":3.806186247,"remaining_time":78.04728142,"test":[0.6737364374]},
|
||||||
|
{"learn":[0.6714957378],"iteration":93,"passed_time":3.83798807,"remaining_time":77.82133257,"test":[0.6737093719]},
|
||||||
|
{"learn":[0.6714364567],"iteration":94,"passed_time":3.871278973,"remaining_time":77.62933099,"test":[0.6736630475]},
|
||||||
|
{"learn":[0.6713881758],"iteration":95,"passed_time":3.913531039,"remaining_time":77.6183656,"test":[0.67364367]},
|
||||||
|
{"learn":[0.6713336502],"iteration":96,"passed_time":3.945433866,"remaining_time":77.40371802,"test":[0.6735998081]},
|
||||||
|
{"learn":[0.6712700267],"iteration":97,"passed_time":3.989716281,"remaining_time":77.43306496,"test":[0.6735526984]},
|
||||||
|
{"learn":[0.6712154424],"iteration":98,"passed_time":4.020621946,"remaining_time":77.20406384,"test":[0.6735012924]},
|
||||||
|
{"learn":[0.6711600413],"iteration":99,"passed_time":4.053732144,"remaining_time":77.02091074,"test":[0.6734818024]},
|
||||||
|
{"learn":[0.6711060533],"iteration":100,"passed_time":4.084124711,"remaining_time":76.78963194,"test":[0.6734379341]},
|
||||||
|
{"learn":[0.6710494943],"iteration":101,"passed_time":4.116434744,"remaining_time":76.59797199,"test":[0.6734059869]},
|
||||||
|
{"learn":[0.6709936897],"iteration":102,"passed_time":4.148330356,"remaining_time":76.40177365,"test":[0.6733740852]},
|
||||||
|
{"learn":[0.6709472183],"iteration":103,"passed_time":4.176511193,"remaining_time":76.14101176,"test":[0.6733330971]},
|
||||||
|
{"learn":[0.6708914508],"iteration":104,"passed_time":4.2025065,"remaining_time":75.84523636,"test":[0.6733060254]},
|
||||||
|
{"learn":[0.6708388195],"iteration":105,"passed_time":4.232975206,"remaining_time":75.63448151,"test":[0.6732755898]},
|
||||||
|
{"learn":[0.6707885854],"iteration":106,"passed_time":4.261364958,"remaining_time":75.39031649,"test":[0.6732294722]},
|
||||||
|
{"learn":[0.6707454167],"iteration":107,"passed_time":4.290824713,"remaining_time":75.1688922,"test":[0.6732035176]},
|
||||||
|
{"learn":[0.6706973013],"iteration":108,"passed_time":4.324192493,"remaining_time":75.01878903,"test":[0.673196437]},
|
||||||
|
{"learn":[0.6706577031],"iteration":109,"passed_time":4.351512102,"remaining_time":74.76688976,"test":[0.6731652709]},
|
||||||
|
{"learn":[0.67061108],"iteration":110,"passed_time":4.38641502,"remaining_time":74.64808984,"test":[0.673138808]},
|
||||||
|
{"learn":[0.6705625485],"iteration":111,"passed_time":4.424063991,"remaining_time":74.57707871,"test":[0.6731062725]},
|
||||||
|
{"learn":[0.6705146484],"iteration":112,"passed_time":4.45863849,"remaining_time":74.45531709,"test":[0.6730726625]},
|
||||||
|
{"learn":[0.6704704423],"iteration":113,"passed_time":4.497153675,"remaining_time":74.40027922,"test":[0.6730285927]},
|
||||||
|
{"learn":[0.6704155922],"iteration":114,"passed_time":4.533368584,"remaining_time":74.30782417,"test":[0.6729872702]},
|
||||||
|
{"learn":[0.6703687117],"iteration":115,"passed_time":4.564651269,"remaining_time":74.13623268,"test":[0.6729721425]},
|
||||||
|
{"learn":[0.6703324232],"iteration":116,"passed_time":4.596824343,"remaining_time":73.98136956,"test":[0.6729564624]},
|
||||||
|
{"learn":[0.6702884624],"iteration":117,"passed_time":4.628377967,"remaining_time":73.81870623,"test":[0.6729312424]},
|
||||||
|
{"learn":[0.670253478],"iteration":118,"passed_time":4.668052254,"remaining_time":73.78660748,"test":[0.6729354345]},
|
||||||
|
{"learn":[0.6702140804],"iteration":119,"passed_time":4.692108266,"remaining_time":73.50969617,"test":[0.6729085401]},
|
||||||
|
{"learn":[0.6701682529],"iteration":120,"passed_time":4.723741667,"remaining_time":73.354633,"test":[0.6728898322]},
|
||||||
|
{"learn":[0.6701320588],"iteration":121,"passed_time":4.756626425,"remaining_time":73.22085595,"test":[0.6728773638]},
|
||||||
|
{"learn":[0.6700939824],"iteration":122,"passed_time":4.788008428,"remaining_time":73.06578714,"test":[0.6728618874]},
|
||||||
|
{"learn":[0.6700655902],"iteration":123,"passed_time":4.815546648,"remaining_time":72.85456058,"test":[0.6728540413]},
|
||||||
|
{"learn":[0.6700190743],"iteration":124,"passed_time":4.843186806,"remaining_time":72.64780209,"test":[0.6728441291]},
|
||||||
|
{"learn":[0.6699792296],"iteration":125,"passed_time":4.875548614,"remaining_time":72.51411192,"test":[0.672815631]},
|
||||||
|
{"learn":[0.6699379404],"iteration":126,"passed_time":4.916953662,"remaining_time":72.51538748,"test":[0.6728082021]},
|
||||||
|
{"learn":[0.669895454],"iteration":127,"passed_time":4.952918369,"remaining_time":72.43643115,"test":[0.6727900064]},
|
||||||
|
{"learn":[0.6698563938],"iteration":128,"passed_time":4.991585558,"remaining_time":72.39733782,"test":[0.6727649552]},
|
||||||
|
{"learn":[0.6698215571],"iteration":129,"passed_time":5.028084166,"remaining_time":72.32705685,"test":[0.6727467657]},
|
||||||
|
{"learn":[0.6697857067],"iteration":130,"passed_time":5.059198996,"remaining_time":72.18048033,"test":[0.6727396032]},
|
||||||
|
{"learn":[0.6697449303],"iteration":131,"passed_time":5.096035515,"remaining_time":72.1166238,"test":[0.6727245271]},
|
||||||
|
{"learn":[0.6697052425],"iteration":132,"passed_time":5.125282589,"remaining_time":71.94663604,"test":[0.6726955143]},
|
||||||
|
{"learn":[0.6696695553],"iteration":133,"passed_time":5.156392608,"remaining_time":71.80469109,"test":[0.67269209]},
|
||||||
|
{"learn":[0.6696269265],"iteration":134,"passed_time":5.190402292,"remaining_time":71.70444647,"test":[0.672677932]},
|
||||||
|
{"learn":[0.6695969271],"iteration":135,"passed_time":5.221466142,"remaining_time":71.56480065,"test":[0.6726540285]},
|
||||||
|
{"learn":[0.6695489786],"iteration":136,"passed_time":5.251144663,"remaining_time":71.40790151,"test":[0.6726288583]},
|
||||||
|
{"learn":[0.6695173859],"iteration":137,"passed_time":5.274361693,"remaining_time":71.16566285,"test":[0.6725863431]},
|
||||||
|
{"learn":[0.6694811164],"iteration":138,"passed_time":5.309398952,"remaining_time":71.08483058,"test":[0.6725837967]},
|
||||||
|
{"learn":[0.6694477439],"iteration":139,"passed_time":5.344693175,"remaining_time":71.00806646,"test":[0.6725772977]},
|
||||||
|
{"learn":[0.6694082161],"iteration":140,"passed_time":5.377737126,"remaining_time":70.90222211,"test":[0.6725685594]},
|
||||||
|
{"learn":[0.6693679185],"iteration":141,"passed_time":5.416087925,"remaining_time":70.8668406,"test":[0.6725553829]},
|
||||||
|
{"learn":[0.6693341916],"iteration":142,"passed_time":5.452286939,"remaining_time":70.80347444,"test":[0.6725484347]},
|
||||||
|
{"learn":[0.6692933159],"iteration":143,"passed_time":5.490006789,"remaining_time":70.7600875,"test":[0.6725306172]},
|
||||||
|
{"learn":[0.6692619696],"iteration":144,"passed_time":5.521869859,"remaining_time":70.64185233,"test":[0.672543149]},
|
||||||
|
{"learn":[0.6692229289],"iteration":145,"passed_time":5.553520721,"remaining_time":70.5221056,"test":[0.6725196247]},
|
||||||
|
{"learn":[0.6691840164],"iteration":146,"passed_time":5.582178524,"remaining_time":70.3658286,"test":[0.6725226452]},
|
||||||
|
{"learn":[0.6691581406],"iteration":147,"passed_time":5.611368671,"remaining_time":70.21793769,"test":[0.6725056913]},
|
||||||
|
{"learn":[0.6691177196],"iteration":148,"passed_time":5.636941079,"remaining_time":70.02669757,"test":[0.6724771476]},
|
||||||
|
{"learn":[0.6690851126],"iteration":149,"passed_time":5.673704689,"remaining_time":69.97569117,"test":[0.6724439435]},
|
||||||
|
{"learn":[0.6690518144],"iteration":150,"passed_time":5.706346207,"remaining_time":69.87439826,"test":[0.672442532]},
|
||||||
|
{"learn":[0.6690149711],"iteration":151,"passed_time":5.738210991,"remaining_time":69.76456521,"test":[0.6724303064]},
|
||||||
|
{"learn":[0.668993877],"iteration":152,"passed_time":5.765951318,"remaining_time":69.60596133,"test":[0.6724235788]},
|
||||||
|
{"learn":[0.6689596579],"iteration":153,"passed_time":5.795573467,"remaining_time":69.47161442,"test":[0.6724294499]},
|
||||||
|
{"learn":[0.6689372651],"iteration":154,"passed_time":5.81744896,"remaining_time":69.24640858,"test":[0.6724285935]},
|
||||||
|
{"learn":[0.6689003045],"iteration":155,"passed_time":5.853529431,"remaining_time":69.19171968,"test":[0.6724172017]},
|
||||||
|
{"learn":[0.6688680182],"iteration":156,"passed_time":5.888380392,"remaining_time":69.12283479,"test":[0.6724130745]},
|
||||||
|
{"learn":[0.6688348164],"iteration":157,"passed_time":5.924601775,"remaining_time":69.07035741,"test":[0.6723860878]},
|
||||||
|
{"learn":[0.6687947046],"iteration":158,"passed_time":5.964531924,"remaining_time":69.06102687,"test":[0.6723707604]},
|
||||||
|
{"learn":[0.6687605251],"iteration":159,"passed_time":5.996805452,"remaining_time":68.9632627,"test":[0.6723566111]},
|
||||||
|
{"learn":[0.668726253],"iteration":160,"passed_time":6.022341459,"remaining_time":68.78935368,"test":[0.6723469906]},
|
||||||
|
{"learn":[0.6686862718],"iteration":161,"passed_time":6.05082584,"remaining_time":68.65072774,"test":[0.6723287161]},
|
||||||
|
{"learn":[0.668663478],"iteration":162,"passed_time":6.079027554,"remaining_time":68.51026759,"test":[0.6723155898]},
|
||||||
|
{"learn":[0.6686399521],"iteration":163,"passed_time":6.108511297,"remaining_time":68.38552891,"test":[0.6722970834]},
|
||||||
|
{"learn":[0.6686058279],"iteration":164,"passed_time":6.140719309,"remaining_time":68.29224202,"test":[0.6722872244]},
|
||||||
|
{"learn":[0.6685761282],"iteration":165,"passed_time":6.169540017,"remaining_time":68.16226742,"test":[0.6722800481]},
|
||||||
|
{"learn":[0.6685469327],"iteration":166,"passed_time":6.2020892,"remaining_time":68.07442817,"test":[0.6722550973]},
|
||||||
|
{"learn":[0.6685157003],"iteration":167,"passed_time":6.231576547,"remaining_time":67.95385854,"test":[0.6722394313]},
|
||||||
|
{"learn":[0.6684805143],"iteration":168,"passed_time":6.263261652,"remaining_time":67.85817802,"test":[0.6722204135]},
|
||||||
|
{"learn":[0.6684485765],"iteration":169,"passed_time":6.295102833,"remaining_time":67.7649305,"test":[0.6721982148]},
|
||||||
|
{"learn":[0.6684144429],"iteration":170,"passed_time":6.325415964,"remaining_time":67.65605729,"test":[0.6721971176]},
|
||||||
|
{"learn":[0.6683849752],"iteration":171,"passed_time":6.35697084,"remaining_time":67.56129474,"test":[0.6721880705]},
|
||||||
|
{"learn":[0.6683568537],"iteration":172,"passed_time":6.395913563,"remaining_time":67.5452837,"test":[0.672179176]},
|
||||||
|
{"learn":[0.6683266628],"iteration":173,"passed_time":6.437330522,"remaining_time":67.55497433,"test":[0.6721769709]},
|
||||||
|
{"learn":[0.6682937842],"iteration":174,"passed_time":6.472195712,"remaining_time":67.49575528,"test":[0.6721693215]},
|
||||||
|
{"learn":[0.6682657097],"iteration":175,"passed_time":6.503044842,"remaining_time":67.395192,"test":[0.6721581386]},
|
||||||
|
{"learn":[0.6682301443],"iteration":176,"passed_time":6.533528251,"remaining_time":67.29164972,"test":[0.6721638661]},
|
||||||
|
{"learn":[0.6681995916],"iteration":177,"passed_time":6.562589882,"remaining_time":67.17437509,"test":[0.6721598475]},
|
||||||
|
{"learn":[0.6681658267],"iteration":178,"passed_time":6.590816982,"remaining_time":67.04959623,"test":[0.6721433342]},
|
||||||
|
{"learn":[0.6681422687],"iteration":179,"passed_time":6.624646227,"remaining_time":66.98253407,"test":[0.6721335599]},
|
||||||
|
{"learn":[0.6681216601],"iteration":180,"passed_time":6.655147334,"remaining_time":66.88239227,"test":[0.6721300594]},
|
||||||
|
{"learn":[0.6680899019],"iteration":181,"passed_time":6.687788902,"remaining_time":66.80439684,"test":[0.6721153533]},
|
||||||
|
{"learn":[0.6680676394],"iteration":182,"passed_time":6.718057043,"remaining_time":66.7033314,"test":[0.6721076397]},
|
||||||
|
{"learn":[0.6680413672],"iteration":183,"passed_time":6.751300957,"remaining_time":66.6324051,"test":[0.6721009911]},
|
||||||
|
{"learn":[0.6680088406],"iteration":184,"passed_time":6.784288393,"remaining_time":66.55936991,"test":[0.6720999252]},
|
||||||
|
{"learn":[0.6679873982],"iteration":185,"passed_time":6.810905309,"remaining_time":66.42463565,"test":[0.6720953028]},
|
||||||
|
{"learn":[0.6679663544],"iteration":186,"passed_time":6.832974292,"remaining_time":66.24696466,"test":[0.6720942505]},
|
||||||
|
{"learn":[0.6679417375],"iteration":187,"passed_time":6.867184511,"remaining_time":66.18796986,"test":[0.6720856237]},
|
||||||
|
{"learn":[0.6679100197],"iteration":188,"passed_time":6.918652024,"remaining_time":66.29459691,"test":[0.6720876136]},
|
||||||
|
{"learn":[0.667881208],"iteration":189,"passed_time":6.96948149,"remaining_time":66.39348156,"test":[0.6720880182]},
|
||||||
|
{"learn":[0.6678475427],"iteration":190,"passed_time":7.018176318,"remaining_time":66.47058094,"test":[0.6720743856]},
|
||||||
|
{"learn":[0.6678310341],"iteration":191,"passed_time":7.074099623,"remaining_time":66.61443812,"test":[0.6720598415]},
|
||||||
|
{"learn":[0.6678060257],"iteration":192,"passed_time":7.117099742,"remaining_time":66.63522919,"test":[0.6720563492]},
|
||||||
|
{"learn":[0.6677789336],"iteration":193,"passed_time":7.191058554,"remaining_time":66.94356571,"test":[0.6720389527]},
|
||||||
|
{"learn":[0.6677478773],"iteration":194,"passed_time":7.2421897,"remaining_time":67.03667902,"test":[0.6720317324]},
|
||||||
|
{"learn":[0.6677212408],"iteration":195,"passed_time":7.282401129,"remaining_time":67.02781447,"test":[0.672000736]},
|
||||||
|
{"learn":[0.667704316],"iteration":196,"passed_time":7.317019235,"remaining_time":66.96744,"test":[0.6719895017]},
|
||||||
|
{"learn":[0.6676819639],"iteration":197,"passed_time":7.351194179,"remaining_time":66.90329248,"test":[0.6719725302]},
|
||||||
|
{"learn":[0.6676554448],"iteration":198,"passed_time":7.389840926,"remaining_time":66.87991712,"test":[0.6719770493]},
|
||||||
|
{"learn":[0.6676318346],"iteration":199,"passed_time":7.432994652,"remaining_time":66.89695187,"test":[0.6719667172]},
|
||||||
|
{"learn":[0.6676074705],"iteration":200,"passed_time":7.471295231,"remaining_time":66.86995085,"test":[0.6719511616]},
|
||||||
|
{"learn":[0.6675849784],"iteration":201,"passed_time":7.506377837,"remaining_time":66.8141948,"test":[0.6719427289]},
|
||||||
|
{"learn":[0.6675631744],"iteration":202,"passed_time":7.540821494,"remaining_time":66.75298633,"test":[0.6719299116]},
|
||||||
|
{"learn":[0.6675397619],"iteration":203,"passed_time":7.56808212,"remaining_time":66.62880141,"test":[0.6719106583]},
|
||||||
|
{"learn":[0.6675169086],"iteration":204,"passed_time":7.605676901,"remaining_time":66.59604896,"test":[0.6718967065]},
|
||||||
|
{"learn":[0.6674864762],"iteration":205,"passed_time":7.638300222,"remaining_time":66.51995436,"test":[0.671890967]},
|
||||||
|
{"learn":[0.6674670714],"iteration":206,"passed_time":7.665554951,"remaining_time":66.39777791,"test":[0.6718896293]},
|
||||||
|
{"learn":[0.6674375599],"iteration":207,"passed_time":7.700277678,"remaining_time":66.34085384,"test":[0.6718883534]},
|
||||||
|
{"learn":[0.6674148457],"iteration":208,"passed_time":7.734145802,"remaining_time":66.27681881,"test":[0.6718827289]},
|
||||||
|
{"learn":[0.6673974446],"iteration":209,"passed_time":7.766232144,"remaining_time":66.19788351,"test":[0.6718763224]},
|
||||||
|
{"learn":[0.6673812139],"iteration":210,"passed_time":7.796801222,"remaining_time":66.1065279,"test":[0.67187262]},
|
||||||
|
{"learn":[0.6673515687],"iteration":211,"passed_time":7.831891449,"remaining_time":66.05387693,"test":[0.6718590402]},
|
||||||
|
{"learn":[0.6673197956],"iteration":212,"passed_time":7.871259964,"remaining_time":66.0372843,"test":[0.6718455115]},
|
||||||
|
{"learn":[0.6672900754],"iteration":213,"passed_time":7.910110502,"remaining_time":66.01615587,"test":[0.6718253747]},
|
||||||
|
{"learn":[0.6672550009],"iteration":214,"passed_time":7.951342226,"remaining_time":66.01463197,"test":[0.671794877]},
|
||||||
|
{"learn":[0.6672271563],"iteration":215,"passed_time":7.989001461,"remaining_time":65.98323429,"test":[0.6717873786]},
|
||||||
|
{"learn":[0.667204521],"iteration":216,"passed_time":8.025973631,"remaining_time":65.94613357,"test":[0.6717765089]},
|
||||||
|
{"learn":[0.667181968],"iteration":217,"passed_time":8.058434478,"remaining_time":65.87215707,"test":[0.6717616726]},
|
||||||
|
{"learn":[0.6671640023],"iteration":218,"passed_time":8.087145957,"remaining_time":65.76806826,"test":[0.6717499215]},
|
||||||
|
{"learn":[0.66714351],"iteration":219,"passed_time":8.112590578,"remaining_time":65.63823286,"test":[0.6717326052]},
|
||||||
|
{"learn":[0.6671167156],"iteration":220,"passed_time":8.148644349,"remaining_time":65.59474342,"test":[0.6717161937]},
|
||||||
|
{"learn":[0.6670915937],"iteration":221,"passed_time":8.197662625,"remaining_time":65.65515382,"test":[0.6717056951]},
|
||||||
|
{"learn":[0.6670595279],"iteration":222,"passed_time":8.239228431,"remaining_time":65.65519696,"test":[0.6717021438]},
|
||||||
|
{"learn":[0.667033994],"iteration":223,"passed_time":8.268371203,"remaining_time":65.55637168,"test":[0.6716868488]},
|
||||||
|
{"learn":[0.6670008246],"iteration":224,"passed_time":8.298555216,"remaining_time":65.46638004,"test":[0.6716751909]},
|
||||||
|
{"learn":[0.6669858319],"iteration":225,"passed_time":8.327401394,"remaining_time":65.36641625,"test":[0.671670116]},
|
||||||
|
{"learn":[0.6669553964],"iteration":226,"passed_time":8.357648377,"remaining_time":65.27802014,"test":[0.6716558757]},
|
||||||
|
{"learn":[0.6669274683],"iteration":227,"passed_time":8.384989701,"remaining_time":65.16755154,"test":[0.6716559962]},
|
||||||
|
{"learn":[0.666896348],"iteration":228,"passed_time":8.418297538,"remaining_time":65.1039517,"test":[0.6716487875]},
|
||||||
|
{"learn":[0.6668698686],"iteration":229,"passed_time":8.453919972,"remaining_time":65.05842761,"test":[0.6716427451]},
|
||||||
|
{"learn":[0.6668513411],"iteration":230,"passed_time":8.49049033,"remaining_time":65.02024846,"test":[0.6716323255]},
|
||||||
|
{"learn":[0.6668309985],"iteration":231,"passed_time":8.523986676,"remaining_time":64.95865708,"test":[0.6716303547]},
|
||||||
|
{"learn":[0.6668058585],"iteration":232,"passed_time":8.550998228,"remaining_time":64.84812819,"test":[0.6716309509]},
|
||||||
|
{"learn":[0.6667845908],"iteration":233,"passed_time":8.575382398,"remaining_time":64.71848425,"test":[0.6716215401]},
|
||||||
|
{"learn":[0.6667582863],"iteration":234,"passed_time":8.607602961,"remaining_time":64.64859245,"test":[0.6716162103]},
|
||||||
|
{"learn":[0.6667332943],"iteration":235,"passed_time":8.6353786,"remaining_time":64.54579597,"test":[0.6716135097]},
|
||||||
|
{"learn":[0.6667070085],"iteration":236,"passed_time":8.66085309,"remaining_time":64.42651476,"test":[0.6716156696]},
|
||||||
|
{"learn":[0.6666907315],"iteration":237,"passed_time":8.691362456,"remaining_time":64.34529684,"test":[0.6716020054]},
|
||||||
|
{"learn":[0.6666633028],"iteration":238,"passed_time":8.719983169,"remaining_time":64.25058728,"test":[0.6715921704]},
|
||||||
|
{"learn":[0.6666406707],"iteration":239,"passed_time":8.746012652,"remaining_time":64.13742611,"test":[0.6715804466]},
|
||||||
|
{"learn":[0.6666134624],"iteration":240,"passed_time":8.773898765,"remaining_time":64.03853912,"test":[0.6715882966]},
|
||||||
|
{"learn":[0.6665850522],"iteration":241,"passed_time":8.803292064,"remaining_time":63.9511878,"test":[0.6715753942]},
|
||||||
|
{"learn":[0.6665631193],"iteration":242,"passed_time":8.833976809,"remaining_time":63.87365125,"test":[0.6715752261]},
|
||||||
|
{"learn":[0.6665412643],"iteration":243,"passed_time":8.862338006,"remaining_time":63.7797768,"test":[0.6715625509]},
|
||||||
|
{"learn":[0.6665168385],"iteration":244,"passed_time":8.892424073,"remaining_time":63.69879285,"test":[0.6715628214]},
|
||||||
|
{"learn":[0.6664904845],"iteration":245,"passed_time":8.932383667,"remaining_time":63.68862175,"test":[0.6715601629]},
|
||||||
|
{"learn":[0.6664678274],"iteration":246,"passed_time":8.962911123,"remaining_time":63.61126801,"test":[0.6715576255]},
|
||||||
|
{"learn":[0.6664539777],"iteration":247,"passed_time":8.991624872,"remaining_time":63.52147894,"test":[0.6715550274]},
|
||||||
|
{"learn":[0.6664334121],"iteration":248,"passed_time":9.021847081,"remaining_time":63.44278811,"test":[0.6715448645]},
|
||||||
|
{"learn":[0.6664121724],"iteration":249,"passed_time":9.05121341,"remaining_time":63.35849387,"test":[0.6715308166]},
|
||||||
|
{"learn":[0.666392034],"iteration":250,"passed_time":9.085113431,"remaining_time":63.30622865,"test":[0.671519334]},
|
||||||
|
{"learn":[0.666366899],"iteration":251,"passed_time":9.110250512,"remaining_time":63.19332498,"test":[0.6715184071]},
|
||||||
|
{"learn":[0.6663414098],"iteration":252,"passed_time":9.137253573,"remaining_time":63.09399997,"test":[0.6715163019]},
|
||||||
|
{"learn":[0.6663157816],"iteration":253,"passed_time":9.174559864,"remaining_time":63.06606899,"test":[0.6715096094]},
|
||||||
|
{"learn":[0.6662989799],"iteration":254,"passed_time":9.196898204,"remaining_time":62.93563673,"test":[0.6714992963]},
|
||||||
|
{"learn":[0.6662696102],"iteration":255,"passed_time":9.238149902,"remaining_time":62.9348962,"test":[0.6714917256]},
|
||||||
|
{"learn":[0.6662479711],"iteration":256,"passed_time":9.267818291,"remaining_time":62.85528125,"test":[0.671477406]},
|
||||||
|
{"learn":[0.6662231874],"iteration":257,"passed_time":9.297538986,"remaining_time":62.77640665,"test":[0.6714741542]},
|
||||||
|
{"learn":[0.6661947927],"iteration":258,"passed_time":9.324772701,"remaining_time":62.68119411,"test":[0.6714576155]},
|
||||||
|
{"learn":[0.6661669951],"iteration":259,"passed_time":9.357824574,"remaining_time":62.62544138,"test":[0.6714473645]},
|
||||||
|
{"learn":[0.6661426137],"iteration":260,"passed_time":9.388345461,"remaining_time":62.55299907,"test":[0.6714427232]},
|
||||||
|
{"learn":[0.6661216749],"iteration":261,"passed_time":9.427290804,"remaining_time":62.53676114,"test":[0.6714364275]},
|
||||||
|
{"learn":[0.6660983123],"iteration":262,"passed_time":9.461913185,"remaining_time":62.49179925,"test":[0.6714339587]},
|
||||||
|
{"learn":[0.6660803402],"iteration":263,"passed_time":9.496090562,"remaining_time":62.44398945,"test":[0.6714336287]},
|
||||||
|
{"learn":[0.6660617842],"iteration":264,"passed_time":9.524189317,"remaining_time":62.35648477,"test":[0.6714283568]},
|
||||||
|
{"learn":[0.6660443878],"iteration":265,"passed_time":9.55372419,"remaining_time":62.27878852,"test":[0.6714271895]},
|
||||||
|
{"learn":[0.6660176079],"iteration":266,"passed_time":9.590356068,"remaining_time":62.2475171,"test":[0.671413471]},
|
||||||
|
{"learn":[0.6659967546],"iteration":267,"passed_time":9.620235131,"remaining_time":62.17256436,"test":[0.6714072396]},
|
||||||
|
{"learn":[0.6659751467],"iteration":268,"passed_time":9.645948482,"remaining_time":62.0711406,"test":[0.6714002677]},
|
||||||
|
{"learn":[0.6659539329],"iteration":269,"passed_time":9.682675077,"remaining_time":62.04084401,"test":[0.6714001163]},
|
||||||
|
{"learn":[0.6659263951],"iteration":270,"passed_time":9.711914203,"remaining_time":61.96272936,"test":[0.6713933952]},
|
||||||
|
{"learn":[0.6659038921],"iteration":271,"passed_time":9.739142426,"remaining_time":61.87219894,"test":[0.6713926761]},
|
||||||
|
{"learn":[0.6658767418],"iteration":272,"passed_time":9.768751964,"remaining_time":61.79719649,"test":[0.6713836619]},
|
||||||
|
{"learn":[0.6658510507],"iteration":273,"passed_time":9.804576737,"remaining_time":61.76167682,"test":[0.6713772112]},
|
||||||
|
{"learn":[0.6658210119],"iteration":274,"passed_time":9.848653906,"remaining_time":61.77791996,"test":[0.6713603715]},
|
||||||
|
{"learn":[0.6657963011],"iteration":275,"passed_time":9.88663261,"remaining_time":61.75563268,"test":[0.6713560246]},
|
||||||
|
{"learn":[0.6657748552],"iteration":276,"passed_time":9.925808942,"remaining_time":61.74068161,"test":[0.6713837913]},
|
||||||
|
{"learn":[0.6657490013],"iteration":277,"passed_time":9.965409489,"remaining_time":61.72818396,"test":[0.6713684274]},
|
||||||
|
{"learn":[0.665732402],"iteration":278,"passed_time":9.99537326,"remaining_time":61.65604796,"test":[0.6713619356]},
|
||||||
|
{"learn":[0.6657118786],"iteration":279,"passed_time":10.02216777,"remaining_time":61.5647449,"test":[0.6713584836]},
|
||||||
|
{"learn":[0.665684467],"iteration":280,"passed_time":10.05593393,"remaining_time":61.51654955,"test":[0.6713673572]},
|
||||||
|
{"learn":[0.6656584634],"iteration":281,"passed_time":10.08025153,"remaining_time":61.41089406,"test":[0.6713625568]},
|
||||||
|
{"learn":[0.6656309991],"iteration":282,"passed_time":10.11102202,"remaining_time":61.34496401,"test":[0.6713542652]},
|
||||||
|
{"learn":[0.6656073482],"iteration":283,"passed_time":10.14714598,"remaining_time":61.31162855,"test":[0.6713512017]},
|
||||||
|
{"learn":[0.6655890957],"iteration":284,"passed_time":10.17528061,"remaining_time":61.23019734,"test":[0.671342038]},
|
||||||
|
{"learn":[0.6655665563],"iteration":285,"passed_time":10.2021403,"remaining_time":61.14149818,"test":[0.6713279798]},
|
||||||
|
{"learn":[0.6655452454],"iteration":286,"passed_time":10.23423432,"remaining_time":61.08447174,"test":[0.6713123285]},
|
||||||
|
{"learn":[0.6655255286],"iteration":287,"passed_time":10.26481698,"remaining_time":61.0186343,"test":[0.6713035326]},
|
||||||
|
{"learn":[0.6655053548],"iteration":288,"passed_time":10.29945844,"remaining_time":60.97707056,"test":[0.6713022203]},
|
||||||
|
{"learn":[0.6654893396],"iteration":289,"passed_time":10.32366496,"remaining_time":60.87402441,"test":[0.671296041]},
|
||||||
|
{"learn":[0.6654648912],"iteration":290,"passed_time":10.35344703,"remaining_time":60.80426453,"test":[0.6712829551]},
|
||||||
|
{"learn":[0.6654442759],"iteration":291,"passed_time":10.3949915,"remaining_time":60.8035804,"test":[0.6712769751]},
|
||||||
|
{"learn":[0.6654173127],"iteration":292,"passed_time":10.43148765,"remaining_time":60.77320621,"test":[0.6712702915]},
|
||||||
|
{"learn":[0.6653914518],"iteration":293,"passed_time":10.47162738,"remaining_time":60.76393303,"test":[0.6712379343]},
|
||||||
|
{"learn":[0.6653648946],"iteration":294,"passed_time":10.50360107,"remaining_time":60.70725362,"test":[0.6712192006]},
|
||||||
|
{"learn":[0.665344141],"iteration":295,"passed_time":10.53460819,"remaining_time":60.64517686,"test":[0.6712074061]},
|
||||||
|
{"learn":[0.6653140817],"iteration":296,"passed_time":10.57659448,"remaining_time":60.64626395,"test":[0.6711953324]},
|
||||||
|
{"learn":[0.665295365],"iteration":297,"passed_time":10.61260262,"remaining_time":60.61291829,"test":[0.6711891001]},
|
||||||
|
{"learn":[0.6652787488],"iteration":298,"passed_time":10.63910358,"remaining_time":60.52546889,"test":[0.6711870526]},
|
||||||
|
{"learn":[0.6652502991],"iteration":299,"passed_time":10.6681867,"remaining_time":60.45305797,"test":[0.6711812809]},
|
||||||
|
{"learn":[0.665231168],"iteration":300,"passed_time":10.70260503,"remaining_time":60.41104967,"test":[0.6711768946]},
|
||||||
|
{"learn":[0.6652136682],"iteration":301,"passed_time":10.72952096,"remaining_time":60.32690925,"test":[0.6711845012]},
|
||||||
|
{"learn":[0.6651903001],"iteration":302,"passed_time":10.76489952,"remaining_time":60.29054288,"test":[0.6711869636]},
|
||||||
|
{"learn":[0.6651697153],"iteration":303,"passed_time":10.80197155,"remaining_time":60.26363073,"test":[0.671186884]},
|
||||||
|
{"learn":[0.6651525958],"iteration":304,"passed_time":10.82922271,"remaining_time":60.18207375,"test":[0.6711890401]},
|
||||||
|
{"learn":[0.6651322685],"iteration":305,"passed_time":10.8578399,"remaining_time":60.10843394,"test":[0.6711868603]},
|
||||||
|
{"learn":[0.6651113828],"iteration":306,"passed_time":10.89228879,"remaining_time":60.06724727,"test":[0.6711900892]},
|
||||||
|
{"learn":[0.6650886807],"iteration":307,"passed_time":10.93056436,"remaining_time":60.04712628,"test":[0.6711884242]},
|
||||||
|
{"learn":[0.6650622251],"iteration":308,"passed_time":10.97231236,"remaining_time":60.04589061,"test":[0.6711837119]},
|
||||||
|
{"learn":[0.6650429987],"iteration":309,"passed_time":11.00296848,"remaining_time":59.98392494,"test":[0.6711766645]},
|
||||||
|
{"learn":[0.665015513],"iteration":310,"passed_time":11.03002276,"remaining_time":59.90259947,"test":[0.671172959]},
|
||||||
|
{"learn":[0.6650019022],"iteration":311,"passed_time":11.05828865,"remaining_time":59.82817707,"test":[0.6711740433]},
|
||||||
|
{"learn":[0.664979951],"iteration":312,"passed_time":11.09287745,"remaining_time":59.78812863,"test":[0.6711715069]},
|
||||||
|
{"learn":[0.6649549638],"iteration":313,"passed_time":11.1177757,"remaining_time":59.69608229,"test":[0.6711589843]},
|
||||||
|
{"learn":[0.6649340455],"iteration":314,"passed_time":11.14959087,"remaining_time":59.64146228,"test":[0.6711446402]},
|
||||||
|
{"learn":[0.6649162445],"iteration":315,"passed_time":11.18718772,"remaining_time":59.61779784,"test":[0.6711415366]},
|
||||||
|
{"learn":[0.6649048119],"iteration":316,"passed_time":11.21179073,"remaining_time":59.52505932,"test":[0.6711359351]},
|
||||||
|
{"learn":[0.6648796463],"iteration":317,"passed_time":11.24311165,"remaining_time":59.46828238,"test":[0.671143361]},
|
||||||
|
{"learn":[0.6648605481],"iteration":318,"passed_time":11.27486028,"remaining_time":59.41391889,"test":[0.6711353638]},
|
||||||
|
{"learn":[0.6648429084],"iteration":319,"passed_time":11.30400807,"remaining_time":59.34604237,"test":[0.6711444387]},
|
||||||
|
{"learn":[0.6648238121],"iteration":320,"passed_time":11.33488419,"remaining_time":59.28744721,"test":[0.6711487352]},
|
||||||
|
{"learn":[0.6647969527],"iteration":321,"passed_time":11.36208838,"remaining_time":59.20988915,"test":[0.67114436]},
|
||||||
|
{"learn":[0.6647854723],"iteration":322,"passed_time":11.39429642,"remaining_time":59.15862259,"test":[0.6711444722]},
|
||||||
|
{"learn":[0.6647589304],"iteration":323,"passed_time":11.4363998,"remaining_time":59.15866068,"test":[0.6711325635]},
|
||||||
|
{"learn":[0.6647429024],"iteration":324,"passed_time":11.47751019,"remaining_time":59.15332173,"test":[0.6711269403]},
|
||||||
|
{"learn":[0.6647237508],"iteration":325,"passed_time":11.5136833,"remaining_time":59.12241054,"test":[0.6711154078]},
|
||||||
|
{"learn":[0.6647059396],"iteration":326,"passed_time":11.54795566,"remaining_time":59.08174257,"test":[0.6711203043]},
|
||||||
|
{"learn":[0.664686288],"iteration":327,"passed_time":11.57245915,"remaining_time":58.99131613,"test":[0.6711241333]},
|
||||||
|
{"learn":[0.6646532527],"iteration":328,"passed_time":11.60790333,"remaining_time":58.95685857,"test":[0.6711213497]},
|
||||||
|
{"learn":[0.6646306438],"iteration":329,"passed_time":11.63787346,"remaining_time":58.89469298,"test":[0.6711231641]},
|
||||||
|
{"learn":[0.6646098516],"iteration":330,"passed_time":11.66805718,"remaining_time":58.83379887,"test":[0.6711049215]},
|
||||||
|
{"learn":[0.6645858284],"iteration":331,"passed_time":11.70070223,"remaining_time":58.78545579,"test":[0.6711031963]},
|
||||||
|
{"learn":[0.6645707188],"iteration":332,"passed_time":11.724753,"remaining_time":58.69418391,"test":[0.6710996314]},
|
||||||
|
{"learn":[0.6645485788],"iteration":333,"passed_time":11.75795297,"remaining_time":58.64895104,"test":[0.6710867309]},
|
||||||
|
{"learn":[0.6645305696],"iteration":334,"passed_time":11.78053066,"remaining_time":58.55099567,"test":[0.6710914578]},
|
||||||
|
{"learn":[0.6645108881],"iteration":335,"passed_time":11.81570271,"remaining_time":58.51586106,"test":[0.6710929585]},
|
||||||
|
{"learn":[0.6644923286],"iteration":336,"passed_time":11.8448851,"remaining_time":58.45116888,"test":[0.6710984779]},
|
||||||
|
{"learn":[0.6644805222],"iteration":337,"passed_time":11.86964023,"remaining_time":58.36491734,"test":[0.6710923199]},
|
||||||
|
{"learn":[0.6644572776],"iteration":338,"passed_time":11.90591446,"remaining_time":58.33546879,"test":[0.6710893917]},
|
||||||
|
{"learn":[0.6644320741],"iteration":339,"passed_time":11.94145444,"remaining_time":58.30239521,"test":[0.6710923306]},
|
||||||
|
{"learn":[0.6644115048],"iteration":340,"passed_time":11.98658051,"remaining_time":58.31594449,"test":[0.6710927901]},
|
||||||
|
{"learn":[0.6643949013],"iteration":341,"passed_time":12.02038848,"remaining_time":58.27428098,"test":[0.6711092802]},
|
||||||
|
{"learn":[0.6643619789],"iteration":342,"passed_time":12.06653941,"remaining_time":58.29229096,"test":[0.6711012995]},
|
||||||
|
{"learn":[0.6643389502],"iteration":343,"passed_time":12.12283646,"remaining_time":58.35877087,"test":[0.6711015305]},
|
||||||
|
{"learn":[0.6643088915],"iteration":344,"passed_time":12.17733618,"remaining_time":58.41591705,"test":[0.6710975574]},
|
||||||
|
{"learn":[0.664286972],"iteration":345,"passed_time":12.22133732,"remaining_time":58.42223099,"test":[0.6710899474]},
|
||||||
|
{"learn":[0.664274149],"iteration":346,"passed_time":12.2642467,"remaining_time":58.42305415,"test":[0.671085152]},
|
||||||
|
{"learn":[0.6642536926],"iteration":347,"passed_time":12.30091895,"remaining_time":58.39401755,"test":[0.6710814533]},
|
||||||
|
{"learn":[0.6642357634],"iteration":348,"passed_time":12.32484094,"remaining_time":58.30462002,"test":[0.6710701892]},
|
||||||
|
{"learn":[0.664207914],"iteration":349,"passed_time":12.35469303,"remaining_time":58.24355287,"test":[0.67105503]},
|
||||||
|
{"learn":[0.6641853097],"iteration":350,"passed_time":12.40148755,"remaining_time":58.26225919,"test":[0.6710527861]},
|
||||||
|
{"learn":[0.6641654917],"iteration":351,"passed_time":12.43803877,"remaining_time":58.23263605,"test":[0.6710508715]},
|
||||||
|
{"learn":[0.664143804],"iteration":352,"passed_time":12.47995438,"remaining_time":58.22800245,"test":[0.6710560803]},
|
||||||
|
{"learn":[0.6641290647],"iteration":353,"passed_time":12.51241326,"remaining_time":58.17918707,"test":[0.6710465693]},
|
||||||
|
{"learn":[0.6641117244],"iteration":354,"passed_time":12.5417829,"remaining_time":58.11614893,"test":[0.6710440741]},
|
||||||
|
{"learn":[0.6640880219],"iteration":355,"passed_time":12.5692936,"remaining_time":58.0447154,"test":[0.6710496913]},
|
||||||
|
{"learn":[0.6640669415],"iteration":356,"passed_time":12.5976392,"remaining_time":57.97737034,"test":[0.6710404659]},
|
||||||
|
{"learn":[0.6640462999],"iteration":357,"passed_time":12.62815847,"remaining_time":57.92021287,"test":[0.6710293986]},
|
||||||
|
{"learn":[0.664030296],"iteration":358,"passed_time":12.65342509,"remaining_time":57.8391938,"test":[0.6710353817]},
|
||||||
|
{"learn":[0.6640028542],"iteration":359,"passed_time":12.68233453,"remaining_time":57.77507954,"test":[0.6710271815]},
|
||||||
|
{"learn":[0.6639813347],"iteration":360,"passed_time":12.72037964,"remaining_time":57.75263774,"test":[0.6710288077]},
|
||||||
|
{"learn":[0.6639597941],"iteration":361,"passed_time":12.744473,"remaining_time":57.66698004,"test":[0.6710169894]},
|
||||||
|
{"learn":[0.6639429832],"iteration":362,"passed_time":12.77086568,"remaining_time":57.59203063,"test":[0.6710119848]},
|
||||||
|
{"learn":[0.6639222708],"iteration":363,"passed_time":12.81194554,"remaining_time":57.58335961,"test":[0.6710114775]},
|
||||||
|
{"learn":[0.6639065546],"iteration":364,"passed_time":12.84133287,"remaining_time":57.52213492,"test":[0.6710013614]},
|
||||||
|
{"learn":[0.6638823236],"iteration":365,"passed_time":12.87057337,"remaining_time":57.46042866,"test":[0.6709985657]},
|
||||||
|
{"learn":[0.6638648195],"iteration":366,"passed_time":12.8971183,"remaining_time":57.38690512,"test":[0.6709948954]},
|
||||||
|
{"learn":[0.6638436235],"iteration":367,"passed_time":12.93825161,"remaining_time":57.37833324,"test":[0.6709970591]},
|
||||||
|
{"learn":[0.6638208732],"iteration":368,"passed_time":12.97444296,"remaining_time":57.3477411,"test":[0.6709739289]},
|
||||||
|
{"learn":[0.6637956357],"iteration":369,"passed_time":13.00974924,"remaining_time":57.31321963,"test":[0.6709754911]},
|
||||||
|
{"learn":[0.6637718453],"iteration":370,"passed_time":13.03832239,"remaining_time":57.24912984,"test":[0.6709717066]},
|
||||||
|
{"learn":[0.663756918],"iteration":371,"passed_time":13.07843077,"remaining_time":57.23571316,"test":[0.67096845]},
|
||||||
|
{"learn":[0.6637353525],"iteration":372,"passed_time":13.11729124,"remaining_time":57.21671005,"test":[0.6709739445]},
|
||||||
|
{"learn":[0.6637143112],"iteration":373,"passed_time":13.14745329,"remaining_time":57.15978354,"test":[0.6709728881]},
|
||||||
|
{"learn":[0.6636956547],"iteration":374,"passed_time":13.18118022,"remaining_time":57.11844761,"test":[0.6709694284]},
|
||||||
|
{"learn":[0.663680995],"iteration":375,"passed_time":13.20539229,"remaining_time":57.03605604,"test":[0.6709604166]},
|
||||||
|
{"learn":[0.66366728],"iteration":376,"passed_time":13.23563977,"remaining_time":56.97995583,"test":[0.6709605025]},
|
||||||
|
{"learn":[0.6636487567],"iteration":377,"passed_time":13.27428255,"remaining_time":56.96001665,"test":[0.6709603727]},
|
||||||
|
{"learn":[0.6636266904],"iteration":378,"passed_time":13.30625754,"remaining_time":56.91146033,"test":[0.670944339]},
|
||||||
|
{"learn":[0.6636116064],"iteration":379,"passed_time":13.33327871,"remaining_time":56.84187241,"test":[0.6709447187]},
|
||||||
|
{"learn":[0.6635902746],"iteration":380,"passed_time":13.36632239,"remaining_time":56.79809961,"test":[0.6709538679]},
|
||||||
|
{"learn":[0.6635654896],"iteration":381,"passed_time":13.39639051,"remaining_time":56.74177969,"test":[0.6709640912]},
|
||||||
|
{"learn":[0.6635393029],"iteration":382,"passed_time":13.42189438,"remaining_time":56.66632694,"test":[0.6709534847]},
|
||||||
|
{"learn":[0.6635171734],"iteration":383,"passed_time":13.46730432,"remaining_time":56.6749057,"test":[0.6709471555]},
|
||||||
|
{"learn":[0.663500789],"iteration":384,"passed_time":13.50832777,"remaining_time":56.66480351,"test":[0.6709506783]},
|
||||||
|
{"learn":[0.663477743],"iteration":385,"passed_time":13.54029627,"remaining_time":56.61667921,"test":[0.6709546729]},
|
||||||
|
{"learn":[0.6634584806],"iteration":386,"passed_time":13.56996301,"remaining_time":56.5590448,"test":[0.670930774]},
|
||||||
|
{"learn":[0.6634337499],"iteration":387,"passed_time":13.59835745,"remaining_time":56.4962686,"test":[0.6709287322]},
|
||||||
|
{"learn":[0.6634135584],"iteration":388,"passed_time":13.6279617,"remaining_time":56.43867943,"test":[0.6709198643]},
|
||||||
|
{"learn":[0.6633868455],"iteration":389,"passed_time":13.65633448,"remaining_time":56.37615005,"test":[0.6709220389]},
|
||||||
|
{"learn":[0.6633755323],"iteration":390,"passed_time":13.68565529,"remaining_time":56.31769658,"test":[0.6709230923]},
|
||||||
|
{"learn":[0.663356103],"iteration":391,"passed_time":13.71789303,"remaining_time":56.27135714,"test":[0.670930414]},
|
||||||
|
{"learn":[0.6633337631],"iteration":392,"passed_time":13.75060752,"remaining_time":56.2270389,"test":[0.6709354296]},
|
||||||
|
{"learn":[0.663319422],"iteration":393,"passed_time":13.77167974,"remaining_time":56.13532403,"test":[0.6709351544]},
|
||||||
|
{"learn":[0.6632911566],"iteration":394,"passed_time":13.80416242,"remaining_time":56.09033084,"test":[0.6709414935]},
|
||||||
|
{"learn":[0.6632687875],"iteration":395,"passed_time":13.82525369,"remaining_time":55.9992599,"test":[0.6709445943]},
|
||||||
|
{"learn":[0.6632431997],"iteration":396,"passed_time":13.85836516,"remaining_time":55.95707646,"test":[0.6709475685]},
|
||||||
|
{"learn":[0.6632189331],"iteration":397,"passed_time":13.88898168,"remaining_time":55.90489613,"test":[0.6709533591]},
|
||||||
|
{"learn":[0.663201035],"iteration":398,"passed_time":13.91726355,"remaining_time":55.84345598,"test":[0.6709592222]},
|
||||||
|
{"learn":[0.6631898553],"iteration":399,"passed_time":13.95316828,"remaining_time":55.81267311,"test":[0.6709508704]},
|
||||||
|
{"learn":[0.6631712482],"iteration":400,"passed_time":13.99418497,"remaining_time":55.80224881,"test":[0.6709479912]},
|
||||||
|
{"learn":[0.663143025],"iteration":401,"passed_time":14.0253575,"remaining_time":55.75254052,"test":[0.6709417519]},
|
||||||
|
{"learn":[0.663121538],"iteration":402,"passed_time":14.04844239,"remaining_time":55.67087467,"test":[0.6709476082]},
|
||||||
|
{"learn":[0.6631087792],"iteration":403,"passed_time":14.0761289,"remaining_time":55.60767753,"test":[0.6709480979]},
|
||||||
|
{"learn":[0.6630859067],"iteration":404,"passed_time":14.10555105,"remaining_time":55.55149118,"test":[0.6709448724]},
|
||||||
|
{"learn":[0.663066483],"iteration":405,"passed_time":14.1427661,"remaining_time":55.52603242,"test":[0.6709421934]},
|
||||||
|
{"learn":[0.6630443652],"iteration":406,"passed_time":14.18285552,"remaining_time":55.51176619,"test":[0.6709386261]},
|
||||||
|
{"learn":[0.6630250376],"iteration":407,"passed_time":14.21458769,"remaining_time":55.46476372,"test":[0.6709461564]},
|
||||||
|
{"learn":[0.6630007822],"iteration":408,"passed_time":14.24035708,"remaining_time":55.39464088,"test":[0.670934384]},
|
||||||
|
{"learn":[0.6629768728],"iteration":409,"passed_time":14.26711915,"remaining_time":55.32858403,"test":[0.6709312987]},
|
||||||
|
{"learn":[0.6629528093],"iteration":410,"passed_time":14.29943785,"remaining_time":55.28420133,"test":[0.670931806]},
|
||||||
|
{"learn":[0.6629260936],"iteration":411,"passed_time":14.32489173,"remaining_time":55.21341763,"test":[0.6709286111]},
|
||||||
|
{"learn":[0.6629102182],"iteration":412,"passed_time":14.35119075,"remaining_time":55.14610101,"test":[0.6709224729]},
|
||||||
|
{"learn":[0.6628863488],"iteration":413,"passed_time":14.37946054,"remaining_time":55.08653242,"test":[0.6709236504]},
|
||||||
|
{"learn":[0.6628648972],"iteration":414,"passed_time":14.41005914,"remaining_time":55.03600899,"test":[0.6709245901]},
|
||||||
|
{"learn":[0.6628454339],"iteration":415,"passed_time":14.45103793,"remaining_time":55.02510598,"test":[0.6709463437]},
|
||||||
|
{"learn":[0.6628200274],"iteration":416,"passed_time":14.48428995,"remaining_time":54.98472661,"test":[0.6709567049]},
|
||||||
|
{"learn":[0.6627942591],"iteration":417,"passed_time":14.5135184,"remaining_time":54.92915339,"test":[0.670945606]},
|
||||||
|
{"learn":[0.6627744647],"iteration":418,"passed_time":14.53698524,"remaining_time":54.85196578,"test":[0.6709479298]},
|
||||||
|
{"learn":[0.662765485],"iteration":419,"passed_time":14.56542473,"remaining_time":54.79374067,"test":[0.6709464351]},
|
||||||
|
{"learn":[0.6627503257],"iteration":420,"passed_time":14.58728594,"remaining_time":54.71098455,"test":[0.6709414048]},
|
||||||
|
{"learn":[0.6627323029],"iteration":421,"passed_time":14.61501375,"remaining_time":54.65045425,"test":[0.6709414427]},
|
||||||
|
{"learn":[0.6627111509],"iteration":422,"passed_time":14.64231614,"remaining_time":54.58849302,"test":[0.6709296343]},
|
||||||
|
{"learn":[0.6626785863],"iteration":423,"passed_time":14.66665432,"remaining_time":54.51567739,"test":[0.670924721]},
|
||||||
|
{"learn":[0.6626576561],"iteration":424,"passed_time":14.69050441,"remaining_time":54.44128104,"test":[0.670906284]},
|
||||||
|
{"learn":[0.6626363113],"iteration":425,"passed_time":14.71910475,"remaining_time":54.38467341,"test":[0.6708996826]},
|
||||||
|
{"learn":[0.6626181065],"iteration":426,"passed_time":14.73941058,"remaining_time":54.2976413,"test":[0.6708987677]},
|
||||||
|
{"learn":[0.66259794],"iteration":427,"passed_time":14.77242451,"remaining_time":54.25759657,"test":[0.670909526]},
|
||||||
|
{"learn":[0.6625765658],"iteration":428,"passed_time":14.79088688,"remaining_time":54.1642967,"test":[0.6709033226]},
|
||||||
|
{"learn":[0.6625526572],"iteration":429,"passed_time":14.82430966,"remaining_time":54.12596783,"test":[0.6708750209]},
|
||||||
|
{"learn":[0.66253135],"iteration":430,"passed_time":14.84439175,"remaining_time":54.03909666,"test":[0.6708752079]},
|
||||||
|
{"learn":[0.6625035695],"iteration":431,"passed_time":14.8764415,"remaining_time":53.99597284,"test":[0.6708776566]},
|
||||||
|
{"learn":[0.662480212],"iteration":432,"passed_time":14.90666075,"remaining_time":53.94627573,"test":[0.6708736133]},
|
||||||
|
{"learn":[0.6624611632],"iteration":433,"passed_time":14.93845927,"remaining_time":53.90236684,"test":[0.6708754298]},
|
||||||
|
{"learn":[0.6624332625],"iteration":434,"passed_time":14.98024104,"remaining_time":53.89443041,"test":[0.6708751084]},
|
||||||
|
{"learn":[0.6624120584],"iteration":435,"passed_time":15.00605075,"remaining_time":53.82904442,"test":[0.6708642042]},
|
||||||
|
{"learn":[0.6623941719],"iteration":436,"passed_time":15.03384083,"remaining_time":53.77092268,"test":[0.6708610465]},
|
||||||
|
{"learn":[0.6623766304],"iteration":437,"passed_time":15.05972545,"remaining_time":53.70614417,"test":[0.6708574768]},
|
||||||
|
{"learn":[0.6623623329],"iteration":438,"passed_time":15.08505889,"remaining_time":53.63958297,"test":[0.6708557953]},
|
||||||
|
{"learn":[0.6623442925],"iteration":439,"passed_time":15.11080547,"remaining_time":53.57467393,"test":[0.670871378]},
|
||||||
|
{"learn":[0.6623212715],"iteration":440,"passed_time":15.13466304,"remaining_time":53.50326458,"test":[0.6708640187]},
|
||||||
|
{"learn":[0.6623025941],"iteration":441,"passed_time":15.16037021,"remaining_time":53.43859001,"test":[0.6708700565]},
|
||||||
|
{"learn":[0.6622749791],"iteration":442,"passed_time":15.18471062,"remaining_time":53.36928767,"test":[0.6708667534]},
|
||||||
|
{"learn":[0.6622534499],"iteration":443,"passed_time":15.21140556,"remaining_time":53.30843931,"test":[0.6708675383]},
|
||||||
|
{"learn":[0.6622305473],"iteration":444,"passed_time":15.23498219,"remaining_time":53.23684787,"test":[0.6708740175]},
|
||||||
|
{"learn":[0.6622059333],"iteration":445,"passed_time":15.26647355,"remaining_time":53.19304911,"test":[0.6708774523]},
|
||||||
|
{"learn":[0.6621871707],"iteration":446,"passed_time":15.28793136,"remaining_time":53.11444609,"test":[0.6708697231]},
|
||||||
|
{"learn":[0.6621638454],"iteration":447,"passed_time":15.31613827,"remaining_time":53.05947899,"test":[0.6708614971]},
|
||||||
|
{"learn":[0.6621511296],"iteration":448,"passed_time":15.33689091,"remaining_time":52.9788815,"test":[0.6708607946]},
|
||||||
|
{"learn":[0.6621349978],"iteration":449,"passed_time":15.36674634,"remaining_time":52.92990406,"test":[0.6708740865]},
|
||||||
|
{"learn":[0.6621120424],"iteration":450,"passed_time":15.393642,"remaining_time":52.87084582,"test":[0.6708729562]},
|
||||||
|
{"learn":[0.6620958271],"iteration":451,"passed_time":15.42984657,"remaining_time":52.84381082,"test":[0.6708674017]},
|
||||||
|
{"learn":[0.6620793528],"iteration":452,"passed_time":15.46956188,"remaining_time":52.82872456,"test":[0.6708693088]},
|
||||||
|
{"learn":[0.6620572713],"iteration":453,"passed_time":15.49032259,"remaining_time":52.74898396,"test":[0.6708712037]},
|
||||||
|
{"learn":[0.6620395025],"iteration":454,"passed_time":15.52379393,"remaining_time":52.71266289,"test":[0.6708703905]},
|
||||||
|
{"learn":[0.6620188044],"iteration":455,"passed_time":15.55053135,"remaining_time":52.65355352,"test":[0.6708577595]},
|
||||||
|
{"learn":[0.6620017347],"iteration":456,"passed_time":15.57735398,"remaining_time":52.59487352,"test":[0.6708493546]},
|
||||||
|
{"learn":[0.6619811454],"iteration":457,"passed_time":15.60434803,"remaining_time":52.53690973,"test":[0.6708523777]},
|
||||||
|
{"learn":[0.6619695569],"iteration":458,"passed_time":15.63056555,"remaining_time":52.47647387,"test":[0.6708454134]},
|
||||||
|
{"learn":[0.661952377],"iteration":459,"passed_time":15.656355,"remaining_time":52.41475368,"test":[0.6708404483]},
|
||||||
|
{"learn":[0.6619237442],"iteration":460,"passed_time":15.68232112,"remaining_time":52.35377918,"test":[0.6708274771]},
|
||||||
|
{"learn":[0.6619089407],"iteration":461,"passed_time":15.71164945,"remaining_time":52.30414904,"test":[0.6708244992]},
|
||||||
|
{"learn":[0.6618886168],"iteration":462,"passed_time":15.7361944,"remaining_time":52.23872743,"test":[0.6708344314]},
|
||||||
|
{"learn":[0.6618831383],"iteration":463,"passed_time":15.76527735,"remaining_time":52.18850433,"test":[0.6708279081]},
|
||||||
|
{"learn":[0.6618690774],"iteration":464,"passed_time":15.78652262,"remaining_time":52.11249942,"test":[0.6708258106]},
|
||||||
|
{"learn":[0.661845878],"iteration":465,"passed_time":15.81756836,"remaining_time":52.06899113,"test":[0.6708049714]},
|
||||||
|
{"learn":[0.6618290213],"iteration":466,"passed_time":15.83979966,"remaining_time":51.99660146,"test":[0.670810989]},
|
||||||
|
{"learn":[0.6618050064],"iteration":467,"passed_time":15.87342473,"remaining_time":51.9617237,"test":[0.6708212237]},
|
||||||
|
{"learn":[0.6617832833],"iteration":468,"passed_time":15.90381555,"remaining_time":51.9162934,"test":[0.6708221741]},
|
||||||
|
{"learn":[0.6617652311],"iteration":469,"passed_time":15.93502938,"remaining_time":51.87360627,"test":[0.6708259658]},
|
||||||
|
{"learn":[0.6617443144],"iteration":470,"passed_time":15.96919221,"remaining_time":51.84054117,"test":[0.6708159692]},
|
||||||
|
{"learn":[0.6617202619],"iteration":471,"passed_time":15.99477329,"remaining_time":51.77968981,"test":[0.6708136212]},
|
||||||
|
{"learn":[0.6617005831],"iteration":472,"passed_time":16.02279091,"remaining_time":51.72685354,"test":[0.6708224942]},
|
||||||
|
{"learn":[0.6616824419],"iteration":473,"passed_time":16.04763422,"remaining_time":51.66390258,"test":[0.6708363084]},
|
||||||
|
{"learn":[0.6616538226],"iteration":474,"passed_time":16.07374645,"remaining_time":51.60518598,"test":[0.670850875]},
|
||||||
|
{"learn":[0.6616314155],"iteration":475,"passed_time":16.09993591,"remaining_time":51.54685363,"test":[0.6708527236]},
|
||||||
|
{"learn":[0.6616127861],"iteration":476,"passed_time":16.12811357,"remaining_time":51.49500411,"test":[0.6708453401]},
|
||||||
|
{"learn":[0.6616029072],"iteration":477,"passed_time":16.15264086,"remaining_time":51.43163051,"test":[0.6708413844]},
|
||||||
|
{"learn":[0.6615843751],"iteration":478,"passed_time":16.17696751,"remaining_time":51.36778201,"test":[0.6708364569]},
|
||||||
|
{"learn":[0.661563216],"iteration":479,"passed_time":16.20551145,"remaining_time":51.31745293,"test":[0.6708251774]},
|
||||||
|
{"learn":[0.6615432257],"iteration":480,"passed_time":16.22860577,"remaining_time":51.2500045,"test":[0.6708154393]},
|
||||||
|
{"learn":[0.6615263324],"iteration":481,"passed_time":16.25544093,"remaining_time":51.19452144,"test":[0.6708111613]},
|
||||||
|
{"learn":[0.6615033259],"iteration":482,"passed_time":16.27729221,"remaining_time":51.12350369,"test":[0.6708102339]},
|
||||||
|
{"learn":[0.661484293],"iteration":483,"passed_time":16.30502335,"remaining_time":51.07110619,"test":[0.6707929623]},
|
||||||
|
{"learn":[0.6614678231],"iteration":484,"passed_time":16.32842702,"remaining_time":51.00529266,"test":[0.6707900226]},
|
||||||
|
{"learn":[0.6614463024],"iteration":485,"passed_time":16.36272839,"remaining_time":50.97360242,"test":[0.6707832384]},
|
||||||
|
{"learn":[0.6614155436],"iteration":486,"passed_time":16.39272506,"remaining_time":50.92852776,"test":[0.6707739118]},
|
||||||
|
{"learn":[0.6613958945],"iteration":487,"passed_time":16.42636604,"remaining_time":50.89480625,"test":[0.6707737538]},
|
||||||
|
{"learn":[0.661380611],"iteration":488,"passed_time":16.4597142,"remaining_time":50.86018027,"test":[0.6707730234]},
|
||||||
|
{"learn":[0.6613677802],"iteration":489,"passed_time":16.48056007,"remaining_time":50.78703206,"test":[0.6707796291]},
|
||||||
|
{"learn":[0.6613530086],"iteration":490,"passed_time":16.51091177,"remaining_time":50.74331132,"test":[0.670791408]},
|
||||||
|
{"learn":[0.6613248211],"iteration":491,"passed_time":16.53097438,"remaining_time":50.66810846,"test":[0.6707944906]},
|
||||||
|
{"learn":[0.6613059359],"iteration":492,"passed_time":16.56161402,"remaining_time":50.62546112,"test":[0.6707835635]},
|
||||||
|
{"learn":[0.6612729965],"iteration":493,"passed_time":16.5854633,"remaining_time":50.56216139,"test":[0.6707908928]},
|
||||||
|
{"learn":[0.6612624948],"iteration":494,"passed_time":16.61302735,"remaining_time":50.51031547,"test":[0.670796262]},
|
||||||
|
{"learn":[0.6612401679],"iteration":495,"passed_time":16.63896978,"remaining_time":50.45365029,"test":[0.6707877825]},
|
||||||
|
{"learn":[0.6612191637],"iteration":496,"passed_time":16.663707,"remaining_time":50.39346403,"test":[0.6707854132]},
|
||||||
|
{"learn":[0.6611912219],"iteration":497,"passed_time":16.69040179,"remaining_time":50.33932428,"test":[0.6707756206]},
|
||||||
|
{"learn":[0.6611773017],"iteration":498,"passed_time":16.71612789,"remaining_time":50.28238068,"test":[0.6707707899]},
|
||||||
|
{"learn":[0.6611638216],"iteration":499,"passed_time":16.74072553,"remaining_time":50.2221766,"test":[0.6707704386]},
|
||||||
|
{"learn":[0.6611450533],"iteration":500,"passed_time":16.77346538,"remaining_time":50.18647626,"test":[0.6707621465]},
|
||||||
|
{"learn":[0.6611179111],"iteration":501,"passed_time":16.80230735,"remaining_time":50.13915621,"test":[0.6707661931]},
|
||||||
|
{"learn":[0.6610959069],"iteration":502,"passed_time":16.83637769,"remaining_time":50.10747,"test":[0.6707651988]},
|
||||||
|
{"learn":[0.6610728788],"iteration":503,"passed_time":16.87382128,"remaining_time":50.08578697,"test":[0.6707607827]},
|
||||||
|
{"learn":[0.6610436668],"iteration":504,"passed_time":16.92151611,"remaining_time":50.09438927,"test":[0.670760242]},
|
||||||
|
{"learn":[0.6610188976],"iteration":505,"passed_time":16.9898618,"remaining_time":50.16374216,"test":[0.6707506008]},
|
||||||
|
{"learn":[0.6610030555],"iteration":506,"passed_time":17.03818668,"remaining_time":50.17359509,"test":[0.6707452886]},
|
||||||
|
{"learn":[0.6609831174],"iteration":507,"passed_time":17.06933058,"remaining_time":50.13275833,"test":[0.6707355189]},
|
||||||
|
{"learn":[0.6609586562],"iteration":508,"passed_time":17.1106164,"remaining_time":50.12166807,"test":[0.6707312551]},
|
||||||
|
{"learn":[0.660935882],"iteration":509,"passed_time":17.14537899,"remaining_time":50.09140137,"test":[0.6707199485]},
|
||||||
|
{"learn":[0.6609202024],"iteration":510,"passed_time":17.19066307,"remaining_time":50.09177556,"test":[0.6707131947]},
|
||||||
|
{"learn":[0.6609011137],"iteration":511,"passed_time":17.21958034,"remaining_time":50.04440537,"test":[0.6707154112]},
|
||||||
|
{"learn":[0.6608726737],"iteration":512,"passed_time":17.24756917,"remaining_time":49.99441591,"test":[0.6706982346]},
|
||||||
|
{"learn":[0.6608608849],"iteration":513,"passed_time":17.27150822,"remaining_time":49.93280391,"test":[0.6706988941]},
|
||||||
|
{"learn":[0.6608387256],"iteration":514,"passed_time":17.29800365,"remaining_time":49.87870957,"test":[0.6706989098]},
|
||||||
|
{"learn":[0.6608136063],"iteration":515,"passed_time":17.34332283,"remaining_time":49.87885868,"test":[0.670693306]},
|
||||||
|
{"learn":[0.6607946343],"iteration":516,"passed_time":17.37393636,"remaining_time":49.83664916,"test":[0.6706944515]},
|
||||||
|
{"learn":[0.6607703935],"iteration":517,"passed_time":17.4173655,"remaining_time":49.83114994,"test":[0.6706899688]},
|
||||||
|
{"learn":[0.6607509625],"iteration":518,"passed_time":17.46008645,"remaining_time":49.82348368,"test":[0.6706909374]},
|
||||||
|
{"learn":[0.6607238109],"iteration":519,"passed_time":17.4906988,"remaining_time":49.78121967,"test":[0.6706855074]},
|
||||||
|
{"learn":[0.6606999858],"iteration":520,"passed_time":17.5186435,"remaining_time":49.7314275,"test":[0.6706787779]},
|
||||||
|
{"learn":[0.6606813873],"iteration":521,"passed_time":17.54613056,"remaining_time":49.6804233,"test":[0.6706737082]},
|
||||||
|
{"learn":[0.6606610372],"iteration":522,"passed_time":17.57100039,"remaining_time":49.62211774,"test":[0.6706761225]},
|
||||||
|
{"learn":[0.660638456],"iteration":523,"passed_time":17.60084283,"remaining_time":49.5779466,"test":[0.670685455]},
|
||||||
|
{"learn":[0.6606156483],"iteration":524,"passed_time":17.62599925,"remaining_time":49.52066456,"test":[0.6706693855]},
|
||||||
|
{"learn":[0.6605968623],"iteration":525,"passed_time":17.65519625,"remaining_time":49.47482751,"test":[0.6706647216]},
|
||||||
|
{"learn":[0.6605735776],"iteration":526,"passed_time":17.67910836,"remaining_time":49.41428199,"test":[0.6706569188]},
|
||||||
|
{"learn":[0.6605517294],"iteration":527,"passed_time":17.70744827,"remaining_time":49.36621942,"test":[0.6706549134]},
|
||||||
|
{"learn":[0.6605309239],"iteration":528,"passed_time":17.72943083,"remaining_time":49.3005534,"test":[0.6706547978]},
|
||||||
|
{"learn":[0.6605086434],"iteration":529,"passed_time":17.75830336,"remaining_time":49.25416215,"test":[0.6706564214]},
|
||||||
|
{"learn":[0.6604803349],"iteration":530,"passed_time":17.78141858,"remaining_time":49.19190939,"test":[0.6706559196]},
|
||||||
|
{"learn":[0.6604566326],"iteration":531,"passed_time":17.80870208,"remaining_time":49.14130574,"test":[0.6706515072]},
|
||||||
|
{"learn":[0.6604430839],"iteration":532,"passed_time":17.82904188,"remaining_time":49.07167811,"test":[0.6706474616]},
|
||||||
|
{"learn":[0.6604273738],"iteration":533,"passed_time":17.86246645,"remaining_time":49.03815696,"test":[0.6706424204]},
|
||||||
|
{"learn":[0.6604048016],"iteration":534,"passed_time":17.90552779,"remaining_time":49.03102469,"test":[0.6706520008]},
|
||||||
|
{"learn":[0.6603845173],"iteration":535,"passed_time":18.02843143,"remaining_time":49.24183511,"test":[0.6706448306]},
|
||||||
|
{"learn":[0.6603669212],"iteration":536,"passed_time":18.07245966,"remaining_time":49.23651485,"test":[0.6706415789]},
|
||||||
|
{"learn":[0.6603488983],"iteration":537,"passed_time":18.10631942,"remaining_time":49.20341819,"test":[0.6706305359]},
|
||||||
|
{"learn":[0.6603176881],"iteration":538,"passed_time":18.13531438,"remaining_time":49.1571323,"test":[0.6706152774]},
|
||||||
|
{"learn":[0.6602953862],"iteration":539,"passed_time":18.16575265,"remaining_time":49.11481272,"test":[0.670616585]},
|
||||||
|
{"learn":[0.6602672025],"iteration":540,"passed_time":18.20025584,"remaining_time":49.08349958,"test":[0.6705963243]},
|
||||||
|
{"learn":[0.6602568636],"iteration":541,"passed_time":18.22381751,"remaining_time":49.02274158,"test":[0.6706027368]},
|
||||||
|
{"learn":[0.660235705],"iteration":542,"passed_time":18.25438575,"remaining_time":48.98092088,"test":[0.6706003522]},
|
||||||
|
{"learn":[0.6602152295],"iteration":543,"passed_time":18.28070524,"remaining_time":48.9277699,"test":[0.6706044301]},
|
||||||
|
{"learn":[0.6601897709],"iteration":544,"passed_time":18.30768805,"remaining_time":48.87648827,"test":[0.6706047241]},
|
||||||
|
{"learn":[0.6601683731],"iteration":545,"passed_time":18.33807201,"remaining_time":48.83435294,"test":[0.6706038235]},
|
||||||
|
{"learn":[0.6601472267],"iteration":546,"passed_time":18.36776304,"remaining_time":48.79041993,"test":[0.6706026913]},
|
||||||
|
{"learn":[0.6601262337],"iteration":547,"passed_time":18.41134623,"remaining_time":48.78334803,"test":[0.6705845786]},
|
||||||
|
{"learn":[0.6601119991],"iteration":548,"passed_time":18.44405381,"remaining_time":48.74739905,"test":[0.6705873967]},
|
||||||
|
{"learn":[0.6600869973],"iteration":549,"passed_time":18.47010718,"remaining_time":48.69391893,"test":[0.6705755426]},
|
||||||
|
{"learn":[0.6600667497],"iteration":550,"passed_time":18.5036553,"remaining_time":48.66024779,"test":[0.6705715731]},
|
||||||
|
{"learn":[0.6600397508],"iteration":551,"passed_time":18.53164471,"remaining_time":48.61199556,"test":[0.6705757153]},
|
||||||
|
{"learn":[0.660016863],"iteration":552,"passed_time":18.5577607,"remaining_time":48.55891452,"test":[0.6705516814]},
|
||||||
|
{"learn":[0.6599933158],"iteration":553,"passed_time":18.58492994,"remaining_time":48.50867995,"test":[0.6705530864]},
|
||||||
|
{"learn":[0.6599632649],"iteration":554,"passed_time":18.62562092,"remaining_time":48.49373376,"test":[0.6705552479]},
|
||||||
|
{"learn":[0.6599446007],"iteration":555,"passed_time":18.65010209,"remaining_time":48.43659608,"test":[0.6705563336]},
|
||||||
|
{"learn":[0.6599138126],"iteration":556,"passed_time":18.67796421,"remaining_time":48.38833458,"test":[0.6705718544]},
|
||||||
|
{"learn":[0.6598965504],"iteration":557,"passed_time":18.70319381,"remaining_time":48.33334314,"test":[0.6705688384]},
|
||||||
|
{"learn":[0.6598785723],"iteration":558,"passed_time":18.72995694,"remaining_time":48.28241136,"test":[0.6705641528]},
|
||||||
|
{"learn":[0.659860838],"iteration":559,"passed_time":18.75657945,"remaining_time":48.23120429,"test":[0.6705628467]},
|
||||||
|
{"learn":[0.6598408724],"iteration":560,"passed_time":18.78181322,"remaining_time":48.17652269,"test":[0.670558488]},
|
||||||
|
{"learn":[0.6598244857],"iteration":561,"passed_time":18.80867415,"remaining_time":48.12610931,"test":[0.6705544404]},
|
||||||
|
{"learn":[0.6598082469],"iteration":562,"passed_time":18.83488797,"remaining_time":48.0741279,"test":[0.6705617451]},
|
||||||
|
{"learn":[0.6597851673],"iteration":563,"passed_time":18.86939449,"remaining_time":48.04335193,"test":[0.6705631717]},
|
||||||
|
{"learn":[0.6597683521],"iteration":564,"passed_time":18.90235988,"remaining_time":48.00864854,"test":[0.6705636201]},
|
||||||
|
{"learn":[0.6597479006],"iteration":565,"passed_time":18.93001053,"remaining_time":47.96048604,"test":[0.6705537522]},
|
||||||
|
{"learn":[0.6597310938],"iteration":566,"passed_time":18.95858079,"remaining_time":47.91472006,"test":[0.670555083]},
|
||||||
|
{"learn":[0.6597096581],"iteration":567,"passed_time":18.9833487,"remaining_time":47.85942842,"test":[0.6705524541]},
|
||||||
|
{"learn":[0.6596862311],"iteration":568,"passed_time":19.0162481,"remaining_time":47.82469425,"test":[0.6705503132]},
|
||||||
|
{"learn":[0.6596574779],"iteration":569,"passed_time":19.03781666,"remaining_time":47.76154004,"test":[0.6705354602]},
|
||||||
|
{"learn":[0.6596385418],"iteration":570,"passed_time":19.0681355,"remaining_time":47.72043018,"test":[0.6705387012]},
|
||||||
|
{"learn":[0.6596189903],"iteration":571,"passed_time":19.09073714,"remaining_time":47.66009201,"test":[0.6705411923]},
|
||||||
|
{"learn":[0.65959275],"iteration":572,"passed_time":19.11146842,"remaining_time":47.59522765,"test":[0.6705390018]},
|
||||||
|
{"learn":[0.6595730662],"iteration":573,"passed_time":19.141368,"remaining_time":47.55329403,"test":[0.6705354939]},
|
||||||
|
{"learn":[0.6595566809],"iteration":574,"passed_time":19.16428373,"remaining_time":47.49409447,"test":[0.670531296]},
|
||||||
|
{"learn":[0.6595365076],"iteration":575,"passed_time":19.19652276,"remaining_time":47.45807015,"test":[0.6705377163]},
|
||||||
|
{"learn":[0.6595163446],"iteration":576,"passed_time":19.21727405,"remaining_time":47.39372785,"test":[0.6705248875]},
|
||||||
|
{"learn":[0.6594816637],"iteration":577,"passed_time":19.24969594,"remaining_time":47.35824848,"test":[0.6705252902]},
|
||||||
|
{"learn":[0.6594570142],"iteration":578,"passed_time":19.27445137,"remaining_time":47.30396442,"test":[0.6705181562]},
|
||||||
|
{"learn":[0.6594353055],"iteration":579,"passed_time":19.29822455,"remaining_time":47.24737734,"test":[0.6705123446]},
|
||||||
|
{"learn":[0.6594162362],"iteration":580,"passed_time":19.32403522,"remaining_time":47.19587948,"test":[0.6705128345]},
|
||||||
|
{"learn":[0.659395036],"iteration":581,"passed_time":19.35739555,"remaining_time":47.16286408,"test":[0.6705173712]},
|
||||||
|
{"learn":[0.6593798831],"iteration":582,"passed_time":19.39112791,"remaining_time":47.13075172,"test":[0.670541941]},
|
||||||
|
{"learn":[0.6593556719],"iteration":583,"passed_time":19.42704318,"remaining_time":47.1039266,"test":[0.6705463243]},
|
||||||
|
{"learn":[0.6593292627],"iteration":584,"passed_time":19.46022169,"remaining_time":47.07045077,"test":[0.6705513215]},
|
||||||
|
{"learn":[0.6592976737],"iteration":585,"passed_time":19.48332075,"remaining_time":47.01265452,"test":[0.6705455889]},
|
||||||
|
{"learn":[0.6592754841],"iteration":586,"passed_time":19.5115578,"remaining_time":46.9673444,"test":[0.6705408087]},
|
||||||
|
{"learn":[0.6592510441],"iteration":587,"passed_time":19.54275193,"remaining_time":46.92919341,"test":[0.6705510193]},
|
||||||
|
{"learn":[0.6592290326],"iteration":588,"passed_time":19.56411389,"remaining_time":46.86751222,"test":[0.6705456751]},
|
||||||
|
{"learn":[0.6592097404],"iteration":589,"passed_time":19.59700884,"remaining_time":46.8335296,"test":[0.6705402427]},
|
||||||
|
{"learn":[0.6591876204],"iteration":590,"passed_time":19.62169623,"remaining_time":46.77998306,"test":[0.6705443402]},
|
||||||
|
{"learn":[0.6591705995],"iteration":591,"passed_time":19.64747626,"remaining_time":46.72913272,"test":[0.67054441]},
|
||||||
|
{"learn":[0.6591456195],"iteration":592,"passed_time":19.67090184,"remaining_time":46.67278059,"test":[0.6705441955]},
|
||||||
|
{"learn":[0.6591107122],"iteration":593,"passed_time":19.69910949,"remaining_time":46.62785848,"test":[0.6705319356]},
|
||||||
|
{"learn":[0.6590819533],"iteration":594,"passed_time":19.72694709,"remaining_time":46.58211876,"test":[0.6705358843]},
|
||||||
|
{"learn":[0.6590551327],"iteration":595,"passed_time":19.7530808,"remaining_time":46.53242523,"test":[0.6705334396]},
|
||||||
|
{"learn":[0.6590373916],"iteration":596,"passed_time":19.77835609,"remaining_time":46.48079328,"test":[0.6705320462]},
|
||||||
|
{"learn":[0.6590177149],"iteration":597,"passed_time":19.80378809,"remaining_time":46.4296169,"test":[0.6705332043]},
|
||||||
|
{"learn":[0.6589946095],"iteration":598,"passed_time":19.83052585,"remaining_time":46.38158048,"test":[0.6705328363]},
|
||||||
|
{"learn":[0.6589697628],"iteration":599,"passed_time":19.8579153,"remaining_time":46.33513569,"test":[0.6705315638]},
|
||||||
|
{"learn":[0.6589442269],"iteration":600,"passed_time":19.89600309,"remaining_time":46.31365777,"test":[0.6705274435]},
|
||||||
|
{"learn":[0.6589182437],"iteration":601,"passed_time":19.92518872,"remaining_time":46.27145155,"test":[0.670509808]},
|
||||||
|
{"learn":[0.6588837179],"iteration":602,"passed_time":19.95754179,"remaining_time":46.23662666,"test":[0.6705077789]},
|
||||||
|
{"learn":[0.6588674101],"iteration":603,"passed_time":19.99116426,"remaining_time":46.20474388,"test":[0.6705212132]},
|
||||||
|
{"learn":[0.6588406916],"iteration":604,"passed_time":20.01900069,"remaining_time":46.15951398,"test":[0.6705098442]},
|
||||||
|
{"learn":[0.6588149945],"iteration":605,"passed_time":20.04735837,"remaining_time":46.11554053,"test":[0.6705061509]},
|
||||||
|
{"learn":[0.6587866031],"iteration":606,"passed_time":20.07232044,"remaining_time":46.06382599,"test":[0.6705003071]},
|
||||||
|
{"learn":[0.6587636648],"iteration":607,"passed_time":20.09871086,"remaining_time":46.01546959,"test":[0.6705045031]},
|
||||||
|
{"learn":[0.6587502469],"iteration":608,"passed_time":20.12348304,"remaining_time":45.96348917,"test":[0.6705083194]},
|
||||||
|
{"learn":[0.6587292784],"iteration":609,"passed_time":20.14920752,"remaining_time":45.91376797,"test":[0.6705329997]},
|
||||||
|
{"learn":[0.6587104112],"iteration":610,"passed_time":20.17662353,"remaining_time":45.86797068,"test":[0.6705269987]},
|
||||||
|
{"learn":[0.6586953782],"iteration":611,"passed_time":20.20202219,"remaining_time":45.81765818,"test":[0.6705315607]},
|
||||||
|
{"learn":[0.6586641191],"iteration":612,"passed_time":20.23050051,"remaining_time":45.77439512,"test":[0.6705142835]},
|
||||||
|
{"learn":[0.6586450136],"iteration":613,"passed_time":20.25381994,"remaining_time":45.71953492,"test":[0.6705165015]},
|
||||||
|
{"learn":[0.6586136263],"iteration":614,"passed_time":20.28518384,"remaining_time":45.68289369,"test":[0.6705001061]},
|
||||||
|
{"learn":[0.6585862768],"iteration":615,"passed_time":20.3078175,"remaining_time":45.62665489,"test":[0.6705013916]},
|
||||||
|
{"learn":[0.6585585235],"iteration":616,"passed_time":20.33878033,"remaining_time":45.5891948,"test":[0.6705037253]},
|
||||||
|
{"learn":[0.6585371631],"iteration":617,"passed_time":20.36122842,"remaining_time":45.53271469,"test":[0.67049647]},
|
||||||
|
{"learn":[0.6585092632],"iteration":618,"passed_time":20.3943397,"remaining_time":45.50013429,"test":[0.6705005632]},
|
||||||
|
{"learn":[0.6584914317],"iteration":619,"passed_time":20.42384285,"remaining_time":45.45952119,"test":[0.6704957943]},
|
||||||
|
{"learn":[0.6584662432],"iteration":620,"passed_time":20.45411533,"remaining_time":45.42065225,"test":[0.6704955333]},
|
||||||
|
{"learn":[0.6584454668],"iteration":621,"passed_time":20.488223,"remaining_time":45.39030754,"test":[0.6704961207]},
|
||||||
|
{"learn":[0.6584249408],"iteration":622,"passed_time":20.51043528,"remaining_time":45.33365872,"test":[0.6704921459]},
|
||||||
|
{"learn":[0.6583931228],"iteration":623,"passed_time":20.54384208,"remaining_time":45.30180561,"test":[0.6704751713]},
|
||||||
|
{"learn":[0.6583660767],"iteration":624,"passed_time":20.56912557,"remaining_time":45.25207624,"test":[0.6704753101]},
|
||||||
|
{"learn":[0.658354264],"iteration":625,"passed_time":20.59414123,"remaining_time":45.20183714,"test":[0.6704620888]},
|
||||||
|
{"learn":[0.6583253625],"iteration":626,"passed_time":20.61901142,"remaining_time":45.15135993,"test":[0.6704604282]},
|
||||||
|
{"learn":[0.6582968632],"iteration":627,"passed_time":20.6468542,"remaining_time":45.10745855,"test":[0.6704663192]},
|
||||||
|
{"learn":[0.6582687399],"iteration":628,"passed_time":20.67583093,"remaining_time":45.06607981,"test":[0.6704680085]},
|
||||||
|
{"learn":[0.658242535],"iteration":629,"passed_time":20.7010198,"remaining_time":45.01650336,"test":[0.670453228]},
|
||||||
|
{"learn":[0.6582199874],"iteration":630,"passed_time":20.72783977,"remaining_time":44.97054302,"test":[0.6704577785]},
|
||||||
|
{"learn":[0.6581918101],"iteration":631,"passed_time":20.75222724,"remaining_time":44.91937795,"test":[0.67046675]},
|
||||||
|
{"learn":[0.6581735218],"iteration":632,"passed_time":20.78264004,"remaining_time":44.88130954,"test":[0.6704731863]},
|
||||||
|
{"learn":[0.6581445869],"iteration":633,"passed_time":20.80459182,"remaining_time":44.82503538,"test":[0.6704811116]},
|
||||||
|
{"learn":[0.6581202427],"iteration":634,"passed_time":20.83717209,"remaining_time":44.79171637,"test":[0.6704839644]},
|
||||||
|
{"learn":[0.6580977862],"iteration":635,"passed_time":20.86231353,"remaining_time":44.74244599,"test":[0.6704854798]},
|
||||||
|
{"learn":[0.6580724179],"iteration":636,"passed_time":20.89269601,"remaining_time":44.70446572,"test":[0.6704835837]},
|
||||||
|
{"learn":[0.6580426322],"iteration":637,"passed_time":20.93117347,"remaining_time":44.68379039,"test":[0.6704736198]},
|
||||||
|
{"learn":[0.6580111256],"iteration":638,"passed_time":20.96066949,"remaining_time":44.64392985,"test":[0.6704640242]},
|
||||||
|
{"learn":[0.6579834747],"iteration":639,"passed_time":20.9941179,"remaining_time":44.61250055,"test":[0.670465663]},
|
||||||
|
{"learn":[0.6579541367],"iteration":640,"passed_time":21.0224519,"remaining_time":44.57022174,"test":[0.6704646829]},
|
||||||
|
{"learn":[0.6579254503],"iteration":641,"passed_time":21.0522529,"remaining_time":44.53108946,"test":[0.6704600961]},
|
||||||
|
{"learn":[0.657898555],"iteration":642,"passed_time":21.08260618,"remaining_time":44.49315178,"test":[0.6704643207]},
|
||||||
|
{"learn":[0.6578676875],"iteration":643,"passed_time":21.10716702,"remaining_time":44.44304112,"test":[0.6704600533]},
|
||||||
|
{"learn":[0.6578324163],"iteration":644,"passed_time":21.13594828,"remaining_time":44.40187584,"test":[0.6704614691]},
|
||||||
|
{"learn":[0.6578062223],"iteration":645,"passed_time":21.1601277,"remaining_time":44.35110357,"test":[0.6704728212]},
|
||||||
|
{"learn":[0.6577760631],"iteration":646,"passed_time":21.18552999,"remaining_time":44.30297075,"test":[0.6704758731]},
|
||||||
|
{"learn":[0.6577483474],"iteration":647,"passed_time":21.21048648,"remaining_time":44.25397797,"test":[0.6704833026]},
|
||||||
|
{"learn":[0.6577249642],"iteration":648,"passed_time":21.23686209,"remaining_time":44.20801337,"test":[0.6704767664]},
|
||||||
|
{"learn":[0.6576974966],"iteration":649,"passed_time":21.26287585,"remaining_time":44.16135753,"test":[0.6704702727]},
|
||||||
|
{"learn":[0.657675114],"iteration":650,"passed_time":21.28806218,"remaining_time":44.11305051,"test":[0.6704671372]},
|
||||||
|
{"learn":[0.6576447891],"iteration":651,"passed_time":21.31506267,"remaining_time":44.06856515,"test":[0.6704699936]},
|
||||||
|
{"learn":[0.6576102356],"iteration":652,"passed_time":21.3435081,"remaining_time":44.02711394,"test":[0.6704587989]},
|
||||||
|
{"learn":[0.6575793887],"iteration":653,"passed_time":21.37776713,"remaining_time":43.99766753,"test":[0.6704637668]},
|
||||||
|
{"learn":[0.6575543309],"iteration":654,"passed_time":21.40301154,"remaining_time":43.94969545,"test":[0.6704653717]},
|
||||||
|
{"learn":[0.6575340787],"iteration":655,"passed_time":21.44023109,"remaining_time":43.92632711,"test":[0.6704598273]},
|
||||||
|
{"learn":[0.6575061464],"iteration":656,"passed_time":21.4778965,"remaining_time":43.903828,"test":[0.6704522865]},
|
||||||
|
{"learn":[0.657476113],"iteration":657,"passed_time":21.50245582,"remaining_time":43.85455275,"test":[0.6704558586]},
|
||||||
|
{"learn":[0.6574447014],"iteration":658,"passed_time":21.53379663,"remaining_time":43.81915217,"test":[0.6704466331]},
|
||||||
|
{"learn":[0.6574247361],"iteration":659,"passed_time":21.55955041,"remaining_time":43.77242053,"test":[0.6704405886]},
|
||||||
|
{"learn":[0.6574034983],"iteration":660,"passed_time":21.58626671,"remaining_time":43.72770215,"test":[0.6704463767]},
|
||||||
|
{"learn":[0.6573783832],"iteration":661,"passed_time":21.61183918,"remaining_time":43.68072633,"test":[0.6704475216]},
|
||||||
|
{"learn":[0.657357694],"iteration":662,"passed_time":21.6373217,"remaining_time":43.63363366,"test":[0.6704572386]},
|
||||||
|
{"learn":[0.6573411592],"iteration":663,"passed_time":21.66283476,"remaining_time":43.58666753,"test":[0.6704658153]},
|
||||||
|
{"learn":[0.6573118559],"iteration":664,"passed_time":21.68841321,"remaining_time":43.5398972,"test":[0.6704600945]},
|
||||||
|
{"learn":[0.6572819076],"iteration":665,"passed_time":21.71420973,"remaining_time":43.4936273,"test":[0.6704561998]},
|
||||||
|
{"learn":[0.6572430097],"iteration":666,"passed_time":21.74213421,"remaining_time":43.45167151,"test":[0.6704535154]},
|
||||||
|
{"learn":[0.6572160391],"iteration":667,"passed_time":21.77174463,"remaining_time":43.41311953,"test":[0.6704413781]},
|
||||||
|
{"learn":[0.6571931413],"iteration":668,"passed_time":21.81895309,"remaining_time":43.40960622,"test":[0.6704450013]},
|
||||||
|
{"learn":[0.6571737099],"iteration":669,"passed_time":21.84627583,"remaining_time":43.36648784,"test":[0.6704422199]},
|
||||||
|
{"learn":[0.6571532872],"iteration":670,"passed_time":21.88834724,"remaining_time":43.35262814,"test":[0.67044342]},
|
||||||
|
{"learn":[0.6571208939],"iteration":671,"passed_time":21.93403139,"remaining_time":43.34582395,"test":[0.6704415341]},
|
||||||
|
{"learn":[0.6570887673],"iteration":672,"passed_time":21.9714274,"remaining_time":43.32256191,"test":[0.6704439539]},
|
||||||
|
{"learn":[0.6570633692],"iteration":673,"passed_time":22.01942449,"remaining_time":43.32011406,"test":[0.6704498197]},
|
||||||
|
{"learn":[0.6570454361],"iteration":674,"passed_time":22.05319867,"remaining_time":43.2896122,"test":[0.6704452194]},
|
||||||
|
{"learn":[0.6570231031],"iteration":675,"passed_time":22.09079747,"remaining_time":43.26659149,"test":[0.6704366524]},
|
||||||
|
{"learn":[0.6570052089],"iteration":676,"passed_time":22.14192346,"remaining_time":43.26996269,"test":[0.6704427124]},
|
||||||
|
{"learn":[0.6569855794],"iteration":677,"passed_time":22.17624471,"remaining_time":43.24040635,"test":[0.6704395579]},
|
||||||
|
{"learn":[0.6569579709],"iteration":678,"passed_time":22.213192,"remaining_time":43.21594497,"test":[0.6704401246]},
|
||||||
|
{"learn":[0.6569333354],"iteration":679,"passed_time":22.23966403,"remaining_time":43.17111253,"test":[0.6704415621]},
|
||||||
|
{"learn":[0.6569069617],"iteration":680,"passed_time":22.27051241,"remaining_time":43.13481039,"test":[0.6704341343]},
|
||||||
|
{"learn":[0.6568931857],"iteration":681,"passed_time":22.29625075,"remaining_time":43.08864881,"test":[0.6704369615]},
|
||||||
|
{"learn":[0.6568734532],"iteration":682,"passed_time":22.32160622,"remaining_time":43.04180877,"test":[0.6704357425]},
|
||||||
|
{"learn":[0.6568435196],"iteration":683,"passed_time":22.35059872,"remaining_time":43.00202911,"test":[0.6704294622]},
|
||||||
|
{"learn":[0.6568108038],"iteration":684,"passed_time":22.37956576,"remaining_time":42.96223208,"test":[0.6704289794]},
|
||||||
|
{"learn":[0.6567811374],"iteration":685,"passed_time":22.41993338,"remaining_time":42.94430389,"test":[0.6704272409]},
|
||||||
|
{"learn":[0.6567467284],"iteration":686,"passed_time":22.45285267,"remaining_time":42.91207504,"test":[0.6704101162]},
|
||||||
|
{"learn":[0.6567172734],"iteration":687,"passed_time":22.4848431,"remaining_time":42.8780729,"test":[0.6704069439]},
|
||||||
|
{"learn":[0.6566967606],"iteration":688,"passed_time":22.51193834,"remaining_time":42.83476221,"test":[0.6704100747]},
|
||||||
|
{"learn":[0.6566720128],"iteration":689,"passed_time":22.53798671,"remaining_time":42.78951101,"test":[0.6704122261]},
|
||||||
|
{"learn":[0.6566441608],"iteration":690,"passed_time":22.57108439,"remaining_time":42.75766928,"test":[0.6704137826]},
|
||||||
|
{"learn":[0.6566172287],"iteration":691,"passed_time":22.59836588,"remaining_time":42.7148303,"test":[0.6704207952]},
|
||||||
|
{"learn":[0.6565952549],"iteration":692,"passed_time":22.62447507,"remaining_time":42.66982528,"test":[0.6704154834]},
|
||||||
|
{"learn":[0.6565702687],"iteration":693,"passed_time":22.65349415,"remaining_time":42.63035067,"test":[0.6704253514]},
|
||||||
|
{"learn":[0.6565392213],"iteration":694,"passed_time":22.68028991,"remaining_time":42.58673141,"test":[0.6704155636]},
|
||||||
|
{"learn":[0.6565157938],"iteration":695,"passed_time":22.70844406,"remaining_time":42.54570555,"test":[0.6704141298]},
|
||||||
|
{"learn":[0.6564902789],"iteration":696,"passed_time":22.73944116,"remaining_time":42.51003133,"test":[0.6704207635]},
|
||||||
|
{"learn":[0.6564644734],"iteration":697,"passed_time":22.7613976,"remaining_time":42.45750671,"test":[0.6704268341]},
|
||||||
|
{"learn":[0.6564349549],"iteration":698,"passed_time":22.79216825,"remaining_time":42.42147482,"test":[0.6704243126]},
|
||||||
|
{"learn":[0.6564046572],"iteration":699,"passed_time":22.8167121,"remaining_time":42.37389389,"test":[0.6704235165]},
|
||||||
|
{"learn":[0.6563744107],"iteration":700,"passed_time":22.84507296,"remaining_time":42.33345189,"test":[0.6704257736]},
|
||||||
|
{"learn":[0.6563525063],"iteration":701,"passed_time":22.87088832,"remaining_time":42.28833766,"test":[0.6704247758]},
|
||||||
|
{"learn":[0.6563189867],"iteration":702,"passed_time":22.90238907,"remaining_time":42.25376759,"test":[0.6704331799]},
|
||||||
|
{"learn":[0.6562939062],"iteration":703,"passed_time":22.94246813,"remaining_time":42.23499815,"test":[0.6704252722]},
|
||||||
|
{"learn":[0.6562739297],"iteration":704,"passed_time":22.97441688,"remaining_time":42.20123385,"test":[0.6704146644]},
|
||||||
|
{"learn":[0.656256438],"iteration":705,"passed_time":23.00262167,"remaining_time":42.16061253,"test":[0.6704164122]},
|
||||||
|
{"learn":[0.6562366475],"iteration":706,"passed_time":23.033437,"remaining_time":42.12480062,"test":[0.6704118954]},
|
||||||
|
{"learn":[0.6562073096],"iteration":707,"passed_time":23.0545813,"remaining_time":42.07135458,"test":[0.6704043129]},
|
||||||
|
{"learn":[0.6561864222],"iteration":708,"passed_time":23.08699831,"remaining_time":42.03852584,"test":[0.6703978198]},
|
||||||
|
{"learn":[0.6561578826],"iteration":709,"passed_time":23.11590694,"remaining_time":41.99932387,"test":[0.6703935976]},
|
||||||
|
{"learn":[0.6561208567],"iteration":710,"passed_time":23.14362702,"remaining_time":41.9579961,"test":[0.6703839683]},
|
||||||
|
{"learn":[0.6560924703],"iteration":711,"passed_time":23.16985155,"remaining_time":41.91400112,"test":[0.6703843723]},
|
||||||
|
{"learn":[0.6560656907],"iteration":712,"passed_time":23.19510285,"remaining_time":41.86829925,"test":[0.6703879502]},
|
||||||
|
{"learn":[0.6560362588],"iteration":713,"passed_time":23.23034771,"remaining_time":41.84065429,"test":[0.6703895978]},
|
||||||
|
{"learn":[0.6560124527],"iteration":714,"passed_time":23.25923754,"remaining_time":41.80156678,"test":[0.6703894359]},
|
||||||
|
{"learn":[0.6559875055],"iteration":715,"passed_time":23.28703452,"remaining_time":41.76054794,"test":[0.6703928777]},
|
||||||
|
{"learn":[0.6559547281],"iteration":716,"passed_time":23.31161175,"remaining_time":41.71380457,"test":[0.6703933128]},
|
||||||
|
{"learn":[0.6559230866],"iteration":717,"passed_time":23.34170355,"remaining_time":41.67696929,"test":[0.6703844355]},
|
||||||
|
{"learn":[0.6558924823],"iteration":718,"passed_time":23.37263658,"remaining_time":41.64165155,"test":[0.6703825151]},
|
||||||
|
{"learn":[0.6558676469],"iteration":719,"passed_time":23.40571088,"remaining_time":41.61015268,"test":[0.6703983542]},
|
||||||
|
{"learn":[0.6558459277],"iteration":720,"passed_time":23.4389719,"remaining_time":41.57898067,"test":[0.670399556]},
|
||||||
|
{"learn":[0.6558149638],"iteration":721,"passed_time":23.48304084,"remaining_time":41.56693379,"test":[0.6703931808]},
|
||||||
|
{"learn":[0.6557812248],"iteration":722,"passed_time":23.50734531,"remaining_time":41.5198893,"test":[0.6703886918]},
|
||||||
|
{"learn":[0.6557546502],"iteration":723,"passed_time":23.54055835,"remaining_time":41.48860836,"test":[0.6703847574]},
|
||||||
|
{"learn":[0.6557274948],"iteration":724,"passed_time":23.56652491,"remaining_time":41.44457829,"test":[0.6703885941]},
|
||||||
|
{"learn":[0.6557044723],"iteration":725,"passed_time":23.59580183,"remaining_time":41.40640708,"test":[0.6703788615]},
|
||||||
|
{"learn":[0.6556751811],"iteration":726,"passed_time":23.62334313,"remaining_time":41.36522119,"test":[0.6703799906]},
|
||||||
|
{"learn":[0.6556539158],"iteration":727,"passed_time":23.64879831,"remaining_time":41.32042782,"test":[0.6703774518]},
|
||||||
|
{"learn":[0.6556182915],"iteration":728,"passed_time":23.67755213,"remaining_time":41.28143862,"test":[0.6703783496]},
|
||||||
|
{"learn":[0.6555977079],"iteration":729,"passed_time":23.70012944,"remaining_time":41.23173204,"test":[0.6703648854]},
|
||||||
|
{"learn":[0.6555667903],"iteration":730,"passed_time":23.72866102,"remaining_time":41.19243615,"test":[0.6703716654]},
|
||||||
|
{"learn":[0.6555394075],"iteration":731,"passed_time":23.75226732,"remaining_time":41.14463793,"test":[0.6703550938]},
|
||||||
|
{"learn":[0.6555122742],"iteration":732,"passed_time":23.7844108,"remaining_time":41.11166233,"test":[0.6703467057]},
|
||||||
|
{"learn":[0.6554814941],"iteration":733,"passed_time":23.80747563,"remaining_time":41.06303017,"test":[0.6703484503]},
|
||||||
|
{"learn":[0.6554517373],"iteration":734,"passed_time":23.84023587,"remaining_time":41.03115425,"test":[0.6703549183]},
|
||||||
|
{"learn":[0.655429552],"iteration":735,"passed_time":23.87042124,"remaining_time":40.99485387,"test":[0.6703501504]},
|
||||||
|
{"learn":[0.655396579],"iteration":736,"passed_time":23.9087808,"remaining_time":40.97257823,"test":[0.6703672622]},
|
||||||
|
{"learn":[0.6553735864],"iteration":737,"passed_time":23.94161529,"remaining_time":40.94081097,"test":[0.6703560249]},
|
||||||
|
{"learn":[0.6553472597],"iteration":738,"passed_time":23.97478791,"remaining_time":40.90961779,"test":[0.6703547155]},
|
||||||
|
{"learn":[0.6553252832],"iteration":739,"passed_time":24.00628859,"remaining_time":40.87557247,"test":[0.6703593236]},
|
||||||
|
{"learn":[0.6552971659],"iteration":740,"passed_time":24.03623034,"remaining_time":40.83888528,"test":[0.6703606827]},
|
||||||
|
{"learn":[0.6552763852],"iteration":741,"passed_time":24.06404686,"remaining_time":40.79861313,"test":[0.6703511404]},
|
||||||
|
{"learn":[0.6552488203],"iteration":742,"passed_time":24.09270947,"remaining_time":40.75980593,"test":[0.6703431646]},
|
||||||
|
{"learn":[0.65521229],"iteration":743,"passed_time":24.12724624,"remaining_time":40.73094258,"test":[0.6703475116]},
|
||||||
|
{"learn":[0.6551949744],"iteration":744,"passed_time":24.15397955,"remaining_time":40.68891857,"test":[0.6703483634]},
|
||||||
|
{"learn":[0.6551673797],"iteration":745,"passed_time":24.17955779,"remaining_time":40.64499392,"test":[0.6703475713]},
|
||||||
|
{"learn":[0.6551421856],"iteration":746,"passed_time":24.20715317,"remaining_time":40.60450191,"test":[0.670360457]},
|
||||||
|
{"learn":[0.6551255516],"iteration":747,"passed_time":24.23336836,"remaining_time":40.5617342,"test":[0.6703664352]},
|
||||||
|
{"learn":[0.6551019608],"iteration":748,"passed_time":24.2614437,"remaining_time":40.52211759,"test":[0.6703617612]},
|
||||||
|
{"learn":[0.6550758728],"iteration":749,"passed_time":24.29512083,"remaining_time":40.49186805,"test":[0.6703669926]},
|
||||||
|
{"learn":[0.655051966],"iteration":750,"passed_time":24.31839238,"remaining_time":40.44430371,"test":[0.6703670837]},
|
||||||
|
{"learn":[0.6550351058],"iteration":751,"passed_time":24.34977118,"remaining_time":40.41025856,"test":[0.6703706628]},
|
||||||
|
{"learn":[0.6549998756],"iteration":752,"passed_time":24.3762114,"remaining_time":40.36804198,"test":[0.670369618]},
|
||||||
|
{"learn":[0.6549721212],"iteration":753,"passed_time":24.40831154,"remaining_time":40.3352204,"test":[0.6703692351]},
|
||||||
|
{"learn":[0.6549401744],"iteration":754,"passed_time":24.44267281,"remaining_time":40.30612934,"test":[0.6703624433]},
|
||||||
|
{"learn":[0.6549207325],"iteration":755,"passed_time":24.47460721,"remaining_time":40.27303091,"test":[0.6703686285]},
|
||||||
|
{"learn":[0.6548900891],"iteration":756,"passed_time":24.50826603,"remaining_time":40.24276708,"test":[0.6703598432]},
|
||||||
|
{"learn":[0.6548682731],"iteration":757,"passed_time":24.54826542,"remaining_time":40.22288345,"test":[0.6703618766]},
|
||||||
|
{"learn":[0.6548418938],"iteration":758,"passed_time":24.57546587,"remaining_time":40.18201996,"test":[0.6703694148]},
|
||||||
|
{"learn":[0.6548234717],"iteration":759,"passed_time":24.60502723,"remaining_time":40.14504442,"test":[0.6703683652]},
|
||||||
|
{"learn":[0.6547996833],"iteration":760,"passed_time":24.63261096,"remaining_time":40.10486856,"test":[0.6703604855]},
|
||||||
|
{"learn":[0.6547726174],"iteration":761,"passed_time":24.66001655,"remaining_time":40.06443634,"test":[0.6703758987]},
|
||||||
|
{"learn":[0.6547509314],"iteration":762,"passed_time":24.68929907,"remaining_time":40.02708119,"test":[0.6703773302]},
|
||||||
|
{"learn":[0.6547168175],"iteration":763,"passed_time":24.71425118,"remaining_time":39.98274144,"test":[0.6703641028]},
|
||||||
|
{"learn":[0.6546907846],"iteration":764,"passed_time":24.74589169,"remaining_time":39.94924999,"test":[0.6703649602]},
|
||||||
|
{"learn":[0.6546671611],"iteration":765,"passed_time":24.76625006,"remaining_time":39.89758822,"test":[0.6703567811]},
|
||||||
|
{"learn":[0.6546475893],"iteration":766,"passed_time":24.79734832,"remaining_time":39.86327312,"test":[0.6703544688]},
|
||||||
|
{"learn":[0.6546206223],"iteration":767,"passed_time":24.82531049,"remaining_time":39.82393558,"test":[0.6703611821]},
|
||||||
|
{"learn":[0.6545874193],"iteration":768,"passed_time":24.85435247,"remaining_time":39.78635616,"test":[0.6703527821]},
|
||||||
|
{"learn":[0.6545620629],"iteration":769,"passed_time":24.88095966,"remaining_time":39.74490958,"test":[0.6703523616]},
|
||||||
|
{"learn":[0.6545346297],"iteration":770,"passed_time":24.90935211,"remaining_time":39.70634726,"test":[0.6703616298]},
|
||||||
|
{"learn":[0.6545172316],"iteration":771,"passed_time":24.94098876,"remaining_time":39.67297175,"test":[0.6703603551]},
|
||||||
|
{"learn":[0.6544943049],"iteration":772,"passed_time":24.97035098,"remaining_time":39.6359905,"test":[0.6703675655]},
|
||||||
|
{"learn":[0.6544632323],"iteration":773,"passed_time":25.00434422,"remaining_time":39.60636436,"test":[0.6703582411]},
|
||||||
|
{"learn":[0.6544384097],"iteration":774,"passed_time":25.03067441,"remaining_time":39.56461439,"test":[0.6703581437]},
|
||||||
|
{"learn":[0.6544084745],"iteration":775,"passed_time":25.05692652,"remaining_time":39.522781,"test":[0.6703551885]},
|
||||||
|
{"learn":[0.6543765257],"iteration":776,"passed_time":25.08660163,"remaining_time":39.48637554,"test":[0.6703608491]},
|
||||||
|
{"learn":[0.6543536123],"iteration":777,"passed_time":25.10764591,"remaining_time":39.43643098,"test":[0.6703674554]},
|
||||||
|
{"learn":[0.6543303593],"iteration":778,"passed_time":25.13940138,"remaining_time":39.40334928,"test":[0.6703679619]},
|
||||||
|
{"learn":[0.6543005831],"iteration":779,"passed_time":25.15916899,"remaining_time":39.35152074,"test":[0.6703701757]},
|
||||||
|
{"learn":[0.6542678123],"iteration":780,"passed_time":25.18841105,"remaining_time":39.31456219,"test":[0.6703603462]},
|
||||||
|
{"learn":[0.6542439303],"iteration":781,"passed_time":25.21444083,"remaining_time":39.27262012,"test":[0.670359801]},
|
||||||
|
{"learn":[0.6542100401],"iteration":782,"passed_time":25.24017824,"remaining_time":39.23026426,"test":[0.6703523669]},
|
||||||
|
{"learn":[0.6541836178],"iteration":783,"passed_time":25.2660091,"remaining_time":39.18809574,"test":[0.6703365674]},
|
||||||
|
{"learn":[0.654158129],"iteration":784,"passed_time":25.28891553,"remaining_time":39.1414425,"test":[0.6703486118]},
|
||||||
|
{"learn":[0.6541343464],"iteration":785,"passed_time":25.31589904,"remaining_time":39.10114686,"test":[0.6703450011]},
|
||||||
|
{"learn":[0.6541092921],"iteration":786,"passed_time":25.34123581,"remaining_time":39.05834694,"test":[0.6703473135]},
|
||||||
|
{"learn":[0.6540812254],"iteration":787,"passed_time":25.36728606,"remaining_time":39.01668871,"test":[0.670350998]},
|
||||||
|
{"learn":[0.654060259],"iteration":788,"passed_time":25.39177931,"remaining_time":38.97268028,"test":[0.6703417767]},
|
||||||
|
{"learn":[0.6540467253],"iteration":789,"passed_time":25.41712461,"remaining_time":38.9300263,"test":[0.6703349821]},
|
||||||
|
{"learn":[0.6540306837],"iteration":790,"passed_time":25.44804125,"remaining_time":38.89593157,"test":[0.6703457717]},
|
||||||
|
{"learn":[0.6540103667],"iteration":791,"passed_time":25.48249341,"remaining_time":38.86723743,"test":[0.6703506266]},
|
||||||
|
{"learn":[0.6539821302],"iteration":792,"passed_time":25.51450657,"remaining_time":38.83481643,"test":[0.6703596395]},
|
||||||
|
{"learn":[0.6539577914],"iteration":793,"passed_time":25.54216564,"remaining_time":38.79578307,"test":[0.6703799895]},
|
||||||
|
{"learn":[0.653923724],"iteration":794,"passed_time":25.56982738,"remaining_time":38.75678238,"test":[0.6703687687]},
|
||||||
|
{"learn":[0.6539086888],"iteration":795,"passed_time":25.59539769,"remaining_time":38.71464675,"test":[0.6703780675]},
|
||||||
|
{"learn":[0.6538798424],"iteration":796,"passed_time":25.61874122,"remaining_time":38.66919157,"test":[0.670374835]},
|
||||||
|
{"learn":[0.6538566996],"iteration":797,"passed_time":25.64394874,"remaining_time":38.62659947,"test":[0.6703831387]},
|
||||||
|
{"learn":[0.6538290752],"iteration":798,"passed_time":25.66776244,"remaining_time":38.58195581,"test":[0.670377656]},
|
||||||
|
{"learn":[0.6538051255],"iteration":799,"passed_time":25.69593415,"remaining_time":38.54390122,"test":[0.6703689741]},
|
||||||
|
{"learn":[0.6537917354],"iteration":800,"passed_time":25.71651353,"remaining_time":38.49450652,"test":[0.6703709756]},
|
||||||
|
{"learn":[0.6537684302],"iteration":801,"passed_time":25.74304126,"remaining_time":38.45406912,"test":[0.6703737517]},
|
||||||
|
{"learn":[0.6537402991],"iteration":802,"passed_time":25.77084871,"remaining_time":38.41557398,"test":[0.6703818964]},
|
||||||
|
{"learn":[0.6537165427],"iteration":803,"passed_time":25.79028824,"remaining_time":38.36465763,"test":[0.6703812173]},
|
||||||
|
{"learn":[0.6536853601],"iteration":804,"passed_time":25.82203653,"remaining_time":38.3320915,"test":[0.6703960068]},
|
||||||
|
{"learn":[0.6536681479],"iteration":805,"passed_time":25.84395064,"remaining_time":38.28495914,"test":[0.6703976729]},
|
||||||
|
{"learn":[0.6536409101],"iteration":806,"passed_time":25.87390688,"remaining_time":38.24977808,"test":[0.6704024604]},
|
||||||
|
{"learn":[0.6536120189],"iteration":807,"passed_time":25.89606204,"remaining_time":38.20310143,"test":[0.6704085008]},
|
||||||
|
{"learn":[0.6535912493],"iteration":808,"passed_time":25.92585483,"remaining_time":38.16772942,"test":[0.6704076633]},
|
||||||
|
{"learn":[0.6535617421],"iteration":809,"passed_time":25.95539059,"remaining_time":38.13199358,"test":[0.6704111719]},
|
||||||
|
{"learn":[0.6535315174],"iteration":810,"passed_time":25.98822968,"remaining_time":38.10111601,"test":[0.6704220803]},
|
||||||
|
{"learn":[0.6534972927],"iteration":811,"passed_time":26.02835773,"remaining_time":38.08089777,"test":[0.6704265011]},
|
||||||
|
{"learn":[0.6534818476],"iteration":812,"passed_time":26.0558565,"remaining_time":38.04219146,"test":[0.6704251162]},
|
||||||
|
{"learn":[0.6534498323],"iteration":813,"passed_time":26.08151817,"remaining_time":38.00083606,"test":[0.6704375472]},
|
||||||
|
{"learn":[0.6534305025],"iteration":814,"passed_time":26.10848988,"remaining_time":37.96142393,"test":[0.6704319336]},
|
||||||
|
{"learn":[0.6534081059],"iteration":815,"passed_time":26.13143346,"remaining_time":37.91619757,"test":[0.670437614]},
|
||||||
|
{"learn":[0.6533765804],"iteration":816,"passed_time":26.15923661,"remaining_time":37.87806231,"test":[0.6704554331]},
|
||||||
|
{"learn":[0.6533441549],"iteration":817,"passed_time":26.18805523,"remaining_time":37.84141966,"test":[0.6704603317]},
|
||||||
|
{"learn":[0.6533053405],"iteration":818,"passed_time":26.2140726,"remaining_time":37.8007567,"test":[0.6704548042]},
|
||||||
|
{"learn":[0.6532838469],"iteration":819,"passed_time":26.24289367,"remaining_time":37.76416405,"test":[0.6704502654]},
|
||||||
|
{"learn":[0.6532604302],"iteration":820,"passed_time":26.27260776,"remaining_time":37.72887277,"test":[0.6704512072]},
|
||||||
|
{"learn":[0.6532364412],"iteration":821,"passed_time":26.29880394,"remaining_time":37.68855358,"test":[0.6704433481]},
|
||||||
|
{"learn":[0.6532100089],"iteration":822,"passed_time":26.32785215,"remaining_time":37.65234749,"test":[0.6704095112]},
|
||||||
|
{"learn":[0.6531782515],"iteration":823,"passed_time":26.35925682,"remaining_time":37.61952188,"test":[0.6704086019]},
|
||||||
|
{"learn":[0.6531449701],"iteration":824,"passed_time":26.38596096,"remaining_time":37.580005,"test":[0.6703987131]},
|
||||||
|
{"learn":[0.653115452],"iteration":825,"passed_time":26.40854839,"remaining_time":37.53466805,"test":[0.6704019708]},
|
||||||
|
{"learn":[0.6530787602],"iteration":826,"passed_time":26.44419918,"remaining_time":37.50791492,"test":[0.6704046556]},
|
||||||
|
{"learn":[0.653052397],"iteration":827,"passed_time":26.47784276,"remaining_time":37.47829917,"test":[0.6704091961]},
|
||||||
|
{"learn":[0.6530313579],"iteration":828,"passed_time":26.51701028,"remaining_time":37.45647652,"test":[0.6704103204]},
|
||||||
|
{"learn":[0.6530010363],"iteration":829,"passed_time":26.53963123,"remaining_time":37.41128739,"test":[0.6704074257]},
|
||||||
|
{"learn":[0.6529752146],"iteration":830,"passed_time":26.57362226,"remaining_time":37.38214732,"test":[0.6704115335]},
|
||||||
|
{"learn":[0.652954801],"iteration":831,"passed_time":26.59767057,"remaining_time":37.33903754,"test":[0.6704041275]},
|
||||||
|
{"learn":[0.6529330351],"iteration":832,"passed_time":26.62378941,"remaining_time":37.29887425,"test":[0.6704004556]},
|
||||||
|
{"learn":[0.6528993709],"iteration":833,"passed_time":26.65024746,"remaining_time":37.25921887,"test":[0.6704037097]},
|
||||||
|
{"learn":[0.6528665883],"iteration":834,"passed_time":26.67774911,"remaining_time":37.22105115,"test":[0.6704035477]},
|
||||||
|
{"learn":[0.6528413041],"iteration":835,"passed_time":26.70473813,"remaining_time":37.1821952,"test":[0.6704025281]},
|
||||||
|
{"learn":[0.6528217161],"iteration":836,"passed_time":26.72833235,"remaining_time":37.13865056,"test":[0.6704024549]},
|
||||||
|
{"learn":[0.6527978782],"iteration":837,"passed_time":26.76384162,"remaining_time":37.11167537,"test":[0.670405721]},
|
||||||
|
{"learn":[0.6527789461],"iteration":838,"passed_time":26.79137369,"remaining_time":37.07364106,"test":[0.6703983189]},
|
||||||
|
{"learn":[0.6527432001],"iteration":839,"passed_time":26.82295602,"remaining_time":37.04122498,"test":[0.6704035256]},
|
||||||
|
{"learn":[0.6527139767],"iteration":840,"passed_time":26.87217031,"remaining_time":37.03310985,"test":[0.6704047613]},
|
||||||
|
{"learn":[0.6526857244],"iteration":841,"passed_time":26.92488006,"remaining_time":37.0297044,"test":[0.6704139617]},
|
||||||
|
{"learn":[0.652657086],"iteration":842,"passed_time":26.98258041,"remaining_time":37.03303147,"test":[0.6704066193]},
|
||||||
|
{"learn":[0.6526355016],"iteration":843,"passed_time":27.05424841,"remaining_time":37.05534497,"test":[0.670402892]},
|
||||||
|
{"learn":[0.6526054936],"iteration":844,"passed_time":27.09765154,"remaining_time":37.03880181,"test":[0.6704081961]},
|
||||||
|
{"learn":[0.6525793707],"iteration":845,"passed_time":27.12038959,"remaining_time":36.99400661,"test":[0.6704029862]},
|
||||||
|
{"learn":[0.6525584692],"iteration":846,"passed_time":27.14691224,"remaining_time":36.95441537,"test":[0.6704014281]},
|
||||||
|
{"learn":[0.6525279747],"iteration":847,"passed_time":27.18096334,"remaining_time":36.92508227,"test":[0.6704036115]},
|
||||||
|
{"learn":[0.6525038765],"iteration":848,"passed_time":27.20686017,"remaining_time":36.88468322,"test":[0.6704016777]},
|
||||||
|
{"learn":[0.6524849104],"iteration":849,"passed_time":27.23465701,"remaining_time":36.8468889,"test":[0.6704085392]},
|
||||||
|
{"learn":[0.6524610603],"iteration":850,"passed_time":27.26094834,"remaining_time":36.80708536,"test":[0.6704042952]},
|
||||||
|
{"learn":[0.6524357337],"iteration":851,"passed_time":27.28945577,"remaining_time":36.77029957,"test":[0.670394789]},
|
||||||
|
{"learn":[0.6524082286],"iteration":852,"passed_time":27.31865398,"remaining_time":36.73446203,"test":[0.6703885644]},
|
||||||
|
{"learn":[0.65238051],"iteration":853,"passed_time":27.34791322,"remaining_time":36.69872195,"test":[0.6703946813]},
|
||||||
|
{"learn":[0.6523557826],"iteration":854,"passed_time":27.3865535,"remaining_time":36.67555995,"test":[0.6704042137]},
|
||||||
|
{"learn":[0.6523391233],"iteration":855,"passed_time":27.41370907,"remaining_time":36.63701306,"test":[0.6704077517]},
|
||||||
|
{"learn":[0.652325347],"iteration":856,"passed_time":27.43905921,"remaining_time":36.5960848,"test":[0.6704118698]},
|
||||||
|
{"learn":[0.6522924958],"iteration":857,"passed_time":27.47159295,"remaining_time":36.56475425,"test":[0.6704114259]},
|
||||||
|
{"learn":[0.6522623584],"iteration":858,"passed_time":27.50124299,"remaining_time":36.52959052,"test":[0.6704157567]},
|
||||||
|
{"learn":[0.6522343891],"iteration":859,"passed_time":27.53509105,"remaining_time":36.50000442,"test":[0.6703837005]},
|
||||||
|
{"learn":[0.6522094424],"iteration":860,"passed_time":27.57211091,"remaining_time":36.47460432,"test":[0.6703829482]},
|
||||||
|
{"learn":[0.6521841478],"iteration":861,"passed_time":27.59555719,"remaining_time":36.43125764,"test":[0.6703818491]},
|
||||||
|
{"learn":[0.6521657946],"iteration":862,"passed_time":27.6272049,"remaining_time":36.39876242,"test":[0.6703826129]},
|
||||||
|
{"learn":[0.6521304278],"iteration":863,"passed_time":27.65462267,"remaining_time":36.36070759,"test":[0.6703834487]},
|
||||||
|
{"learn":[0.6521045712],"iteration":864,"passed_time":27.68321566,"remaining_time":36.3242194,"test":[0.6703868275]},
|
||||||
|
{"learn":[0.6520753696],"iteration":865,"passed_time":27.71151671,"remaining_time":36.28736714,"test":[0.6703853357]},
|
||||||
|
{"learn":[0.6520519528],"iteration":866,"passed_time":27.73884016,"remaining_time":36.2492571,"test":[0.670450644]},
|
||||||
|
{"learn":[0.6520216555],"iteration":867,"passed_time":27.76583897,"remaining_time":36.21074851,"test":[0.6704556991]},
|
||||||
|
{"learn":[0.6519926935],"iteration":868,"passed_time":27.79498714,"remaining_time":36.17506382,"test":[0.6704535742]},
|
||||||
|
{"learn":[0.6519734186],"iteration":869,"passed_time":27.82082723,"remaining_time":36.13509744,"test":[0.6704495915]}
|
||||||
|
]}
|
||||||
Binary file not shown.
@@ -0,0 +1,871 @@
|
|||||||
|
iter Logloss
|
||||||
|
0 0.692389481
|
||||||
|
1 0.6916338586
|
||||||
|
2 0.6910159214
|
||||||
|
3 0.6903417151
|
||||||
|
4 0.6896961461
|
||||||
|
5 0.6890979366
|
||||||
|
6 0.6884946167
|
||||||
|
7 0.6879503686
|
||||||
|
8 0.6874528094
|
||||||
|
9 0.6869036785
|
||||||
|
10 0.6863761921
|
||||||
|
11 0.6859038678
|
||||||
|
12 0.685410175
|
||||||
|
13 0.6849483392
|
||||||
|
14 0.6845417792
|
||||||
|
15 0.6841038875
|
||||||
|
16 0.6836957422
|
||||||
|
17 0.6832947461
|
||||||
|
18 0.6829014105
|
||||||
|
19 0.6825264546
|
||||||
|
20 0.6822106577
|
||||||
|
21 0.6818649349
|
||||||
|
22 0.6815467855
|
||||||
|
23 0.6812293319
|
||||||
|
24 0.6808837443
|
||||||
|
25 0.6805816494
|
||||||
|
26 0.6803209634
|
||||||
|
27 0.6800350862
|
||||||
|
28 0.6797703947
|
||||||
|
29 0.6794926675
|
||||||
|
30 0.6792251865
|
||||||
|
31 0.6789670166
|
||||||
|
32 0.678722402
|
||||||
|
33 0.678476935
|
||||||
|
34 0.6782297335
|
||||||
|
35 0.6780226701
|
||||||
|
36 0.6778291026
|
||||||
|
37 0.6776045324
|
||||||
|
38 0.6773969079
|
||||||
|
39 0.6771819602
|
||||||
|
40 0.6769816736
|
||||||
|
41 0.6767984027
|
||||||
|
42 0.6766201184
|
||||||
|
43 0.6764394377
|
||||||
|
44 0.6762698797
|
||||||
|
45 0.6760974263
|
||||||
|
46 0.6759245179
|
||||||
|
47 0.6757673909
|
||||||
|
48 0.6756172628
|
||||||
|
49 0.675474531
|
||||||
|
50 0.6753286933
|
||||||
|
51 0.6751900513
|
||||||
|
52 0.6750574835
|
||||||
|
53 0.6749329567
|
||||||
|
54 0.6748033265
|
||||||
|
55 0.6746797823
|
||||||
|
56 0.674535525
|
||||||
|
57 0.6744256514
|
||||||
|
58 0.674310819
|
||||||
|
59 0.6741967947
|
||||||
|
60 0.6740879654
|
||||||
|
61 0.6739772476
|
||||||
|
62 0.67388281
|
||||||
|
63 0.6737789726
|
||||||
|
64 0.6736812332
|
||||||
|
65 0.6735930009
|
||||||
|
66 0.6734947116
|
||||||
|
67 0.6733961481
|
||||||
|
68 0.6732990195
|
||||||
|
69 0.6732133575
|
||||||
|
70 0.673111539
|
||||||
|
71 0.6730080451
|
||||||
|
72 0.6729157861
|
||||||
|
73 0.6728347949
|
||||||
|
74 0.6727640693
|
||||||
|
75 0.6726808811
|
||||||
|
76 0.6726029645
|
||||||
|
77 0.6725356026
|
||||||
|
78 0.6724606887
|
||||||
|
79 0.6723849561
|
||||||
|
80 0.6723050519
|
||||||
|
81 0.6722508802
|
||||||
|
82 0.6721773904
|
||||||
|
83 0.6721007598
|
||||||
|
84 0.6720353564
|
||||||
|
85 0.6719790902
|
||||||
|
86 0.6719140024
|
||||||
|
87 0.6718573633
|
||||||
|
88 0.671795602
|
||||||
|
89 0.6717369134
|
||||||
|
90 0.6716711079
|
||||||
|
91 0.6716070843
|
||||||
|
92 0.6715517232
|
||||||
|
93 0.6714957378
|
||||||
|
94 0.6714364567
|
||||||
|
95 0.6713881758
|
||||||
|
96 0.6713336502
|
||||||
|
97 0.6712700267
|
||||||
|
98 0.6712154424
|
||||||
|
99 0.6711600413
|
||||||
|
100 0.6711060533
|
||||||
|
101 0.6710494943
|
||||||
|
102 0.6709936897
|
||||||
|
103 0.6709472183
|
||||||
|
104 0.6708914508
|
||||||
|
105 0.6708388195
|
||||||
|
106 0.6707885854
|
||||||
|
107 0.6707454167
|
||||||
|
108 0.6706973013
|
||||||
|
109 0.6706577031
|
||||||
|
110 0.67061108
|
||||||
|
111 0.6705625485
|
||||||
|
112 0.6705146484
|
||||||
|
113 0.6704704423
|
||||||
|
114 0.6704155922
|
||||||
|
115 0.6703687117
|
||||||
|
116 0.6703324232
|
||||||
|
117 0.6702884624
|
||||||
|
118 0.670253478
|
||||||
|
119 0.6702140804
|
||||||
|
120 0.6701682529
|
||||||
|
121 0.6701320588
|
||||||
|
122 0.6700939824
|
||||||
|
123 0.6700655902
|
||||||
|
124 0.6700190743
|
||||||
|
125 0.6699792296
|
||||||
|
126 0.6699379404
|
||||||
|
127 0.669895454
|
||||||
|
128 0.6698563938
|
||||||
|
129 0.6698215571
|
||||||
|
130 0.6697857067
|
||||||
|
131 0.6697449303
|
||||||
|
132 0.6697052425
|
||||||
|
133 0.6696695553
|
||||||
|
134 0.6696269265
|
||||||
|
135 0.6695969271
|
||||||
|
136 0.6695489786
|
||||||
|
137 0.6695173859
|
||||||
|
138 0.6694811164
|
||||||
|
139 0.6694477439
|
||||||
|
140 0.6694082161
|
||||||
|
141 0.6693679185
|
||||||
|
142 0.6693341916
|
||||||
|
143 0.6692933159
|
||||||
|
144 0.6692619696
|
||||||
|
145 0.6692229289
|
||||||
|
146 0.6691840164
|
||||||
|
147 0.6691581406
|
||||||
|
148 0.6691177196
|
||||||
|
149 0.6690851126
|
||||||
|
150 0.6690518144
|
||||||
|
151 0.6690149711
|
||||||
|
152 0.668993877
|
||||||
|
153 0.6689596579
|
||||||
|
154 0.6689372651
|
||||||
|
155 0.6689003045
|
||||||
|
156 0.6688680182
|
||||||
|
157 0.6688348164
|
||||||
|
158 0.6687947046
|
||||||
|
159 0.6687605251
|
||||||
|
160 0.668726253
|
||||||
|
161 0.6686862718
|
||||||
|
162 0.668663478
|
||||||
|
163 0.6686399521
|
||||||
|
164 0.6686058279
|
||||||
|
165 0.6685761282
|
||||||
|
166 0.6685469327
|
||||||
|
167 0.6685157003
|
||||||
|
168 0.6684805143
|
||||||
|
169 0.6684485765
|
||||||
|
170 0.6684144429
|
||||||
|
171 0.6683849752
|
||||||
|
172 0.6683568537
|
||||||
|
173 0.6683266628
|
||||||
|
174 0.6682937842
|
||||||
|
175 0.6682657097
|
||||||
|
176 0.6682301443
|
||||||
|
177 0.6681995916
|
||||||
|
178 0.6681658267
|
||||||
|
179 0.6681422687
|
||||||
|
180 0.6681216601
|
||||||
|
181 0.6680899019
|
||||||
|
182 0.6680676394
|
||||||
|
183 0.6680413672
|
||||||
|
184 0.6680088406
|
||||||
|
185 0.6679873982
|
||||||
|
186 0.6679663544
|
||||||
|
187 0.6679417375
|
||||||
|
188 0.6679100197
|
||||||
|
189 0.667881208
|
||||||
|
190 0.6678475427
|
||||||
|
191 0.6678310341
|
||||||
|
192 0.6678060257
|
||||||
|
193 0.6677789336
|
||||||
|
194 0.6677478773
|
||||||
|
195 0.6677212408
|
||||||
|
196 0.667704316
|
||||||
|
197 0.6676819639
|
||||||
|
198 0.6676554448
|
||||||
|
199 0.6676318346
|
||||||
|
200 0.6676074705
|
||||||
|
201 0.6675849784
|
||||||
|
202 0.6675631744
|
||||||
|
203 0.6675397619
|
||||||
|
204 0.6675169086
|
||||||
|
205 0.6674864762
|
||||||
|
206 0.6674670714
|
||||||
|
207 0.6674375599
|
||||||
|
208 0.6674148457
|
||||||
|
209 0.6673974446
|
||||||
|
210 0.6673812139
|
||||||
|
211 0.6673515687
|
||||||
|
212 0.6673197956
|
||||||
|
213 0.6672900754
|
||||||
|
214 0.6672550009
|
||||||
|
215 0.6672271563
|
||||||
|
216 0.667204521
|
||||||
|
217 0.667181968
|
||||||
|
218 0.6671640023
|
||||||
|
219 0.66714351
|
||||||
|
220 0.6671167156
|
||||||
|
221 0.6670915937
|
||||||
|
222 0.6670595279
|
||||||
|
223 0.667033994
|
||||||
|
224 0.6670008246
|
||||||
|
225 0.6669858319
|
||||||
|
226 0.6669553964
|
||||||
|
227 0.6669274683
|
||||||
|
228 0.666896348
|
||||||
|
229 0.6668698686
|
||||||
|
230 0.6668513411
|
||||||
|
231 0.6668309985
|
||||||
|
232 0.6668058585
|
||||||
|
233 0.6667845908
|
||||||
|
234 0.6667582863
|
||||||
|
235 0.6667332943
|
||||||
|
236 0.6667070085
|
||||||
|
237 0.6666907315
|
||||||
|
238 0.6666633028
|
||||||
|
239 0.6666406707
|
||||||
|
240 0.6666134624
|
||||||
|
241 0.6665850522
|
||||||
|
242 0.6665631193
|
||||||
|
243 0.6665412643
|
||||||
|
244 0.6665168385
|
||||||
|
245 0.6664904845
|
||||||
|
246 0.6664678274
|
||||||
|
247 0.6664539777
|
||||||
|
248 0.6664334121
|
||||||
|
249 0.6664121724
|
||||||
|
250 0.666392034
|
||||||
|
251 0.666366899
|
||||||
|
252 0.6663414098
|
||||||
|
253 0.6663157816
|
||||||
|
254 0.6662989799
|
||||||
|
255 0.6662696102
|
||||||
|
256 0.6662479711
|
||||||
|
257 0.6662231874
|
||||||
|
258 0.6661947927
|
||||||
|
259 0.6661669951
|
||||||
|
260 0.6661426137
|
||||||
|
261 0.6661216749
|
||||||
|
262 0.6660983123
|
||||||
|
263 0.6660803402
|
||||||
|
264 0.6660617842
|
||||||
|
265 0.6660443878
|
||||||
|
266 0.6660176079
|
||||||
|
267 0.6659967546
|
||||||
|
268 0.6659751467
|
||||||
|
269 0.6659539329
|
||||||
|
270 0.6659263951
|
||||||
|
271 0.6659038921
|
||||||
|
272 0.6658767418
|
||||||
|
273 0.6658510507
|
||||||
|
274 0.6658210119
|
||||||
|
275 0.6657963011
|
||||||
|
276 0.6657748552
|
||||||
|
277 0.6657490013
|
||||||
|
278 0.665732402
|
||||||
|
279 0.6657118786
|
||||||
|
280 0.665684467
|
||||||
|
281 0.6656584634
|
||||||
|
282 0.6656309991
|
||||||
|
283 0.6656073482
|
||||||
|
284 0.6655890957
|
||||||
|
285 0.6655665563
|
||||||
|
286 0.6655452454
|
||||||
|
287 0.6655255286
|
||||||
|
288 0.6655053548
|
||||||
|
289 0.6654893396
|
||||||
|
290 0.6654648912
|
||||||
|
291 0.6654442759
|
||||||
|
292 0.6654173127
|
||||||
|
293 0.6653914518
|
||||||
|
294 0.6653648946
|
||||||
|
295 0.665344141
|
||||||
|
296 0.6653140817
|
||||||
|
297 0.665295365
|
||||||
|
298 0.6652787488
|
||||||
|
299 0.6652502991
|
||||||
|
300 0.665231168
|
||||||
|
301 0.6652136682
|
||||||
|
302 0.6651903001
|
||||||
|
303 0.6651697153
|
||||||
|
304 0.6651525958
|
||||||
|
305 0.6651322685
|
||||||
|
306 0.6651113828
|
||||||
|
307 0.6650886807
|
||||||
|
308 0.6650622251
|
||||||
|
309 0.6650429987
|
||||||
|
310 0.665015513
|
||||||
|
311 0.6650019022
|
||||||
|
312 0.664979951
|
||||||
|
313 0.6649549638
|
||||||
|
314 0.6649340455
|
||||||
|
315 0.6649162445
|
||||||
|
316 0.6649048119
|
||||||
|
317 0.6648796463
|
||||||
|
318 0.6648605481
|
||||||
|
319 0.6648429084
|
||||||
|
320 0.6648238121
|
||||||
|
321 0.6647969527
|
||||||
|
322 0.6647854723
|
||||||
|
323 0.6647589304
|
||||||
|
324 0.6647429024
|
||||||
|
325 0.6647237508
|
||||||
|
326 0.6647059396
|
||||||
|
327 0.664686288
|
||||||
|
328 0.6646532527
|
||||||
|
329 0.6646306438
|
||||||
|
330 0.6646098516
|
||||||
|
331 0.6645858284
|
||||||
|
332 0.6645707188
|
||||||
|
333 0.6645485788
|
||||||
|
334 0.6645305696
|
||||||
|
335 0.6645108881
|
||||||
|
336 0.6644923286
|
||||||
|
337 0.6644805222
|
||||||
|
338 0.6644572776
|
||||||
|
339 0.6644320741
|
||||||
|
340 0.6644115048
|
||||||
|
341 0.6643949013
|
||||||
|
342 0.6643619789
|
||||||
|
343 0.6643389502
|
||||||
|
344 0.6643088915
|
||||||
|
345 0.664286972
|
||||||
|
346 0.664274149
|
||||||
|
347 0.6642536926
|
||||||
|
348 0.6642357634
|
||||||
|
349 0.664207914
|
||||||
|
350 0.6641853097
|
||||||
|
351 0.6641654917
|
||||||
|
352 0.664143804
|
||||||
|
353 0.6641290647
|
||||||
|
354 0.6641117244
|
||||||
|
355 0.6640880219
|
||||||
|
356 0.6640669415
|
||||||
|
357 0.6640462999
|
||||||
|
358 0.664030296
|
||||||
|
359 0.6640028542
|
||||||
|
360 0.6639813347
|
||||||
|
361 0.6639597941
|
||||||
|
362 0.6639429832
|
||||||
|
363 0.6639222708
|
||||||
|
364 0.6639065546
|
||||||
|
365 0.6638823236
|
||||||
|
366 0.6638648195
|
||||||
|
367 0.6638436235
|
||||||
|
368 0.6638208732
|
||||||
|
369 0.6637956357
|
||||||
|
370 0.6637718453
|
||||||
|
371 0.663756918
|
||||||
|
372 0.6637353525
|
||||||
|
373 0.6637143112
|
||||||
|
374 0.6636956547
|
||||||
|
375 0.663680995
|
||||||
|
376 0.66366728
|
||||||
|
377 0.6636487567
|
||||||
|
378 0.6636266904
|
||||||
|
379 0.6636116064
|
||||||
|
380 0.6635902746
|
||||||
|
381 0.6635654896
|
||||||
|
382 0.6635393029
|
||||||
|
383 0.6635171734
|
||||||
|
384 0.663500789
|
||||||
|
385 0.663477743
|
||||||
|
386 0.6634584806
|
||||||
|
387 0.6634337499
|
||||||
|
388 0.6634135584
|
||||||
|
389 0.6633868455
|
||||||
|
390 0.6633755323
|
||||||
|
391 0.663356103
|
||||||
|
392 0.6633337631
|
||||||
|
393 0.663319422
|
||||||
|
394 0.6632911566
|
||||||
|
395 0.6632687875
|
||||||
|
396 0.6632431997
|
||||||
|
397 0.6632189331
|
||||||
|
398 0.663201035
|
||||||
|
399 0.6631898553
|
||||||
|
400 0.6631712482
|
||||||
|
401 0.663143025
|
||||||
|
402 0.663121538
|
||||||
|
403 0.6631087792
|
||||||
|
404 0.6630859067
|
||||||
|
405 0.663066483
|
||||||
|
406 0.6630443652
|
||||||
|
407 0.6630250376
|
||||||
|
408 0.6630007822
|
||||||
|
409 0.6629768728
|
||||||
|
410 0.6629528093
|
||||||
|
411 0.6629260936
|
||||||
|
412 0.6629102182
|
||||||
|
413 0.6628863488
|
||||||
|
414 0.6628648972
|
||||||
|
415 0.6628454339
|
||||||
|
416 0.6628200274
|
||||||
|
417 0.6627942591
|
||||||
|
418 0.6627744647
|
||||||
|
419 0.662765485
|
||||||
|
420 0.6627503257
|
||||||
|
421 0.6627323029
|
||||||
|
422 0.6627111509
|
||||||
|
423 0.6626785863
|
||||||
|
424 0.6626576561
|
||||||
|
425 0.6626363113
|
||||||
|
426 0.6626181065
|
||||||
|
427 0.66259794
|
||||||
|
428 0.6625765658
|
||||||
|
429 0.6625526572
|
||||||
|
430 0.66253135
|
||||||
|
431 0.6625035695
|
||||||
|
432 0.662480212
|
||||||
|
433 0.6624611632
|
||||||
|
434 0.6624332625
|
||||||
|
435 0.6624120584
|
||||||
|
436 0.6623941719
|
||||||
|
437 0.6623766304
|
||||||
|
438 0.6623623329
|
||||||
|
439 0.6623442925
|
||||||
|
440 0.6623212715
|
||||||
|
441 0.6623025941
|
||||||
|
442 0.6622749791
|
||||||
|
443 0.6622534499
|
||||||
|
444 0.6622305473
|
||||||
|
445 0.6622059333
|
||||||
|
446 0.6621871707
|
||||||
|
447 0.6621638454
|
||||||
|
448 0.6621511296
|
||||||
|
449 0.6621349978
|
||||||
|
450 0.6621120424
|
||||||
|
451 0.6620958271
|
||||||
|
452 0.6620793528
|
||||||
|
453 0.6620572713
|
||||||
|
454 0.6620395025
|
||||||
|
455 0.6620188044
|
||||||
|
456 0.6620017347
|
||||||
|
457 0.6619811454
|
||||||
|
458 0.6619695569
|
||||||
|
459 0.661952377
|
||||||
|
460 0.6619237442
|
||||||
|
461 0.6619089407
|
||||||
|
462 0.6618886168
|
||||||
|
463 0.6618831383
|
||||||
|
464 0.6618690774
|
||||||
|
465 0.661845878
|
||||||
|
466 0.6618290213
|
||||||
|
467 0.6618050064
|
||||||
|
468 0.6617832833
|
||||||
|
469 0.6617652311
|
||||||
|
470 0.6617443144
|
||||||
|
471 0.6617202619
|
||||||
|
472 0.6617005831
|
||||||
|
473 0.6616824419
|
||||||
|
474 0.6616538226
|
||||||
|
475 0.6616314155
|
||||||
|
476 0.6616127861
|
||||||
|
477 0.6616029072
|
||||||
|
478 0.6615843751
|
||||||
|
479 0.661563216
|
||||||
|
480 0.6615432257
|
||||||
|
481 0.6615263324
|
||||||
|
482 0.6615033259
|
||||||
|
483 0.661484293
|
||||||
|
484 0.6614678231
|
||||||
|
485 0.6614463024
|
||||||
|
486 0.6614155436
|
||||||
|
487 0.6613958945
|
||||||
|
488 0.661380611
|
||||||
|
489 0.6613677802
|
||||||
|
490 0.6613530086
|
||||||
|
491 0.6613248211
|
||||||
|
492 0.6613059359
|
||||||
|
493 0.6612729965
|
||||||
|
494 0.6612624948
|
||||||
|
495 0.6612401679
|
||||||
|
496 0.6612191637
|
||||||
|
497 0.6611912219
|
||||||
|
498 0.6611773017
|
||||||
|
499 0.6611638216
|
||||||
|
500 0.6611450533
|
||||||
|
501 0.6611179111
|
||||||
|
502 0.6610959069
|
||||||
|
503 0.6610728788
|
||||||
|
504 0.6610436668
|
||||||
|
505 0.6610188976
|
||||||
|
506 0.6610030555
|
||||||
|
507 0.6609831174
|
||||||
|
508 0.6609586562
|
||||||
|
509 0.660935882
|
||||||
|
510 0.6609202024
|
||||||
|
511 0.6609011137
|
||||||
|
512 0.6608726737
|
||||||
|
513 0.6608608849
|
||||||
|
514 0.6608387256
|
||||||
|
515 0.6608136063
|
||||||
|
516 0.6607946343
|
||||||
|
517 0.6607703935
|
||||||
|
518 0.6607509625
|
||||||
|
519 0.6607238109
|
||||||
|
520 0.6606999858
|
||||||
|
521 0.6606813873
|
||||||
|
522 0.6606610372
|
||||||
|
523 0.660638456
|
||||||
|
524 0.6606156483
|
||||||
|
525 0.6605968623
|
||||||
|
526 0.6605735776
|
||||||
|
527 0.6605517294
|
||||||
|
528 0.6605309239
|
||||||
|
529 0.6605086434
|
||||||
|
530 0.6604803349
|
||||||
|
531 0.6604566326
|
||||||
|
532 0.6604430839
|
||||||
|
533 0.6604273738
|
||||||
|
534 0.6604048016
|
||||||
|
535 0.6603845173
|
||||||
|
536 0.6603669212
|
||||||
|
537 0.6603488983
|
||||||
|
538 0.6603176881
|
||||||
|
539 0.6602953862
|
||||||
|
540 0.6602672025
|
||||||
|
541 0.6602568636
|
||||||
|
542 0.660235705
|
||||||
|
543 0.6602152295
|
||||||
|
544 0.6601897709
|
||||||
|
545 0.6601683731
|
||||||
|
546 0.6601472267
|
||||||
|
547 0.6601262337
|
||||||
|
548 0.6601119991
|
||||||
|
549 0.6600869973
|
||||||
|
550 0.6600667497
|
||||||
|
551 0.6600397508
|
||||||
|
552 0.660016863
|
||||||
|
553 0.6599933158
|
||||||
|
554 0.6599632649
|
||||||
|
555 0.6599446007
|
||||||
|
556 0.6599138126
|
||||||
|
557 0.6598965504
|
||||||
|
558 0.6598785723
|
||||||
|
559 0.659860838
|
||||||
|
560 0.6598408724
|
||||||
|
561 0.6598244857
|
||||||
|
562 0.6598082469
|
||||||
|
563 0.6597851673
|
||||||
|
564 0.6597683521
|
||||||
|
565 0.6597479006
|
||||||
|
566 0.6597310938
|
||||||
|
567 0.6597096581
|
||||||
|
568 0.6596862311
|
||||||
|
569 0.6596574779
|
||||||
|
570 0.6596385418
|
||||||
|
571 0.6596189903
|
||||||
|
572 0.65959275
|
||||||
|
573 0.6595730662
|
||||||
|
574 0.6595566809
|
||||||
|
575 0.6595365076
|
||||||
|
576 0.6595163446
|
||||||
|
577 0.6594816637
|
||||||
|
578 0.6594570142
|
||||||
|
579 0.6594353055
|
||||||
|
580 0.6594162362
|
||||||
|
581 0.659395036
|
||||||
|
582 0.6593798831
|
||||||
|
583 0.6593556719
|
||||||
|
584 0.6593292627
|
||||||
|
585 0.6592976737
|
||||||
|
586 0.6592754841
|
||||||
|
587 0.6592510441
|
||||||
|
588 0.6592290326
|
||||||
|
589 0.6592097404
|
||||||
|
590 0.6591876204
|
||||||
|
591 0.6591705995
|
||||||
|
592 0.6591456195
|
||||||
|
593 0.6591107122
|
||||||
|
594 0.6590819533
|
||||||
|
595 0.6590551327
|
||||||
|
596 0.6590373916
|
||||||
|
597 0.6590177149
|
||||||
|
598 0.6589946095
|
||||||
|
599 0.6589697628
|
||||||
|
600 0.6589442269
|
||||||
|
601 0.6589182437
|
||||||
|
602 0.6588837179
|
||||||
|
603 0.6588674101
|
||||||
|
604 0.6588406916
|
||||||
|
605 0.6588149945
|
||||||
|
606 0.6587866031
|
||||||
|
607 0.6587636648
|
||||||
|
608 0.6587502469
|
||||||
|
609 0.6587292784
|
||||||
|
610 0.6587104112
|
||||||
|
611 0.6586953782
|
||||||
|
612 0.6586641191
|
||||||
|
613 0.6586450136
|
||||||
|
614 0.6586136263
|
||||||
|
615 0.6585862768
|
||||||
|
616 0.6585585235
|
||||||
|
617 0.6585371631
|
||||||
|
618 0.6585092632
|
||||||
|
619 0.6584914317
|
||||||
|
620 0.6584662432
|
||||||
|
621 0.6584454668
|
||||||
|
622 0.6584249408
|
||||||
|
623 0.6583931228
|
||||||
|
624 0.6583660767
|
||||||
|
625 0.658354264
|
||||||
|
626 0.6583253625
|
||||||
|
627 0.6582968632
|
||||||
|
628 0.6582687399
|
||||||
|
629 0.658242535
|
||||||
|
630 0.6582199874
|
||||||
|
631 0.6581918101
|
||||||
|
632 0.6581735218
|
||||||
|
633 0.6581445869
|
||||||
|
634 0.6581202427
|
||||||
|
635 0.6580977862
|
||||||
|
636 0.6580724179
|
||||||
|
637 0.6580426322
|
||||||
|
638 0.6580111256
|
||||||
|
639 0.6579834747
|
||||||
|
640 0.6579541367
|
||||||
|
641 0.6579254503
|
||||||
|
642 0.657898555
|
||||||
|
643 0.6578676875
|
||||||
|
644 0.6578324163
|
||||||
|
645 0.6578062223
|
||||||
|
646 0.6577760631
|
||||||
|
647 0.6577483474
|
||||||
|
648 0.6577249642
|
||||||
|
649 0.6576974966
|
||||||
|
650 0.657675114
|
||||||
|
651 0.6576447891
|
||||||
|
652 0.6576102356
|
||||||
|
653 0.6575793887
|
||||||
|
654 0.6575543309
|
||||||
|
655 0.6575340787
|
||||||
|
656 0.6575061464
|
||||||
|
657 0.657476113
|
||||||
|
658 0.6574447014
|
||||||
|
659 0.6574247361
|
||||||
|
660 0.6574034983
|
||||||
|
661 0.6573783832
|
||||||
|
662 0.657357694
|
||||||
|
663 0.6573411592
|
||||||
|
664 0.6573118559
|
||||||
|
665 0.6572819076
|
||||||
|
666 0.6572430097
|
||||||
|
667 0.6572160391
|
||||||
|
668 0.6571931413
|
||||||
|
669 0.6571737099
|
||||||
|
670 0.6571532872
|
||||||
|
671 0.6571208939
|
||||||
|
672 0.6570887673
|
||||||
|
673 0.6570633692
|
||||||
|
674 0.6570454361
|
||||||
|
675 0.6570231031
|
||||||
|
676 0.6570052089
|
||||||
|
677 0.6569855794
|
||||||
|
678 0.6569579709
|
||||||
|
679 0.6569333354
|
||||||
|
680 0.6569069617
|
||||||
|
681 0.6568931857
|
||||||
|
682 0.6568734532
|
||||||
|
683 0.6568435196
|
||||||
|
684 0.6568108038
|
||||||
|
685 0.6567811374
|
||||||
|
686 0.6567467284
|
||||||
|
687 0.6567172734
|
||||||
|
688 0.6566967606
|
||||||
|
689 0.6566720128
|
||||||
|
690 0.6566441608
|
||||||
|
691 0.6566172287
|
||||||
|
692 0.6565952549
|
||||||
|
693 0.6565702687
|
||||||
|
694 0.6565392213
|
||||||
|
695 0.6565157938
|
||||||
|
696 0.6564902789
|
||||||
|
697 0.6564644734
|
||||||
|
698 0.6564349549
|
||||||
|
699 0.6564046572
|
||||||
|
700 0.6563744107
|
||||||
|
701 0.6563525063
|
||||||
|
702 0.6563189867
|
||||||
|
703 0.6562939062
|
||||||
|
704 0.6562739297
|
||||||
|
705 0.656256438
|
||||||
|
706 0.6562366475
|
||||||
|
707 0.6562073096
|
||||||
|
708 0.6561864222
|
||||||
|
709 0.6561578826
|
||||||
|
710 0.6561208567
|
||||||
|
711 0.6560924703
|
||||||
|
712 0.6560656907
|
||||||
|
713 0.6560362588
|
||||||
|
714 0.6560124527
|
||||||
|
715 0.6559875055
|
||||||
|
716 0.6559547281
|
||||||
|
717 0.6559230866
|
||||||
|
718 0.6558924823
|
||||||
|
719 0.6558676469
|
||||||
|
720 0.6558459277
|
||||||
|
721 0.6558149638
|
||||||
|
722 0.6557812248
|
||||||
|
723 0.6557546502
|
||||||
|
724 0.6557274948
|
||||||
|
725 0.6557044723
|
||||||
|
726 0.6556751811
|
||||||
|
727 0.6556539158
|
||||||
|
728 0.6556182915
|
||||||
|
729 0.6555977079
|
||||||
|
730 0.6555667903
|
||||||
|
731 0.6555394075
|
||||||
|
732 0.6555122742
|
||||||
|
733 0.6554814941
|
||||||
|
734 0.6554517373
|
||||||
|
735 0.655429552
|
||||||
|
736 0.655396579
|
||||||
|
737 0.6553735864
|
||||||
|
738 0.6553472597
|
||||||
|
739 0.6553252832
|
||||||
|
740 0.6552971659
|
||||||
|
741 0.6552763852
|
||||||
|
742 0.6552488203
|
||||||
|
743 0.65521229
|
||||||
|
744 0.6551949744
|
||||||
|
745 0.6551673797
|
||||||
|
746 0.6551421856
|
||||||
|
747 0.6551255516
|
||||||
|
748 0.6551019608
|
||||||
|
749 0.6550758728
|
||||||
|
750 0.655051966
|
||||||
|
751 0.6550351058
|
||||||
|
752 0.6549998756
|
||||||
|
753 0.6549721212
|
||||||
|
754 0.6549401744
|
||||||
|
755 0.6549207325
|
||||||
|
756 0.6548900891
|
||||||
|
757 0.6548682731
|
||||||
|
758 0.6548418938
|
||||||
|
759 0.6548234717
|
||||||
|
760 0.6547996833
|
||||||
|
761 0.6547726174
|
||||||
|
762 0.6547509314
|
||||||
|
763 0.6547168175
|
||||||
|
764 0.6546907846
|
||||||
|
765 0.6546671611
|
||||||
|
766 0.6546475893
|
||||||
|
767 0.6546206223
|
||||||
|
768 0.6545874193
|
||||||
|
769 0.6545620629
|
||||||
|
770 0.6545346297
|
||||||
|
771 0.6545172316
|
||||||
|
772 0.6544943049
|
||||||
|
773 0.6544632323
|
||||||
|
774 0.6544384097
|
||||||
|
775 0.6544084745
|
||||||
|
776 0.6543765257
|
||||||
|
777 0.6543536123
|
||||||
|
778 0.6543303593
|
||||||
|
779 0.6543005831
|
||||||
|
780 0.6542678123
|
||||||
|
781 0.6542439303
|
||||||
|
782 0.6542100401
|
||||||
|
783 0.6541836178
|
||||||
|
784 0.654158129
|
||||||
|
785 0.6541343464
|
||||||
|
786 0.6541092921
|
||||||
|
787 0.6540812254
|
||||||
|
788 0.654060259
|
||||||
|
789 0.6540467253
|
||||||
|
790 0.6540306837
|
||||||
|
791 0.6540103667
|
||||||
|
792 0.6539821302
|
||||||
|
793 0.6539577914
|
||||||
|
794 0.653923724
|
||||||
|
795 0.6539086888
|
||||||
|
796 0.6538798424
|
||||||
|
797 0.6538566996
|
||||||
|
798 0.6538290752
|
||||||
|
799 0.6538051255
|
||||||
|
800 0.6537917354
|
||||||
|
801 0.6537684302
|
||||||
|
802 0.6537402991
|
||||||
|
803 0.6537165427
|
||||||
|
804 0.6536853601
|
||||||
|
805 0.6536681479
|
||||||
|
806 0.6536409101
|
||||||
|
807 0.6536120189
|
||||||
|
808 0.6535912493
|
||||||
|
809 0.6535617421
|
||||||
|
810 0.6535315174
|
||||||
|
811 0.6534972927
|
||||||
|
812 0.6534818476
|
||||||
|
813 0.6534498323
|
||||||
|
814 0.6534305025
|
||||||
|
815 0.6534081059
|
||||||
|
816 0.6533765804
|
||||||
|
817 0.6533441549
|
||||||
|
818 0.6533053405
|
||||||
|
819 0.6532838469
|
||||||
|
820 0.6532604302
|
||||||
|
821 0.6532364412
|
||||||
|
822 0.6532100089
|
||||||
|
823 0.6531782515
|
||||||
|
824 0.6531449701
|
||||||
|
825 0.653115452
|
||||||
|
826 0.6530787602
|
||||||
|
827 0.653052397
|
||||||
|
828 0.6530313579
|
||||||
|
829 0.6530010363
|
||||||
|
830 0.6529752146
|
||||||
|
831 0.652954801
|
||||||
|
832 0.6529330351
|
||||||
|
833 0.6528993709
|
||||||
|
834 0.6528665883
|
||||||
|
835 0.6528413041
|
||||||
|
836 0.6528217161
|
||||||
|
837 0.6527978782
|
||||||
|
838 0.6527789461
|
||||||
|
839 0.6527432001
|
||||||
|
840 0.6527139767
|
||||||
|
841 0.6526857244
|
||||||
|
842 0.652657086
|
||||||
|
843 0.6526355016
|
||||||
|
844 0.6526054936
|
||||||
|
845 0.6525793707
|
||||||
|
846 0.6525584692
|
||||||
|
847 0.6525279747
|
||||||
|
848 0.6525038765
|
||||||
|
849 0.6524849104
|
||||||
|
850 0.6524610603
|
||||||
|
851 0.6524357337
|
||||||
|
852 0.6524082286
|
||||||
|
853 0.65238051
|
||||||
|
854 0.6523557826
|
||||||
|
855 0.6523391233
|
||||||
|
856 0.652325347
|
||||||
|
857 0.6522924958
|
||||||
|
858 0.6522623584
|
||||||
|
859 0.6522343891
|
||||||
|
860 0.6522094424
|
||||||
|
861 0.6521841478
|
||||||
|
862 0.6521657946
|
||||||
|
863 0.6521304278
|
||||||
|
864 0.6521045712
|
||||||
|
865 0.6520753696
|
||||||
|
866 0.6520519528
|
||||||
|
867 0.6520216555
|
||||||
|
868 0.6519926935
|
||||||
|
869 0.6519734186
|
||||||
|
@@ -0,0 +1,871 @@
|
|||||||
|
iter Passed Remaining
|
||||||
|
0 46 93548
|
||||||
|
1 83 83419
|
||||||
|
2 132 88415
|
||||||
|
3 162 81250
|
||||||
|
4 196 78573
|
||||||
|
5 230 76747
|
||||||
|
6 269 76701
|
||||||
|
7 319 79674
|
||||||
|
8 364 80653
|
||||||
|
9 411 81918
|
||||||
|
10 456 82497
|
||||||
|
11 491 81432
|
||||||
|
12 522 79809
|
||||||
|
13 555 78774
|
||||||
|
14 595 78777
|
||||||
|
15 630 78123
|
||||||
|
16 662 77290
|
||||||
|
17 700 77124
|
||||||
|
18 730 76120
|
||||||
|
19 764 75651
|
||||||
|
20 804 75774
|
||||||
|
21 835 75128
|
||||||
|
22 886 76169
|
||||||
|
23 920 75764
|
||||||
|
24 960 75853
|
||||||
|
25 989 75130
|
||||||
|
26 1025 74941
|
||||||
|
27 1060 74714
|
||||||
|
28 1104 75079
|
||||||
|
29 1141 74976
|
||||||
|
30 1180 74975
|
||||||
|
31 1213 74640
|
||||||
|
32 1245 74260
|
||||||
|
33 1287 74434
|
||||||
|
34 1327 74528
|
||||||
|
35 1376 75071
|
||||||
|
36 1427 75741
|
||||||
|
37 1468 75804
|
||||||
|
38 1508 75857
|
||||||
|
39 1549 75922
|
||||||
|
40 1586 75781
|
||||||
|
41 1621 75590
|
||||||
|
42 1663 75705
|
||||||
|
43 1701 75621
|
||||||
|
44 1739 75591
|
||||||
|
45 1776 75460
|
||||||
|
46 1819 75616
|
||||||
|
47 1869 76025
|
||||||
|
48 1916 76288
|
||||||
|
49 1953 76191
|
||||||
|
50 1993 76197
|
||||||
|
51 2038 76381
|
||||||
|
52 2080 76420
|
||||||
|
53 2158 77788
|
||||||
|
54 2220 78529
|
||||||
|
55 2286 79390
|
||||||
|
56 2328 79372
|
||||||
|
57 2367 79254
|
||||||
|
58 2409 79257
|
||||||
|
59 2444 79049
|
||||||
|
60 2484 78985
|
||||||
|
61 2521 78820
|
||||||
|
62 2554 78528
|
||||||
|
63 2593 78466
|
||||||
|
64 2623 78111
|
||||||
|
65 2660 77969
|
||||||
|
66 2695 77776
|
||||||
|
67 2725 77446
|
||||||
|
68 2761 77291
|
||||||
|
69 2791 76975
|
||||||
|
70 2824 76739
|
||||||
|
71 2861 76611
|
||||||
|
72 2897 76476
|
||||||
|
73 2935 76408
|
||||||
|
74 3040 78027
|
||||||
|
75 3097 78411
|
||||||
|
76 3152 78741
|
||||||
|
77 3216 79248
|
||||||
|
78 3256 79195
|
||||||
|
79 3305 79336
|
||||||
|
80 3348 79320
|
||||||
|
81 3381 79089
|
||||||
|
82 3416 78911
|
||||||
|
83 3480 79399
|
||||||
|
84 3535 79649
|
||||||
|
85 3581 79716
|
||||||
|
86 3612 79428
|
||||||
|
87 3644 79185
|
||||||
|
88 3678 78975
|
||||||
|
89 3712 78785
|
||||||
|
90 3743 78531
|
||||||
|
91 3775 78297
|
||||||
|
92 3806 78047
|
||||||
|
93 3837 77821
|
||||||
|
94 3871 77629
|
||||||
|
95 3913 77618
|
||||||
|
96 3945 77403
|
||||||
|
97 3989 77433
|
||||||
|
98 4020 77204
|
||||||
|
99 4053 77020
|
||||||
|
100 4084 76789
|
||||||
|
101 4116 76597
|
||||||
|
102 4148 76401
|
||||||
|
103 4176 76141
|
||||||
|
104 4202 75845
|
||||||
|
105 4232 75634
|
||||||
|
106 4261 75390
|
||||||
|
107 4290 75168
|
||||||
|
108 4324 75018
|
||||||
|
109 4351 74766
|
||||||
|
110 4386 74648
|
||||||
|
111 4424 74577
|
||||||
|
112 4458 74455
|
||||||
|
113 4497 74400
|
||||||
|
114 4533 74307
|
||||||
|
115 4564 74136
|
||||||
|
116 4596 73981
|
||||||
|
117 4628 73818
|
||||||
|
118 4668 73786
|
||||||
|
119 4692 73509
|
||||||
|
120 4723 73354
|
||||||
|
121 4756 73220
|
||||||
|
122 4788 73065
|
||||||
|
123 4815 72854
|
||||||
|
124 4843 72647
|
||||||
|
125 4875 72514
|
||||||
|
126 4916 72515
|
||||||
|
127 4952 72436
|
||||||
|
128 4991 72397
|
||||||
|
129 5028 72327
|
||||||
|
130 5059 72180
|
||||||
|
131 5096 72116
|
||||||
|
132 5125 71946
|
||||||
|
133 5156 71804
|
||||||
|
134 5190 71704
|
||||||
|
135 5221 71564
|
||||||
|
136 5251 71407
|
||||||
|
137 5274 71165
|
||||||
|
138 5309 71084
|
||||||
|
139 5344 71008
|
||||||
|
140 5377 70902
|
||||||
|
141 5416 70866
|
||||||
|
142 5452 70803
|
||||||
|
143 5490 70760
|
||||||
|
144 5521 70641
|
||||||
|
145 5553 70522
|
||||||
|
146 5582 70365
|
||||||
|
147 5611 70217
|
||||||
|
148 5636 70026
|
||||||
|
149 5673 69975
|
||||||
|
150 5706 69874
|
||||||
|
151 5738 69764
|
||||||
|
152 5765 69605
|
||||||
|
153 5795 69471
|
||||||
|
154 5817 69246
|
||||||
|
155 5853 69191
|
||||||
|
156 5888 69122
|
||||||
|
157 5924 69070
|
||||||
|
158 5964 69061
|
||||||
|
159 5996 68963
|
||||||
|
160 6022 68789
|
||||||
|
161 6050 68650
|
||||||
|
162 6079 68510
|
||||||
|
163 6108 68385
|
||||||
|
164 6140 68292
|
||||||
|
165 6169 68162
|
||||||
|
166 6202 68074
|
||||||
|
167 6231 67953
|
||||||
|
168 6263 67858
|
||||||
|
169 6295 67764
|
||||||
|
170 6325 67656
|
||||||
|
171 6356 67561
|
||||||
|
172 6395 67545
|
||||||
|
173 6437 67554
|
||||||
|
174 6472 67495
|
||||||
|
175 6503 67395
|
||||||
|
176 6533 67291
|
||||||
|
177 6562 67174
|
||||||
|
178 6590 67049
|
||||||
|
179 6624 66982
|
||||||
|
180 6655 66882
|
||||||
|
181 6687 66804
|
||||||
|
182 6718 66703
|
||||||
|
183 6751 66632
|
||||||
|
184 6784 66559
|
||||||
|
185 6810 66424
|
||||||
|
186 6832 66246
|
||||||
|
187 6867 66187
|
||||||
|
188 6918 66294
|
||||||
|
189 6969 66393
|
||||||
|
190 7018 66470
|
||||||
|
191 7074 66614
|
||||||
|
192 7117 66635
|
||||||
|
193 7191 66943
|
||||||
|
194 7242 67036
|
||||||
|
195 7282 67027
|
||||||
|
196 7317 66967
|
||||||
|
197 7351 66903
|
||||||
|
198 7389 66879
|
||||||
|
199 7432 66896
|
||||||
|
200 7471 66869
|
||||||
|
201 7506 66814
|
||||||
|
202 7540 66752
|
||||||
|
203 7568 66628
|
||||||
|
204 7605 66596
|
||||||
|
205 7638 66519
|
||||||
|
206 7665 66397
|
||||||
|
207 7700 66340
|
||||||
|
208 7734 66276
|
||||||
|
209 7766 66197
|
||||||
|
210 7796 66106
|
||||||
|
211 7831 66053
|
||||||
|
212 7871 66037
|
||||||
|
213 7910 66016
|
||||||
|
214 7951 66014
|
||||||
|
215 7989 65983
|
||||||
|
216 8025 65946
|
||||||
|
217 8058 65872
|
||||||
|
218 8087 65768
|
||||||
|
219 8112 65638
|
||||||
|
220 8148 65594
|
||||||
|
221 8197 65655
|
||||||
|
222 8239 65655
|
||||||
|
223 8268 65556
|
||||||
|
224 8298 65466
|
||||||
|
225 8327 65366
|
||||||
|
226 8357 65278
|
||||||
|
227 8384 65167
|
||||||
|
228 8418 65103
|
||||||
|
229 8453 65058
|
||||||
|
230 8490 65020
|
||||||
|
231 8523 64958
|
||||||
|
232 8550 64848
|
||||||
|
233 8575 64718
|
||||||
|
234 8607 64648
|
||||||
|
235 8635 64545
|
||||||
|
236 8660 64426
|
||||||
|
237 8691 64345
|
||||||
|
238 8719 64250
|
||||||
|
239 8746 64137
|
||||||
|
240 8773 64038
|
||||||
|
241 8803 63951
|
||||||
|
242 8833 63873
|
||||||
|
243 8862 63779
|
||||||
|
244 8892 63698
|
||||||
|
245 8932 63688
|
||||||
|
246 8962 63611
|
||||||
|
247 8991 63521
|
||||||
|
248 9021 63442
|
||||||
|
249 9051 63358
|
||||||
|
250 9085 63306
|
||||||
|
251 9110 63193
|
||||||
|
252 9137 63093
|
||||||
|
253 9174 63066
|
||||||
|
254 9196 62935
|
||||||
|
255 9238 62934
|
||||||
|
256 9267 62855
|
||||||
|
257 9297 62776
|
||||||
|
258 9324 62681
|
||||||
|
259 9357 62625
|
||||||
|
260 9388 62552
|
||||||
|
261 9427 62536
|
||||||
|
262 9461 62491
|
||||||
|
263 9496 62443
|
||||||
|
264 9524 62356
|
||||||
|
265 9553 62278
|
||||||
|
266 9590 62247
|
||||||
|
267 9620 62172
|
||||||
|
268 9645 62071
|
||||||
|
269 9682 62040
|
||||||
|
270 9711 61962
|
||||||
|
271 9739 61872
|
||||||
|
272 9768 61797
|
||||||
|
273 9804 61761
|
||||||
|
274 9848 61777
|
||||||
|
275 9886 61755
|
||||||
|
276 9925 61740
|
||||||
|
277 9965 61728
|
||||||
|
278 9995 61656
|
||||||
|
279 10022 61564
|
||||||
|
280 10055 61516
|
||||||
|
281 10080 61410
|
||||||
|
282 10111 61344
|
||||||
|
283 10147 61311
|
||||||
|
284 10175 61230
|
||||||
|
285 10202 61141
|
||||||
|
286 10234 61084
|
||||||
|
287 10264 61018
|
||||||
|
288 10299 60977
|
||||||
|
289 10323 60874
|
||||||
|
290 10353 60804
|
||||||
|
291 10394 60803
|
||||||
|
292 10431 60773
|
||||||
|
293 10471 60763
|
||||||
|
294 10503 60707
|
||||||
|
295 10534 60645
|
||||||
|
296 10576 60646
|
||||||
|
297 10612 60612
|
||||||
|
298 10639 60525
|
||||||
|
299 10668 60453
|
||||||
|
300 10702 60411
|
||||||
|
301 10729 60326
|
||||||
|
302 10764 60290
|
||||||
|
303 10801 60263
|
||||||
|
304 10829 60182
|
||||||
|
305 10857 60108
|
||||||
|
306 10892 60067
|
||||||
|
307 10930 60047
|
||||||
|
308 10972 60045
|
||||||
|
309 11002 59983
|
||||||
|
310 11030 59902
|
||||||
|
311 11058 59828
|
||||||
|
312 11092 59788
|
||||||
|
313 11117 59696
|
||||||
|
314 11149 59641
|
||||||
|
315 11187 59617
|
||||||
|
316 11211 59525
|
||||||
|
317 11243 59468
|
||||||
|
318 11274 59413
|
||||||
|
319 11304 59346
|
||||||
|
320 11334 59287
|
||||||
|
321 11362 59209
|
||||||
|
322 11394 59158
|
||||||
|
323 11436 59158
|
||||||
|
324 11477 59153
|
||||||
|
325 11513 59122
|
||||||
|
326 11547 59081
|
||||||
|
327 11572 58991
|
||||||
|
328 11607 58956
|
||||||
|
329 11637 58894
|
||||||
|
330 11668 58833
|
||||||
|
331 11700 58785
|
||||||
|
332 11724 58694
|
||||||
|
333 11757 58648
|
||||||
|
334 11780 58550
|
||||||
|
335 11815 58515
|
||||||
|
336 11844 58451
|
||||||
|
337 11869 58364
|
||||||
|
338 11905 58335
|
||||||
|
339 11941 58302
|
||||||
|
340 11986 58315
|
||||||
|
341 12020 58274
|
||||||
|
342 12066 58292
|
||||||
|
343 12122 58358
|
||||||
|
344 12177 58415
|
||||||
|
345 12221 58422
|
||||||
|
346 12264 58423
|
||||||
|
347 12300 58394
|
||||||
|
348 12324 58304
|
||||||
|
349 12354 58243
|
||||||
|
350 12401 58262
|
||||||
|
351 12438 58232
|
||||||
|
352 12479 58228
|
||||||
|
353 12512 58179
|
||||||
|
354 12541 58116
|
||||||
|
355 12569 58044
|
||||||
|
356 12597 57977
|
||||||
|
357 12628 57920
|
||||||
|
358 12653 57839
|
||||||
|
359 12682 57775
|
||||||
|
360 12720 57752
|
||||||
|
361 12744 57666
|
||||||
|
362 12770 57592
|
||||||
|
363 12811 57583
|
||||||
|
364 12841 57522
|
||||||
|
365 12870 57460
|
||||||
|
366 12897 57386
|
||||||
|
367 12938 57378
|
||||||
|
368 12974 57347
|
||||||
|
369 13009 57313
|
||||||
|
370 13038 57249
|
||||||
|
371 13078 57235
|
||||||
|
372 13117 57216
|
||||||
|
373 13147 57159
|
||||||
|
374 13181 57118
|
||||||
|
375 13205 57036
|
||||||
|
376 13235 56979
|
||||||
|
377 13274 56960
|
||||||
|
378 13306 56911
|
||||||
|
379 13333 56841
|
||||||
|
380 13366 56798
|
||||||
|
381 13396 56741
|
||||||
|
382 13421 56666
|
||||||
|
383 13467 56674
|
||||||
|
384 13508 56664
|
||||||
|
385 13540 56616
|
||||||
|
386 13569 56559
|
||||||
|
387 13598 56496
|
||||||
|
388 13627 56438
|
||||||
|
389 13656 56376
|
||||||
|
390 13685 56317
|
||||||
|
391 13717 56271
|
||||||
|
392 13750 56227
|
||||||
|
393 13771 56135
|
||||||
|
394 13804 56090
|
||||||
|
395 13825 55999
|
||||||
|
396 13858 55957
|
||||||
|
397 13888 55904
|
||||||
|
398 13917 55843
|
||||||
|
399 13953 55812
|
||||||
|
400 13994 55802
|
||||||
|
401 14025 55752
|
||||||
|
402 14048 55670
|
||||||
|
403 14076 55607
|
||||||
|
404 14105 55551
|
||||||
|
405 14142 55526
|
||||||
|
406 14182 55511
|
||||||
|
407 14214 55464
|
||||||
|
408 14240 55394
|
||||||
|
409 14267 55328
|
||||||
|
410 14299 55284
|
||||||
|
411 14324 55213
|
||||||
|
412 14351 55146
|
||||||
|
413 14379 55086
|
||||||
|
414 14410 55036
|
||||||
|
415 14451 55025
|
||||||
|
416 14484 54984
|
||||||
|
417 14513 54929
|
||||||
|
418 14536 54851
|
||||||
|
419 14565 54793
|
||||||
|
420 14587 54710
|
||||||
|
421 14615 54650
|
||||||
|
422 14642 54588
|
||||||
|
423 14666 54515
|
||||||
|
424 14690 54441
|
||||||
|
425 14719 54384
|
||||||
|
426 14739 54297
|
||||||
|
427 14772 54257
|
||||||
|
428 14790 54164
|
||||||
|
429 14824 54125
|
||||||
|
430 14844 54039
|
||||||
|
431 14876 53995
|
||||||
|
432 14906 53946
|
||||||
|
433 14938 53902
|
||||||
|
434 14980 53894
|
||||||
|
435 15006 53829
|
||||||
|
436 15033 53770
|
||||||
|
437 15059 53706
|
||||||
|
438 15085 53639
|
||||||
|
439 15110 53574
|
||||||
|
440 15134 53503
|
||||||
|
441 15160 53438
|
||||||
|
442 15184 53369
|
||||||
|
443 15211 53308
|
||||||
|
444 15234 53236
|
||||||
|
445 15266 53193
|
||||||
|
446 15287 53114
|
||||||
|
447 15316 53059
|
||||||
|
448 15336 52978
|
||||||
|
449 15366 52929
|
||||||
|
450 15393 52870
|
||||||
|
451 15429 52843
|
||||||
|
452 15469 52828
|
||||||
|
453 15490 52748
|
||||||
|
454 15523 52712
|
||||||
|
455 15550 52653
|
||||||
|
456 15577 52594
|
||||||
|
457 15604 52536
|
||||||
|
458 15630 52476
|
||||||
|
459 15656 52414
|
||||||
|
460 15682 52353
|
||||||
|
461 15711 52304
|
||||||
|
462 15736 52238
|
||||||
|
463 15765 52188
|
||||||
|
464 15786 52112
|
||||||
|
465 15817 52068
|
||||||
|
466 15839 51996
|
||||||
|
467 15873 51961
|
||||||
|
468 15903 51916
|
||||||
|
469 15935 51873
|
||||||
|
470 15969 51840
|
||||||
|
471 15994 51779
|
||||||
|
472 16022 51726
|
||||||
|
473 16047 51663
|
||||||
|
474 16073 51605
|
||||||
|
475 16099 51546
|
||||||
|
476 16128 51495
|
||||||
|
477 16152 51431
|
||||||
|
478 16176 51367
|
||||||
|
479 16205 51317
|
||||||
|
480 16228 51250
|
||||||
|
481 16255 51194
|
||||||
|
482 16277 51123
|
||||||
|
483 16305 51071
|
||||||
|
484 16328 51005
|
||||||
|
485 16362 50973
|
||||||
|
486 16392 50928
|
||||||
|
487 16426 50894
|
||||||
|
488 16459 50860
|
||||||
|
489 16480 50787
|
||||||
|
490 16510 50743
|
||||||
|
491 16530 50668
|
||||||
|
492 16561 50625
|
||||||
|
493 16585 50562
|
||||||
|
494 16613 50510
|
||||||
|
495 16638 50453
|
||||||
|
496 16663 50393
|
||||||
|
497 16690 50339
|
||||||
|
498 16716 50282
|
||||||
|
499 16740 50222
|
||||||
|
500 16773 50186
|
||||||
|
501 16802 50139
|
||||||
|
502 16836 50107
|
||||||
|
503 16873 50085
|
||||||
|
504 16921 50094
|
||||||
|
505 16989 50163
|
||||||
|
506 17038 50173
|
||||||
|
507 17069 50132
|
||||||
|
508 17110 50121
|
||||||
|
509 17145 50091
|
||||||
|
510 17190 50091
|
||||||
|
511 17219 50044
|
||||||
|
512 17247 49994
|
||||||
|
513 17271 49932
|
||||||
|
514 17298 49878
|
||||||
|
515 17343 49878
|
||||||
|
516 17373 49836
|
||||||
|
517 17417 49831
|
||||||
|
518 17460 49823
|
||||||
|
519 17490 49781
|
||||||
|
520 17518 49731
|
||||||
|
521 17546 49680
|
||||||
|
522 17571 49622
|
||||||
|
523 17600 49577
|
||||||
|
524 17625 49520
|
||||||
|
525 17655 49474
|
||||||
|
526 17679 49414
|
||||||
|
527 17707 49366
|
||||||
|
528 17729 49300
|
||||||
|
529 17758 49254
|
||||||
|
530 17781 49191
|
||||||
|
531 17808 49141
|
||||||
|
532 17829 49071
|
||||||
|
533 17862 49038
|
||||||
|
534 17905 49031
|
||||||
|
535 18028 49241
|
||||||
|
536 18072 49236
|
||||||
|
537 18106 49203
|
||||||
|
538 18135 49157
|
||||||
|
539 18165 49114
|
||||||
|
540 18200 49083
|
||||||
|
541 18223 49022
|
||||||
|
542 18254 48980
|
||||||
|
543 18280 48927
|
||||||
|
544 18307 48876
|
||||||
|
545 18338 48834
|
||||||
|
546 18367 48790
|
||||||
|
547 18411 48783
|
||||||
|
548 18444 48747
|
||||||
|
549 18470 48693
|
||||||
|
550 18503 48660
|
||||||
|
551 18531 48611
|
||||||
|
552 18557 48558
|
||||||
|
553 18584 48508
|
||||||
|
554 18625 48493
|
||||||
|
555 18650 48436
|
||||||
|
556 18677 48388
|
||||||
|
557 18703 48333
|
||||||
|
558 18729 48282
|
||||||
|
559 18756 48231
|
||||||
|
560 18781 48176
|
||||||
|
561 18808 48126
|
||||||
|
562 18834 48074
|
||||||
|
563 18869 48043
|
||||||
|
564 18902 48008
|
||||||
|
565 18930 47960
|
||||||
|
566 18958 47914
|
||||||
|
567 18983 47859
|
||||||
|
568 19016 47824
|
||||||
|
569 19037 47761
|
||||||
|
570 19068 47720
|
||||||
|
571 19090 47660
|
||||||
|
572 19111 47595
|
||||||
|
573 19141 47553
|
||||||
|
574 19164 47494
|
||||||
|
575 19196 47458
|
||||||
|
576 19217 47393
|
||||||
|
577 19249 47358
|
||||||
|
578 19274 47303
|
||||||
|
579 19298 47247
|
||||||
|
580 19324 47195
|
||||||
|
581 19357 47162
|
||||||
|
582 19391 47130
|
||||||
|
583 19427 47103
|
||||||
|
584 19460 47070
|
||||||
|
585 19483 47012
|
||||||
|
586 19511 46967
|
||||||
|
587 19542 46929
|
||||||
|
588 19564 46867
|
||||||
|
589 19597 46833
|
||||||
|
590 19621 46779
|
||||||
|
591 19647 46729
|
||||||
|
592 19670 46672
|
||||||
|
593 19699 46627
|
||||||
|
594 19726 46582
|
||||||
|
595 19753 46532
|
||||||
|
596 19778 46480
|
||||||
|
597 19803 46429
|
||||||
|
598 19830 46381
|
||||||
|
599 19857 46335
|
||||||
|
600 19896 46313
|
||||||
|
601 19925 46271
|
||||||
|
602 19957 46236
|
||||||
|
603 19991 46204
|
||||||
|
604 20019 46159
|
||||||
|
605 20047 46115
|
||||||
|
606 20072 46063
|
||||||
|
607 20098 46015
|
||||||
|
608 20123 45963
|
||||||
|
609 20149 45913
|
||||||
|
610 20176 45867
|
||||||
|
611 20202 45817
|
||||||
|
612 20230 45774
|
||||||
|
613 20253 45719
|
||||||
|
614 20285 45682
|
||||||
|
615 20307 45626
|
||||||
|
616 20338 45589
|
||||||
|
617 20361 45532
|
||||||
|
618 20394 45500
|
||||||
|
619 20423 45459
|
||||||
|
620 20454 45420
|
||||||
|
621 20488 45390
|
||||||
|
622 20510 45333
|
||||||
|
623 20543 45301
|
||||||
|
624 20569 45252
|
||||||
|
625 20594 45201
|
||||||
|
626 20619 45151
|
||||||
|
627 20646 45107
|
||||||
|
628 20675 45066
|
||||||
|
629 20701 45016
|
||||||
|
630 20727 44970
|
||||||
|
631 20752 44919
|
||||||
|
632 20782 44881
|
||||||
|
633 20804 44825
|
||||||
|
634 20837 44791
|
||||||
|
635 20862 44742
|
||||||
|
636 20892 44704
|
||||||
|
637 20931 44683
|
||||||
|
638 20960 44643
|
||||||
|
639 20994 44612
|
||||||
|
640 21022 44570
|
||||||
|
641 21052 44531
|
||||||
|
642 21082 44493
|
||||||
|
643 21107 44443
|
||||||
|
644 21135 44401
|
||||||
|
645 21160 44351
|
||||||
|
646 21185 44302
|
||||||
|
647 21210 44253
|
||||||
|
648 21236 44208
|
||||||
|
649 21262 44161
|
||||||
|
650 21288 44113
|
||||||
|
651 21315 44068
|
||||||
|
652 21343 44027
|
||||||
|
653 21377 43997
|
||||||
|
654 21403 43949
|
||||||
|
655 21440 43926
|
||||||
|
656 21477 43903
|
||||||
|
657 21502 43854
|
||||||
|
658 21533 43819
|
||||||
|
659 21559 43772
|
||||||
|
660 21586 43727
|
||||||
|
661 21611 43680
|
||||||
|
662 21637 43633
|
||||||
|
663 21662 43586
|
||||||
|
664 21688 43539
|
||||||
|
665 21714 43493
|
||||||
|
666 21742 43451
|
||||||
|
667 21771 43413
|
||||||
|
668 21818 43409
|
||||||
|
669 21846 43366
|
||||||
|
670 21888 43352
|
||||||
|
671 21934 43345
|
||||||
|
672 21971 43322
|
||||||
|
673 22019 43320
|
||||||
|
674 22053 43289
|
||||||
|
675 22090 43266
|
||||||
|
676 22141 43269
|
||||||
|
677 22176 43240
|
||||||
|
678 22213 43215
|
||||||
|
679 22239 43171
|
||||||
|
680 22270 43134
|
||||||
|
681 22296 43088
|
||||||
|
682 22321 43041
|
||||||
|
683 22350 43002
|
||||||
|
684 22379 42962
|
||||||
|
685 22419 42944
|
||||||
|
686 22452 42912
|
||||||
|
687 22484 42878
|
||||||
|
688 22511 42834
|
||||||
|
689 22537 42789
|
||||||
|
690 22571 42757
|
||||||
|
691 22598 42714
|
||||||
|
692 22624 42669
|
||||||
|
693 22653 42630
|
||||||
|
694 22680 42586
|
||||||
|
695 22708 42545
|
||||||
|
696 22739 42510
|
||||||
|
697 22761 42457
|
||||||
|
698 22792 42421
|
||||||
|
699 22816 42373
|
||||||
|
700 22845 42333
|
||||||
|
701 22870 42288
|
||||||
|
702 22902 42253
|
||||||
|
703 22942 42234
|
||||||
|
704 22974 42201
|
||||||
|
705 23002 42160
|
||||||
|
706 23033 42124
|
||||||
|
707 23054 42071
|
||||||
|
708 23086 42038
|
||||||
|
709 23115 41999
|
||||||
|
710 23143 41957
|
||||||
|
711 23169 41914
|
||||||
|
712 23195 41868
|
||||||
|
713 23230 41840
|
||||||
|
714 23259 41801
|
||||||
|
715 23287 41760
|
||||||
|
716 23311 41713
|
||||||
|
717 23341 41676
|
||||||
|
718 23372 41641
|
||||||
|
719 23405 41610
|
||||||
|
720 23438 41578
|
||||||
|
721 23483 41566
|
||||||
|
722 23507 41519
|
||||||
|
723 23540 41488
|
||||||
|
724 23566 41444
|
||||||
|
725 23595 41406
|
||||||
|
726 23623 41365
|
||||||
|
727 23648 41320
|
||||||
|
728 23677 41281
|
||||||
|
729 23700 41231
|
||||||
|
730 23728 41192
|
||||||
|
731 23752 41144
|
||||||
|
732 23784 41111
|
||||||
|
733 23807 41063
|
||||||
|
734 23840 41031
|
||||||
|
735 23870 40994
|
||||||
|
736 23908 40972
|
||||||
|
737 23941 40940
|
||||||
|
738 23974 40909
|
||||||
|
739 24006 40875
|
||||||
|
740 24036 40838
|
||||||
|
741 24064 40798
|
||||||
|
742 24092 40759
|
||||||
|
743 24127 40730
|
||||||
|
744 24153 40688
|
||||||
|
745 24179 40644
|
||||||
|
746 24207 40604
|
||||||
|
747 24233 40561
|
||||||
|
748 24261 40522
|
||||||
|
749 24295 40491
|
||||||
|
750 24318 40444
|
||||||
|
751 24349 40410
|
||||||
|
752 24376 40368
|
||||||
|
753 24408 40335
|
||||||
|
754 24442 40306
|
||||||
|
755 24474 40273
|
||||||
|
756 24508 40242
|
||||||
|
757 24548 40222
|
||||||
|
758 24575 40182
|
||||||
|
759 24605 40145
|
||||||
|
760 24632 40104
|
||||||
|
761 24660 40064
|
||||||
|
762 24689 40027
|
||||||
|
763 24714 39982
|
||||||
|
764 24745 39949
|
||||||
|
765 24766 39897
|
||||||
|
766 24797 39863
|
||||||
|
767 24825 39823
|
||||||
|
768 24854 39786
|
||||||
|
769 24880 39744
|
||||||
|
770 24909 39706
|
||||||
|
771 24940 39672
|
||||||
|
772 24970 39635
|
||||||
|
773 25004 39606
|
||||||
|
774 25030 39564
|
||||||
|
775 25056 39522
|
||||||
|
776 25086 39486
|
||||||
|
777 25107 39436
|
||||||
|
778 25139 39403
|
||||||
|
779 25159 39351
|
||||||
|
780 25188 39314
|
||||||
|
781 25214 39272
|
||||||
|
782 25240 39230
|
||||||
|
783 25266 39188
|
||||||
|
784 25288 39141
|
||||||
|
785 25315 39101
|
||||||
|
786 25341 39058
|
||||||
|
787 25367 39016
|
||||||
|
788 25391 38972
|
||||||
|
789 25417 38930
|
||||||
|
790 25448 38895
|
||||||
|
791 25482 38867
|
||||||
|
792 25514 38834
|
||||||
|
793 25542 38795
|
||||||
|
794 25569 38756
|
||||||
|
795 25595 38714
|
||||||
|
796 25618 38669
|
||||||
|
797 25643 38626
|
||||||
|
798 25667 38581
|
||||||
|
799 25695 38543
|
||||||
|
800 25716 38494
|
||||||
|
801 25743 38454
|
||||||
|
802 25770 38415
|
||||||
|
803 25790 38364
|
||||||
|
804 25822 38332
|
||||||
|
805 25843 38284
|
||||||
|
806 25873 38249
|
||||||
|
807 25896 38203
|
||||||
|
808 25925 38167
|
||||||
|
809 25955 38131
|
||||||
|
810 25988 38101
|
||||||
|
811 26028 38080
|
||||||
|
812 26055 38042
|
||||||
|
813 26081 38000
|
||||||
|
814 26108 37961
|
||||||
|
815 26131 37916
|
||||||
|
816 26159 37878
|
||||||
|
817 26188 37841
|
||||||
|
818 26214 37800
|
||||||
|
819 26242 37764
|
||||||
|
820 26272 37728
|
||||||
|
821 26298 37688
|
||||||
|
822 26327 37652
|
||||||
|
823 26359 37619
|
||||||
|
824 26385 37580
|
||||||
|
825 26408 37534
|
||||||
|
826 26444 37507
|
||||||
|
827 26477 37478
|
||||||
|
828 26517 37456
|
||||||
|
829 26539 37411
|
||||||
|
830 26573 37382
|
||||||
|
831 26597 37339
|
||||||
|
832 26623 37298
|
||||||
|
833 26650 37259
|
||||||
|
834 26677 37221
|
||||||
|
835 26704 37182
|
||||||
|
836 26728 37138
|
||||||
|
837 26763 37111
|
||||||
|
838 26791 37073
|
||||||
|
839 26822 37041
|
||||||
|
840 26872 37033
|
||||||
|
841 26924 37029
|
||||||
|
842 26982 37033
|
||||||
|
843 27054 37055
|
||||||
|
844 27097 37038
|
||||||
|
845 27120 36994
|
||||||
|
846 27146 36954
|
||||||
|
847 27180 36925
|
||||||
|
848 27206 36884
|
||||||
|
849 27234 36846
|
||||||
|
850 27260 36807
|
||||||
|
851 27289 36770
|
||||||
|
852 27318 36734
|
||||||
|
853 27347 36698
|
||||||
|
854 27386 36675
|
||||||
|
855 27413 36637
|
||||||
|
856 27439 36596
|
||||||
|
857 27471 36564
|
||||||
|
858 27501 36529
|
||||||
|
859 27535 36500
|
||||||
|
860 27572 36474
|
||||||
|
861 27595 36431
|
||||||
|
862 27627 36398
|
||||||
|
863 27654 36360
|
||||||
|
864 27683 36324
|
||||||
|
865 27711 36287
|
||||||
|
866 27738 36249
|
||||||
|
867 27765 36210
|
||||||
|
868 27794 36175
|
||||||
|
869 27820 36135
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,243 @@
|
|||||||
|
"""
|
||||||
|
V27 Rolling Window Feature Calculator
|
||||||
|
======================================
|
||||||
|
Computes rolling averages over 5/10/20 match windows,
|
||||||
|
with home/away splits and trend detection.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def calc_rolling_features(
|
||||||
|
team_matches: List[Tuple], # [(mst, is_home, team_goals, opp_goals, opp_id), ...]
|
||||||
|
before_date: int,
|
||||||
|
team_is_home: bool,
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""Calculate rolling window features for a team before a given date."""
|
||||||
|
valid = [m for m in team_matches if m[0] < before_date]
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
"rolling5_goals_avg": 1.3, "rolling5_conceded_avg": 1.2,
|
||||||
|
"rolling10_goals_avg": 1.3, "rolling10_conceded_avg": 1.2,
|
||||||
|
"rolling20_goals_avg": 1.3, "rolling20_conceded_avg": 1.2,
|
||||||
|
"rolling5_clean_sheets": 0.25,
|
||||||
|
"venue_goals_avg": 1.3, "venue_conceded_avg": 1.2,
|
||||||
|
"goal_trend": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(valid) < 3:
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for window in [5, 10, 20]:
|
||||||
|
recent = valid[-window:] if len(valid) >= window else valid
|
||||||
|
n = len(recent)
|
||||||
|
g_sum = sum(m[2] for m in recent)
|
||||||
|
c_sum = sum(m[3] for m in recent)
|
||||||
|
result[f"rolling{window}_goals_avg"] = g_sum / n
|
||||||
|
result[f"rolling{window}_conceded_avg"] = c_sum / n
|
||||||
|
|
||||||
|
# Clean sheet rate (last 5)
|
||||||
|
r5 = valid[-5:] if len(valid) >= 5 else valid
|
||||||
|
result["rolling5_clean_sheets"] = sum(1 for m in r5 if m[3] == 0) / len(r5)
|
||||||
|
|
||||||
|
# Venue-specific (home-only or away-only)
|
||||||
|
venue_matches = [m for m in valid if m[1] == team_is_home]
|
||||||
|
if venue_matches:
|
||||||
|
vm = venue_matches[-10:] if len(venue_matches) >= 10 else venue_matches
|
||||||
|
result["venue_goals_avg"] = sum(m[2] for m in vm) / len(vm)
|
||||||
|
result["venue_conceded_avg"] = sum(m[3] for m in vm) / len(vm)
|
||||||
|
else:
|
||||||
|
result["venue_goals_avg"] = defaults["venue_goals_avg"]
|
||||||
|
result["venue_conceded_avg"] = defaults["venue_conceded_avg"]
|
||||||
|
|
||||||
|
# Goal trend: compare last 3 vs previous 3
|
||||||
|
if len(valid) >= 6:
|
||||||
|
last3 = sum(m[2] for m in valid[-3:]) / 3
|
||||||
|
prev3 = sum(m[2] for m in valid[-6:-3]) / 3
|
||||||
|
result["goal_trend"] = last3 - prev3
|
||||||
|
else:
|
||||||
|
result["goal_trend"] = 0.0
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def calc_league_quality(
|
||||||
|
all_matches: List[Tuple], # all FT matches in this league
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""Calculate league-level quality features."""
|
||||||
|
defaults = {
|
||||||
|
"league_home_win_rate": 0.45,
|
||||||
|
"league_draw_rate": 0.25,
|
||||||
|
"league_btts_rate": 0.50,
|
||||||
|
"league_ou25_rate": 0.50,
|
||||||
|
"league_reliability_score": 0.50,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(all_matches) < 20:
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
n = len(all_matches)
|
||||||
|
home_wins = sum(1 for m in all_matches if m[2] > m[3])
|
||||||
|
draws = sum(1 for m in all_matches if m[2] == m[3])
|
||||||
|
btts = sum(1 for m in all_matches if m[2] > 0 and m[3] > 0)
|
||||||
|
ou25 = sum(1 for m in all_matches if (m[2] + m[3]) > 2.5)
|
||||||
|
|
||||||
|
hw_rate = home_wins / n
|
||||||
|
dr_rate = draws / n
|
||||||
|
btts_rate = btts / n
|
||||||
|
ou25_rate = ou25 / n
|
||||||
|
|
||||||
|
# Reliability: leagues closer to averages are more predictable
|
||||||
|
predictability = 1.0 - abs(hw_rate - 0.45) - abs(dr_rate - 0.27) * 0.5
|
||||||
|
reliability = max(0.2, min(0.95, predictability))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"league_home_win_rate": round(hw_rate, 4),
|
||||||
|
"league_draw_rate": round(dr_rate, 4),
|
||||||
|
"league_btts_rate": round(btts_rate, 4),
|
||||||
|
"league_ou25_rate": round(ou25_rate, 4),
|
||||||
|
"league_reliability_score": round(reliability, 4),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calc_time_features(
|
||||||
|
team_matches: List[Tuple],
|
||||||
|
match_mst: int,
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""Calculate time-based features."""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Days since last match
|
||||||
|
valid = [m for m in team_matches if m[0] < match_mst]
|
||||||
|
if valid:
|
||||||
|
last_mst = valid[-1][0]
|
||||||
|
days_rest = (match_mst - last_mst) / 86_400_000 # ms to days
|
||||||
|
days_rest = min(days_rest, 60.0) # cap at 60 days
|
||||||
|
else:
|
||||||
|
days_rest = 14.0
|
||||||
|
|
||||||
|
# Month and season flags
|
||||||
|
try:
|
||||||
|
dt = datetime.utcfromtimestamp(match_mst / 1000)
|
||||||
|
month = dt.month
|
||||||
|
is_season_start = 1.0 if month in (7, 8) else 0.0
|
||||||
|
is_season_end = 1.0 if month in (5, 6) else 0.0
|
||||||
|
except Exception:
|
||||||
|
month = 6
|
||||||
|
is_season_start = 0.0
|
||||||
|
is_season_end = 0.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"days_rest": round(days_rest, 2),
|
||||||
|
"match_month": month,
|
||||||
|
"is_season_start": is_season_start,
|
||||||
|
"is_season_end": is_season_end,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calc_advanced_h2h(
|
||||||
|
team_matches: List[Tuple],
|
||||||
|
home_id: int,
|
||||||
|
away_id: int,
|
||||||
|
before_date: int,
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""Calculate advanced H2H features."""
|
||||||
|
defaults = {
|
||||||
|
"h2h_home_goals_avg": 1.3,
|
||||||
|
"h2h_away_goals_avg": 1.1,
|
||||||
|
"h2h_recent_trend": 0.0,
|
||||||
|
"h2h_venue_advantage": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
h2h = [m for m in team_matches if m[4] == away_id and m[0] < before_date]
|
||||||
|
if not h2h:
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
recent = h2h[-10:]
|
||||||
|
home_goals_total = 0
|
||||||
|
away_goals_total = 0
|
||||||
|
venue_home_wins = 0
|
||||||
|
venue_total = 0
|
||||||
|
|
||||||
|
for mst, is_home, team_goals, opp_goals, _ in recent:
|
||||||
|
if is_home:
|
||||||
|
home_goals_total += team_goals
|
||||||
|
away_goals_total += opp_goals
|
||||||
|
venue_total += 1
|
||||||
|
if team_goals > opp_goals:
|
||||||
|
venue_home_wins += 1
|
||||||
|
else:
|
||||||
|
home_goals_total += opp_goals
|
||||||
|
away_goals_total += team_goals
|
||||||
|
|
||||||
|
n = len(recent)
|
||||||
|
result = {
|
||||||
|
"h2h_home_goals_avg": home_goals_total / n,
|
||||||
|
"h2h_away_goals_avg": away_goals_total / n,
|
||||||
|
"h2h_venue_advantage": venue_home_wins / venue_total if venue_total > 0 else 0.5,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Recent trend: last 3 vs overall
|
||||||
|
if len(h2h) >= 4:
|
||||||
|
last3_pts = sum(
|
||||||
|
1.0 if m[2] > m[3] else (0.5 if m[2] == m[3] else 0.0)
|
||||||
|
for m in h2h[-3:]
|
||||||
|
) / 3
|
||||||
|
overall_pts = sum(
|
||||||
|
1.0 if m[2] > m[3] else (0.5 if m[2] == m[3] else 0.0)
|
||||||
|
for m in h2h
|
||||||
|
) / len(h2h)
|
||||||
|
result["h2h_recent_trend"] = round(last3_pts - overall_pts, 4)
|
||||||
|
else:
|
||||||
|
result["h2h_recent_trend"] = 0.0
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def calc_strength_diff(
|
||||||
|
home_form: Dict[str, float],
|
||||||
|
away_form: Dict[str, float],
|
||||||
|
home_elo: Dict[str, float],
|
||||||
|
away_elo: Dict[str, float],
|
||||||
|
home_momentum: float,
|
||||||
|
away_momentum: float,
|
||||||
|
upset_potential: float,
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""Calculate strength differential features."""
|
||||||
|
# Attack vs Defense mismatches
|
||||||
|
h_attack = home_form.get("goals_avg", 1.3)
|
||||||
|
a_defense = away_form.get("conceded_avg", 1.2)
|
||||||
|
a_attack = away_form.get("goals_avg", 1.3)
|
||||||
|
h_defense = home_form.get("conceded_avg", 1.2)
|
||||||
|
|
||||||
|
atk_def_home = h_attack - a_defense # positive = home attack > away defense
|
||||||
|
atk_def_away = a_attack - h_defense
|
||||||
|
|
||||||
|
# XG diff approximation
|
||||||
|
xg_diff = (h_attack + a_defense) / 2 - (a_attack + h_defense) / 2
|
||||||
|
|
||||||
|
# Form × Momentum interaction
|
||||||
|
form_mom = (home_momentum - away_momentum) * (
|
||||||
|
home_form.get("scoring_rate", 0.75) - away_form.get("scoring_rate", 0.75)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ELO-Form consistency
|
||||||
|
elo_diff = home_elo.get("overall", 1500) - away_elo.get("overall", 1500)
|
||||||
|
form_diff = h_attack - a_attack
|
||||||
|
elo_form_consistency = 1.0 if (elo_diff > 0 and form_diff > 0) or (elo_diff < 0 and form_diff < 0) else 0.0
|
||||||
|
|
||||||
|
# Upset × ELO gap
|
||||||
|
elo_gap = abs(elo_diff)
|
||||||
|
upset_x_elo = upset_potential * (elo_gap / 400.0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"attack_vs_defense_home": round(atk_def_home, 4),
|
||||||
|
"attack_vs_defense_away": round(atk_def_away, 4),
|
||||||
|
"xg_diff": round(xg_diff, 4),
|
||||||
|
"form_momentum_interaction": round(form_mom, 4),
|
||||||
|
"elo_form_consistency": elo_form_consistency,
|
||||||
|
"upset_x_elo_gap": round(upset_x_elo, 4),
|
||||||
|
}
|
||||||
+18
-9
@@ -16,6 +16,7 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from models.basketball_v25 import get_basketball_v25_predictor
|
from models.basketball_v25 import get_basketball_v25_predictor
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||||
|
from services.v26_shadow_engine import get_v26_shadow_engine
|
||||||
from data.database import dispose_engine
|
from data.database import dispose_engine
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -36,9 +37,10 @@ class CouponRequest(BaseModel):
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(_: FastAPI):
|
async def lifespan(_: FastAPI):
|
||||||
try:
|
try:
|
||||||
print("🚀 Initializing V25 orchestrator...", flush=True)
|
print("🚀 Initializing V28 orchestrator...", flush=True)
|
||||||
get_single_match_orchestrator()
|
get_single_match_orchestrator()
|
||||||
print("✅ V25 orchestrator ready", flush=True)
|
get_v26_shadow_engine()
|
||||||
|
print("✅ V28 orchestrator ready", flush=True)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
print(f"❌ Failed to initialize orchestrator: {error}", flush=True)
|
print(f"❌ Failed to initialize orchestrator: {error}", flush=True)
|
||||||
import traceback
|
import traceback
|
||||||
@@ -53,8 +55,8 @@ async def lifespan(_: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Suggest-Bet AI Engine",
|
title="Suggest-Bet AI Engine",
|
||||||
version="25.0.0",
|
version="28.0.0",
|
||||||
description="V25 Single Match Prediction Package API",
|
description="V28 Single Match Prediction Package API",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,8 +104,9 @@ async def global_exception_handler(_: Request, exc: Exception):
|
|||||||
@app.get("/")
|
@app.get("/")
|
||||||
def read_root() -> dict[str, Any]:
|
def read_root() -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"status": "Suggest-Bet AI Engine v25",
|
"status": "Suggest-Bet AI Engine v28",
|
||||||
"engine": "V25 Single Match Orchestrator",
|
"engine": "V28 Single Match Orchestrator",
|
||||||
|
"mode": os.getenv("AI_ENGINE_MODE", "v28"),
|
||||||
"routes": [
|
"routes": [
|
||||||
"POST /v20plus/analyze/{match_id}",
|
"POST /v20plus/analyze/{match_id}",
|
||||||
"GET /v20plus/analyze-htms/{match_id}",
|
"GET /v20plus/analyze-htms/{match_id}",
|
||||||
@@ -118,15 +121,21 @@ def read_root() -> dict[str, Any]:
|
|||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health_check() -> dict[str, Any]:
|
def health_check() -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
get_single_match_orchestrator()
|
orchestrator = get_single_match_orchestrator()
|
||||||
|
shadow_engine = get_v26_shadow_engine()
|
||||||
basketball_predictor = get_basketball_v25_predictor()
|
basketball_predictor = get_basketball_v25_predictor()
|
||||||
basketball_readiness = basketball_predictor.readiness_summary()
|
basketball_readiness = basketball_predictor.readiness_summary()
|
||||||
ready = bool(basketball_readiness["fully_loaded"])
|
ready = bool(basketball_readiness["fully_loaded"])
|
||||||
return {
|
return {
|
||||||
"status": "healthy" if ready else "degraded",
|
"status": "healthy" if ready else "degraded",
|
||||||
"engine": "v25.main",
|
"engine": "v28.main",
|
||||||
|
"mode": os.getenv("AI_ENGINE_MODE", "v28"),
|
||||||
"ready": ready,
|
"ready": ready,
|
||||||
"basketball_v25": basketball_readiness,
|
"basketball_v25": basketball_readiness,
|
||||||
|
"v26_shadow": shadow_engine.readiness_summary(),
|
||||||
|
"prediction_service_ready": True,
|
||||||
|
"model_loaded": ready,
|
||||||
|
"orchestrator_mode": getattr(orchestrator, "engine_mode", "v28"),
|
||||||
}
|
}
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
return {"status": "unhealthy", "ready": False, "error": str(error)}
|
return {"status": "unhealthy", "ready": False, "error": str(error)}
|
||||||
@@ -196,7 +205,7 @@ async def analyze_match_htft_v20plus(match_id: str, timeout_sec: int = 30) -> di
|
|||||||
key=lambda item: float(item[1]),
|
key=lambda item: float(item[1]),
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"engine": "v25.main",
|
"engine": "v28.main",
|
||||||
"match_info": result.get("match_info", {}),
|
"match_info": result.get("match_info", {}),
|
||||||
"timing_ms": int((time.time() - started_at) * 1000),
|
"timing_ms": int((time.time() - started_at) * 1000),
|
||||||
"ht_ft_probs": htft_probs,
|
"ht_ft_probs": htft_probs,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"version": "v26.shadow.0",
|
||||||
|
"calibration_version": "v26.shadow.calib.0",
|
||||||
|
"train_rows": 6853,
|
||||||
|
"validation_rows": 1469,
|
||||||
|
"label_priors": {
|
||||||
|
"MS": 0.4404,
|
||||||
|
"OU25": 0.5214,
|
||||||
|
"BTTS": 0.5398,
|
||||||
|
"HT": 0.4275,
|
||||||
|
"HTFT": 0.26,
|
||||||
|
"CARDS": 0.6052
|
||||||
|
},
|
||||||
|
"artifact_path": "/Users/piton/Documents/GitHub/iddaai/iddaai-be/ai-engine/models/v26_shadow/market_profiles.json",
|
||||||
|
"notes": [
|
||||||
|
"v26.shadow runtime currently uses artifact-based calibration and ROI gating",
|
||||||
|
"market profile JSON remains the source of truth for runtime thresholds"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -17,3 +17,4 @@ pyyaml>=6.0
|
|||||||
# V2 async database
|
# V2 async database
|
||||||
asyncpg>=0.29.0
|
asyncpg>=0.29.0
|
||||||
pydantic>=2.5.0
|
pydantic>=2.5.0
|
||||||
|
pytest>=8.0.0
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
"""
|
|
||||||
Backtest for September 13th (Top Leagues Only)
|
|
||||||
==============================================
|
|
||||||
Simulates the NEW 'Skip Logic' on matches from Sept 13, 2025.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Load .env manually to ensure correct DB connection
|
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
sys.path.insert(0, project_root) # Add root to path if needed
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
# ─── Configuration ─────────
|
|
||||||
MIN_CONF_THRESHOLDS = {
|
|
||||||
"MS": 45.0, "DC": 40.0, "OU15": 50.0, "OU25": 45.0,
|
|
||||||
"OU35": 45.0, "BTTS": 45.0, "HT": 40.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
def run_backtest():
|
|
||||||
print("🚀 Backtest: 13 Eylül 2024 - Top Leagues")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# 1. Load Top Leagues
|
|
||||||
leagues_path = os.path.join(project_root, "top_leagues.json")
|
|
||||||
try:
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
# Ensure they are strings for SQL IN clause
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
print(f"📋 Loaded {len(top_leagues)} top leagues.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error loading top_leagues.json: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2. Define Date Range (Sept 13, 2024 UTC)
|
|
||||||
start_dt = datetime(2024, 9, 13, 0, 0, 0)
|
|
||||||
end_dt = datetime(2024, 9, 13, 23, 59, 59)
|
|
||||||
start_ts = int(start_dt.timestamp() * 1000)
|
|
||||||
end_ts = int(end_dt.timestamp() * 1000)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# 3. Fetch Matches & Predictions
|
|
||||||
# We need matches that are FT and have a prediction
|
|
||||||
query = """
|
|
||||||
SELECT p.match_id, p.prediction_json,
|
|
||||||
m.score_home, m.score_away, m.status, m.league_id
|
|
||||||
FROM predictions p
|
|
||||||
JOIN matches m ON p.match_id = m.id
|
|
||||||
WHERE m.mst_utc BETWEEN %s AND %s
|
|
||||||
AND m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND p.prediction_json IS NOT NULL
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
cur.execute(query, (start_ts, end_ts, league_ids))
|
|
||||||
rows = cur.fetchall()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ DB Error: {e}")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"📊 Found {len(rows)} matches with predictions on Sept 13, 2024.")
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print("⚠️ No predictions found for this date. The AI Engine might not have processed these historical matches yet.")
|
|
||||||
print("💡 Tip: Run the feeder or AI engine on this date range to generate predictions first.")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bets = 0
|
|
||||||
winning_bets = 0
|
|
||||||
skipped_bets = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
data = row['prediction_json']
|
|
||||||
if isinstance(data, str):
|
|
||||||
data = json.loads(data)
|
|
||||||
|
|
||||||
home_score = row['score_home'] or 0
|
|
||||||
away_score = row['score_away'] or 0
|
|
||||||
total_goals = home_score + away_score
|
|
||||||
|
|
||||||
# Extract Main Pick
|
|
||||||
main_pick = None
|
|
||||||
main_pick_conf = 0.0
|
|
||||||
main_pick_odds = 0.0
|
|
||||||
|
|
||||||
if "main_pick" in data and isinstance(data["main_pick"], dict):
|
|
||||||
mp = data["main_pick"]
|
|
||||||
main_pick = mp.get("pick")
|
|
||||||
main_pick_conf = mp.get("confidence", 0.0)
|
|
||||||
main_pick_odds = mp.get("odds", 0.0)
|
|
||||||
|
|
||||||
if not main_pick or not main_pick_conf:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Determine Market Type
|
|
||||||
pick_str = str(main_pick).upper()
|
|
||||||
market_type = "MS"
|
|
||||||
if "1X" in pick_str or "X2" in pick_str or "12" in pick_str: market_type = "DC"
|
|
||||||
elif "ÜST" in pick_str or "ALT" in pick_str or "OVER" in pick_str or "UNDER" in pick_str:
|
|
||||||
if "1.5" in pick_str: market_type = "OU15"
|
|
||||||
elif "3.5" in pick_str: market_type = "OU35"
|
|
||||||
else: market_type = "OU25"
|
|
||||||
elif "VAR" in pick_str or "YOK" in pick_str or "BTTS" in pick_str: market_type = "BTTS"
|
|
||||||
|
|
||||||
threshold = MIN_CONF_THRESHOLDS.get(market_type, 45.0)
|
|
||||||
|
|
||||||
# --- SKIP LOGIC ---
|
|
||||||
# 1. Confidence Gate
|
|
||||||
if main_pick_conf < threshold:
|
|
||||||
skipped_bets += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 2. Value Gate
|
|
||||||
if main_pick_odds > 0:
|
|
||||||
implied_prob = 1.0 / main_pick_odds
|
|
||||||
my_prob = main_pick_conf / 100.0
|
|
||||||
edge = my_prob - implied_prob
|
|
||||||
if edge < -0.03:
|
|
||||||
skipped_bets += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# --- BET PLAYED ---
|
|
||||||
total_bets += 1
|
|
||||||
is_won = False
|
|
||||||
|
|
||||||
# Resolve Result
|
|
||||||
if market_type == "MS":
|
|
||||||
if (main_pick == "1" or main_pick == "MS 1") and home_score > away_score: is_won = True
|
|
||||||
elif (main_pick == "X" or main_pick == "MS X") and home_score == away_score: is_won = True
|
|
||||||
elif (main_pick == "2" or main_pick == "MS 2") and away_score > home_score: is_won = True
|
|
||||||
|
|
||||||
elif market_type.startswith("OU"):
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_str: line = 1.5
|
|
||||||
elif "3.5" in pick_str: line = 3.5
|
|
||||||
is_over = total_goals > line
|
|
||||||
is_under = total_goals < line
|
|
||||||
if ("ÜST" in pick_str or "OVER" in pick_str) and is_over: is_won = True
|
|
||||||
elif ("ALT" in pick_str or "UNDER" in pick_str) and is_under: is_won = True
|
|
||||||
|
|
||||||
elif market_type == "BTTS":
|
|
||||||
if home_score > 0 and away_score > 0:
|
|
||||||
if "VAR" in pick_str: is_won = True
|
|
||||||
else:
|
|
||||||
if "YOK" in pick_str: is_won = True
|
|
||||||
|
|
||||||
elif market_type == "DC":
|
|
||||||
if "1X" in pick_str and home_score >= away_score: is_won = True
|
|
||||||
elif "X2" in pick_str and away_score >= home_score: is_won = True
|
|
||||||
elif "12" in pick_str and home_score != away_score: is_won = True
|
|
||||||
|
|
||||||
if is_won:
|
|
||||||
winning_bets += 1
|
|
||||||
profit = main_pick_odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
|
|
||||||
# Report
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📈 BACKTEST RESULTS: 13 EYLÜL 2025 (TOP LEAGUES)")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Total Matches Analyzed: {len(rows)}")
|
|
||||||
print(f"🚫 Bets SKIPPED (Low Conf/Bad Value): {skipped_bets}")
|
|
||||||
print(f"✅ Bets PLAYED: {total_bets}")
|
|
||||||
|
|
||||||
if total_bets > 0:
|
|
||||||
win_rate = (winning_bets / total_bets) * 100
|
|
||||||
roi = (total_profit / total_bets) * 100
|
|
||||||
|
|
||||||
print(f"🏆 Winning Bets: {winning_bets}")
|
|
||||||
print(f"💀 Losing Bets: {total_bets - winning_bets}")
|
|
||||||
print("-" * 40)
|
|
||||||
print(f" Win Rate: {win_rate:.2f}%")
|
|
||||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
|
||||||
print(f"📊 ROI: {roi:.2f}%")
|
|
||||||
|
|
||||||
if roi > 0:
|
|
||||||
print("🟢 STRATEGY IS PROFITABLE!")
|
|
||||||
else:
|
|
||||||
print("🔴 STRATEGY IS LOSING")
|
|
||||||
else:
|
|
||||||
print("⚠️ No bets were played. Thresholds might be too high or no suitable matches found.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_backtest()
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
"""
|
|
||||||
Detailed Backtest with 50 Top League Matches
|
|
||||||
============================================
|
|
||||||
Runs AI Engine predictions on 50 real historical matches and shows
|
|
||||||
exactly which predictions were correct and which were skipped.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python ai-engine/scripts/backtest_50_detailed.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
# Add paths
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
# 50 Match IDs from the query
|
|
||||||
MATCH_IDS = [
|
|
||||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
|
||||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
|
||||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
|
||||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
|
||||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
|
||||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
|
||||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
|
||||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
|
||||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
|
||||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
|
||||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
|
||||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
|
||||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
|
||||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
|
||||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
|
||||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
|
||||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
|
||||||
]
|
|
||||||
|
|
||||||
def run_detailed_backtest():
|
|
||||||
print("🚀 DETAILED BACKTEST: 50 Top League Matches")
|
|
||||||
print("🧠 Engine: V30 Ensemble (V20+V25) + Skip Logic")
|
|
||||||
print("="*80)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# Fetch match details with odds
|
|
||||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
|
||||||
cur.execute(f"""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away, m.league_id,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
l.name as league_name
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
LEFT JOIN leagues l ON m.league_id = l.id
|
|
||||||
WHERE m.id IN ({placeholders})
|
|
||||||
AND m.status = 'FT'
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
""", MATCH_IDS)
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 Found {len(rows)} matches. Starting AI Analysis...")
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print("⚠️ No matches found.")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Initialize AI Engine
|
|
||||||
try:
|
|
||||||
orchestrator = get_single_match_orchestrator()
|
|
||||||
print("✅ AI Engine Loaded.\n")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Failed to load AI Engine: {e}")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# ─── Backtest Loop ───
|
|
||||||
results = []
|
|
||||||
total_skipped = 0
|
|
||||||
total_played = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
MIN_CONF = 45.0
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home_team = row['home_team'] or "Unknown"
|
|
||||||
away_team = row['away_team'] or "Unknown"
|
|
||||||
league = row['league_name'] or "Unknown"
|
|
||||||
home_score = row['score_home'] or 0
|
|
||||||
away_score = row['score_away'] or 0
|
|
||||||
total_goals = home_score + away_score
|
|
||||||
|
|
||||||
print(f"[{i+1}/{len(rows)}] {home_team} vs {away_team} ({league}) ... ", end="", flush=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
prediction = orchestrator.analyze_match(match_id)
|
|
||||||
|
|
||||||
if not prediction:
|
|
||||||
print("⚠️ No prediction")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Extract Main Pick
|
|
||||||
main_pick = prediction.get("main_pick") or {}
|
|
||||||
pick_name = main_pick.get("pick", "")
|
|
||||||
confidence = main_pick.get("confidence", 0)
|
|
||||||
odds = main_pick.get("odds", 0)
|
|
||||||
|
|
||||||
# Apply Skip Logic
|
|
||||||
if confidence < MIN_CONF:
|
|
||||||
print(f"🚫 SKIP (Conf {confidence:.0f}%)")
|
|
||||||
total_skipped += 1
|
|
||||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
|
||||||
"conf": confidence, "odds": odds, "result": "SKIPPED", "profit": 0})
|
|
||||||
continue
|
|
||||||
|
|
||||||
if odds > 0:
|
|
||||||
implied_prob = 1.0 / odds
|
|
||||||
my_prob = confidence / 100.0
|
|
||||||
if my_prob - implied_prob < -0.03:
|
|
||||||
print(f"🚫 SKIP (Bad Value)")
|
|
||||||
total_skipped += 1
|
|
||||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
|
||||||
"conf": confidence, "odds": odds, "result": "SKIPPED", "profit": 0})
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Bet Played
|
|
||||||
total_played += 1
|
|
||||||
won = False
|
|
||||||
|
|
||||||
# Resolve
|
|
||||||
pick_clean = str(pick_name).upper()
|
|
||||||
if pick_clean in ["1", "MS 1", "İY 1"] and home_score > away_score: won = True
|
|
||||||
elif pick_clean in ["X", "MS X", "İY X"] and home_score == away_score: won = True
|
|
||||||
elif pick_clean in ["2", "MS 2", "İY 2"] and away_score > home_score: won = True
|
|
||||||
elif pick_clean in ["1X", "X2"] or ("1X" in pick_clean or "X2" in pick_clean):
|
|
||||||
if "1X" in pick_clean and home_score >= away_score: won = True
|
|
||||||
elif "X2" in pick_clean and away_score >= home_score: won = True
|
|
||||||
elif pick_clean in ["12"] and home_score != away_score: won = True
|
|
||||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
elif "3.5" in pick_clean: line = 3.5
|
|
||||||
if total_goals > line: won = True
|
|
||||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
elif "3.5" in pick_clean: line = 3.5
|
|
||||||
if total_goals < line: won = True
|
|
||||||
elif "VAR" in pick_clean and home_score > 0 and away_score > 0: won = True
|
|
||||||
elif "YOK" in pick_clean and (home_score == 0 or away_score == 0): won = True
|
|
||||||
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
print(f"✅ WON ({pick_name} @ {odds:.2f}, +{profit:.2f})")
|
|
||||||
else:
|
|
||||||
profit = -1.0
|
|
||||||
print(f"❌ LOST ({pick_name} @ {odds:.2f})")
|
|
||||||
|
|
||||||
total_profit += profit
|
|
||||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
|
||||||
"conf": confidence, "odds": odds,
|
|
||||||
"result": "WON" if won else "LOST", "profit": profit,
|
|
||||||
"score": f"{home_score}-{away_score}"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"💥 Error: {e}")
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
|
|
||||||
# ─── DETAILED REPORT ───
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print("📈 DETAILED BACKTEST RESULTS")
|
|
||||||
print(f"⏱️ Time: {elapsed:.1f}s")
|
|
||||||
print("="*80)
|
|
||||||
print(f"📊 Total Matches: {len(rows)}")
|
|
||||||
print(f"🚫 Skipped: {total_skipped}")
|
|
||||||
print(f"🎲 Played: {total_played}")
|
|
||||||
print(f"✅ Won: {total_won}")
|
|
||||||
print(f"💀 Lost: {total_played - total_won}")
|
|
||||||
print(f"💰 Profit: {total_profit:+.2f} units")
|
|
||||||
|
|
||||||
if total_played > 0:
|
|
||||||
win_rate = (total_won / total_played) * 100
|
|
||||||
roi = (total_profit / total_played) * 100
|
|
||||||
print(f"📊 Win Rate: {win_rate:.1f}%")
|
|
||||||
print(f"📊 ROI: {roi:.1f}%")
|
|
||||||
if roi > 0:
|
|
||||||
print("🟢 STRATEGY IS PROFITABLE!")
|
|
||||||
else:
|
|
||||||
print("🔴 STRATEGY IS LOSING")
|
|
||||||
|
|
||||||
# ─── TABLE OF ALL RESULTS ───
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print("📋 DETAILED MATCH RESULTS")
|
|
||||||
print("="*80)
|
|
||||||
print(f"{'Match':<40} {'Pick':<15} {'Conf':<6} {'Odds':<6} {'Result':<8} {'Score':<6}")
|
|
||||||
print("-"*80)
|
|
||||||
for r in results:
|
|
||||||
match_str = r['match'][:38]
|
|
||||||
pick_str = str(r['pick'])[:13]
|
|
||||||
conf_str = f"{r['conf']:.0f}%"
|
|
||||||
odds_str = f"{r['odds']:.2f}" if r['odds'] > 0 else "N/A"
|
|
||||||
res_str = r['result']
|
|
||||||
score_str = r.get('score', '')
|
|
||||||
|
|
||||||
# Color coding
|
|
||||||
if res_str == "WON": res_display = f"✅ {res_str}"
|
|
||||||
elif res_str == "LOST": res_display = f"❌ {res_str}"
|
|
||||||
else: res_display = f"🚫 {res_str}"
|
|
||||||
|
|
||||||
print(f"{match_str:<40} {pick_str:<15} {conf_str:<6} {odds_str:<6} {res_display:<12} {score_str:<6}")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_detailed_backtest()
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
"""
|
|
||||||
Adaptive 500 Match Backtest
|
|
||||||
=============================
|
|
||||||
Skips NO match unless NO odds exist.
|
|
||||||
Evaluates ALL available markets (MS, OU, BTTS) and picks the BEST value bet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_adaptive_backtest():
|
|
||||||
print("🔄 ADAPTIVE 500 MATCH BACKTEST")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# 1. Load Top Leagues
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# 2. Fetch 500 Finished Matches with Odds
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away, m.league_id,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 500
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 Found {len(rows)} matches. Analyzing...\n")
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print("⚠️ No matches found.")
|
|
||||||
return
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Error: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Stats
|
|
||||||
total_evaluated = 0
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
skipped_count = 0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
total_evaluated += 1
|
|
||||||
# print(f"[{i+1}] {home} vs {away} ... ", end="", flush=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred:
|
|
||||||
# print("⚠️ No Data")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ─── ADAPTIVE PICKING ───
|
|
||||||
# Check ALL recommendations (Expert or Standard) to find the BEST option
|
|
||||||
candidates = []
|
|
||||||
|
|
||||||
# Add main picks
|
|
||||||
if pred.get("expert_recommendation"):
|
|
||||||
rec = pred["expert_recommendation"]
|
|
||||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
|
||||||
if rec.get("safe_alternative"): candidates.append(rec["safe_alternative"])
|
|
||||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
|
||||||
elif pred.get("main_pick"):
|
|
||||||
candidates.append(pred["main_pick"])
|
|
||||||
|
|
||||||
best_bet = None
|
|
||||||
for c in candidates:
|
|
||||||
if not c: continue
|
|
||||||
conf = c.get("confidence", 0)
|
|
||||||
odds = c.get("odds", 0)
|
|
||||||
pick = c.get("pick")
|
|
||||||
|
|
||||||
# Flexible Criteria:
|
|
||||||
# 1. Confidence > 60%
|
|
||||||
# 2. Odds > 1.10 (Not "free" odds like 1.00)
|
|
||||||
# 3. Edge > -2% (Slightly tolerant)
|
|
||||||
if conf >= 60 and odds > 1.10:
|
|
||||||
implied = 1.0 / odds
|
|
||||||
edge = ((conf/100) - implied) * 100
|
|
||||||
|
|
||||||
# Prioritize positive edge, but accept small negative if confidence is high
|
|
||||||
if edge > -2.0:
|
|
||||||
if best_bet is None or (conf > best_bet.get("confidence", 0)):
|
|
||||||
best_bet = c
|
|
||||||
|
|
||||||
if best_bet:
|
|
||||||
pick = str(best_bet.get("pick")).upper()
|
|
||||||
conf = best_bet.get("confidence")
|
|
||||||
odds = best_bet.get("odds")
|
|
||||||
|
|
||||||
# Resolution Logic
|
|
||||||
won = False
|
|
||||||
if pick in ["1", "MS 1", "İY 1"] and h_score > a_score: won = True
|
|
||||||
elif pick in ["X", "MS X", "İY X"] and h_score == a_score: won = True
|
|
||||||
elif pick in ["2", "MS 2", "İY 2"] and a_score > h_score: won = True
|
|
||||||
elif pick in ["1X", "X2"]:
|
|
||||||
if "1X" in pick and h_score >= a_score: won = True
|
|
||||||
elif "X2" in pick and a_score >= h_score: won = True
|
|
||||||
elif pick == "12" and h_score != a_score: won = True
|
|
||||||
elif "ÜST" in pick or "OVER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick or "UNDER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
total_bet += 1
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
# print(f"✅ WON (+{profit:.2f}) | {pick}")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
# print(f"❌ LOST ({pick} @ {odds:.2f})")
|
|
||||||
else:
|
|
||||||
skipped_count += 1
|
|
||||||
# print(f"🚫 SKIP (No Value)")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# print(f"💥 Error: {e}")
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("🔄 ADAPTIVE BACKTEST RESULTS (500 Matches)")
|
|
||||||
print("="*60)
|
|
||||||
print(f"📊 Evaluated: {total_evaluated}")
|
|
||||||
print(f"🎲 Played: {total_bet}")
|
|
||||||
print(f"🚫 Skipped: {skipped_count}")
|
|
||||||
print(f"✅ Won: {total_won}")
|
|
||||||
|
|
||||||
if total_bet > 0:
|
|
||||||
win_rate = (total_won / total_bet) * 100
|
|
||||||
roi = (total_profit / total_bet) * 100
|
|
||||||
print(f"📈 Win Rate: {win_rate:.2f}%")
|
|
||||||
print(f"💰 Total Profit: {total_profit:.2f} Units")
|
|
||||||
print(f"📊 ROI: {roi:.2f}%")
|
|
||||||
if total_profit > 0: print("🟢 KARLI STRATEJİ")
|
|
||||||
else: print("🔴 ZARARDA")
|
|
||||||
else:
|
|
||||||
print("⚠️ Hiç bahis oynanmadı. Veri kalitesi çok düşük.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_adaptive_backtest()
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
"""
|
|
||||||
Diagnostic Backtest - Hangi Pazar Kanıyor?
|
|
||||||
===========================================
|
|
||||||
Analyses the 500 matches to see WHICH markets are losing money.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_diagnostic():
|
|
||||||
print("🔍 TANI BACKTESTİ: NEREDE KAYBETTİK?")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away, m.league_id,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 500
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç analiz ediliyor...\n")
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Market Stats: { "MS": {"won": 10, "lost": 20, "profit": -5.0}, ... }
|
|
||||||
market_stats = defaultdict(lambda: {"won": 0, "lost": 0, "profit": 0.0, "total": 0})
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred: continue
|
|
||||||
|
|
||||||
candidates = []
|
|
||||||
if pred.get("expert_recommendation"):
|
|
||||||
rec = pred["expert_recommendation"]
|
|
||||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
|
||||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
|
||||||
elif pred.get("main_pick"):
|
|
||||||
candidates.append(pred["main_pick"])
|
|
||||||
|
|
||||||
played_this = False
|
|
||||||
for c in candidates:
|
|
||||||
if not c: continue
|
|
||||||
conf = c.get("confidence", 0)
|
|
||||||
odds = c.get("odds", 0)
|
|
||||||
pick = str(c.get("pick")).upper()
|
|
||||||
market_type = c.get("market_type", "Unknown")
|
|
||||||
|
|
||||||
# Criteria
|
|
||||||
if conf >= 60 and odds > 1.10:
|
|
||||||
implied = 1.0 / odds
|
|
||||||
edge = ((conf/100) - implied) * 100
|
|
||||||
if edge > -2.0:
|
|
||||||
# Resolve
|
|
||||||
won = False
|
|
||||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif pick in ["1X", "X2"]:
|
|
||||||
if "1X" in pick and h_score >= a_score: won = True
|
|
||||||
elif "X2" in pick and a_score >= h_score: won = True
|
|
||||||
elif pick == "12" and h_score != a_score: won = True
|
|
||||||
elif "ÜST" in pick or "OVER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick or "UNDER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
market_stats[market_type]["total"] += 1
|
|
||||||
if won:
|
|
||||||
market_stats[market_type]["won"] += 1
|
|
||||||
market_stats[market_type]["profit"] += (odds - 1.0)
|
|
||||||
else:
|
|
||||||
market_stats[market_type]["lost"] += 1
|
|
||||||
market_stats[market_type]["profit"] -= 1.0
|
|
||||||
|
|
||||||
played_this = True
|
|
||||||
break # Only one bet per match
|
|
||||||
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
# Print Results
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📊 PAZAR BAZLI KAR/ZARAR TABLOSU")
|
|
||||||
print("="*60)
|
|
||||||
print(f"{'Market':<15} {'Oynanan':<10} {'Kazanılan':<10} {'Win%':<8} {'Kâr':<10}")
|
|
||||||
print("-" * 60)
|
|
||||||
|
|
||||||
for mkt, stats in sorted(market_stats.items(), key=lambda x: x[1]["profit"], reverse=True):
|
|
||||||
wr = (stats["won"] / stats["total"] * 100) if stats["total"] > 0 else 0
|
|
||||||
print(f"{mkt:<15} {stats['total']:<10} {stats['won']:<10} {wr:.1f}% {stats['profit']:+.2f} Units")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_diagnostic()
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
"""
|
|
||||||
Real AI Engine Backtest Script
|
|
||||||
==============================
|
|
||||||
Uses the ACTUAL models (V20/V25 Ensemble) to predict historical matches.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python ai-engine/scripts/backtest_real.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Add paths
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
|
|
||||||
# Fix for Windows path issues in scripts
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR) # One level up if inside scripts folder
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator, MatchData
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_backtest():
|
|
||||||
print("🚀 REAL AI BACKTEST: Sept 13, 2024 - Top Leagues")
|
|
||||||
print("🧠 Engine: V30 Ensemble (V20+V25)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# Load Top Leagues
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
try:
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
print(f"📋 Loaded {len(top_leagues)} top leagues.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error loading top_leagues.json: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Date Range (Sept 13, 2024)
|
|
||||||
start_dt = datetime(2024, 9, 13, 0, 0, 0)
|
|
||||||
end_dt = datetime(2024, 9, 13, 23, 59, 59)
|
|
||||||
start_ts = int(start_dt.timestamp() * 1000)
|
|
||||||
end_ts = int(end_dt.timestamp() * 1000)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# Fetch Matches
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.mst_utc, m.league_id, m.status, m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
l.name as league_name
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
LEFT JOIN leagues l ON m.league_id = l.id
|
|
||||||
WHERE m.mst_utc BETWEEN %s AND %s
|
|
||||||
AND m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
ORDER BY m.mst_utc ASC
|
|
||||||
LIMIT 20 -- Limit to 20 matches to avoid running for hours on a single backtest
|
|
||||||
""", (start_ts, end_ts, league_ids))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 Found {len(rows)} finished matches. Starting AI Analysis...")
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
print("⚠️ No matches found for this date.")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Initialize AI Engine
|
|
||||||
try:
|
|
||||||
orchestrator = get_single_match_orchestrator()
|
|
||||||
print("✅ AI Engine (SingleMatchOrchestrator) Loaded.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Failed to load AI Engine: {e}")
|
|
||||||
print("💡 Make sure models are trained/present in ai-engine/models/")
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# ─── Backtest Loop ───
|
|
||||||
total_matches_analyzed = 0
|
|
||||||
bets_skipped = 0
|
|
||||||
bets_played = 0
|
|
||||||
bets_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
# Thresholds matching the NEW Skip Logic
|
|
||||||
MIN_CONF = 45.0
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home_team = row['home_team']
|
|
||||||
away_team = row['away_team']
|
|
||||||
home_score = row['score_home']
|
|
||||||
away_score = row['score_away']
|
|
||||||
|
|
||||||
print(f"\n[{i+1}/{len(rows)}] Analyzing: {home_team} vs {away_team} ...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. AI PREDICTION (Actual Model Call)
|
|
||||||
prediction = orchestrator.analyze_match(match_id)
|
|
||||||
|
|
||||||
if not prediction:
|
|
||||||
print(f" ⚠️ AI returned no prediction.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
total_matches_analyzed += 1
|
|
||||||
|
|
||||||
# 2. Extract Main Pick
|
|
||||||
main_pick = prediction.get("main_pick") or {}
|
|
||||||
pick_name = main_pick.get("pick")
|
|
||||||
confidence = main_pick.get("confidence", 0)
|
|
||||||
odds = main_pick.get("odds", 0)
|
|
||||||
|
|
||||||
if not pick_name or not confidence:
|
|
||||||
print(f" ⚠️ No main pick found in prediction.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f" 🤖 Pick: {pick_name} | Conf: {confidence}% | Odds: {odds}")
|
|
||||||
|
|
||||||
# 3. Apply Skip Logic (New Backtest Logic)
|
|
||||||
if confidence < MIN_CONF:
|
|
||||||
print(f" 🚫 SKIPPED (Confidence {confidence}% < {MIN_CONF}%)")
|
|
||||||
bets_skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if odds > 0:
|
|
||||||
implied_prob = 1.0 / odds
|
|
||||||
my_prob = confidence / 100.0
|
|
||||||
if my_prob - implied_prob < -0.03: # Negative edge
|
|
||||||
print(f" 🚫 SKIPPED (Negative Edge)")
|
|
||||||
bets_skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 4. Bet Played
|
|
||||||
bets_played += 1
|
|
||||||
print(f" 🎲 BET PLAYED: {pick_name} @ {odds}")
|
|
||||||
|
|
||||||
# 5. Resolve Bet
|
|
||||||
won = False
|
|
||||||
# Basic resolution logic (Need to parse pick_name like "1", "X", "2", "2.5 Üst", etc.)
|
|
||||||
pick_clean = str(pick_name).upper()
|
|
||||||
|
|
||||||
# MS
|
|
||||||
if pick_clean in ["1", "MS 1"] and home_score > away_score: won = True
|
|
||||||
elif pick_clean in ["X", "MS X"] and home_score == away_score: won = True
|
|
||||||
elif pick_clean in ["2", "MS 2"] and away_score > home_score: won = True
|
|
||||||
|
|
||||||
# OU25
|
|
||||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
|
||||||
if (home_score + away_score) > 2.5: won = True
|
|
||||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
|
||||||
if (home_score + away_score) < 2.5: won = True
|
|
||||||
|
|
||||||
# BTTS
|
|
||||||
elif "VAR" in pick_clean and home_score > 0 and away_score > 0: won = True
|
|
||||||
elif "YOK" in pick_clean and (home_score == 0 or away_score == 0): won = True
|
|
||||||
|
|
||||||
if won:
|
|
||||||
bets_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
print(f" ✅ WON! (+{profit:.2f} units)")
|
|
||||||
else:
|
|
||||||
profit = -1.0
|
|
||||||
print(f" ❌ LOST! (-1.00 units)")
|
|
||||||
|
|
||||||
total_profit += profit
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" 💥 Error during analysis: {e}")
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
|
|
||||||
# ─── FINAL REPORT ───
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📈 REAL AI BACKTEST RESULTS")
|
|
||||||
print(f"🕒 Time taken: {elapsed:.1f} seconds")
|
|
||||||
print("="*60)
|
|
||||||
print(f"📊 Matches Analyzed: {total_matches_analyzed}")
|
|
||||||
print(f"🚫 Bets SKIPPED: {bets_skipped}")
|
|
||||||
print(f"✅ Bets PLAYED: {bets_played}")
|
|
||||||
|
|
||||||
if bets_played > 0:
|
|
||||||
win_rate = (bets_won / bets_played) * 100
|
|
||||||
roi = (total_profit / bets_played) * 100
|
|
||||||
yield_val = total_profit # Net Units
|
|
||||||
|
|
||||||
print(f"🏆 Bets Won: {bets_won}")
|
|
||||||
print(f"💀 Bets Lost: {bets_played - bets_won}")
|
|
||||||
print("-" * 40)
|
|
||||||
print(f" Win Rate: {win_rate:.2f}%")
|
|
||||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
|
||||||
print(f"📊 ROI: {roi:.2f}%")
|
|
||||||
|
|
||||||
if roi > 0:
|
|
||||||
print("🟢 STRATEGY IS PROFITABLE!")
|
|
||||||
else:
|
|
||||||
print("🔴 STRATEGY IS LOSING")
|
|
||||||
else:
|
|
||||||
print("⚠️ No bets were played. All were skipped or failed.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_backtest()
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
"""
|
|
||||||
Backtest ROI Engine
|
|
||||||
===================
|
|
||||||
Simulates the NEW "Skip Logic" on historical predictions.
|
|
||||||
Answers: "What if we only played the bets the model was confident about?"
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python ai-engine/scripts/backtest_roi.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from typing import Dict, List, Any
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
# Load .env from project root (2 levels up from this script)
|
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
load_dotenv(os.path.join(project_root, ".env"))
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
"""Return a psycopg2-compatible DSN from DATABASE_URL."""
|
|
||||||
# HARDCODED FOR BACKTEST (Bypassing dotenv issues)
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
# ─── Configuration (Matching the NEW BetRecommender Logic) ─────────
|
|
||||||
# Minimum confidence to even consider a bet (Hard Gate)
|
|
||||||
MIN_CONF_THRESHOLDS = {
|
|
||||||
"MS": 45.0,
|
|
||||||
"DC": 40.0,
|
|
||||||
"OU15": 50.0,
|
|
||||||
"OU25": 45.0,
|
|
||||||
"OU35": 45.0,
|
|
||||||
"BTTS": 45.0,
|
|
||||||
"HT": 40.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_market_type_from_key(key: str) -> str:
|
|
||||||
"""Map prediction keys to market types for thresholding."""
|
|
||||||
if key.startswith("ms_") or key in ["1", "X", "2"]: return "MS"
|
|
||||||
if key.startswith("dc_") or key in ["1X", "X2", "12"]: return "DC"
|
|
||||||
if key.startswith("ou15_") or key.startswith("1.5"): return "OU15"
|
|
||||||
if key.startswith("ou25_") or key.startswith("2.5"): return "OU25"
|
|
||||||
if key.startswith("ou35_") or key.startswith("3.5"): return "OU35"
|
|
||||||
if key.startswith("btts_") or key in ["Var", "Yok"]: return "BTTS"
|
|
||||||
if key.startswith("ht_") or key.startswith("İY"): return "HT"
|
|
||||||
return "MS"
|
|
||||||
|
|
||||||
def simulate_backtest():
|
|
||||||
print("🚀 Starting Backtest with NEW 'Skip Logic'...")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
# 1. Fetch PREDICTIONS that have a confidence score
|
|
||||||
# We limit to last 1000 finished matches to keep it fast but representative
|
|
||||||
cur.execute("""
|
|
||||||
SELECT p.match_id, p.prediction_json,
|
|
||||||
m.score_home, m.score_away, m.status
|
|
||||||
FROM predictions p
|
|
||||||
JOIN matches m ON p.match_id = m.id
|
|
||||||
WHERE m.status = 'FT'
|
|
||||||
AND p.prediction_json IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 2000
|
|
||||||
""")
|
|
||||||
predictions = cur.fetchall()
|
|
||||||
|
|
||||||
print(f"📊 Loaded {len(predictions)} historical predictions.")
|
|
||||||
|
|
||||||
total_bets = 0
|
|
||||||
winning_bets = 0
|
|
||||||
skipped_bets = 0
|
|
||||||
total_profit = 0.0 # Assuming unit stake of 1.0
|
|
||||||
|
|
||||||
# 2. Process each prediction
|
|
||||||
for pred_row in predictions:
|
|
||||||
match_id = pred_row['match_id']
|
|
||||||
data = pred_row['prediction_json']
|
|
||||||
if isinstance(data, str):
|
|
||||||
data = json.loads(data)
|
|
||||||
|
|
||||||
# Real result
|
|
||||||
home_score = pred_row['score_home'] or 0
|
|
||||||
away_score = pred_row['score_away'] or 0
|
|
||||||
total_goals = home_score + away_score
|
|
||||||
|
|
||||||
# Extract prediction details from the JSON structure
|
|
||||||
# The structure varies, but usually contains 'main_pick', 'bet_summary', or 'market_board'
|
|
||||||
|
|
||||||
# Try to get the main pick recommendation
|
|
||||||
main_pick = None
|
|
||||||
main_pick_conf = 0.0
|
|
||||||
main_pick_odds = 0.0
|
|
||||||
|
|
||||||
# Navigate the V20+ JSON structure
|
|
||||||
market_board = data.get("market_board", {})
|
|
||||||
|
|
||||||
# Check Main Pick
|
|
||||||
if "main_pick" in data:
|
|
||||||
mp = data["main_pick"]
|
|
||||||
if isinstance(mp, dict):
|
|
||||||
main_pick = mp.get("pick")
|
|
||||||
main_pick_conf = mp.get("confidence", 0.0)
|
|
||||||
main_pick_odds = mp.get("odds", 0.0)
|
|
||||||
|
|
||||||
# If no main pick, try bet_summary
|
|
||||||
if not main_pick and "bet_summary" in data:
|
|
||||||
summary = data["bet_summary"]
|
|
||||||
if isinstance(summary, list) and len(summary) > 0:
|
|
||||||
# Take the highest confidence one
|
|
||||||
best = max(summary, key=lambda x: x.get("confidence", 0))
|
|
||||||
main_pick = best.get("pick")
|
|
||||||
main_pick_conf = best.get("confidence", 0.0)
|
|
||||||
main_pick_odds = best.get("odds", 0.0)
|
|
||||||
|
|
||||||
if not main_pick or not main_pick_conf:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ─── NEW LOGIC: APPLY FILTERS ───
|
|
||||||
# 1. Determine Market Type
|
|
||||||
# Simple heuristic based on pick string
|
|
||||||
pick_str = str(main_pick).upper()
|
|
||||||
market_type = "MS"
|
|
||||||
if "1X" in pick_str or "X2" in pick_str or "12" in pick_str: market_type = "DC"
|
|
||||||
elif "ÜST" in pick_str or "ALT" in pick_str or "OVER" in pick_str or "UNDER" in pick_str:
|
|
||||||
if "1.5" in pick_str: market_type = "OU15"
|
|
||||||
elif "3.5" in pick_str: market_type = "OU35"
|
|
||||||
else: market_type = "OU25"
|
|
||||||
elif "VAR" in pick_str or "YOK" in pick_str or "BTTS" in pick_str: market_type = "BTTS"
|
|
||||||
|
|
||||||
threshold = MIN_CONF_THRESHOLDS.get(market_type, 45.0)
|
|
||||||
|
|
||||||
# 2. Check Confidence Gate
|
|
||||||
if main_pick_conf < threshold:
|
|
||||||
skipped_bets += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 3. Check Value Gate (Edge)
|
|
||||||
if main_pick_odds > 0:
|
|
||||||
implied_prob = 1.0 / main_pick_odds
|
|
||||||
my_prob = main_pick_conf / 100.0
|
|
||||||
edge = my_prob - implied_prob
|
|
||||||
if edge < -0.03: # Negative value
|
|
||||||
skipped_bets += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ─── BET IS PLAYED ───
|
|
||||||
total_bets += 1
|
|
||||||
|
|
||||||
# Determine if WON
|
|
||||||
is_won = False
|
|
||||||
|
|
||||||
# Resolve MS (1, X, 2)
|
|
||||||
if market_type == "MS":
|
|
||||||
if main_pick == "1" and home_score > away_score: is_won = True
|
|
||||||
elif main_pick == "X" and home_score == away_score: is_won = True
|
|
||||||
elif main_pick == "2" and away_score > home_score: is_won = True
|
|
||||||
elif main_pick == "MS 1" and home_score > away_score: is_won = True
|
|
||||||
elif main_pick == "MS X" and home_score == away_score: is_won = True
|
|
||||||
elif main_pick == "MS 2" and away_score > home_score: is_won = True
|
|
||||||
|
|
||||||
# Resolve OU (Over/Under)
|
|
||||||
elif market_type.startswith("OU"):
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_str: line = 1.5
|
|
||||||
elif "3.5" in pick_str: line = 3.5
|
|
||||||
|
|
||||||
is_over = total_goals > line
|
|
||||||
is_under = total_goals < line # Simplification (usually line is X.5 so no draw)
|
|
||||||
|
|
||||||
if "ÜST" in pick_str or "OVER" in pick_str:
|
|
||||||
if is_over: is_won = True
|
|
||||||
elif "ALT" in pick_str or "UNDER" in pick_str:
|
|
||||||
if is_under: is_won = True
|
|
||||||
|
|
||||||
# Resolve BTTS
|
|
||||||
elif market_type == "BTTS":
|
|
||||||
if home_score > 0 and away_score > 0:
|
|
||||||
if "VAR" in pick_str: is_won = True
|
|
||||||
else:
|
|
||||||
if "YOK" in pick_str: is_won = True
|
|
||||||
|
|
||||||
# Resolve DC (Double Chance) - Simplified
|
|
||||||
elif market_type == "DC":
|
|
||||||
if "1X" in pick_str and (home_score >= away_score): is_won = True
|
|
||||||
elif "X2" in pick_str and (away_score >= home_score): is_won = True
|
|
||||||
elif "12" in pick_str and (home_score != away_score): is_won = True
|
|
||||||
|
|
||||||
if is_won:
|
|
||||||
winning_bets += 1
|
|
||||||
profit = main_pick_odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
|
|
||||||
# ─── REPORT ───
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📈 BACKTEST RESULTS (With NEW Skip Logic)")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Total Historical Matches Analyzed: {len(predictions)}")
|
|
||||||
print(f"🚫 Bets SKIPPED (Low Conf/Bad Value): {skipped_bets}")
|
|
||||||
print(f"✅ Bets PLAYED: {total_bets}")
|
|
||||||
|
|
||||||
if total_bets > 0:
|
|
||||||
win_rate = (winning_bets / total_bets) * 100
|
|
||||||
roi = (total_profit / total_bets) * 100
|
|
||||||
|
|
||||||
print(f"🏆 Winning Bets: {winning_bets}")
|
|
||||||
print(f"💀 Losing Bets: {total_bets - winning_bets}")
|
|
||||||
print("-" * 40)
|
|
||||||
print(f" Win Rate: {win_rate:.2f}%")
|
|
||||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
|
||||||
print(f"📊 ROI: {roi:.2f}%")
|
|
||||||
|
|
||||||
if roi > 0:
|
|
||||||
print("🟢 STRATEGY IS PROFITABLE!")
|
|
||||||
else:
|
|
||||||
print("🔴 STRATEGY IS LOSING (Adjust thresholds!)")
|
|
||||||
else:
|
|
||||||
print("⚠️ No bets were played. Thresholds might be too high.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
simulate_backtest()
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
"""
|
|
||||||
SNIPER Backtest
|
|
||||||
===============
|
|
||||||
Sadece en yüksek güvenilirlik ve değere sahip bahisleri oynar.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
MATCH_IDS = [
|
|
||||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
|
||||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
|
||||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
|
||||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
|
||||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
|
||||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
|
||||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
|
||||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
|
||||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
|
||||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
|
||||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
|
||||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
|
||||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
|
||||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
|
||||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
|
||||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
|
||||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
|
||||||
]
|
|
||||||
|
|
||||||
def run_sniper_backtest():
|
|
||||||
print("🎯 SNIPER BACKTEST: SADECE NET OLANLAR")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
|
||||||
cur.execute(f"""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
l.name as league_name
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
LEFT JOIN leagues l ON m.league_id = l.id
|
|
||||||
WHERE m.id IN ({placeholders}) AND m.status = 'FT'
|
|
||||||
""", MATCH_IDS)
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 Analiz edilecek {len(rows)} maç var.\n")
|
|
||||||
|
|
||||||
try:
|
|
||||||
orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
print(f"[{i+1}/{len(rows)}] {home} vs {away} ... ", end="", flush=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred:
|
|
||||||
print("⚠️ Veri Yok")
|
|
||||||
continue
|
|
||||||
|
|
||||||
pick_data = pred.get("expert_recommendation", {}).get("main_pick") or pred.get("main_pick", {})
|
|
||||||
pick = pick_data.get("pick") or pick_data.get("market_type")
|
|
||||||
conf = pick_data.get("confidence", 0)
|
|
||||||
odds = pick_data.get("odds", 0)
|
|
||||||
|
|
||||||
# SNIPER FİLTRELERİ
|
|
||||||
if conf < 75:
|
|
||||||
print(f"🚫 PASS (Conf: {conf:.0f}%)")
|
|
||||||
continue
|
|
||||||
if odds < 1.35:
|
|
||||||
print(f"🚫 PASS (Odds: {odds:.2f} çok düşük)")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Value Control
|
|
||||||
implied = 1.0 / odds
|
|
||||||
if (conf/100) < implied:
|
|
||||||
print(f"🚫 PASS (Negatif Value)")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# OYNA
|
|
||||||
total_bet += 1
|
|
||||||
won = False
|
|
||||||
pick_clean = str(pick).upper()
|
|
||||||
|
|
||||||
if pick_clean in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick_clean in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick_clean in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
elif "3.5" in pick_clean: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
elif "3.5" in pick_clean: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick_clean and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick_clean and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
print(f"✅ WON! (+{profit:.2f})")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
print(f"❌ LOST! ({pick} @ {odds:.2f})")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"💥 Hata: {e}")
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("🎯 SNIPER SONUÇLARI")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Oynanan: {total_bet}")
|
|
||||||
print(f"Kazanılan: {total_won}")
|
|
||||||
print(f"Kazanma Oranı: %{(total_won/total_bet)*100:.1f}" if total_bet > 0 else "Kazanma Oranı: N/A")
|
|
||||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
|
||||||
|
|
||||||
if total_profit > 0:
|
|
||||||
print("🟢 PARA KAZANDIK!")
|
|
||||||
else:
|
|
||||||
print("🔴 PARA KAYBETTİK!")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_sniper_backtest()
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
"""
|
|
||||||
Strict Sniper Backtest (Calibrated)
|
|
||||||
===================================
|
|
||||||
Sadece Güven > %75 ve Oran > 1.30 olan bahisleri oynar.
|
|
||||||
Modelin şişirilmiş özgüvenini elemek için yapıldı.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_strict_backtest():
|
|
||||||
print("🎯 STRICT SNIPER BACKTEST (Conf > 75%)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 500
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç taranıyor. Sadece NET OLANLAR oynanacak...\n")
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred: continue
|
|
||||||
|
|
||||||
# Check all picks for a HIGH CONFIDENCE bet
|
|
||||||
candidates = []
|
|
||||||
if pred.get("expert_recommendation"):
|
|
||||||
rec = pred["expert_recommendation"]
|
|
||||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
|
||||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
|
||||||
elif pred.get("main_pick"):
|
|
||||||
candidates.append(pred["main_pick"])
|
|
||||||
|
|
||||||
best_bet = None
|
|
||||||
for c in candidates:
|
|
||||||
if not c: continue
|
|
||||||
# Access attributes safely (Dict or Object)
|
|
||||||
conf = c.get("confidence", 0) if isinstance(c, dict) else getattr(c, 'confidence', 0)
|
|
||||||
odds = c.get("odds", 0) if isinstance(c, dict) else getattr(c, 'odds', 0)
|
|
||||||
pick = c.get("pick", "") if isinstance(c, dict) else getattr(c, 'pick', "")
|
|
||||||
|
|
||||||
# STRICT CRITERIA
|
|
||||||
if conf >= 75.0 and odds >= 1.30:
|
|
||||||
# Check Value (Edge)
|
|
||||||
implied = 1.0 / odds
|
|
||||||
edge = ((conf/100) - implied) * 100
|
|
||||||
if edge > -5.0: # Tolerant edge
|
|
||||||
if best_bet is None or (conf > (best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0))):
|
|
||||||
best_bet = c
|
|
||||||
|
|
||||||
if best_bet:
|
|
||||||
pick = str(best_bet.get("pick") if isinstance(best_bet, dict) else getattr(best_bet, 'pick', "")).upper()
|
|
||||||
conf = best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0)
|
|
||||||
odds = best_bet.get("odds", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'odds', 0)
|
|
||||||
|
|
||||||
# Resolution
|
|
||||||
won = False
|
|
||||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif pick in ["1X", "X2"]:
|
|
||||||
if "1X" in pick and h_score >= a_score: won = True
|
|
||||||
elif "X2" in pick and a_score >= h_score: won = True
|
|
||||||
elif "ÜST" in pick or "OVER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick or "UNDER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
total_bet += 1
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({conf:.0f}%) -> WON (+{profit:.2f})")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({conf:.0f}%) -> LOST")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("🎯 STRICT SNIPER SONUÇLARI")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Oynanan Bahis: {total_bet}")
|
|
||||||
print(f"Kazanılan: {total_won}")
|
|
||||||
|
|
||||||
if total_bet > 0:
|
|
||||||
win_rate = (total_won / total_bet) * 100
|
|
||||||
roi = (total_profit / total_bet) * 100
|
|
||||||
print(f"Kazanma Oranı: %{win_rate:.2f}")
|
|
||||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
|
||||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
|
||||||
else: print("🔴 PARA KAYBETTİK!")
|
|
||||||
else:
|
|
||||||
print("⚠️ Yeteri kadar NET maç bulunamadı.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_strict_backtest()
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
"""
|
|
||||||
Backtest the live V2 predictor stack against recent finished football matches.
|
|
||||||
|
|
||||||
This script uses the same path as production:
|
|
||||||
database -> feature extractor -> betting predictor -> quant ranking.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from sqlalchemy import text
|
|
||||||
|
|
||||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
|
||||||
if str(ROOT_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(ROOT_DIR))
|
|
||||||
|
|
||||||
from core.quant import MarketPick, analyze_market
|
|
||||||
from data.database import dispose_engine, get_session
|
|
||||||
from features.extractor import extract_features
|
|
||||||
from models.betting_engine import get_predictor
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BacktestStats:
|
|
||||||
sampled_matches: int = 0
|
|
||||||
analyzed_matches: int = 0
|
|
||||||
skipped_matches: int = 0
|
|
||||||
ms_correct: int = 0
|
|
||||||
ou25_correct: int = 0
|
|
||||||
btts_correct: int = 0
|
|
||||||
main_pick_count: int = 0
|
|
||||||
main_pick_correct: int = 0
|
|
||||||
playable_pick_count: int = 0
|
|
||||||
playable_pick_correct: int = 0
|
|
||||||
playable_units_staked: float = 0.0
|
|
||||||
playable_units_profit: float = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--limit", type=int, default=50)
|
|
||||||
parser.add_argument("--days", type=int, default=45)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def _actual_ms(score_home: int, score_away: int) -> str:
|
|
||||||
if score_home > score_away:
|
|
||||||
return "1"
|
|
||||||
if score_home < score_away:
|
|
||||||
return "2"
|
|
||||||
return "X"
|
|
||||||
|
|
||||||
|
|
||||||
def _actual_ou25(score_home: int, score_away: int) -> str:
|
|
||||||
return "Over" if (score_home + score_away) > 2 else "Under"
|
|
||||||
|
|
||||||
|
|
||||||
def _actual_btts(score_home: int, score_away: int) -> str:
|
|
||||||
return "Yes" if score_home > 0 and score_away > 0 else "No"
|
|
||||||
|
|
||||||
|
|
||||||
def _odds_map_from_features(feats) -> dict[str, dict[str, float]]:
|
|
||||||
return {
|
|
||||||
"MS": {"1": feats.odds_home, "X": feats.odds_draw, "2": feats.odds_away},
|
|
||||||
"OU25": {"Under": feats.odds_under25, "Over": feats.odds_over25},
|
|
||||||
"BTTS": {"No": feats.odds_btts_no, "Yes": feats.odds_btts_yes},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _best_pick(feats, all_probs: dict[str, dict[str, float]]) -> MarketPick | None:
|
|
||||||
odds_map = _odds_map_from_features(feats)
|
|
||||||
picks = [
|
|
||||||
analyze_market("MS", all_probs["MS"], odds_map["MS"], feats.data_quality_score),
|
|
||||||
analyze_market("OU25", all_probs["OU25"], odds_map["OU25"], feats.data_quality_score),
|
|
||||||
analyze_market("BTTS", all_probs["BTTS"], odds_map["BTTS"], feats.data_quality_score),
|
|
||||||
]
|
|
||||||
ranked = sorted(
|
|
||||||
[pick for pick in picks if pick.pick],
|
|
||||||
key=lambda pick: pick.play_score,
|
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
return ranked[0] if ranked else None
|
|
||||||
|
|
||||||
|
|
||||||
def _pick_won(pick: MarketPick, actuals: dict[str, str]) -> bool:
|
|
||||||
return actuals.get(pick.market) == pick.pick
|
|
||||||
|
|
||||||
|
|
||||||
async def _load_match_rows(limit: int, days: int) -> list[dict[str, object]]:
|
|
||||||
min_mst_utc = days * 86400000
|
|
||||||
query = text("""
|
|
||||||
SELECT
|
|
||||||
m.id,
|
|
||||||
m.match_name,
|
|
||||||
m.score_home,
|
|
||||||
m.score_away,
|
|
||||||
m.mst_utc
|
|
||||||
FROM matches m
|
|
||||||
WHERE m.sport = 'football'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND m.score_away IS NOT NULL
|
|
||||||
AND m.mst_utc >= (
|
|
||||||
EXTRACT(EPOCH FROM NOW()) * 1000 - :min_mst_utc
|
|
||||||
)
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM odd_categories oc
|
|
||||||
WHERE oc.match_id = m.id
|
|
||||||
AND oc.name IN ('Maç Sonucu', '2,5 Alt/Üst', 'Karşılıklı Gol')
|
|
||||||
)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT :limit
|
|
||||||
""")
|
|
||||||
async with get_session() as session:
|
|
||||||
result = await session.execute(
|
|
||||||
query,
|
|
||||||
{"limit": limit, "min_mst_utc": min_mst_utc},
|
|
||||||
)
|
|
||||||
rows = result.mappings().all()
|
|
||||||
return [dict(row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def _run(limit: int, days: int) -> BacktestStats:
|
|
||||||
stats = BacktestStats()
|
|
||||||
predictor = get_predictor()
|
|
||||||
rows = await _load_match_rows(limit, days)
|
|
||||||
stats.sampled_matches = len(rows)
|
|
||||||
|
|
||||||
async with get_session() as session:
|
|
||||||
for row in rows:
|
|
||||||
match_id = str(row["id"])
|
|
||||||
score_home = int(row["score_home"])
|
|
||||||
score_away = int(row["score_away"])
|
|
||||||
feats = await extract_features(session, match_id)
|
|
||||||
|
|
||||||
if feats is None:
|
|
||||||
stats.skipped_matches += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if feats.data_quality_score <= 0.0:
|
|
||||||
stats.skipped_matches += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
all_probs = predictor.predict_all(feats.to_model_array(), feats)
|
|
||||||
stats.analyzed_matches += 1
|
|
||||||
|
|
||||||
actuals = {
|
|
||||||
"MS": _actual_ms(score_home, score_away),
|
|
||||||
"OU25": _actual_ou25(score_home, score_away),
|
|
||||||
"BTTS": _actual_btts(score_home, score_away),
|
|
||||||
}
|
|
||||||
|
|
||||||
if max(all_probs["MS"], key=all_probs["MS"].get) == actuals["MS"]:
|
|
||||||
stats.ms_correct += 1
|
|
||||||
if max(all_probs["OU25"], key=all_probs["OU25"].get) == actuals["OU25"]:
|
|
||||||
stats.ou25_correct += 1
|
|
||||||
if max(all_probs["BTTS"], key=all_probs["BTTS"].get) == actuals["BTTS"]:
|
|
||||||
stats.btts_correct += 1
|
|
||||||
|
|
||||||
best_pick = _best_pick(feats, all_probs)
|
|
||||||
if best_pick is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
stats.main_pick_count += 1
|
|
||||||
if _pick_won(best_pick, actuals):
|
|
||||||
stats.main_pick_correct += 1
|
|
||||||
|
|
||||||
if best_pick.playable:
|
|
||||||
stats.playable_pick_count += 1
|
|
||||||
stats.playable_units_staked += best_pick.stake_units
|
|
||||||
if _pick_won(best_pick, actuals):
|
|
||||||
stats.playable_pick_correct += 1
|
|
||||||
stats.playable_units_profit += best_pick.stake_units * (best_pick.odds - 1.0)
|
|
||||||
else:
|
|
||||||
stats.playable_units_profit -= best_pick.stake_units
|
|
||||||
|
|
||||||
return stats
|
|
||||||
|
|
||||||
|
|
||||||
def _pct(numerator: int, denominator: int) -> float:
|
|
||||||
if denominator <= 0:
|
|
||||||
return 0.0
|
|
||||||
return round((numerator / denominator) * 100.0, 2)
|
|
||||||
|
|
||||||
|
|
||||||
def _roi(profit: float, staked: float) -> float:
|
|
||||||
if staked <= 0:
|
|
||||||
return 0.0
|
|
||||||
return round((profit / staked) * 100.0, 2)
|
|
||||||
|
|
||||||
|
|
||||||
def _print_summary(stats: BacktestStats) -> None:
|
|
||||||
print("=== V2 Runtime Backtest ===")
|
|
||||||
print(f"Sampled matches : {stats.sampled_matches}")
|
|
||||||
print(f"Analyzed matches : {stats.analyzed_matches}")
|
|
||||||
print(f"Skipped matches : {stats.skipped_matches}")
|
|
||||||
print(f"MS accuracy : {_pct(stats.ms_correct, stats.analyzed_matches)}%")
|
|
||||||
print(f"OU2.5 accuracy : {_pct(stats.ou25_correct, stats.analyzed_matches)}%")
|
|
||||||
print(f"BTTS accuracy : {_pct(stats.btts_correct, stats.analyzed_matches)}%")
|
|
||||||
print(
|
|
||||||
"Main pick accuracy : "
|
|
||||||
f"{_pct(stats.main_pick_correct, stats.main_pick_count)}% "
|
|
||||||
f"({stats.main_pick_correct}/{stats.main_pick_count})"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
"Playable accuracy : "
|
|
||||||
f"{_pct(stats.playable_pick_correct, stats.playable_pick_count)}% "
|
|
||||||
f"({stats.playable_pick_correct}/{stats.playable_pick_count})"
|
|
||||||
)
|
|
||||||
print(f"Units staked : {stats.playable_units_staked:.2f}")
|
|
||||||
print(f"Units profit : {stats.playable_units_profit:.2f}")
|
|
||||||
print(f"ROI : {_roi(stats.playable_units_profit, stats.playable_units_staked)}%")
|
|
||||||
|
|
||||||
|
|
||||||
async def _main() -> None:
|
|
||||||
args = _parse_args()
|
|
||||||
try:
|
|
||||||
stats = await _run(args.limit, args.days)
|
|
||||||
_print_summary(stats)
|
|
||||||
finally:
|
|
||||||
await dispose_engine()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(_main())
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
"""
|
|
||||||
Value Hunter Backtest
|
|
||||||
=====================
|
|
||||||
Sadece modelin büroyu yendiği (Pozitif Edge) maçları oynar.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os, sys, json, time, psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR): ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
MATCH_IDS = [
|
|
||||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
|
||||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
|
||||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
|
||||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
|
||||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
|
||||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
|
||||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
|
||||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
|
||||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
|
||||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
|
||||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
|
||||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
|
||||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
|
||||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
|
||||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
|
||||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
|
||||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
|
||||||
]
|
|
||||||
|
|
||||||
def run_value_hunter():
|
|
||||||
print("💎 VALUE HUNTER: SADECE HATALI ORANLARI YAKALA")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
|
||||||
cur.execute(f"""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.id IN ({placeholders}) AND m.status = 'FT'
|
|
||||||
""", MATCH_IDS)
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç taranıyor...\n")
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
total_edge_found = 0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred: continue
|
|
||||||
|
|
||||||
# Tüm önerileri kontrol et
|
|
||||||
picks = pred.get("expert_recommendation", {}).get("value_picks", [])
|
|
||||||
if not picks: picks = [pred.get("expert_recommendation", {}).get("main_pick")]
|
|
||||||
|
|
||||||
played_this_match = False
|
|
||||||
|
|
||||||
for pick_data in picks:
|
|
||||||
if not pick_data: continue
|
|
||||||
pick = pick_data.get("pick")
|
|
||||||
conf = pick_data.get("confidence", 0)
|
|
||||||
odds = pick_data.get("odds", 0)
|
|
||||||
edge = pick_data.get("edge", 0)
|
|
||||||
|
|
||||||
# VALUE KURALI: Model bürodan en az %10 daha iyi olmalı
|
|
||||||
if edge < 10: continue
|
|
||||||
if odds < 1.20: continue
|
|
||||||
|
|
||||||
total_bet += 1
|
|
||||||
total_edge_found += edge
|
|
||||||
won = False
|
|
||||||
pick_clean = str(pick).upper()
|
|
||||||
|
|
||||||
if pick_clean in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick_clean in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick_clean in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick_clean: line = 1.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick_clean and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick_clean and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({edge:.0f}% Edge) -> WON! (+{profit:.2f})")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({edge:.0f}% Edge) -> LOST")
|
|
||||||
|
|
||||||
played_this_match = True
|
|
||||||
break # Maç başına tek bahis
|
|
||||||
|
|
||||||
except Exception: pass
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("💎 VALUE HUNTER SONUÇLARI")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Toplam Value Bulunan Bahis: {total_bet}")
|
|
||||||
print(f"Ortalama Edge: {total_edge_found/total_bet:.1f}%" if total_bet > 0 else "N/A")
|
|
||||||
print(f"Kazanılan: {total_won}")
|
|
||||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
|
||||||
|
|
||||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
|
||||||
else: print("🔴 PARA KAYBETTİK!")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_value_hunter()
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
"""
|
|
||||||
Value Sniper Backtest (High Odds)
|
|
||||||
=================================
|
|
||||||
Sadece Oran > 1.50 ve Güven > %70 olan bahisleri oynar.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
sys.path.insert(0, ROOT_DIR)
|
|
||||||
if "scripts" in os.path.basename(AI_DIR):
|
|
||||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_value_sniper():
|
|
||||||
print("💰 VALUE SNIPER BACKTEST (Odds > 1.50)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
|
||||||
with open(leagues_path, 'r') as f:
|
|
||||||
top_leagues = json.load(f)
|
|
||||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
|
||||||
m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s
|
|
||||||
AND m.status = 'FT'
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 500
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç taranıyor...\n")
|
|
||||||
|
|
||||||
try: orchestrator = get_single_match_orchestrator()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ AI Hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_bet = 0
|
|
||||||
total_won = 0
|
|
||||||
total_profit = 0.0
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
match_id = str(row['id'])
|
|
||||||
home = row['home_team'] or "?"
|
|
||||||
away = row['away_team'] or "?"
|
|
||||||
h_score = row['score_home'] or 0
|
|
||||||
a_score = row['score_away'] or 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
pred = orchestrator.analyze_match(match_id)
|
|
||||||
if not pred: continue
|
|
||||||
|
|
||||||
candidates = []
|
|
||||||
if pred.get("expert_recommendation"):
|
|
||||||
rec = pred["expert_recommendation"]
|
|
||||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
|
||||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
|
||||||
elif pred.get("main_pick"):
|
|
||||||
candidates.append(pred["main_pick"])
|
|
||||||
|
|
||||||
best_bet = None
|
|
||||||
for c in candidates:
|
|
||||||
if not c: continue
|
|
||||||
conf = c.get("confidence", 0) if isinstance(c, dict) else getattr(c, 'confidence', 0)
|
|
||||||
odds = c.get("odds", 0) if isinstance(c, dict) else getattr(c, 'odds', 0)
|
|
||||||
|
|
||||||
# VALUE CRITERIA: Odds > 1.50 AND Conf > 70%
|
|
||||||
if conf >= 70.0 and odds >= 1.50:
|
|
||||||
# Check Edge
|
|
||||||
implied = 1.0 / odds
|
|
||||||
edge = ((conf/100) - implied) * 100
|
|
||||||
if edge > 0: # Must be positive value
|
|
||||||
if best_bet is None or (conf > (best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0))):
|
|
||||||
best_bet = c
|
|
||||||
|
|
||||||
if best_bet:
|
|
||||||
pick = str(best_bet.get("pick") if isinstance(best_bet, dict) else getattr(best_bet, 'pick', "")).upper()
|
|
||||||
conf = best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0)
|
|
||||||
odds = best_bet.get("odds", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'odds', 0)
|
|
||||||
|
|
||||||
won = False
|
|
||||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
|
||||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
|
||||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
|
||||||
elif "ÜST" in pick or "OVER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) > line: won = True
|
|
||||||
elif "ALT" in pick or "UNDER" in pick:
|
|
||||||
line = 2.5
|
|
||||||
if "1.5" in pick: line = 1.5
|
|
||||||
elif "3.5" in pick: line = 3.5
|
|
||||||
if (h_score + a_score) < line: won = True
|
|
||||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
|
||||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
|
||||||
|
|
||||||
total_bet += 1
|
|
||||||
if won:
|
|
||||||
total_won += 1
|
|
||||||
profit = odds - 1.0
|
|
||||||
total_profit += profit
|
|
||||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({odds:.2f}) -> WON (+{profit:.2f})")
|
|
||||||
else:
|
|
||||||
total_profit -= 1.0
|
|
||||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({odds:.2f}) -> LOST")
|
|
||||||
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("💰 VALUE SNIPER SONUÇLARI")
|
|
||||||
print("="*60)
|
|
||||||
print(f"Oynanan Bahis: {total_bet}")
|
|
||||||
print(f"Kazanılan: {total_won}")
|
|
||||||
|
|
||||||
if total_bet > 0:
|
|
||||||
win_rate = (total_won / total_bet) * 100
|
|
||||||
roi = (total_profit / total_bet) * 100
|
|
||||||
print(f"Kazanma Oranı: %{win_rate:.2f}")
|
|
||||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
|
||||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
|
||||||
else: print("🔴 PARA KAYBETTİK!")
|
|
||||||
else:
|
|
||||||
print("⚠️ Yeterli VALUE bulunamadı.")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_value_sniper()
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN Full Backtest
|
|
||||||
===================
|
|
||||||
Tests all 3 VQWEN models (MS, OU25, BTTS) on 1000 historical matches.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import pickle
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_vqwen_backtest():
|
|
||||||
print("🧠 VQWEN FULL BACKTEST")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# Load Models
|
|
||||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
|
||||||
try:
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
|
||||||
print("✅ VQWEN MS, OU25, BTTS modelleri yüklendi.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Model hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
|
||||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa,
|
|
||||||
COALESCE((SELECT AVG(CASE WHEN m2.home_team_id = m.home_team_id AND m2.score_home > m2.score_away THEN 3 WHEN m2.home_team_id = m.home_team_id AND m2.score_home = m2.score_away THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as h_form,
|
|
||||||
COALESCE((SELECT AVG(CASE WHEN m2.away_team_id = m.away_team_id AND m2.score_away > m2.score_home THEN 3 WHEN m2.away_team_id = m.away_team_id AND m2.score_away = m2.score_home THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as a_form,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_sc,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_co,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_sc,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_co
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 1000
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
|
||||||
|
|
||||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
oh, od, oa = float(row['oh'] or 0), float(row['od'] or 0), float(row['oa'] or 0)
|
|
||||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
|
||||||
|
|
||||||
h_xg = (float(row['h_sc'] or 1.2) + float(row['a_co'] or 1.2)) / 2
|
|
||||||
a_xg = (float(row['a_sc'] or 1.2) + float(row['h_co'] or 1.2)) / 2
|
|
||||||
h_p = (float(row['h_form'] or 0)*10) + (float(row['h_sc'] or 1.2)*5) - (float(row['h_co'] or 1.2)*5)
|
|
||||||
a_p = (float(row['a_form'] or 0)*10) + (float(row['a_sc'] or 1.2)*5) - (float(row['a_co'] or 1.2)*5)
|
|
||||||
|
|
||||||
margin = (1/oh) + (1/od) + (1/oa)
|
|
||||||
|
|
||||||
# MS Prediction
|
|
||||||
f_ms = pd.DataFrame([{'h_form': float(row['h_form']), 'a_form': float(row['a_form']), 'h_xg': h_xg, 'a_xg': a_xg,
|
|
||||||
'pow_diff': h_p - a_p, 'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
|
||||||
'h_sot': 4.0, 'a_sot': 3.0}])
|
|
||||||
ms_probs = model_ms.predict(f_ms)[0]
|
|
||||||
|
|
||||||
# MS Value Bet
|
|
||||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
|
||||||
if odd <= 1.0: continue
|
|
||||||
edge = prob - (1/odd)
|
|
||||||
if edge > 0.05 and prob > 0.50: # Value ve Güven
|
|
||||||
results['ms']['bet'] += 1
|
|
||||||
h, a = row['score_home'], row['score_away']
|
|
||||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
|
||||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
|
||||||
else: results['ms']['profit'] -= 1.0
|
|
||||||
break
|
|
||||||
|
|
||||||
# OU2.5 Prediction
|
|
||||||
f_ou = pd.DataFrame([{'h_xg': h_xg, 'a_xg': a_xg, 'total_xg': h_xg+a_xg, 'h_sot': 4.0, 'a_sot': 3.0}])
|
|
||||||
p_over = model_ou.predict(f_ou)[0]
|
|
||||||
|
|
||||||
# OU2.5 Value Bet
|
|
||||||
if p_over > 0.55 and oh > 1.0: # Sadece örnek olarak over > %55 ise
|
|
||||||
results['ou25']['bet'] += 1
|
|
||||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85 # Ortalama oran
|
|
||||||
else: results['ou25']['profit'] -= 1.0
|
|
||||||
|
|
||||||
# BTTS Prediction
|
|
||||||
f_btts = pd.DataFrame([{'h_xg': h_xg, 'a_xg': a_xg, 'h_sc': float(row['h_sc']), 'a_sc': float(row['a_sc'])}])
|
|
||||||
p_btts = model_btts.predict(f_btts)[0]
|
|
||||||
|
|
||||||
# BTTS Value Bet
|
|
||||||
if p_btts > 0.55:
|
|
||||||
results['btts']['bet'] += 1
|
|
||||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
|
||||||
else: results['btts']['profit'] -= 1.0
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📊 VQWEN PAZAR BAZLI SONUÇLAR")
|
|
||||||
print("="*60)
|
|
||||||
for mkt in ['ms', 'ou25', 'btts']:
|
|
||||||
r = results[mkt]
|
|
||||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
|
||||||
print(f"{mkt.upper():<10} Oynanan: {r['bet']:<5} Kazanılan: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f} Units")
|
|
||||||
|
|
||||||
total_profit = sum(r['profit'] for r in results.values())
|
|
||||||
print(f"\n💰 TOPLAM KÂR: {total_profit:+.2f} Units")
|
|
||||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
|
||||||
else: print("🔴 ZARARDA")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_vqwen_backtest()
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN Deep Backtest
|
|
||||||
===================
|
|
||||||
Tests the NEW Deep model with player & card data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import pickle
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_vqwen_deep_backtest():
|
|
||||||
print("🧠 VQWEN DEEP BACKTEST")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# Load Models
|
|
||||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
|
||||||
try:
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
|
||||||
print("✅ VQWEN Deep modelleri yüklendi.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Model hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
|
||||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa,
|
|
||||||
COALESCE((SELECT AVG(CASE WHEN m2.home_team_id = m.home_team_id AND m2.score_home > m2.score_away THEN 3 WHEN m2.home_team_id = m.home_team_id AND m2.score_home = m2.score_away THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as h_form,
|
|
||||||
COALESCE((SELECT AVG(CASE WHEN m2.away_team_id = m.away_team_id AND m2.score_away > m2.score_home THEN 3 WHEN m2.away_team_id = m.away_team_id AND m2.score_away = m2.score_home THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as a_form,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_sc,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_co,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_sc,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_co,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.home_team_id AND mp.is_starting = true), 0) as h_xi,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.away_team_id AND mp.is_starting = true), 0) as a_xi,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_events mpe WHERE mpe.match_id = m.id AND mpe.event_type = 'card'), 0) as cards
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 1000
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
|
||||||
|
|
||||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
oh = float(row['oh'] or 0)
|
|
||||||
od = float(row['od'] or 0)
|
|
||||||
oa = float(row['oa'] or 0)
|
|
||||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
|
||||||
|
|
||||||
h_xg = (float(row['h_sc'] or 1.2) + float(row['a_co'] or 1.2)) / 2
|
|
||||||
a_xg = (float(row['a_sc'] or 1.2) + float(row['h_co'] or 1.2)) / 2
|
|
||||||
h_p = (float(row['h_form'] or 0)*10) + (float(row['h_sc'] or 1.2)*5) - (float(row['h_co'] or 1.2)*5)
|
|
||||||
a_p = (float(row['a_form'] or 0)*10) + (float(row['a_sc'] or 1.2)*5) - (float(row['a_co'] or 1.2)*5)
|
|
||||||
|
|
||||||
margin = (1/oh) + (1/od) + (1/oa)
|
|
||||||
h_sot, a_sot = 4.0, 3.0
|
|
||||||
|
|
||||||
# Features
|
|
||||||
f = pd.DataFrame([{
|
|
||||||
'h_form': float(row['h_form']), 'a_form': float(row['a_form']),
|
|
||||||
'h_xg': h_xg, 'a_xg': a_xg, 'pow_diff': h_p - a_p,
|
|
||||||
'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
|
||||||
'h_sot': h_sot, 'a_sot': a_sot,
|
|
||||||
'h_xi': float(row['h_xi']), 'a_xi': float(row['a_xi']),
|
|
||||||
'xi_diff': float(row['h_xi'] - row['a_xi']),
|
|
||||||
'cards': float(row['cards'])
|
|
||||||
}])
|
|
||||||
|
|
||||||
# MS
|
|
||||||
ms_probs = model_ms.predict(f)[0]
|
|
||||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
|
||||||
if odd <= 1.0: continue
|
|
||||||
edge = prob - (1/odd)
|
|
||||||
if edge > 0.05 and prob > 0.50:
|
|
||||||
results['ms']['bet'] += 1
|
|
||||||
h, a = row['score_home'], row['score_away']
|
|
||||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
|
||||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
|
||||||
else: results['ms']['profit'] -= 1.0
|
|
||||||
break
|
|
||||||
|
|
||||||
# OU2.5
|
|
||||||
p_over = float(model_ou.predict(f)[0])
|
|
||||||
if p_over > 0.55:
|
|
||||||
results['ou25']['bet'] += 1
|
|
||||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85
|
|
||||||
else: results['ou25']['profit'] -= 1.0
|
|
||||||
|
|
||||||
# BTTS
|
|
||||||
p_btts = float(model_btts.predict(f)[0])
|
|
||||||
if p_btts > 0.55:
|
|
||||||
results['btts']['bet'] += 1
|
|
||||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
|
||||||
else: results['btts']['profit'] -= 1.0
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📊 VQWEN DEEP SONUÇLAR")
|
|
||||||
print("="*60)
|
|
||||||
for mkt in ['ms', 'ou25', 'btts']:
|
|
||||||
r = results[mkt]
|
|
||||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
|
||||||
print(f"{mkt.upper():<10} Oyn: {r['bet']:<5} Kaz: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f}")
|
|
||||||
|
|
||||||
total = sum(r['profit'] for r in results.values())
|
|
||||||
print(f"\n💰 TOPLAM: {total:+.2f} Units")
|
|
||||||
print("🟢 PARA KAZANDIK!" if total > 0 else "🔴 ZARARDA")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_vqwen_deep_backtest()
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN Final Backtest
|
|
||||||
====================
|
|
||||||
Tests the Final Model (ELO + Rest + Context).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import pickle
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
|
|
||||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
|
||||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
|
||||||
|
|
||||||
def run_final_backtest():
|
|
||||||
print("🧠 VQWEN FINAL BACKTEST (ELO + REST)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# Load Models
|
|
||||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
|
||||||
try:
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
|
||||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
|
||||||
print("✅ VQWEN Final modelleri yüklendi.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Model hatası: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
|
||||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
|
||||||
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
conn = psycopg2.connect(dsn)
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
|
||||||
m.mst_utc,
|
|
||||||
t1.name as home_team, t2.name as away_team,
|
|
||||||
maf.home_elo, maf.away_elo,
|
|
||||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc), 1.2) as h_home_goals,
|
|
||||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc), 1.2) as a_away_goals,
|
|
||||||
COALESCE(EXTRACT(EPOCH FROM (to_timestamp(m.mst_utc/1000) - (SELECT MAX(to_timestamp(m2.mst_utc/1000)) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc)) / 86400), 7) as h_rest,
|
|
||||||
COALESCE(EXTRACT(EPOCH FROM (to_timestamp(m.mst_utc/1000) - (SELECT MAX(to_timestamp(m2.mst_utc/1000)) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc)) / 86400), 7) as a_rest,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.home_team_id AND mp.is_starting = true), 11) as h_xi,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.away_team_id AND mp.is_starting = true), 11) as a_xi,
|
|
||||||
COALESCE((SELECT COUNT(*) FROM match_player_events mpe WHERE mpe.match_id = m.id AND mpe.event_type = 'card'), 4) as cards,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
|
||||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
|
||||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
|
||||||
LEFT JOIN football_ai_features maf ON maf.match_id = m.id
|
|
||||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
LIMIT 1000
|
|
||||||
""", (league_ids,))
|
|
||||||
|
|
||||||
rows = cur.fetchall()
|
|
||||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
|
||||||
|
|
||||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
oh = float(row['oh'] or 0)
|
|
||||||
od = float(row['od'] or 0)
|
|
||||||
oa = float(row['oa'] or 0)
|
|
||||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
|
||||||
|
|
||||||
# Features
|
|
||||||
h_elo = float(row['home_elo'] or 1500)
|
|
||||||
a_elo = float(row['away_elo'] or 1500)
|
|
||||||
h_home_goals = float(row['h_home_goals'] or 1.2)
|
|
||||||
a_away_goals = float(row['a_away_goals'] or 1.2)
|
|
||||||
h_rest = float(row['h_rest'] or 7)
|
|
||||||
a_rest = float(row['a_rest'] or 7)
|
|
||||||
h_xi = float(row['h_xi'] or 11)
|
|
||||||
a_xi = float(row['a_xi'] or 11)
|
|
||||||
cards = float(row['cards'] or 4)
|
|
||||||
|
|
||||||
def fatigue(rest):
|
|
||||||
if rest < 3: return 0.85
|
|
||||||
if rest < 5: return 0.95
|
|
||||||
return 1.0
|
|
||||||
|
|
||||||
h_fat = fatigue(h_rest)
|
|
||||||
a_fat = fatigue(a_rest)
|
|
||||||
|
|
||||||
h_xg = h_home_goals * h_fat
|
|
||||||
a_xg = a_away_goals * a_fat
|
|
||||||
total_xg = h_xg + a_xg
|
|
||||||
|
|
||||||
margin = (1/oh) + (1/od) + (1/oa)
|
|
||||||
f = pd.DataFrame([{
|
|
||||||
'elo_diff': h_elo - a_elo,
|
|
||||||
'h_xg': h_xg, 'a_xg': a_xg,
|
|
||||||
'total_xg': total_xg,
|
|
||||||
'pow_diff': (h_elo/100)*h_fat - (a_elo/100)*a_fat,
|
|
||||||
'rest_diff': h_rest - a_rest,
|
|
||||||
'h_fatigue': h_fat, 'a_fatigue': a_fat,
|
|
||||||
'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
|
||||||
'h_xi': h_xi, 'a_xi': a_xi,
|
|
||||||
'cards': cards
|
|
||||||
}])
|
|
||||||
|
|
||||||
# MS
|
|
||||||
ms_probs = model_ms.predict(f)[0]
|
|
||||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
|
||||||
if odd <= 1.0: continue
|
|
||||||
edge = prob - (1/odd)
|
|
||||||
if edge > 0.05 and prob > 0.45:
|
|
||||||
results['ms']['bet'] += 1
|
|
||||||
h, a = row['score_home'], row['score_away']
|
|
||||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
|
||||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
|
||||||
else: results['ms']['profit'] -= 1.0
|
|
||||||
break
|
|
||||||
|
|
||||||
# OU2.5
|
|
||||||
p_over = float(model_ou.predict(f)[0])
|
|
||||||
if p_over > 0.55:
|
|
||||||
results['ou25']['bet'] += 1
|
|
||||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85
|
|
||||||
else: results['ou25']['profit'] -= 1.0
|
|
||||||
|
|
||||||
# BTTS
|
|
||||||
p_btts = float(model_btts.predict(f)[0])
|
|
||||||
if p_btts > 0.55:
|
|
||||||
results['btts']['bet'] += 1
|
|
||||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
|
||||||
else: results['btts']['profit'] -= 1.0
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("📊 VQWEN FINAL SONUÇLAR")
|
|
||||||
print("="*60)
|
|
||||||
for mkt in ['ms', 'ou25', 'btts']:
|
|
||||||
r = results[mkt]
|
|
||||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
|
||||||
print(f"{mkt.upper():<10} Oyn: {r['bet']:<5} Kaz: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f}")
|
|
||||||
|
|
||||||
total = sum(r['profit'] for r in results.values())
|
|
||||||
print(f"\n💰 TOPLAM: {total:+.2f} Units")
|
|
||||||
print("🟢 PARA KAZANDIK!" if total > 0 else "🔴 ZARARDA")
|
|
||||||
|
|
||||||
cur.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_final_backtest()
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN v3 Shared-Contract Backtest
|
|
||||||
=================================
|
|
||||||
|
|
||||||
Evaluates the retrained VQWEN models on the temporal validation slice using
|
|
||||||
the exact same pre-match feature contract as training/runtime.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import pickle
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import psycopg2
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
AI_DIR = Path(__file__).resolve().parent
|
|
||||||
ENGINE_DIR = AI_DIR.parent
|
|
||||||
REPO_DIR = ENGINE_DIR.parent
|
|
||||||
MODELS_DIR = ENGINE_DIR / "models" / "vqwen"
|
|
||||||
|
|
||||||
if str(ENGINE_DIR) not in sys.path:
|
|
||||||
sys.path.insert(0, str(ENGINE_DIR))
|
|
||||||
|
|
||||||
from features.vqwen_contract import FEATURE_COLUMNS # noqa: E402
|
|
||||||
from train_vqwen_v3 import ( # noqa: E402
|
|
||||||
_enrich_pre_match_context,
|
|
||||||
_fetch_dataframe,
|
|
||||||
_prepare_features,
|
|
||||||
_temporal_split,
|
|
||||||
load_top_league_ids,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_env() -> None:
|
|
||||||
load_dotenv(REPO_DIR / ".env", override=False)
|
|
||||||
load_dotenv(ENGINE_DIR / ".env", override=False)
|
|
||||||
|
|
||||||
|
|
||||||
def get_clean_dsn() -> str:
|
|
||||||
_load_env()
|
|
||||||
raw = os.getenv("DATABASE_URL", "").strip().strip('"').strip("'")
|
|
||||||
if not raw:
|
|
||||||
raise RuntimeError("DATABASE_URL is missing.")
|
|
||||||
return raw.split("?", 1)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
||||||
if len(y_true) == 0:
|
|
||||||
return 0.0
|
|
||||||
return float((y_true == y_pred).mean())
|
|
||||||
|
|
||||||
|
|
||||||
def _binary_metrics(prob: np.ndarray, y_true: np.ndarray) -> tuple[float, float]:
|
|
||||||
pred = (prob >= 0.5).astype(int)
|
|
||||||
acc = _accuracy(y_true, pred)
|
|
||||||
brier = float(np.mean((prob - y_true) ** 2)) if len(y_true) else 1.0
|
|
||||||
return acc, brier
|
|
||||||
|
|
||||||
|
|
||||||
def _multiclass_brier(prob: np.ndarray, y_true: np.ndarray, n_classes: int = 3) -> float:
|
|
||||||
if len(y_true) == 0:
|
|
||||||
return 1.0
|
|
||||||
target = np.zeros((len(y_true), n_classes), dtype=np.float64)
|
|
||||||
target[np.arange(len(y_true)), y_true.astype(int)] = 1.0
|
|
||||||
return float(np.mean(np.sum((prob - target) ** 2, axis=1)))
|
|
||||||
|
|
||||||
|
|
||||||
def _band_label(probability: float) -> str:
|
|
||||||
if probability >= 0.70:
|
|
||||||
return "HIGH"
|
|
||||||
if probability >= 0.60:
|
|
||||||
return "MEDIUM"
|
|
||||||
if probability >= 0.50:
|
|
||||||
return "LOW"
|
|
||||||
return "NO_BET"
|
|
||||||
|
|
||||||
|
|
||||||
def _summarize_bands(
|
|
||||||
name: str,
|
|
||||||
confidence: np.ndarray,
|
|
||||||
is_correct: np.ndarray,
|
|
||||||
) -> list[str]:
|
|
||||||
lines: list[str] = []
|
|
||||||
for band in ("HIGH", "MEDIUM", "LOW"):
|
|
||||||
mask = np.array([_band_label(float(p)) == band for p in confidence], dtype=bool)
|
|
||||||
count = int(mask.sum())
|
|
||||||
accuracy = float(is_correct[mask].mean()) if count else 0.0
|
|
||||||
avg_conf = float(confidence[mask].mean()) if count else 0.0
|
|
||||||
lines.append(
|
|
||||||
f"{name} {band:<6} count={count:<4} accuracy={accuracy*100:5.1f}% avg_conf={avg_conf*100:5.1f}%"
|
|
||||||
)
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
def run_v3_backtest() -> None:
|
|
||||||
print("VQWEN v3 SHARED-CONTRACT BACKTEST")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
league_ids = load_top_league_ids()
|
|
||||||
dsn = get_clean_dsn()
|
|
||||||
|
|
||||||
with psycopg2.connect(dsn) as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
df = _fetch_dataframe(cur, league_ids)
|
|
||||||
df = _enrich_pre_match_context(cur, df)
|
|
||||||
df = _prepare_features(df)
|
|
||||||
|
|
||||||
train_df, valid_df = _temporal_split(df)
|
|
||||||
print(f"Toplam ornek: {len(df)} | Train: {len(train_df)} | Valid: {len(valid_df)}")
|
|
||||||
|
|
||||||
with (MODELS_DIR / "vqwen_ms.pkl").open("rb") as handle:
|
|
||||||
model_ms = pickle.load(handle)
|
|
||||||
with (MODELS_DIR / "vqwen_ou25.pkl").open("rb") as handle:
|
|
||||||
model_ou25 = pickle.load(handle)
|
|
||||||
with (MODELS_DIR / "vqwen_btts.pkl").open("rb") as handle:
|
|
||||||
model_btts = pickle.load(handle)
|
|
||||||
|
|
||||||
X_valid = valid_df[FEATURE_COLUMNS]
|
|
||||||
y_ms = valid_df["t_ms"].to_numpy(dtype=np.int64)
|
|
||||||
y_ou25 = valid_df["t_ou"].to_numpy(dtype=np.int64)
|
|
||||||
y_btts = valid_df["t_btts"].to_numpy(dtype=np.int64)
|
|
||||||
|
|
||||||
ms_prob = np.asarray(model_ms.predict(X_valid), dtype=np.float64)
|
|
||||||
ou25_prob = np.asarray(model_ou25.predict(X_valid), dtype=np.float64).reshape(-1)
|
|
||||||
btts_prob = np.asarray(model_btts.predict(X_valid), dtype=np.float64).reshape(-1)
|
|
||||||
|
|
||||||
ms_pred = np.argmax(ms_prob, axis=1)
|
|
||||||
ms_conf = np.max(ms_prob, axis=1)
|
|
||||||
ms_correct = (ms_pred == y_ms).astype(np.int64)
|
|
||||||
|
|
||||||
ou25_pred = (ou25_prob >= 0.5).astype(np.int64)
|
|
||||||
ou25_conf = np.where(ou25_prob >= 0.5, ou25_prob, 1.0 - ou25_prob)
|
|
||||||
ou25_correct = (ou25_pred == y_ou25).astype(np.int64)
|
|
||||||
|
|
||||||
btts_pred = (btts_prob >= 0.5).astype(np.int64)
|
|
||||||
btts_conf = np.where(btts_prob >= 0.5, btts_prob, 1.0 - btts_prob)
|
|
||||||
btts_correct = (btts_pred == y_btts).astype(np.int64)
|
|
||||||
|
|
||||||
ms_acc = _accuracy(y_ms, ms_pred)
|
|
||||||
ou25_acc, ou25_brier = _binary_metrics(ou25_prob, y_ou25)
|
|
||||||
btts_acc, btts_brier = _binary_metrics(btts_prob, y_btts)
|
|
||||||
ms_brier = _multiclass_brier(ms_prob, y_ms)
|
|
||||||
|
|
||||||
print("\nGenel metrikler")
|
|
||||||
print(f"MS accuracy : {ms_acc*100:.2f}% | multiclass_brier={ms_brier:.4f}")
|
|
||||||
print(f"OU25 accuracy : {ou25_acc*100:.2f}% | brier={ou25_brier:.4f}")
|
|
||||||
print(f"BTTS accuracy : {btts_acc*100:.2f}% | brier={btts_brier:.4f}")
|
|
||||||
|
|
||||||
print("\nConfidence band")
|
|
||||||
for line in _summarize_bands("MS", ms_conf, ms_correct):
|
|
||||||
print(line)
|
|
||||||
for line in _summarize_bands("OU25", ou25_conf, ou25_correct):
|
|
||||||
print(line)
|
|
||||||
for line in _summarize_bands("BTTS", btts_conf, btts_correct):
|
|
||||||
print(line)
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"validation_samples": int(len(valid_df)),
|
|
||||||
"metrics": {
|
|
||||||
"ms_accuracy": round(ms_acc, 4),
|
|
||||||
"ms_brier": round(ms_brier, 4),
|
|
||||||
"ou25_accuracy": round(ou25_acc, 4),
|
|
||||||
"ou25_brier": round(ou25_brier, 4),
|
|
||||||
"btts_accuracy": round(btts_acc, 4),
|
|
||||||
"btts_brier": round(btts_brier, 4),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
(MODELS_DIR / "vqwen_backtest_v3_summary.json").write_text(
|
|
||||||
json.dumps(summary, indent=2),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
print("\nKaydedildi: vqwen_backtest_v3_summary.json")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_v3_backtest()
|
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
"""
|
||||||
|
V28 — CONDITIONAL FREQUENCY ENGINE
|
||||||
|
====================================
|
||||||
|
User's strategy automated at scale:
|
||||||
|
|
||||||
|
For every match (e.g. Beşiktaş vs Konya):
|
||||||
|
1. Look at Beşiktaş's HOME history when their MS1 odds were in the same band (e.g. 1.30-1.40)
|
||||||
|
→ What % of those matches ended OU 1.5 over? OU 2.5 over? MS1?
|
||||||
|
2. Look at Konya's AWAY history when their MS2 odds were in the same band (e.g. 2.00-2.20)
|
||||||
|
→ Same questions
|
||||||
|
3. COMBINE both signals:
|
||||||
|
→ If BOTH teams historically produce >80% OU1.5 over at these odds → BET OU1.5 over
|
||||||
|
→ This is the user's exact Excel strategy, now running on 104K matches
|
||||||
|
|
||||||
|
CRITICAL: Only uses PAST matches for each prediction (no future leakage)
|
||||||
|
"""
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from collections import defaultdict
|
||||||
|
import warnings
|
||||||
|
warnings.filterwarnings('ignore')
|
||||||
|
|
||||||
|
# ─── Load Data ───
|
||||||
|
print("Loading data...")
|
||||||
|
df = pd.read_csv('data/training_data_v27.csv', low_memory=False)
|
||||||
|
KEEP_STR = ['match_id', 'league_name', 'home_team', 'away_team',
|
||||||
|
'home_team_id', 'away_team_id', 'league_id', 'mst_utc']
|
||||||
|
for c in df.columns:
|
||||||
|
if c not in KEEP_STR:
|
||||||
|
df[c] = pd.to_numeric(df[c], errors='coerce')
|
||||||
|
|
||||||
|
# Ensure chronological order (by match_id or date)
|
||||||
|
if 'mst_utc' in df.columns:
|
||||||
|
df['mst_utc'] = pd.to_datetime(df['mst_utc'], errors='coerce')
|
||||||
|
df = df.sort_values('mst_utc').reset_index(drop=True)
|
||||||
|
|
||||||
|
# Filter: need valid odds + scores
|
||||||
|
df = df.dropna(subset=['odds_ms_h', 'odds_ms_a', 'score_home', 'score_away',
|
||||||
|
'home_team_id', 'away_team_id', 'label_ms'])
|
||||||
|
|
||||||
|
# Compute actual goal labels
|
||||||
|
df['total_goals'] = df['score_home'] + df['score_away']
|
||||||
|
df['ou15_actual'] = (df['total_goals'] > 1.5).astype(int)
|
||||||
|
df['ou25_actual'] = (df['total_goals'] > 2.5).astype(int)
|
||||||
|
df['ou35_actual'] = (df['total_goals'] > 3.5).astype(int)
|
||||||
|
df['btts_actual'] = ((df['score_home'] > 0) & (df['score_away'] > 0)).astype(int)
|
||||||
|
df['ms_result'] = df['label_ms'].astype(int) # 0=H, 1=D, 2=A
|
||||||
|
|
||||||
|
N = len(df)
|
||||||
|
print(f"Total matches: {N}")
|
||||||
|
print(f"Unique home teams: {df.home_team_id.nunique()}")
|
||||||
|
print(f"Unique away teams: {df.away_team_id.nunique()}")
|
||||||
|
|
||||||
|
# ─── Odds Band Helper ───
|
||||||
|
def get_odds_band(odds, band_width=0.10):
|
||||||
|
"""Round odds to nearest band. E.g. 1.35 → (1.30, 1.40)"""
|
||||||
|
lower = round(np.floor(odds / band_width) * band_width, 2)
|
||||||
|
upper = round(lower + band_width, 2)
|
||||||
|
return (lower, upper)
|
||||||
|
|
||||||
|
def get_odds_band_wide(odds):
|
||||||
|
"""Wider band for less common teams. E.g. 1.35 → (1.20, 1.50)"""
|
||||||
|
if odds < 1.50:
|
||||||
|
return (1.01, 1.50)
|
||||||
|
elif odds < 2.00:
|
||||||
|
return (1.50, 2.00)
|
||||||
|
elif odds < 2.50:
|
||||||
|
return (2.00, 2.50)
|
||||||
|
elif odds < 3.00:
|
||||||
|
return (2.50, 3.00)
|
||||||
|
elif odds < 4.00:
|
||||||
|
return (3.00, 4.00)
|
||||||
|
elif odds < 6.00:
|
||||||
|
return (4.00, 6.00)
|
||||||
|
else:
|
||||||
|
return (6.00, 20.00)
|
||||||
|
|
||||||
|
# ─── Build Conditional Frequency Lookup (Expanding Window) ───
|
||||||
|
print("\nBuilding conditional frequency features (expanding window)...")
|
||||||
|
|
||||||
|
# We'll compute features for each match using only past data
|
||||||
|
MIN_MATCHES = 5 # minimum historical matches to generate a signal
|
||||||
|
|
||||||
|
# Pre-allocate feature arrays
|
||||||
|
feat_names = [
|
||||||
|
'home_ou15_rate_at_band', 'home_ou25_rate_at_band', 'home_ou35_rate_at_band',
|
||||||
|
'home_btts_rate_at_band', 'home_win_rate_at_band', 'home_n_at_band',
|
||||||
|
'away_ou15_rate_at_band', 'away_ou25_rate_at_band', 'away_ou35_rate_at_band',
|
||||||
|
'away_btts_rate_at_band', 'away_win_rate_at_band', 'away_n_at_band',
|
||||||
|
'combined_ou15', 'combined_ou25', 'combined_ou35', 'combined_btts',
|
||||||
|
'home_goals_at_band', 'away_goals_at_band', 'combined_goals_at_band',
|
||||||
|
'home_conceded_at_band', 'away_conceded_at_band',
|
||||||
|
]
|
||||||
|
features = np.full((N, len(feat_names)), np.nan)
|
||||||
|
|
||||||
|
# Historical ledger: team_id → list of (odds_band, ou15, ou25, ou35, btts, ms_result, goals_scored, goals_conceded)
|
||||||
|
home_history = defaultdict(list) # team performances when playing HOME
|
||||||
|
away_history = defaultdict(list) # team performances when playing AWAY
|
||||||
|
|
||||||
|
for i in range(N):
|
||||||
|
row = df.iloc[i]
|
||||||
|
ht_id = row.home_team_id
|
||||||
|
at_id = row.away_team_id
|
||||||
|
h_odds = row.odds_ms_h
|
||||||
|
a_odds = row.odds_ms_a
|
||||||
|
|
||||||
|
if pd.isna(h_odds) or pd.isna(a_odds):
|
||||||
|
continue
|
||||||
|
|
||||||
|
h_band = get_odds_band_wide(h_odds)
|
||||||
|
a_band = get_odds_band_wide(a_odds)
|
||||||
|
|
||||||
|
# ── Look up HOME team's historical performance at this odds band ──
|
||||||
|
h_hist = [x for x in home_history[ht_id] if h_band[0] <= x[0] < h_band[1]]
|
||||||
|
if len(h_hist) >= MIN_MATCHES:
|
||||||
|
features[i, 0] = np.mean([x[1] for x in h_hist]) # ou15 rate
|
||||||
|
features[i, 1] = np.mean([x[2] for x in h_hist]) # ou25 rate
|
||||||
|
features[i, 2] = np.mean([x[3] for x in h_hist]) # ou35 rate
|
||||||
|
features[i, 3] = np.mean([x[4] for x in h_hist]) # btts rate
|
||||||
|
features[i, 4] = np.mean([x[5] for x in h_hist]) # win rate (home win = 1 if ms==0)
|
||||||
|
features[i, 5] = len(h_hist)
|
||||||
|
features[i, 16] = np.mean([x[6] for x in h_hist]) # avg goals scored
|
||||||
|
features[i, 19] = np.mean([x[7] for x in h_hist]) # avg goals conceded
|
||||||
|
|
||||||
|
# ── Look up AWAY team's historical performance at this odds band ──
|
||||||
|
a_hist = [x for x in away_history[at_id] if a_band[0] <= x[0] < a_band[1]]
|
||||||
|
if len(a_hist) >= MIN_MATCHES:
|
||||||
|
features[i, 6] = np.mean([x[1] for x in a_hist]) # ou15 rate
|
||||||
|
features[i, 7] = np.mean([x[2] for x in a_hist]) # ou25 rate
|
||||||
|
features[i, 8] = np.mean([x[3] for x in a_hist]) # ou35 rate
|
||||||
|
features[i, 9] = np.mean([x[4] for x in a_hist]) # btts rate
|
||||||
|
features[i, 10] = np.mean([x[5] for x in a_hist]) # away win rate
|
||||||
|
features[i, 11] = len(a_hist)
|
||||||
|
features[i, 17] = np.mean([x[6] for x in a_hist]) # avg goals scored (away)
|
||||||
|
features[i, 20] = np.mean([x[7] for x in a_hist]) # avg goals conceded (away)
|
||||||
|
|
||||||
|
# ── Combined signals ──
|
||||||
|
if not np.isnan(features[i, 0]) and not np.isnan(features[i, 6]):
|
||||||
|
features[i, 12] = (features[i, 0] + features[i, 6]) / 2 # combined ou15
|
||||||
|
features[i, 13] = (features[i, 1] + features[i, 7]) / 2 # combined ou25
|
||||||
|
features[i, 14] = (features[i, 2] + features[i, 8]) / 2 # combined ou35
|
||||||
|
features[i, 15] = (features[i, 3] + features[i, 9]) / 2 # combined btts
|
||||||
|
features[i, 18] = features[i, 16] + features[i, 17] # combined goals
|
||||||
|
|
||||||
|
# ── Add THIS match to history (for future lookups) ──
|
||||||
|
ou15 = int(row.total_goals > 1.5)
|
||||||
|
ou25 = int(row.total_goals > 2.5)
|
||||||
|
ou35 = int(row.total_goals > 3.5)
|
||||||
|
btts = int(row.score_home > 0 and row.score_away > 0)
|
||||||
|
h_won = int(row.label_ms == 0)
|
||||||
|
a_won = int(row.label_ms == 2)
|
||||||
|
|
||||||
|
home_history[ht_id].append((h_odds, ou15, ou25, ou35, btts, h_won,
|
||||||
|
row.score_home, row.score_away))
|
||||||
|
away_history[at_id].append((a_odds, ou15, ou25, ou35, btts, a_won,
|
||||||
|
row.score_away, row.score_home))
|
||||||
|
|
||||||
|
if (i+1) % 20000 == 0:
|
||||||
|
valid = np.sum(~np.isnan(features[:i+1, 12]))
|
||||||
|
print(f" Processed {i+1}/{N} matches, {valid} with combined signals")
|
||||||
|
|
||||||
|
# Count valid features
|
||||||
|
valid_mask = ~np.isnan(features[:, 12])
|
||||||
|
print(f"\nMatches with combined conditional signals: {valid_mask.sum()} / {N}")
|
||||||
|
|
||||||
|
# ─── BACKTEST: Walk-Forward ───
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print(" CONDITIONAL FREQUENCY BACKTEST")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
# Only test on last 20% of data (to avoid early sparse data)
|
||||||
|
test_start = int(N * 0.7)
|
||||||
|
test_idx = range(test_start, N)
|
||||||
|
test_valid = [i for i in test_idx if valid_mask[i]]
|
||||||
|
print(f"Test window: matches {test_start}-{N} ({len(test_valid)} with signals)")
|
||||||
|
|
||||||
|
# Strategy: bet on OU1.5 over when combined_ou15 > threshold
|
||||||
|
markets = [
|
||||||
|
('OU 1.5 Over', 'combined_ou15', 12, 'ou15_actual', 'odds_ou15_o'),
|
||||||
|
('OU 2.5 Over', 'combined_ou25', 13, 'ou25_actual', 'odds_ou25_o'),
|
||||||
|
('OU 3.5 Over', 'combined_ou35', 14, 'ou35_actual', 'odds_ou35_o'),
|
||||||
|
('BTTS Yes', 'combined_btts', 15, 'btts_actual', 'odds_btts_y'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for market_name, feat_key, feat_idx, label_col, odds_col in markets:
|
||||||
|
print(f"\n ── {market_name} ──")
|
||||||
|
|
||||||
|
if odds_col not in df.columns:
|
||||||
|
print(f" No odds column '{odds_col}', skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for threshold in [0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]:
|
||||||
|
bets = 0
|
||||||
|
wins = 0
|
||||||
|
pnl = 0.0
|
||||||
|
|
||||||
|
for i in test_valid:
|
||||||
|
signal = features[i, feat_idx]
|
||||||
|
if np.isnan(signal) or signal < threshold:
|
||||||
|
continue
|
||||||
|
odds_val = df.iloc[i][odds_col]
|
||||||
|
if pd.isna(odds_val) or odds_val < 1.05:
|
||||||
|
continue
|
||||||
|
actual = df.iloc[i][label_col]
|
||||||
|
if pd.isna(actual):
|
||||||
|
continue
|
||||||
|
|
||||||
|
bets += 1
|
||||||
|
if actual == 1:
|
||||||
|
wins += 1
|
||||||
|
pnl += odds_val - 1
|
||||||
|
else:
|
||||||
|
pnl -= 1
|
||||||
|
|
||||||
|
if bets >= 20:
|
||||||
|
roi = pnl / bets * 100
|
||||||
|
hit = wins / bets * 100
|
||||||
|
ev = (wins/bets) * (pnl/wins + 1) if wins > 0 else 0
|
||||||
|
marker = " *** PROFITABLE ***" if roi > 0 else ""
|
||||||
|
print(f" Threshold>{threshold:.2f}: {bets:5d} bets, "
|
||||||
|
f"hit={hit:.1f}%, ROI={roi:+.1f}%{marker}")
|
||||||
|
|
||||||
|
# Also test MS (1X2) market
|
||||||
|
print(f"\n ── Maç Sonucu (1X2) ──")
|
||||||
|
# Home win when home_win_rate_at_band > X AND away team loses often at that band
|
||||||
|
for threshold in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
|
||||||
|
bets = wins = 0
|
||||||
|
pnl = 0.0
|
||||||
|
for i in test_valid:
|
||||||
|
h_wr = features[i, 4] # home win rate at band
|
||||||
|
a_lr = 1 - features[i, 10] if not np.isnan(features[i, 10]) else np.nan # away loss rate
|
||||||
|
if np.isnan(h_wr) or np.isnan(a_lr):
|
||||||
|
continue
|
||||||
|
combined = (h_wr + a_lr) / 2
|
||||||
|
if combined < threshold:
|
||||||
|
continue
|
||||||
|
odds_val = df.iloc[i].odds_ms_h
|
||||||
|
if pd.isna(odds_val) or odds_val < 1.10 or odds_val > 5.0:
|
||||||
|
continue
|
||||||
|
bets += 1
|
||||||
|
if df.iloc[i].label_ms == 0:
|
||||||
|
wins += 1
|
||||||
|
pnl += odds_val - 1
|
||||||
|
else:
|
||||||
|
pnl -= 1
|
||||||
|
if bets >= 20:
|
||||||
|
roi = pnl / bets * 100
|
||||||
|
hit = wins / bets * 100
|
||||||
|
marker = " *** PROFITABLE ***" if roi > 0 else ""
|
||||||
|
print(f" Home win comb>{threshold:.2f}: {bets:5d} bets, "
|
||||||
|
f"hit={hit:.1f}%, ROI={roi:+.1f}%{marker}")
|
||||||
|
|
||||||
|
# ─── DEEP DIVE: Best performing niches ───
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print(" DEEP DIVE: Combined OU15 + Odds Value Filter")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
# The user's strategy: high confidence + the odds must pay enough
|
||||||
|
for threshold in [0.75, 0.80, 0.85, 0.90]:
|
||||||
|
for min_odds in [1.10, 1.20, 1.30, 1.40]:
|
||||||
|
bets = wins = 0
|
||||||
|
pnl = 0.0
|
||||||
|
for i in test_valid:
|
||||||
|
signal = features[i, 12] # combined ou15
|
||||||
|
if np.isnan(signal) or signal < threshold:
|
||||||
|
continue
|
||||||
|
odds_val = df.iloc[i].get('odds_ou15_o', np.nan) if 'odds_ou15_o' in df.columns else np.nan
|
||||||
|
if pd.isna(odds_val) or odds_val < min_odds:
|
||||||
|
continue
|
||||||
|
actual = df.iloc[i].ou15_actual
|
||||||
|
|
||||||
|
bets += 1
|
||||||
|
if actual == 1:
|
||||||
|
wins += 1
|
||||||
|
pnl += odds_val - 1
|
||||||
|
else:
|
||||||
|
pnl -= 1
|
||||||
|
|
||||||
|
if bets >= 30:
|
||||||
|
roi = pnl / bets * 100
|
||||||
|
hit = wins / bets * 100
|
||||||
|
if roi > -5: # show near-profitable too
|
||||||
|
marker = " *** PROFITABLE ***" if roi > 0 else ""
|
||||||
|
print(f" OU15 sig>{threshold:.2f} odds>{min_odds}: "
|
||||||
|
f"{bets:5d} bets, hit={hit:.1f}%, ROI={roi:+.1f}%{marker}")
|
||||||
|
|
||||||
|
# ─── Additional: Goal expectation accuracy ───
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print(" GOAL PREDICTION ACCURACY")
|
||||||
|
print("="*70)
|
||||||
|
valid_goals = [i for i in test_valid if not np.isnan(features[i, 18])]
|
||||||
|
if valid_goals:
|
||||||
|
pred_goals = [features[i, 18] for i in valid_goals]
|
||||||
|
actual_goals = [df.iloc[i].total_goals for i in valid_goals]
|
||||||
|
from sklearn.metrics import mean_absolute_error
|
||||||
|
mae = mean_absolute_error(actual_goals, pred_goals)
|
||||||
|
corr = np.corrcoef(pred_goals, actual_goals)[0, 1]
|
||||||
|
print(f" Combined goal prediction MAE: {mae:.3f}")
|
||||||
|
print(f" Correlation: {corr:.4f}")
|
||||||
|
print(f" Avg predicted: {np.mean(pred_goals):.2f}, Avg actual: {np.mean(actual_goals):.2f}")
|
||||||
|
|
||||||
|
# Bucket analysis
|
||||||
|
print("\n Goal prediction buckets:")
|
||||||
|
for low, high in [(0, 1.5), (1.5, 2.0), (2.0, 2.5), (2.5, 3.0), (3.0, 3.5), (3.5, 5.0)]:
|
||||||
|
bucket = [i for i, pg in zip(valid_goals, pred_goals) if low <= pg < high]
|
||||||
|
if len(bucket) >= 20:
|
||||||
|
avg_actual = np.mean([df.iloc[i].total_goals for i in bucket])
|
||||||
|
ou25_rate = np.mean([df.iloc[i].ou25_actual for i in bucket])
|
||||||
|
print(f" Predicted {low:.1f}-{high:.1f}: n={len(bucket)}, "
|
||||||
|
f"actual_avg={avg_actual:.2f}, OU25%={ou25_rate*100:.1f}%")
|
||||||
|
|
||||||
|
print("\nDone!")
|
||||||
@@ -1071,13 +1071,13 @@ class FeatureExtractor:
|
|||||||
|
|
||||||
for mst, poss, sot, total_shots, corners, team_goals in rows:
|
for mst, poss, sot, total_shots, corners, team_goals in rows:
|
||||||
if poss and poss > 0:
|
if poss and poss > 0:
|
||||||
poss_sum += poss
|
poss_sum += float(poss)
|
||||||
poss_count += 1
|
poss_count += 1
|
||||||
sot_sum += sot or 0
|
sot_sum += float(sot or 0)
|
||||||
shots_sum += total_shots or 0
|
shots_sum += float(total_shots or 0)
|
||||||
corners_sum += corners or 0
|
corners_sum += float(corners or 0)
|
||||||
|
|
||||||
goals_scored += team_goals or 0
|
goals_scored += float(team_goals or 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"possession": (poss_sum / poss_count / 100) if poss_count > 0 else 0.50,
|
"possession": (poss_sum / poss_count / 100) if poss_count > 0 else 0.50,
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
SOURCE_CSV = AI_ENGINE_DIR / "data" / "training_data.csv"
|
||||||
|
TARGET_DIR = AI_ENGINE_DIR / "data" / "v26_shadow"
|
||||||
|
TARGET_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _rolling_windows(frame: pd.DataFrame) -> list[dict[str, int]]:
|
||||||
|
ordered = frame.sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
windows: list[dict[str, int]] = []
|
||||||
|
if ordered.empty:
|
||||||
|
return windows
|
||||||
|
|
||||||
|
size = len(ordered)
|
||||||
|
cuts = [0.55, 0.7, 0.85]
|
||||||
|
for idx, cut in enumerate(cuts, start=1):
|
||||||
|
end_ix = max(int(size * cut), 1)
|
||||||
|
test_end = min(size - 1, end_ix + max(int(size * 0.10), 1))
|
||||||
|
windows.append(
|
||||||
|
{
|
||||||
|
"window": idx,
|
||||||
|
"train_end_ix": end_ix - 1,
|
||||||
|
"test_start_ix": end_ix,
|
||||||
|
"test_end_ix": test_end,
|
||||||
|
"train_end_mst_utc": int(ordered.iloc[end_ix - 1]["mst_utc"]),
|
||||||
|
"test_end_mst_utc": int(ordered.iloc[test_end]["mst_utc"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return windows
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if not SOURCE_CSV.exists():
|
||||||
|
raise SystemExit(f"Missing source CSV: {SOURCE_CSV}")
|
||||||
|
|
||||||
|
frame = pd.read_csv(SOURCE_CSV)
|
||||||
|
if "mst_utc" not in frame.columns:
|
||||||
|
raise SystemExit("training_data.csv must include mst_utc")
|
||||||
|
|
||||||
|
ordered = frame.sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
ordered["lineup_completeness"] = 1.0
|
||||||
|
ordered["referee_available"] = (
|
||||||
|
ordered.get("referee_experience", pd.Series([0] * len(ordered))).fillna(0) > 0
|
||||||
|
).astype(float)
|
||||||
|
ordered["league_reliability"] = ordered.get("league_zero_goal_rate", 0).fillna(0).apply(
|
||||||
|
lambda value: round(max(0.25, min(0.95, 0.85 - float(value))), 4)
|
||||||
|
)
|
||||||
|
ordered["odds_snapshot_freshness"] = 1.0
|
||||||
|
|
||||||
|
train_end = max(int(len(ordered) * 0.70), 1)
|
||||||
|
validation_end = max(int(len(ordered) * 0.85), train_end + 1)
|
||||||
|
validation_end = min(validation_end, len(ordered) - 1)
|
||||||
|
|
||||||
|
train_df = ordered.iloc[:train_end].copy()
|
||||||
|
validation_df = ordered.iloc[train_end:validation_end].copy()
|
||||||
|
holdout_df = ordered.iloc[validation_end:].copy()
|
||||||
|
|
||||||
|
train_df.to_csv(TARGET_DIR / "train.csv", index=False)
|
||||||
|
validation_df.to_csv(TARGET_DIR / "validation.csv", index=False)
|
||||||
|
holdout_df.to_csv(TARGET_DIR / "holdout.csv", index=False)
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
"source": str(SOURCE_CSV),
|
||||||
|
"rows": int(len(ordered)),
|
||||||
|
"train_rows": int(len(train_df)),
|
||||||
|
"validation_rows": int(len(validation_df)),
|
||||||
|
"holdout_rows": int(len(holdout_df)),
|
||||||
|
"rolling_windows": _rolling_windows(ordered),
|
||||||
|
"derived_columns": [
|
||||||
|
"lineup_completeness",
|
||||||
|
"referee_available",
|
||||||
|
"league_reliability",
|
||||||
|
"odds_snapshot_freshness",
|
||||||
|
],
|
||||||
|
"feature_policy": "prediction_time_only",
|
||||||
|
}
|
||||||
|
(TARGET_DIR / "dataset_meta.json").write_text(
|
||||||
|
json.dumps(meta, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[OK] V26 dataset written to {TARGET_DIR}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
"""
|
||||||
|
V27 Training Data Extraction - Value Sniper
|
||||||
|
Extends V25 to ALL matches with odds (~104K).
|
||||||
|
Adds rolling window, league quality, time, H2H, strength features.
|
||||||
|
Usage: python3 scripts/extract_training_data_v27.py
|
||||||
|
"""
|
||||||
|
import os, sys, csv, time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, AI_DIR)
|
||||||
|
|
||||||
|
from scripts.extract_training_data import (
|
||||||
|
BatchDataLoader as V25Loader,
|
||||||
|
FeatureExtractor as V25Extractor,
|
||||||
|
FEATURE_COLS as V25_COLS,
|
||||||
|
get_conn,
|
||||||
|
)
|
||||||
|
from features.rolling_features import (
|
||||||
|
calc_rolling_features, calc_league_quality,
|
||||||
|
calc_time_features, calc_advanced_h2h, calc_strength_diff,
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
os.makedirs(os.path.dirname(OUTPUT), exist_ok=True)
|
||||||
|
|
||||||
|
V27_NEW = [
|
||||||
|
"home_rolling5_goals","home_rolling5_conceded",
|
||||||
|
"home_rolling10_goals","home_rolling10_conceded",
|
||||||
|
"home_rolling20_goals","home_rolling20_conceded",
|
||||||
|
"away_rolling5_goals","away_rolling5_conceded",
|
||||||
|
"away_rolling10_goals","away_rolling10_conceded",
|
||||||
|
"home_rolling5_cs","away_rolling5_cs",
|
||||||
|
"home_venue_goals","home_venue_conceded",
|
||||||
|
"away_venue_goals","away_venue_conceded",
|
||||||
|
"home_goal_trend","away_goal_trend",
|
||||||
|
"league_home_win_rate","league_draw_rate",
|
||||||
|
"league_btts_rate","league_ou25_rate",
|
||||||
|
"league_reliability_score",
|
||||||
|
"home_days_rest","away_days_rest",
|
||||||
|
"match_month","is_season_start","is_season_end",
|
||||||
|
"h2h_home_goals_avg","h2h_away_goals_avg",
|
||||||
|
"h2h_recent_trend","h2h_venue_advantage",
|
||||||
|
"attack_vs_defense_home","attack_vs_defense_away",
|
||||||
|
"xg_diff","form_momentum_interaction",
|
||||||
|
"elo_form_consistency","upset_x_elo_gap",
|
||||||
|
]
|
||||||
|
ALL_COLS = V25_COLS + V27_NEW
|
||||||
|
|
||||||
|
|
||||||
|
class V27Loader(V25Loader):
|
||||||
|
"""Load ALL matches with odds, not just top leagues."""
|
||||||
|
def __init__(self, conn):
|
||||||
|
super().__init__(conn, [])
|
||||||
|
self.league_matches_cache = {}
|
||||||
|
|
||||||
|
def _load_matches(self):
|
||||||
|
self.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, m.league_id,
|
||||||
|
ht.name, at.name, l.name
|
||||||
|
FROM matches m
|
||||||
|
JOIN teams ht ON m.home_team_id = ht.id
|
||||||
|
JOIN teams at ON m.away_team_id = at.id
|
||||||
|
JOIN leagues l ON m.league_id = l.id
|
||||||
|
WHERE m.status='FT' AND m.score_home IS NOT NULL
|
||||||
|
AND m.sport='football'
|
||||||
|
AND EXISTS(SELECT 1 FROM odd_categories oc WHERE oc.match_id=m.id)
|
||||||
|
ORDER BY m.mst_utc ASC
|
||||||
|
""")
|
||||||
|
self.matches = self.cur.fetchall()
|
||||||
|
|
||||||
|
def _load_odds(self):
|
||||||
|
self.cur.execute("""
|
||||||
|
SELECT oc.match_id, oc.name, os.name, os.odd_value
|
||||||
|
FROM odd_selections os
|
||||||
|
JOIN odd_categories oc ON os.odd_category_db_id=oc.db_id
|
||||||
|
JOIN matches m ON oc.match_id=m.id
|
||||||
|
WHERE m.status='FT' AND m.sport='football'
|
||||||
|
""")
|
||||||
|
for mid, cat, sel, val in self.cur.fetchall():
|
||||||
|
try:
|
||||||
|
v = float(val) if val else 0
|
||||||
|
if v <= 0 or not cat or not sel: continue
|
||||||
|
if mid not in self.odds_cache: self.odds_cache[mid] = {}
|
||||||
|
c = cat.lower().strip()
|
||||||
|
s = sel.lower().strip()
|
||||||
|
o = self.odds_cache[mid]
|
||||||
|
if c == 'maç sonucu':
|
||||||
|
if sel=='1': o['ms_h']=v
|
||||||
|
elif sel in('0','X'): o['ms_d']=v
|
||||||
|
elif sel=='2': o['ms_a']=v
|
||||||
|
elif c == '1. yarı sonucu':
|
||||||
|
if sel=='1': o['ht_ms_h']=v
|
||||||
|
elif sel in('0','X'): o['ht_ms_d']=v
|
||||||
|
elif sel=='2': o['ht_ms_a']=v
|
||||||
|
elif c == 'karşılıklı gol':
|
||||||
|
if 'var' in s: o['btts_y']=v
|
||||||
|
elif 'yok' in s: o['btts_n']=v
|
||||||
|
elif c == '2,5 alt/üst':
|
||||||
|
if 'alt' in s: o['ou25_u']=v
|
||||||
|
elif 'üst' in s: o['ou25_o']=v
|
||||||
|
elif c == '1,5 alt/üst':
|
||||||
|
if 'alt' in s: o['ou15_u']=v
|
||||||
|
elif 'üst' in s: o['ou15_o']=v
|
||||||
|
elif c == '3,5 alt/üst':
|
||||||
|
if 'alt' in s: o['ou35_u']=v
|
||||||
|
elif 'üst' in s: o['ou35_o']=v
|
||||||
|
elif c == '0,5 alt/üst':
|
||||||
|
if 'alt' in s: o['ou05_u']=v
|
||||||
|
elif 'üst' in s: o['ou05_o']=v
|
||||||
|
elif c == '1. yarı 0,5 alt/üst':
|
||||||
|
if 'alt' in s: o['ht_ou05_u']=v
|
||||||
|
elif 'üst' in s: o['ht_ou05_o']=v
|
||||||
|
elif c == '1. yarı 1,5 alt/üst':
|
||||||
|
if 'alt' in s: o['ht_ou15_u']=v
|
||||||
|
elif 'üst' in s: o['ht_ou15_o']=v
|
||||||
|
except (ValueError, TypeError): pass
|
||||||
|
|
||||||
|
def _load_league_stats(self):
|
||||||
|
self.cur.execute("""
|
||||||
|
SELECT league_id,
|
||||||
|
AVG(score_home+score_away), AVG(CASE WHEN score_home=0 AND score_away=0 THEN 1.0 ELSE 0.0 END),
|
||||||
|
COUNT(*)
|
||||||
|
FROM matches WHERE status='FT' AND score_home IS NOT NULL AND sport='football'
|
||||||
|
GROUP BY league_id
|
||||||
|
""")
|
||||||
|
for lid, ag, zr, cnt in self.cur.fetchall():
|
||||||
|
self.league_stats_cache[lid] = {
|
||||||
|
"avg_goals": float(ag) if ag else 2.5,
|
||||||
|
"zero_rate": float(zr) if zr else 0.07,
|
||||||
|
"match_count": cnt
|
||||||
|
}
|
||||||
|
|
||||||
|
def _load_squad_data(self):
|
||||||
|
self.cur.execute("""
|
||||||
|
SELECT mpp.match_id, mpp.team_id,
|
||||||
|
COUNT(*) FILTER(WHERE mpp.is_starting=true),
|
||||||
|
COUNT(*),
|
||||||
|
COUNT(*) FILTER(WHERE mpp.is_starting=true
|
||||||
|
AND LOWER(COALESCE(mpp.position::TEXT,''))~'(forward|fwd|forvet|striker)')
|
||||||
|
FROM match_player_participation mpp
|
||||||
|
JOIN matches m ON mpp.match_id=m.id
|
||||||
|
WHERE m.status='FT' AND m.sport='football'
|
||||||
|
GROUP BY mpp.match_id, mpp.team_id
|
||||||
|
""")
|
||||||
|
part = {}
|
||||||
|
for mid,tid,st,tot,fwd in self.cur.fetchall():
|
||||||
|
part[(mid,tid)]={'starting_count':st or 0,'total_squad':tot or 0,'fwd_count':fwd or 0}
|
||||||
|
|
||||||
|
self.cur.execute("""
|
||||||
|
SELECT mpe.match_id, mpe.team_id,
|
||||||
|
COUNT(*) FILTER(WHERE mpe.event_type='goal' AND COALESCE(mpe.event_subtype,'') NOT ILIKE '%%penaltı kaçırma%%'),
|
||||||
|
COUNT(DISTINCT mpe.assist_player_id) FILTER(WHERE mpe.event_type='goal' AND mpe.assist_player_id IS NOT NULL),
|
||||||
|
COUNT(DISTINCT mpe.player_id) FILTER(WHERE mpe.event_type='goal' AND COALESCE(mpe.event_subtype,'') NOT ILIKE '%%penaltı kaçırma%%')
|
||||||
|
FROM match_player_events mpe
|
||||||
|
JOIN matches m ON mpe.match_id=m.id
|
||||||
|
WHERE m.status='FT' AND m.sport='football'
|
||||||
|
GROUP BY mpe.match_id, mpe.team_id
|
||||||
|
""")
|
||||||
|
evts = {}
|
||||||
|
for mid,tid,g,a,sc in self.cur.fetchall():
|
||||||
|
evts[(mid,tid)]={'goals':g or 0,'assists':a or 0,'unique_scorers':sc or 0}
|
||||||
|
|
||||||
|
self.cur.execute("""
|
||||||
|
SELECT mpe.team_id, mpe.player_id, COUNT(*)
|
||||||
|
FROM match_player_events mpe JOIN matches m ON mpe.match_id=m.id
|
||||||
|
WHERE m.status='FT' AND m.sport='football' AND mpe.event_type='goal'
|
||||||
|
AND COALESCE(mpe.event_subtype,'') NOT ILIKE '%%penaltı kaçırma%%'
|
||||||
|
GROUP BY mpe.team_id, mpe.player_id HAVING COUNT(*)>=3
|
||||||
|
""")
|
||||||
|
kp_by_team = defaultdict(set)
|
||||||
|
for tid,pid,_ in self.cur.fetchall(): kp_by_team[tid].add(pid)
|
||||||
|
|
||||||
|
self.cur.execute("""
|
||||||
|
SELECT mpp.match_id, mpp.team_id, mpp.player_id
|
||||||
|
FROM match_player_participation mpp JOIN matches m ON mpp.match_id=m.id
|
||||||
|
WHERE mpp.is_starting=true AND m.status='FT' AND m.sport='football'
|
||||||
|
""")
|
||||||
|
starters = defaultdict(list)
|
||||||
|
for mid,tid,pid in self.cur.fetchall(): starters[(mid,tid)].append(pid)
|
||||||
|
|
||||||
|
for key in set(part)|set(evts):
|
||||||
|
mid,tid = key
|
||||||
|
p = part.get(key,{'starting_count':0,'total_squad':0,'fwd_count':0})
|
||||||
|
e = evts.get(key,{'goals':0,'assists':0,'unique_scorers':0})
|
||||||
|
s = starters.get(key,[])
|
||||||
|
kp_in = sum(1 for x in s if x in kp_by_team.get(tid,set()))
|
||||||
|
kp_tot = len(kp_by_team.get(tid,set()))
|
||||||
|
kp_miss = max(0, kp_tot - kp_in)
|
||||||
|
sq = p['starting_count']*0.3 + e['goals']*2.0 + e['assists']*1.0 + kp_in*3.0 + p['fwd_count']*1.5
|
||||||
|
mi = min(kp_miss/max(kp_tot,1), 1.0)
|
||||||
|
self.squad_cache[key] = {'squad_quality':sq,'key_players':kp_in,'missing_impact':mi,'goals_form':e['goals']}
|
||||||
|
|
||||||
|
def _load_cards_data(self):
|
||||||
|
self.cur.execute("""
|
||||||
|
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)
|
||||||
|
FROM match_player_events mpe JOIN matches m ON mpe.match_id=m.id
|
||||||
|
WHERE m.status='FT' AND m.sport='football' AND mpe.event_type::text LIKE '%%card%%'
|
||||||
|
GROUP BY mpe.match_id
|
||||||
|
""")
|
||||||
|
for mid, cw in self.cur.fetchall():
|
||||||
|
self.cards_cache[mid] = float(cw) if cw else 0.0
|
||||||
|
|
||||||
|
def load_league_matches(self):
|
||||||
|
for m in self.matches:
|
||||||
|
lid = m[8]
|
||||||
|
if lid not in self.league_matches_cache:
|
||||||
|
self.league_matches_cache[lid] = []
|
||||||
|
self.league_matches_cache[lid].append((m[7],None,m[3],m[4],None))
|
||||||
|
|
||||||
|
|
||||||
|
class V27Extractor(V25Extractor):
|
||||||
|
"""Adds V27 features on top of V25."""
|
||||||
|
def _extract_one(self, mid, hid, aid, sh, sa, hth, hta, mst, lid,
|
||||||
|
hn, an, ln):
|
||||||
|
row = super()._extract_one(mid,hid,aid,sh,sa,hth,hta,mst,lid,hn,an,ln)
|
||||||
|
if not row: return None
|
||||||
|
|
||||||
|
hm = self.loader.team_matches.get(hid,[])
|
||||||
|
am = self.loader.team_matches.get(aid,[])
|
||||||
|
|
||||||
|
hr = calc_rolling_features(hm, mst, True)
|
||||||
|
ar = calc_rolling_features(am, mst, False)
|
||||||
|
for pfx,r in [("home",hr),("away",ar)]:
|
||||||
|
row[f"{pfx}_rolling5_goals"]=r["rolling5_goals_avg"]
|
||||||
|
row[f"{pfx}_rolling5_conceded"]=r["rolling5_conceded_avg"]
|
||||||
|
row[f"{pfx}_rolling10_goals"]=r["rolling10_goals_avg"]
|
||||||
|
row[f"{pfx}_rolling10_conceded"]=r["rolling10_conceded_avg"]
|
||||||
|
row[f"{pfx}_rolling20_goals"]=r["rolling20_goals_avg"]
|
||||||
|
row[f"{pfx}_rolling20_conceded"]=r["rolling20_conceded_avg"]
|
||||||
|
row[f"{pfx}_rolling5_cs"]=r["rolling5_clean_sheets"]
|
||||||
|
row[f"{pfx}_venue_goals"]=r["venue_goals_avg"]
|
||||||
|
row[f"{pfx}_venue_conceded"]=r["venue_conceded_avg"]
|
||||||
|
row[f"{pfx}_goal_trend"]=r["goal_trend"]
|
||||||
|
|
||||||
|
lb = [x for x in self.loader.league_matches_cache.get(lid,[]) if x[0]<mst]
|
||||||
|
lq = calc_league_quality(lb)
|
||||||
|
for k,v in lq.items(): row[k]=v
|
||||||
|
|
||||||
|
ht = calc_time_features(hm, mst)
|
||||||
|
at = calc_time_features(am, mst)
|
||||||
|
row["home_days_rest"]=ht["days_rest"]
|
||||||
|
row["away_days_rest"]=at["days_rest"]
|
||||||
|
row["match_month"]=ht["match_month"]
|
||||||
|
row["is_season_start"]=ht["is_season_start"]
|
||||||
|
row["is_season_end"]=ht["is_season_end"]
|
||||||
|
|
||||||
|
h2h = calc_advanced_h2h(hm, hid, aid, mst)
|
||||||
|
for k,v in h2h.items(): row[k]=v
|
||||||
|
|
||||||
|
sd = calc_strength_diff(
|
||||||
|
{"goals_avg":row.get("home_goals_avg",1.3),"conceded_avg":row.get("home_conceded_avg",1.2),"scoring_rate":row.get("home_scoring_rate",0.75)},
|
||||||
|
{"goals_avg":row.get("away_goals_avg",1.3),"conceded_avg":row.get("away_conceded_avg",1.2),"scoring_rate":row.get("away_scoring_rate",0.75)},
|
||||||
|
self.elo_ratings[hid], self.elo_ratings[aid],
|
||||||
|
row.get("home_momentum_score",0.5), row.get("away_momentum_score",0.5),
|
||||||
|
row.get("upset_potential",0.0),
|
||||||
|
)
|
||||||
|
row.update(sd)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🚀 V27 Value Sniper — Training Data Extraction")
|
||||||
|
print("="*60)
|
||||||
|
t0 = time.time()
|
||||||
|
conn = get_conn()
|
||||||
|
|
||||||
|
print("\n📦 Loading ALL odds-bearing matches...")
|
||||||
|
loader = V27Loader(conn)
|
||||||
|
loader.load_all()
|
||||||
|
loader.load_league_matches()
|
||||||
|
print(f" Matches: {len(loader.matches)}")
|
||||||
|
print(f" Leagues: {len(loader.league_stats_cache)}")
|
||||||
|
print(f" Odds: {len(loader.odds_cache)}")
|
||||||
|
|
||||||
|
ext = V27Extractor(conn, loader)
|
||||||
|
rows = ext.extract_all()
|
||||||
|
if not rows:
|
||||||
|
print("❌ No data!"); return
|
||||||
|
|
||||||
|
print(f"\n💾 Writing {len(rows)} rows...")
|
||||||
|
with open(OUTPUT,"w",newline="",encoding="utf-8") as f:
|
||||||
|
w = csv.DictWriter(f, fieldnames=ALL_COLS, extrasaction='ignore')
|
||||||
|
w.writeheader(); w.writerows(rows)
|
||||||
|
|
||||||
|
n = len(rows)
|
||||||
|
wo = sum(1 for r in rows if r.get("odds_ms_h",0)>0)
|
||||||
|
md = defaultdict(int)
|
||||||
|
for r in rows: md[r["label_ms"]]+=1
|
||||||
|
print(f"\n📊 Summary:")
|
||||||
|
print(f" Rows: {n}")
|
||||||
|
print(f" With odds: {wo} ({wo/n*100:.1f}%)")
|
||||||
|
print(f" Features: {len(ALL_COLS)} ({len(V25_COLS)} V25 + {len(V27_NEW)} new)")
|
||||||
|
print(f" MS: H={md[0]/n*100:.1f}% D={md[1]/n*100:.1f}% A={md[2]/n*100:.1f}%")
|
||||||
|
print(f" Time: {(time.time()-t0)/60:.1f}min")
|
||||||
|
print(f"\n✅ Done! → {OUTPUT}")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
"""
|
||||||
|
Strategy Generator — Senin Excel mantığını DB üzerinde otomatize eder.
|
||||||
|
|
||||||
|
Mantık:
|
||||||
|
1. Ev sahibi takım X, evinde oran bandı Y'de oynadığında → OU1.5/OU2.5/BTTS oranları
|
||||||
|
2. Deplasman takım Z, deplasmanda oran bandı W'de oynadığında → OU1.5/OU2.5/BTTS oranları
|
||||||
|
3. İkisi de yüksekse → STRATEJİ ÜRET
|
||||||
|
|
||||||
|
Çıktı: Her maç için hangi bahis oynanabilir, neden, ve geçmiş başarı oranı
|
||||||
|
"""
|
||||||
|
import psycopg2
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# DB connection
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host="localhost",
|
||||||
|
port=15432,
|
||||||
|
dbname="boilerplate_db",
|
||||||
|
user="suggestbet",
|
||||||
|
password="SuGGesT2026SecuRe"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print(" STRATEGY GENERATOR — Veritabanından Strateji Üretimi")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# 1. Tüm biten maçları, takım adları ve MS oranlarıyla çek
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
m.id as match_id,
|
||||||
|
m.home_team_id,
|
||||||
|
m.away_team_id,
|
||||||
|
m.league_id,
|
||||||
|
m.score_home,
|
||||||
|
m.score_away,
|
||||||
|
m.mst_utc,
|
||||||
|
ht.name as home_team,
|
||||||
|
at.name as away_team,
|
||||||
|
l.name as league_name
|
||||||
|
FROM matches m
|
||||||
|
JOIN teams ht ON m.home_team_id = ht.id
|
||||||
|
JOIN teams at ON m.away_team_id = at.id
|
||||||
|
JOIN leagues l ON m.league_id = l.id
|
||||||
|
WHERE m.status = 'FT'
|
||||||
|
AND m.score_home IS NOT NULL
|
||||||
|
ORDER BY m.mst_utc ASC
|
||||||
|
"""
|
||||||
|
df = pd.read_sql(query, conn)
|
||||||
|
print(f"\nToplam biten maç: {len(df):,}")
|
||||||
|
|
||||||
|
# 2. Tüm oranları çek (MS, OU25, BTTS, OU15)
|
||||||
|
odds_query = """
|
||||||
|
SELECT
|
||||||
|
oc.match_id,
|
||||||
|
oc.name as market,
|
||||||
|
os.name as selection,
|
||||||
|
CAST(os.odd_value AS DECIMAL) as odds
|
||||||
|
FROM odd_categories oc
|
||||||
|
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id
|
||||||
|
WHERE oc.name IN (
|
||||||
|
'Maç Sonucu',
|
||||||
|
'2,5 Alt/Üst',
|
||||||
|
'1,5 Alt/Üst',
|
||||||
|
'3,5 Alt/Üst',
|
||||||
|
'Karşılıklı Gol'
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
odds_df = pd.read_sql(odds_query, conn)
|
||||||
|
print(f"Toplam oran kaydı: {len(odds_df):,}")
|
||||||
|
|
||||||
|
# Pivot: her maç için oranları sütunlara çevir
|
||||||
|
def get_odds(match_id, market, selection):
|
||||||
|
mask = (odds_df.match_id == match_id) & (odds_df.market == market) & (odds_df.selection == selection)
|
||||||
|
vals = odds_df.loc[mask, 'odds']
|
||||||
|
return float(vals.iloc[0]) if len(vals) > 0 else None
|
||||||
|
|
||||||
|
# Daha verimli: oran lookup dict oluştur
|
||||||
|
print("Oran lookup oluşturuluyor...")
|
||||||
|
odds_lookup = {}
|
||||||
|
for _, row in odds_df.iterrows():
|
||||||
|
key = (row.match_id, row.market, row.selection)
|
||||||
|
odds_lookup[key] = float(row.odds)
|
||||||
|
|
||||||
|
def get_o(mid, market, sel):
|
||||||
|
return odds_lookup.get((mid, market, sel))
|
||||||
|
|
||||||
|
# 3. Her maça oranları ekle
|
||||||
|
print("Maçlara oranlar ekleniyor...")
|
||||||
|
df['odds_ms_h'] = df.match_id.map(lambda x: get_o(x, 'Maç Sonucu', '1'))
|
||||||
|
df['odds_ms_a'] = df.match_id.map(lambda x: get_o(x, 'Maç Sonucu', '2'))
|
||||||
|
df['odds_ms_d'] = df.match_id.map(lambda x: get_o(x, 'Maç Sonucu', '0'))
|
||||||
|
df['odds_ou25_o'] = df.match_id.map(lambda x: get_o(x, '2,5 Alt/Üst', 'Üst'))
|
||||||
|
df['odds_ou25_u'] = df.match_id.map(lambda x: get_o(x, '2,5 Alt/Üst', 'Alt'))
|
||||||
|
df['odds_ou15_o'] = df.match_id.map(lambda x: get_o(x, '1,5 Alt/Üst', 'Üst'))
|
||||||
|
df['odds_ou15_u'] = df.match_id.map(lambda x: get_o(x, '1,5 Alt/Üst', 'Alt'))
|
||||||
|
df['odds_ou35_o'] = df.match_id.map(lambda x: get_o(x, '3,5 Alt/Üst', 'Üst'))
|
||||||
|
df['odds_ou35_u'] = df.match_id.map(lambda x: get_o(x, '3,5 Alt/Üst', 'Alt'))
|
||||||
|
df['odds_btts_y'] = df.match_id.map(lambda x: get_o(x, 'Karşılıklı Gol', 'Var'))
|
||||||
|
df['odds_btts_n'] = df.match_id.map(lambda x: get_o(x, 'Karşılıklı Gol', 'Yok'))
|
||||||
|
|
||||||
|
# Sonuç hesapla
|
||||||
|
df['total_goals'] = df.score_home + df.score_away
|
||||||
|
df['ou15'] = (df.total_goals > 1).astype(int)
|
||||||
|
df['ou25'] = (df.total_goals > 2).astype(int)
|
||||||
|
df['ou35'] = (df.total_goals > 3).astype(int)
|
||||||
|
df['btts'] = ((df.score_home > 0) & (df.score_away > 0)).astype(int)
|
||||||
|
|
||||||
|
print(f"Oranı olan maç sayısı: {df.odds_ms_h.notna().sum():,}")
|
||||||
|
|
||||||
|
# 4. ORAN BANDI fonksiyonu
|
||||||
|
def odds_band(odds):
|
||||||
|
if pd.isna(odds): return None
|
||||||
|
if odds < 1.30: return '1.00-1.30'
|
||||||
|
if odds < 1.50: return '1.30-1.50'
|
||||||
|
if odds < 1.80: return '1.50-1.80'
|
||||||
|
if odds < 2.20: return '1.80-2.20'
|
||||||
|
if odds < 2.80: return '2.20-2.80'
|
||||||
|
if odds < 4.00: return '2.80-4.00'
|
||||||
|
if odds < 6.00: return '4.00-6.00'
|
||||||
|
return '6.00+'
|
||||||
|
|
||||||
|
# 5. STRATEJİ: Expanding window — sadece geçmiş veriye bakarak tahmin
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" STRATEJİ BACKTEST — Expanding Window")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# Ev sahibi geçmişi: {team_id: {odds_band: [ou15, ou25, btts, ou35, ...]}}
|
||||||
|
home_history = defaultdict(lambda: defaultdict(list))
|
||||||
|
away_history = defaultdict(lambda: defaultdict(list))
|
||||||
|
|
||||||
|
MIN_MATCHES = 8 # Minimum geçmiş maç sayısı
|
||||||
|
TEST_PCT = 0.30 # Son %30 test
|
||||||
|
N = len(df)
|
||||||
|
test_start = int(N * (1 - TEST_PCT))
|
||||||
|
|
||||||
|
results = {
|
||||||
|
'ou15_over': [], 'ou25_over': [], 'ou35_over': [],
|
||||||
|
'btts_yes': [], 'btts_no': [],
|
||||||
|
'ou25_under': [], 'ou15_under': [],
|
||||||
|
'ms_home': []
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in range(N):
|
||||||
|
row = df.iloc[i]
|
||||||
|
h_odds = row.odds_ms_h
|
||||||
|
a_odds = row.odds_ms_a
|
||||||
|
|
||||||
|
if pd.isna(h_odds) or pd.isna(a_odds):
|
||||||
|
continue
|
||||||
|
|
||||||
|
h_band = odds_band(h_odds)
|
||||||
|
a_band = odds_band(a_odds)
|
||||||
|
|
||||||
|
# TEST: sadece test bölümünde bahis yap
|
||||||
|
if i >= test_start:
|
||||||
|
h_hist = home_history[row.home_team_id][h_band]
|
||||||
|
a_hist = away_history[row.away_team_id][a_band]
|
||||||
|
|
||||||
|
if len(h_hist) >= MIN_MATCHES and len(a_hist) >= MIN_MATCHES:
|
||||||
|
# Ev sahibi bu oran bandında ne yapmış?
|
||||||
|
h_ou15 = np.mean([x[0] for x in h_hist])
|
||||||
|
h_ou25 = np.mean([x[1] for x in h_hist])
|
||||||
|
h_ou35 = np.mean([x[2] for x in h_hist])
|
||||||
|
h_btts = np.mean([x[3] for x in h_hist])
|
||||||
|
h_win = np.mean([x[4] for x in h_hist])
|
||||||
|
|
||||||
|
# Deplasman bu oran bandında ne yapmış?
|
||||||
|
a_ou15 = np.mean([x[0] for x in a_hist])
|
||||||
|
a_ou25 = np.mean([x[1] for x in a_hist])
|
||||||
|
a_ou35 = np.mean([x[2] for x in a_hist])
|
||||||
|
a_btts = np.mean([x[3] for x in a_hist])
|
||||||
|
a_loss = np.mean([x[4] for x in a_hist]) # deplasman kaybetme oranı
|
||||||
|
|
||||||
|
# KOMBİNE SİNYAL
|
||||||
|
sig_ou15 = (h_ou15 + a_ou15) / 2
|
||||||
|
sig_ou25 = (h_ou25 + a_ou25) / 2
|
||||||
|
sig_ou35 = (h_ou35 + a_ou35) / 2
|
||||||
|
sig_btts = (h_btts + a_btts) / 2
|
||||||
|
sig_hw = (h_win + a_loss) / 2 # ev kazanma + deplasman kaybetme
|
||||||
|
|
||||||
|
base = {
|
||||||
|
'match': f"{row.home_team} vs {row.away_team}",
|
||||||
|
'league': row.league_name,
|
||||||
|
'home_team': row.home_team,
|
||||||
|
'away_team': row.away_team,
|
||||||
|
'h_band': h_band,
|
||||||
|
'a_band': a_band,
|
||||||
|
'h_n': len(h_hist),
|
||||||
|
'a_n': len(a_hist),
|
||||||
|
}
|
||||||
|
|
||||||
|
# OU 1.5 OVER
|
||||||
|
if sig_ou15 >= 0.85 and row.odds_ou15_o and row.odds_ou15_o > 1.01:
|
||||||
|
results['ou15_over'].append({
|
||||||
|
**base, 'signal': sig_ou15, 'odds': row.odds_ou15_o,
|
||||||
|
'won': row.ou15 == 1, 'actual_goals': row.total_goals,
|
||||||
|
'h_sig': h_ou15, 'a_sig': a_ou15
|
||||||
|
})
|
||||||
|
|
||||||
|
# OU 2.5 OVER
|
||||||
|
if sig_ou25 >= 0.70 and row.odds_ou25_o and row.odds_ou25_o > 1.10:
|
||||||
|
results['ou25_over'].append({
|
||||||
|
**base, 'signal': sig_ou25, 'odds': row.odds_ou25_o,
|
||||||
|
'won': row.ou25 == 1, 'actual_goals': row.total_goals,
|
||||||
|
'h_sig': h_ou25, 'a_sig': a_ou25
|
||||||
|
})
|
||||||
|
|
||||||
|
# OU 3.5 OVER
|
||||||
|
if sig_ou35 >= 0.60 and row.odds_ou35_o and row.odds_ou35_o > 1.20:
|
||||||
|
results['ou35_over'].append({
|
||||||
|
**base, 'signal': sig_ou35, 'odds': row.odds_ou35_o,
|
||||||
|
'won': row.ou35 == 1, 'actual_goals': row.total_goals,
|
||||||
|
'h_sig': h_ou35, 'a_sig': a_ou35
|
||||||
|
})
|
||||||
|
|
||||||
|
# BTTS YES
|
||||||
|
if sig_btts >= 0.70 and row.odds_btts_y and row.odds_btts_y > 1.10:
|
||||||
|
results['btts_yes'].append({
|
||||||
|
**base, 'signal': sig_btts, 'odds': row.odds_btts_y,
|
||||||
|
'won': row.btts == 1, 'actual_goals': row.total_goals,
|
||||||
|
'h_sig': h_btts, 'a_sig': a_btts
|
||||||
|
})
|
||||||
|
|
||||||
|
# OU 2.5 UNDER (düşük gol beklentisi)
|
||||||
|
if sig_ou25 <= 0.30 and row.odds_ou25_u and row.odds_ou25_u > 1.10:
|
||||||
|
results['ou25_under'].append({
|
||||||
|
**base, 'signal': 1-sig_ou25, 'odds': row.odds_ou25_u,
|
||||||
|
'won': row.ou25 == 0, 'actual_goals': row.total_goals,
|
||||||
|
'h_sig': 1-h_ou25, 'a_sig': 1-a_ou25
|
||||||
|
})
|
||||||
|
|
||||||
|
# MS HOME WIN (ev sahibi kazanma)
|
||||||
|
if sig_hw >= 0.75 and row.odds_ms_h and 1.10 < row.odds_ms_h < 3.50:
|
||||||
|
results['ms_home'].append({
|
||||||
|
**base, 'signal': sig_hw, 'odds': row.odds_ms_h,
|
||||||
|
'won': row.score_home > row.score_away,
|
||||||
|
'actual_goals': row.total_goals,
|
||||||
|
'h_sig': h_win, 'a_sig': a_loss
|
||||||
|
})
|
||||||
|
|
||||||
|
# History güncelle (her zaman)
|
||||||
|
home_history[row.home_team_id][h_band].append((
|
||||||
|
row.ou15, row.ou25, row.ou35, row.btts,
|
||||||
|
int(row.score_home > row.score_away)
|
||||||
|
))
|
||||||
|
away_history[row.away_team_id][a_band].append((
|
||||||
|
row.ou15, row.ou25, row.ou35, row.btts,
|
||||||
|
int(row.score_away < row.score_home) # deplasman kaybetme
|
||||||
|
))
|
||||||
|
|
||||||
|
# 6. SONUÇLARI YAZIDIR
|
||||||
|
print(f"\nTest bölümü: son {TEST_PCT*100:.0f}% ({N - test_start:,} maç)")
|
||||||
|
print(f"Minimum geçmiş: {MIN_MATCHES} maç\n")
|
||||||
|
|
||||||
|
for market_name, bets in results.items():
|
||||||
|
if not bets:
|
||||||
|
print(f"\n {market_name}: sinyal yok")
|
||||||
|
continue
|
||||||
|
|
||||||
|
bdf = pd.DataFrame(bets)
|
||||||
|
total = len(bdf)
|
||||||
|
wins = bdf.won.sum()
|
||||||
|
hit = wins / total * 100
|
||||||
|
pnl = (bdf.won * (bdf.odds - 1) - (~bdf.won) * 1).sum()
|
||||||
|
roi = pnl / total * 100
|
||||||
|
avg_odds = bdf.odds.mean()
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f" {market_name.upper()}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
print(f" Toplam bahis: {total}")
|
||||||
|
print(f" Kazanan: {wins} ({hit:.1f}%)")
|
||||||
|
print(f" Ortalama odds: {avg_odds:.2f}")
|
||||||
|
print(f" PnL: {pnl:+.1f} birim")
|
||||||
|
print(f" ROI: {roi:+.1f}%")
|
||||||
|
|
||||||
|
# Farklı sinyal eşiklerinde performans
|
||||||
|
print(f"\n Sinyal eşik analizi:")
|
||||||
|
for threshold in [0.70, 0.75, 0.80, 0.85, 0.90, 0.95]:
|
||||||
|
sub = bdf[bdf.signal >= threshold]
|
||||||
|
if len(sub) < 5: continue
|
||||||
|
w = sub.won.sum()
|
||||||
|
p = (sub.won * (sub.odds - 1) - (~sub.won) * 1).sum()
|
||||||
|
r = p / len(sub) * 100
|
||||||
|
star = ' ✅ PROFIT' if r > 0 else (' ⚖️ BE' if r > -3 else '')
|
||||||
|
print(f" ≥{threshold:.2f}: {len(sub):5d} bahis, hit={w/len(sub)*100:.1f}%, ROI={r:+.1f}%{star}")
|
||||||
|
|
||||||
|
# En iyi 10 örnek (kazanan)
|
||||||
|
if wins > 0:
|
||||||
|
best = bdf[bdf.won].nlargest(min(5, wins), 'signal')
|
||||||
|
print(f"\n Örnek kazanan bahisler:")
|
||||||
|
for _, b in best.iterrows():
|
||||||
|
print(f" {b.home_team} vs {b.away_team} ({b.league})")
|
||||||
|
print(f" Ev {b.h_band} ({b.h_sig:.0%}) + Dep {b.a_band} ({b.a_sig:.0%}) → sinyal={b.signal:.0%}, odds={b.odds:.2f}, gol={b.actual_goals:.0f}")
|
||||||
|
|
||||||
|
# 7. ÖZET TABLO
|
||||||
|
print("\n\n" + "=" * 70)
|
||||||
|
print(" ÖZET TABLO")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"{'Market':<15} {'Bahis':>6} {'Hit':>7} {'ROI':>8} {'Avg Odds':>9}")
|
||||||
|
print("-" * 50)
|
||||||
|
for market_name, bets in results.items():
|
||||||
|
if not bets: continue
|
||||||
|
bdf = pd.DataFrame(bets)
|
||||||
|
total = len(bdf)
|
||||||
|
wins = bdf.won.sum()
|
||||||
|
hit = wins / total * 100
|
||||||
|
pnl = (bdf.won * (bdf.odds - 1) - (~bdf.won) * 1).sum()
|
||||||
|
roi = pnl / total * 100
|
||||||
|
avg_odds = bdf.odds.mean()
|
||||||
|
print(f"{market_name:<15} {total:>6} {hit:>6.1f}% {roi:>+7.1f}% {avg_odds:>8.2f}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("\n✅ Tamamlandı!")
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
DATA_DIR = AI_ENGINE_DIR / "data" / "v26_shadow"
|
||||||
|
CONFIG_PATH = AI_ENGINE_DIR / "models" / "v26_shadow" / "market_profiles.json"
|
||||||
|
REPORT_PATH = AI_ENGINE_DIR / "reports" / "training_v26_shadow.json"
|
||||||
|
REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _market_accuracy(frame: pd.DataFrame, target_col: str) -> float:
|
||||||
|
if target_col not in frame.columns or frame.empty:
|
||||||
|
return 0.0
|
||||||
|
counts = frame[target_col].value_counts(normalize=True)
|
||||||
|
if counts.empty:
|
||||||
|
return 0.0
|
||||||
|
return round(float(counts.max()), 4)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
train_csv = DATA_DIR / "train.csv"
|
||||||
|
validation_csv = DATA_DIR / "validation.csv"
|
||||||
|
if not train_csv.exists() or not validation_csv.exists():
|
||||||
|
raise SystemExit("Run extract_training_data_v26.py first")
|
||||||
|
|
||||||
|
train_df = pd.read_csv(train_csv)
|
||||||
|
validation_df = pd.read_csv(validation_csv)
|
||||||
|
config = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
||||||
|
report = {
|
||||||
|
"version": config.get("version"),
|
||||||
|
"calibration_version": config.get("calibration_version"),
|
||||||
|
"train_rows": int(len(train_df)),
|
||||||
|
"validation_rows": int(len(validation_df)),
|
||||||
|
"label_priors": {
|
||||||
|
"MS": _market_accuracy(validation_df, "label_ms"),
|
||||||
|
"OU25": _market_accuracy(validation_df, "label_ou25"),
|
||||||
|
"BTTS": _market_accuracy(validation_df, "label_btts"),
|
||||||
|
"HT": _market_accuracy(validation_df, "label_ht_result"),
|
||||||
|
"HTFT": _market_accuracy(validation_df, "label_ht_ft"),
|
||||||
|
"CARDS": _market_accuracy(validation_df, "label_cards_ou45"),
|
||||||
|
},
|
||||||
|
"artifact_path": str(CONFIG_PATH),
|
||||||
|
"notes": [
|
||||||
|
"v26.shadow runtime currently uses artifact-based calibration and ROI gating",
|
||||||
|
"market profile JSON remains the source of truth for runtime thresholds",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
REPORT_PATH.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||||
|
print(f"[OK] Shadow training report written to {REPORT_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,480 @@
|
|||||||
|
"""
|
||||||
|
V27 Value Sniper — PRO Training Script
|
||||||
|
========================================
|
||||||
|
KEY INSIGHT: Train model WITHOUT odds to get independent probability.
|
||||||
|
Then compare with market odds to find genuine value edges.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
Stage A: "Fundamentals Model" — odds-free, learns from ELO/form/rolling/H2H
|
||||||
|
Stage B: "Value Model" — uses fundamentals + odds disagreement as features
|
||||||
|
Stage C: Multi-market — 1X2, O/U 2.5, BTTS
|
||||||
|
Stage D: Walk-forward backtest with Kelly sizing
|
||||||
|
"""
|
||||||
|
import os, sys, json, pickle, time, warnings
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from pathlib import Path
|
||||||
|
from sklearn.metrics import accuracy_score, log_loss
|
||||||
|
from sklearn.isotonic import IsotonicRegression
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
AI_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
DATA_CSV = AI_DIR / "data" / "training_data_v27.csv"
|
||||||
|
MODELS_DIR = AI_DIR / "models" / "v27"
|
||||||
|
MODELS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# ── Leakage & category definitions ──
|
||||||
|
LEAKAGE_COLS = [
|
||||||
|
"total_goals", "goal_diff", "ht_total_goals", "ht_goal_diff",
|
||||||
|
"score_home", "score_away", "ht_score_home", "ht_score_away",
|
||||||
|
"home_goals_form", "away_goals_form",
|
||||||
|
"home_squad_quality", "away_squad_quality", "squad_diff",
|
||||||
|
"home_key_players", "away_key_players",
|
||||||
|
"home_missing_impact", "away_missing_impact",
|
||||||
|
"referee_home_bias", "referee_avg_goals", "referee_cards_total",
|
||||||
|
"referee_avg_yellow", "referee_avg_red", "referee_penalty_rate",
|
||||||
|
"referee_over25_rate", "referee_experience", "referee_matches",
|
||||||
|
]
|
||||||
|
LABEL_COLS = [c for c in [] ] # populated dynamically
|
||||||
|
META_COLS = ["match_id", "league_name", "home_team", "away_team"]
|
||||||
|
ODDS_COLS_PATTERNS = ["odds_", "implied_"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_odds_cols(df):
|
||||||
|
return [c for c in df.columns if any(c.startswith(p) for p in ODDS_COLS_PATTERNS)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_label_cols(df):
|
||||||
|
return [c for c in df.columns if c.startswith("label_")]
|
||||||
|
|
||||||
|
|
||||||
|
def get_clean_features(df):
|
||||||
|
"""Features with NO odds and NO leakage — pure fundamentals."""
|
||||||
|
odds = set(get_odds_cols(df))
|
||||||
|
labels = set(get_label_cols(df))
|
||||||
|
exclude = odds | labels | set(LEAKAGE_COLS) | set(META_COLS)
|
||||||
|
# Also exclude ID columns
|
||||||
|
exclude |= {c for c in df.columns if c.endswith("_id") and c != "match_id"}
|
||||||
|
feats = [c for c in df.columns if c not in exclude]
|
||||||
|
# Keep only numeric
|
||||||
|
feats = [c for c in feats if pd.to_numeric(df[c], errors="coerce").notna().sum() > len(df)*0.3]
|
||||||
|
return feats
|
||||||
|
|
||||||
|
|
||||||
|
def load_data():
|
||||||
|
print(f"Loading {DATA_CSV}...")
|
||||||
|
df = pd.read_csv(DATA_CSV, low_memory=False)
|
||||||
|
print(f" Raw: {len(df)} rows")
|
||||||
|
|
||||||
|
# Ensure odds exist for value comparison
|
||||||
|
for c in ["odds_ms_h","odds_ms_d","odds_ms_a"]:
|
||||||
|
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)]
|
||||||
|
|
||||||
|
# OU25 odds
|
||||||
|
for c in ["odds_ou25_over","odds_ou25_under"]:
|
||||||
|
if c in df.columns:
|
||||||
|
df[c] = pd.to_numeric(df[c], errors="coerce")
|
||||||
|
|
||||||
|
# Implied probabilities
|
||||||
|
margin = 1/df.odds_ms_h + 1/df.odds_ms_d + 1/df.odds_ms_a
|
||||||
|
df["implied_h"] = (1/df.odds_ms_h)/margin
|
||||||
|
df["implied_d"] = (1/df.odds_ms_d)/margin
|
||||||
|
df["implied_a"] = (1/df.odds_ms_a)/margin
|
||||||
|
|
||||||
|
print(f" After filter: {len(df)} rows")
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def temporal_split(df, val_ratio=0.15, test_ratio=0.10):
|
||||||
|
n = len(df)
|
||||||
|
tr = int(n*(1-val_ratio-test_ratio))
|
||||||
|
va = int(n*(1-test_ratio))
|
||||||
|
return df.iloc[:tr].copy(), df.iloc[tr:va].copy(), df.iloc[va:].copy()
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# STAGE A: Fundamentals-Only Model (NO ODDS)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
def train_fundamentals_model(X_tr, y_tr, X_va, y_va, feat_cols, market="ms"):
|
||||||
|
"""Train ensemble WITHOUT odds features."""
|
||||||
|
models = {}
|
||||||
|
n_class = 3 if market == "ms" else 2
|
||||||
|
|
||||||
|
# XGBoost
|
||||||
|
try:
|
||||||
|
import xgboost as xgb
|
||||||
|
print(f" [XGB] Training {market.upper()}...")
|
||||||
|
dtrain = xgb.DMatrix(X_tr, label=y_tr, feature_names=feat_cols)
|
||||||
|
dval = xgb.DMatrix(X_va, label=y_va, feature_names=feat_cols)
|
||||||
|
params = {
|
||||||
|
"objective": "multi:softprob" if n_class==3 else "binary:logistic",
|
||||||
|
"eval_metric": "mlogloss" if n_class==3 else "logloss",
|
||||||
|
"max_depth": 6, "learning_rate": 0.02, "subsample": 0.75,
|
||||||
|
"colsample_bytree": 0.75, "min_child_weight": 10,
|
||||||
|
"reg_alpha": 0.5, "reg_lambda": 2.0,
|
||||||
|
"verbosity": 0, "tree_method": "hist",
|
||||||
|
}
|
||||||
|
if n_class == 3:
|
||||||
|
params["num_class"] = 3
|
||||||
|
m = xgb.train(params, dtrain, num_boost_round=2000,
|
||||||
|
evals=[(dval,"val")], early_stopping_rounds=80,
|
||||||
|
verbose_eval=False)
|
||||||
|
p = m.predict(dval)
|
||||||
|
if n_class == 2:
|
||||||
|
p = np.column_stack([1-p, p])
|
||||||
|
acc = accuracy_score(y_va, p.argmax(1))
|
||||||
|
print(f" acc={acc:.4f}")
|
||||||
|
models["xgb"] = m
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# LightGBM
|
||||||
|
try:
|
||||||
|
import lightgbm as lgb
|
||||||
|
print(f" [LGB] Training {market.upper()}...")
|
||||||
|
ds_tr = lgb.Dataset(X_tr, label=y_tr)
|
||||||
|
ds_va = lgb.Dataset(X_va, label=y_va, reference=ds_tr)
|
||||||
|
par = {
|
||||||
|
"objective": "multiclass" if n_class==3 else "binary",
|
||||||
|
"metric": "multi_logloss" if n_class==3 else "binary_logloss",
|
||||||
|
"num_leaves": 48, "learning_rate": 0.02,
|
||||||
|
"feature_fraction": 0.7, "bagging_fraction": 0.7,
|
||||||
|
"bagging_freq": 1, "min_child_samples": 30,
|
||||||
|
"lambda_l1": 0.5, "lambda_l2": 2.0, "verbose": -1,
|
||||||
|
}
|
||||||
|
if n_class == 3:
|
||||||
|
par["num_class"] = 3
|
||||||
|
m = lgb.train(par, ds_tr, 2000, valid_sets=[ds_va],
|
||||||
|
callbacks=[lgb.early_stopping(80, verbose=False)])
|
||||||
|
p = m.predict(X_va)
|
||||||
|
if n_class == 2:
|
||||||
|
p = np.column_stack([1-p, p])
|
||||||
|
acc = accuracy_score(y_va, p.argmax(1))
|
||||||
|
print(f" acc={acc:.4f}")
|
||||||
|
models["lgb"] = m
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# CatBoost
|
||||||
|
try:
|
||||||
|
from catboost import CatBoostClassifier
|
||||||
|
print(f" [CB] Training {market.upper()}...")
|
||||||
|
m = CatBoostClassifier(
|
||||||
|
iterations=2000, learning_rate=0.02, depth=6,
|
||||||
|
l2_leaf_reg=5, loss_function="MultiClass" if n_class==3 else "Logloss",
|
||||||
|
early_stopping_rounds=80, verbose=0, task_type="CPU",
|
||||||
|
**({"classes_count": 3} if n_class==3 else {}),
|
||||||
|
)
|
||||||
|
m.fit(X_tr, y_tr, eval_set=(X_va, y_va))
|
||||||
|
p = m.predict_proba(X_va)
|
||||||
|
acc = accuracy_score(y_va, p.argmax(1))
|
||||||
|
print(f" acc={acc:.4f}")
|
||||||
|
models["cb"] = m
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return models
|
||||||
|
|
||||||
|
|
||||||
|
def ensemble_predict(models, X, feat_cols, n_class=3):
|
||||||
|
preds = []
|
||||||
|
for name, m in models.items():
|
||||||
|
if name == "xgb":
|
||||||
|
import xgboost as xgb
|
||||||
|
dm = xgb.DMatrix(X, feature_names=feat_cols)
|
||||||
|
p = m.predict(dm)
|
||||||
|
if n_class == 2 and p.ndim == 1:
|
||||||
|
p = np.column_stack([1-p, p])
|
||||||
|
elif name == "lgb":
|
||||||
|
p = m.predict(X)
|
||||||
|
if n_class == 2 and p.ndim == 1:
|
||||||
|
p = np.column_stack([1-p, p])
|
||||||
|
elif name == "cb":
|
||||||
|
p = m.predict_proba(X)
|
||||||
|
preds.append(np.array(p))
|
||||||
|
if not preds:
|
||||||
|
raise RuntimeError("No models!")
|
||||||
|
return np.mean(preds, axis=0)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# STAGE B: Walk-Forward Backtest with Kelly
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
def kelly_fraction(model_prob, odds, fraction=0.25):
|
||||||
|
"""Fractional Kelly: f = fraction * (p*odds - 1) / (odds - 1)"""
|
||||||
|
edge = model_prob * odds - 1
|
||||||
|
if edge <= 0 or odds <= 1:
|
||||||
|
return 0.0
|
||||||
|
f = edge / (odds - 1)
|
||||||
|
return max(0, min(fraction * f, 0.10)) # cap at 10% bankroll
|
||||||
|
|
||||||
|
|
||||||
|
def backtest_value(models, df_test, feat_cols, market="ms",
|
||||||
|
min_edge=0.05, min_odds=1.40, max_odds=4.50,
|
||||||
|
use_kelly=True):
|
||||||
|
"""Realistic backtest: flat or Kelly sizing, edge filtering."""
|
||||||
|
X = df_test[feat_cols].values
|
||||||
|
n_class = 3 if market == "ms" else 2
|
||||||
|
probs = ensemble_predict(models, X, feat_cols, n_class)
|
||||||
|
|
||||||
|
if market == "ms":
|
||||||
|
y = df_test["label_ms"].values
|
||||||
|
odds_arr = df_test[["odds_ms_h","odds_ms_d","odds_ms_a"]].values
|
||||||
|
implied = df_test[["implied_h","implied_d","implied_a"]].values
|
||||||
|
class_names = ["Home","Draw","Away"]
|
||||||
|
elif market == "ou25":
|
||||||
|
if "label_ou25" not in df_test.columns:
|
||||||
|
return {}
|
||||||
|
y = df_test["label_ou25"].values
|
||||||
|
o_over = pd.to_numeric(df_test["odds_ou25_o"], errors="coerce").fillna(1.85).values if "odds_ou25_o" in df_test.columns else np.full(len(df_test), 1.85)
|
||||||
|
o_under = pd.to_numeric(df_test["odds_ou25_u"], errors="coerce").fillna(1.85).values if "odds_ou25_u" in df_test.columns else np.full(len(df_test), 1.85)
|
||||||
|
odds_arr = np.column_stack([o_under, o_over])
|
||||||
|
m = 1/odds_arr
|
||||||
|
implied = m / m.sum(axis=1, keepdims=True)
|
||||||
|
class_names = ["Under","Over"]
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
results = {"bets": [], "total": 0, "wins": 0, "pnl": 0.0, "bankroll_curve": [1000.0]}
|
||||||
|
bankroll = 1000.0
|
||||||
|
|
||||||
|
for i in range(len(y)):
|
||||||
|
for cls in range(n_class):
|
||||||
|
edge = probs[i, cls] - implied[i, cls]
|
||||||
|
odds_val = odds_arr[i, cls]
|
||||||
|
|
||||||
|
# FILTERS
|
||||||
|
if edge < min_edge:
|
||||||
|
continue
|
||||||
|
if odds_val < min_odds or odds_val > max_odds:
|
||||||
|
continue
|
||||||
|
# Don't bet on heavy favorites with tiny edge
|
||||||
|
if implied[i, cls] > 0.65 and edge < 0.08:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sizing
|
||||||
|
if use_kelly:
|
||||||
|
frac = kelly_fraction(probs[i, cls], odds_val, fraction=0.15)
|
||||||
|
stake = bankroll * frac
|
||||||
|
else:
|
||||||
|
stake = 10.0 # flat
|
||||||
|
|
||||||
|
if stake < 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
won = (y[i] == cls)
|
||||||
|
pnl = stake * (odds_val - 1) if won else -stake
|
||||||
|
bankroll += pnl
|
||||||
|
|
||||||
|
results["bets"].append({
|
||||||
|
"edge": float(edge), "odds": float(odds_val),
|
||||||
|
"model_p": float(probs[i,cls]), "implied_p": float(implied[i,cls]),
|
||||||
|
"won": bool(won), "pnl": float(pnl), "stake": float(stake),
|
||||||
|
"class": class_names[cls],
|
||||||
|
})
|
||||||
|
results["bankroll_curve"].append(bankroll)
|
||||||
|
results["total"] += 1
|
||||||
|
if won:
|
||||||
|
results["wins"] += 1
|
||||||
|
results["pnl"] = bankroll - 1000.0
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def print_backtest(results, label=""):
|
||||||
|
total = results.get("total", 0)
|
||||||
|
if total == 0:
|
||||||
|
print(f" {label}: No bets placed")
|
||||||
|
return
|
||||||
|
wins = results["wins"]
|
||||||
|
pnl = results["pnl"]
|
||||||
|
hit = wins/total*100
|
||||||
|
roi = pnl / sum(b["stake"] for b in results["bets"]) * 100
|
||||||
|
curve = results["bankroll_curve"]
|
||||||
|
peak = max(curve)
|
||||||
|
dd = min((c - peak) / peak * 100 for c in curve if c <= peak) if len(curve) > 1 else 0
|
||||||
|
|
||||||
|
# Per-class breakdown
|
||||||
|
by_class = {}
|
||||||
|
for b in results["bets"]:
|
||||||
|
cls = b["class"]
|
||||||
|
if cls not in by_class:
|
||||||
|
by_class[cls] = {"n": 0, "w": 0, "pnl": 0}
|
||||||
|
by_class[cls]["n"] += 1
|
||||||
|
if b["won"]:
|
||||||
|
by_class[cls]["w"] += 1
|
||||||
|
by_class[cls]["pnl"] += b["pnl"]
|
||||||
|
|
||||||
|
print(f"\n {label}")
|
||||||
|
print(f" Bets: {total} | Hit: {hit:.1f}% | ROI: {roi:+.1f}%")
|
||||||
|
print(f" PnL: {pnl:+.0f} | Final: {curve[-1]:.0f} | MaxDD: {dd:.1f}%")
|
||||||
|
for cls, d in sorted(by_class.items()):
|
||||||
|
r = d["pnl"]/d["n"]*100 if d["n"] > 0 else 0
|
||||||
|
print(f" {cls:6s}: {d['n']:4d} bets, "
|
||||||
|
f"hit={d['w']/d['n']*100:.1f}%, avg_pnl={r:+.1f}%")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# MAIN
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
def main():
|
||||||
|
print("=" * 65)
|
||||||
|
print(" V27 VALUE SNIPER — PRO TRAINING (Odds-Free Fundamentals)")
|
||||||
|
print("=" * 65)
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
df = load_data()
|
||||||
|
clean_feats = get_clean_features(df)
|
||||||
|
print(f" Clean features (no odds): {len(clean_feats)}")
|
||||||
|
|
||||||
|
# Numerify
|
||||||
|
for c in clean_feats:
|
||||||
|
df[c] = pd.to_numeric(df[c], errors="coerce")
|
||||||
|
df[clean_feats] = df[clean_feats].fillna(df[clean_feats].median())
|
||||||
|
|
||||||
|
# Remove constant columns
|
||||||
|
clean_feats = [c for c in clean_feats if df[c].nunique() > 1]
|
||||||
|
print(f" After removing constants: {len(clean_feats)}")
|
||||||
|
|
||||||
|
# Split
|
||||||
|
tr, va, te = temporal_split(df)
|
||||||
|
print(f" Train: {len(tr)}, Val: {len(va)}, Test: {len(te)}")
|
||||||
|
print(f" Target: H={tr.label_ms.eq(0).mean():.1%}, "
|
||||||
|
f"D={tr.label_ms.eq(1).mean():.1%}, A={tr.label_ms.eq(2).mean():.1%}")
|
||||||
|
|
||||||
|
X_tr = tr[clean_feats].values
|
||||||
|
y_tr = tr["label_ms"].values
|
||||||
|
X_va = va[clean_feats].values
|
||||||
|
y_va = va["label_ms"].values
|
||||||
|
|
||||||
|
# ── STAGE A: Train fundamentals model (1X2) ──
|
||||||
|
print("\n" + "─"*65)
|
||||||
|
print(" STAGE A: Fundamentals-Only 1X2 Model")
|
||||||
|
print("─"*65)
|
||||||
|
ms_models = train_fundamentals_model(X_tr, y_tr, X_va, y_va, clean_feats, "ms")
|
||||||
|
|
||||||
|
val_probs = ensemble_predict(ms_models, X_va, clean_feats, 3)
|
||||||
|
val_acc = accuracy_score(y_va, val_probs.argmax(1))
|
||||||
|
val_ll = log_loss(y_va, val_probs)
|
||||||
|
print(f"\n Ensemble Val: acc={val_acc:.4f}, logloss={val_ll:.4f}")
|
||||||
|
|
||||||
|
# Compare with odds baseline
|
||||||
|
odds_pred = va[["implied_h","implied_d","implied_a"]].values.argmax(1)
|
||||||
|
odds_acc = accuracy_score(y_va, odds_pred)
|
||||||
|
print(f" Odds baseline: acc={odds_acc:.4f}")
|
||||||
|
print(f" Model vs Odds: {val_acc - odds_acc:+.4f}")
|
||||||
|
|
||||||
|
# ── STAGE B: O/U 2.5 Model ──
|
||||||
|
ou_models = None
|
||||||
|
if "label_ou25" in tr.columns:
|
||||||
|
print("\n" + "─"*65)
|
||||||
|
print(" STAGE A.2: Fundamentals-Only O/U 2.5 Model")
|
||||||
|
print("─"*65)
|
||||||
|
y_tr_ou = tr["label_ou25"].values
|
||||||
|
y_va_ou = va["label_ou25"].values
|
||||||
|
mask_tr = ~np.isnan(y_tr_ou)
|
||||||
|
mask_va = ~np.isnan(y_va_ou)
|
||||||
|
if mask_tr.sum() > 1000:
|
||||||
|
ou_models = train_fundamentals_model(
|
||||||
|
X_tr[mask_tr], y_tr_ou[mask_tr].astype(int),
|
||||||
|
X_va[mask_va], y_va_ou[mask_va].astype(int),
|
||||||
|
clean_feats, "ou25")
|
||||||
|
|
||||||
|
# ── STAGE C: Backtest ──
|
||||||
|
print("\n" + "─"*65)
|
||||||
|
print(" STAGE B: Walk-Forward Backtest (Test Set)")
|
||||||
|
print("─"*65)
|
||||||
|
|
||||||
|
# Try multiple edge thresholds
|
||||||
|
best_roi = -999
|
||||||
|
best_cfg = {}
|
||||||
|
for min_edge in [0.03, 0.05, 0.07, 0.10, 0.12, 0.15]:
|
||||||
|
for min_odds in [1.35, 1.50, 1.70]:
|
||||||
|
r = backtest_value(ms_models, te, clean_feats, "ms",
|
||||||
|
min_edge=min_edge, min_odds=min_odds,
|
||||||
|
max_odds=5.0, use_kelly=True)
|
||||||
|
if r.get("total", 0) >= 20:
|
||||||
|
invested = sum(b["stake"] for b in r["bets"])
|
||||||
|
roi = r["pnl"] / invested * 100 if invested > 0 else -100
|
||||||
|
if roi > best_roi:
|
||||||
|
best_roi = roi
|
||||||
|
best_cfg = {"edge": min_edge, "min_odds": min_odds, "result": r}
|
||||||
|
|
||||||
|
if best_cfg:
|
||||||
|
cfg = best_cfg
|
||||||
|
print(f"\n Best 1X2 Config: edge>{cfg['edge']}, odds>{cfg['min_odds']}")
|
||||||
|
print_backtest(cfg["result"], "1X2 VALUE")
|
||||||
|
|
||||||
|
# Flat bet comparison
|
||||||
|
print("\n --- Flat Bet Comparison ---")
|
||||||
|
for edge in [0.05, 0.07, 0.10]:
|
||||||
|
r = backtest_value(ms_models, te, clean_feats, "ms",
|
||||||
|
min_edge=edge, min_odds=1.50, max_odds=4.5,
|
||||||
|
use_kelly=False)
|
||||||
|
if r.get("total", 0) > 0:
|
||||||
|
inv = r["total"] * 10
|
||||||
|
roi = r["pnl"]/inv*100
|
||||||
|
print(f" Edge>{edge:.2f}: {r['total']} bets, "
|
||||||
|
f"hit={r['wins']/r['total']*100:.1f}%, ROI={roi:+.1f}%")
|
||||||
|
|
||||||
|
# OU25 backtest
|
||||||
|
if ou_models:
|
||||||
|
print("\n --- O/U 2.5 Backtest ---")
|
||||||
|
for edge in [0.05, 0.07, 0.10]:
|
||||||
|
r = backtest_value(ou_models, te, clean_feats, "ou25",
|
||||||
|
min_edge=edge, min_odds=1.50, max_odds=3.0,
|
||||||
|
use_kelly=True)
|
||||||
|
if r.get("total", 0) > 0:
|
||||||
|
print_backtest(r, f"OU25 edge>{edge}")
|
||||||
|
|
||||||
|
# ── Feature importance ──
|
||||||
|
if "lgb" in ms_models:
|
||||||
|
imp = ms_models["lgb"].feature_importance(importance_type="gain")
|
||||||
|
imp_df = pd.DataFrame({"feature": clean_feats, "importance": imp}
|
||||||
|
).sort_values("importance", ascending=False)
|
||||||
|
print("\n TOP 15 FEATURES (no odds!):")
|
||||||
|
for _, r in imp_df.head(15).iterrows():
|
||||||
|
print(f" {r['feature']:40s} {r['importance']:.0f}")
|
||||||
|
imp_df.to_csv(MODELS_DIR / "v27_feature_importance.csv", index=False)
|
||||||
|
|
||||||
|
# ── Save ──
|
||||||
|
print("\n" + "─"*65)
|
||||||
|
print(" SAVING MODELS")
|
||||||
|
print("─"*65)
|
||||||
|
for name, m in ms_models.items():
|
||||||
|
p = MODELS_DIR / f"v27_ms_{name}.pkl"
|
||||||
|
with open(p, "wb") as f:
|
||||||
|
pickle.dump(m, f)
|
||||||
|
print(f" ✓ {p.name}")
|
||||||
|
|
||||||
|
if ou_models:
|
||||||
|
for name, m in ou_models.items():
|
||||||
|
p = MODELS_DIR / f"v27_ou25_{name}.pkl"
|
||||||
|
with open(p, "wb") as f:
|
||||||
|
pickle.dump(m, f)
|
||||||
|
print(f" ✓ {p.name}")
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
"version": "v27-pro", "trained_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"approach": "odds-free fundamentals + value edge detection",
|
||||||
|
"feature_count": len(clean_feats),
|
||||||
|
"total_samples": len(df),
|
||||||
|
"val_acc": round(val_acc, 4), "val_ll": round(val_ll, 4),
|
||||||
|
"best_config": {k: v for k, v in best_cfg.items() if k != "result"} if best_cfg else {},
|
||||||
|
"markets": ["ms"] + (["ou25"] if ou_models else []),
|
||||||
|
}
|
||||||
|
with open(MODELS_DIR / "v27_metadata.json", "w") as f:
|
||||||
|
json.dump(meta, f, indent=2, default=str)
|
||||||
|
with open(MODELS_DIR / "v27_feature_cols.json", "w") as f:
|
||||||
|
json.dump(clean_feats, f, indent=2)
|
||||||
|
print(f" ✓ metadata + feature_cols")
|
||||||
|
|
||||||
|
print(f"\n Total time: {(time.time()-t0)/60:.1f} min")
|
||||||
|
print(" DONE!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -36,6 +36,11 @@ class FeatureEnrichmentService:
|
|||||||
'avg_goals': 2.5,
|
'avg_goals': 2.5,
|
||||||
'btts_rate': 0.5,
|
'btts_rate': 0.5,
|
||||||
'over25_rate': 0.5,
|
'over25_rate': 0.5,
|
||||||
|
# V27 expanded
|
||||||
|
'home_goals_avg': 1.3,
|
||||||
|
'away_goals_avg': 1.1,
|
||||||
|
'recent_trend': 0.0,
|
||||||
|
'venue_advantage': 0.0,
|
||||||
}
|
}
|
||||||
_DEFAULT_FORM = {
|
_DEFAULT_FORM = {
|
||||||
'clean_sheet_rate': 0.2,
|
'clean_sheet_rate': 0.2,
|
||||||
@@ -53,6 +58,25 @@ class FeatureEnrichmentService:
|
|||||||
_DEFAULT_LEAGUE = {
|
_DEFAULT_LEAGUE = {
|
||||||
'avg_goals': 2.7,
|
'avg_goals': 2.7,
|
||||||
'zero_goal_rate': 0.07,
|
'zero_goal_rate': 0.07,
|
||||||
|
# V27 expanded
|
||||||
|
'home_win_rate': 0.46,
|
||||||
|
'draw_rate': 0.26,
|
||||||
|
'btts_rate': 0.50,
|
||||||
|
'ou25_rate': 0.50,
|
||||||
|
'reliability_score': 0.0,
|
||||||
|
}
|
||||||
|
_DEFAULT_ROLLING = {
|
||||||
|
'rolling5_goals': 1.3,
|
||||||
|
'rolling5_conceded': 1.2,
|
||||||
|
'rolling10_goals': 1.3,
|
||||||
|
'rolling10_conceded': 1.2,
|
||||||
|
'rolling20_goals': 1.3,
|
||||||
|
'rolling20_conceded': 1.2,
|
||||||
|
'rolling5_cs': 0.2,
|
||||||
|
}
|
||||||
|
_DEFAULT_VENUE = {
|
||||||
|
'venue_goals': 1.4,
|
||||||
|
'venue_conceded': 1.1,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── 1. Team Stats ──────────────────────────────────────────────
|
# ─── 1. Team Stats ──────────────────────────────────────────────
|
||||||
@@ -186,6 +210,13 @@ class FeatureEnrichmentService:
|
|||||||
total_goals = 0
|
total_goals = 0
|
||||||
btts_count = 0
|
btts_count = 0
|
||||||
over25_count = 0
|
over25_count = 0
|
||||||
|
# V27 expanded trackers
|
||||||
|
home_team_goals_list = []
|
||||||
|
away_team_goals_list = []
|
||||||
|
home_team_venue_wins = 0
|
||||||
|
home_team_venue_total = 0
|
||||||
|
away_team_venue_wins = 0
|
||||||
|
away_team_venue_total = 0
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
sh = int(row['score_home'])
|
sh = int(row['score_home'])
|
||||||
@@ -195,14 +226,22 @@ class FeatureEnrichmentService:
|
|||||||
|
|
||||||
# Normalise: who is "home team" in THIS prediction context
|
# Normalise: who is "home team" in THIS prediction context
|
||||||
if str(row['home_team_id']) == home_team_id:
|
if str(row['home_team_id']) == home_team_id:
|
||||||
|
home_team_goals_list.append(sh)
|
||||||
|
away_team_goals_list.append(sa)
|
||||||
|
home_team_venue_total += 1
|
||||||
if sh > sa:
|
if sh > sa:
|
||||||
home_wins += 1
|
home_wins += 1
|
||||||
|
home_team_venue_wins += 1
|
||||||
elif sh == sa:
|
elif sh == sa:
|
||||||
draws += 1
|
draws += 1
|
||||||
else:
|
else:
|
||||||
# Reversed fixture: away_team was at home
|
# Reversed fixture: away_team was at home
|
||||||
|
home_team_goals_list.append(sa)
|
||||||
|
away_team_goals_list.append(sh)
|
||||||
|
away_team_venue_total += 1
|
||||||
if sa > sh:
|
if sa > sh:
|
||||||
home_wins += 1
|
home_wins += 1
|
||||||
|
away_team_venue_wins += 1
|
||||||
elif sh == sa:
|
elif sh == sa:
|
||||||
draws += 1
|
draws += 1
|
||||||
|
|
||||||
@@ -211,6 +250,29 @@ class FeatureEnrichmentService:
|
|||||||
if match_goals > 2:
|
if match_goals > 2:
|
||||||
over25_count += 1
|
over25_count += 1
|
||||||
|
|
||||||
|
# V27: recent_trend = last-5 home_win_rate - first-5 home_win_rate
|
||||||
|
recent_trend = 0.0
|
||||||
|
if total >= 6:
|
||||||
|
recent_5_wins = sum(
|
||||||
|
1 for r in rows[:5]
|
||||||
|
if (str(r['home_team_id']) == home_team_id and int(r['score_home']) > int(r['score_away']))
|
||||||
|
or (str(r['home_team_id']) != home_team_id and int(r['score_away']) > int(r['score_home']))
|
||||||
|
)
|
||||||
|
older_5_wins = sum(
|
||||||
|
1 for r in rows[-5:]
|
||||||
|
if (str(r['home_team_id']) == home_team_id and int(r['score_home']) > int(r['score_away']))
|
||||||
|
or (str(r['home_team_id']) != home_team_id and int(r['score_away']) > int(r['score_home']))
|
||||||
|
)
|
||||||
|
recent_trend = (recent_5_wins - older_5_wins) / 5.0
|
||||||
|
|
||||||
|
# V27: venue_advantage = home_win_rate_at_home - home_win_rate_away
|
||||||
|
venue_advantage = 0.0
|
||||||
|
if home_team_venue_total > 0 and away_team_venue_total > 0:
|
||||||
|
venue_advantage = (
|
||||||
|
home_team_venue_wins / home_team_venue_total
|
||||||
|
- away_team_venue_wins / away_team_venue_total
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'total_matches': total,
|
'total_matches': total,
|
||||||
'home_win_rate': home_wins / total,
|
'home_win_rate': home_wins / total,
|
||||||
@@ -218,6 +280,11 @@ class FeatureEnrichmentService:
|
|||||||
'avg_goals': total_goals / total,
|
'avg_goals': total_goals / total,
|
||||||
'btts_rate': btts_count / total,
|
'btts_rate': btts_count / total,
|
||||||
'over25_rate': over25_count / total,
|
'over25_rate': over25_count / total,
|
||||||
|
# V27 expanded
|
||||||
|
'home_goals_avg': _safe_avg(home_team_goals_list, 1.3),
|
||||||
|
'away_goals_avg': _safe_avg(away_team_goals_list, 1.1),
|
||||||
|
'recent_trend': round(recent_trend, 4),
|
||||||
|
'venue_advantage': round(venue_advantage, 4),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── 3. Form & Streaks ──────────────────────────────────────────
|
# ─── 3. Form & Streaks ──────────────────────────────────────────
|
||||||
@@ -433,6 +500,10 @@ class FeatureEnrichmentService:
|
|||||||
total = len(rows)
|
total = len(rows)
|
||||||
total_goals = 0
|
total_goals = 0
|
||||||
zero_goal_matches = 0
|
zero_goal_matches = 0
|
||||||
|
home_wins = 0
|
||||||
|
draw_count = 0
|
||||||
|
btts_count = 0
|
||||||
|
over25_count = 0
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
sh = int(row['score_home'])
|
sh = int(row['score_home'])
|
||||||
@@ -441,10 +512,24 @@ class FeatureEnrichmentService:
|
|||||||
total_goals += match_goals
|
total_goals += match_goals
|
||||||
if match_goals == 0:
|
if match_goals == 0:
|
||||||
zero_goal_matches += 1
|
zero_goal_matches += 1
|
||||||
|
if sh > sa:
|
||||||
|
home_wins += 1
|
||||||
|
elif sh == sa:
|
||||||
|
draw_count += 1
|
||||||
|
if sh > 0 and sa > 0:
|
||||||
|
btts_count += 1
|
||||||
|
if match_goals > 2:
|
||||||
|
over25_count += 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'avg_goals': total_goals / total,
|
'avg_goals': total_goals / total,
|
||||||
'zero_goal_rate': zero_goal_matches / total,
|
'zero_goal_rate': zero_goal_matches / total,
|
||||||
|
# V27 expanded
|
||||||
|
'home_win_rate': home_wins / total,
|
||||||
|
'draw_rate': draw_count / total,
|
||||||
|
'btts_rate': btts_count / total,
|
||||||
|
'ou25_rate': over25_count / total,
|
||||||
|
'reliability_score': min(total / 50.0, 1.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─── 6. Momentum ───────────────────────────────────────────────
|
# ─── 6. Momentum ───────────────────────────────────────────────
|
||||||
@@ -514,6 +599,161 @@ class FeatureEnrichmentService:
|
|||||||
return round(weighted_score / max_possible, 4)
|
return round(weighted_score / max_possible, 4)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 7. Rolling Stats (V27) ─────────────────────────────────────
|
||||||
|
|
||||||
|
def compute_rolling_stats(
|
||||||
|
self,
|
||||||
|
cur: RealDictCursor,
|
||||||
|
team_id: str,
|
||||||
|
before_date_ms: int,
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Rolling goal averages and clean-sheet rates over the last 5/10/20 matches.
|
||||||
|
Single DB query, three windows computed programmatically.
|
||||||
|
"""
|
||||||
|
if not team_id:
|
||||||
|
return dict(self._DEFAULT_ROLLING)
|
||||||
|
try:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
m.home_team_id,
|
||||||
|
m.score_home,
|
||||||
|
m.score_away
|
||||||
|
FROM matches m
|
||||||
|
WHERE (m.home_team_id = %s OR m.away_team_id = %s)
|
||||||
|
AND m.status = 'FT'
|
||||||
|
AND m.score_home IS NOT NULL
|
||||||
|
AND m.score_away IS NOT NULL
|
||||||
|
AND m.mst_utc < %s
|
||||||
|
ORDER BY m.mst_utc DESC
|
||||||
|
LIMIT 20
|
||||||
|
""",
|
||||||
|
(team_id, team_id, before_date_ms),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
except Exception:
|
||||||
|
return dict(self._DEFAULT_ROLLING)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return dict(self._DEFAULT_ROLLING)
|
||||||
|
|
||||||
|
goals = []
|
||||||
|
conceded = []
|
||||||
|
clean_sheets = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
is_home = str(row['home_team_id']) == team_id
|
||||||
|
gf = int(row['score_home'] if is_home else row['score_away'])
|
||||||
|
ga = int(row['score_away'] if is_home else row['score_home'])
|
||||||
|
goals.append(gf)
|
||||||
|
conceded.append(ga)
|
||||||
|
clean_sheets.append(1 if ga == 0 else 0)
|
||||||
|
|
||||||
|
n = len(goals)
|
||||||
|
return {
|
||||||
|
'rolling5_goals': _safe_avg(goals[:5], 1.3),
|
||||||
|
'rolling5_conceded': _safe_avg(conceded[:5], 1.2),
|
||||||
|
'rolling10_goals': _safe_avg(goals[:min(10, n)], 1.3),
|
||||||
|
'rolling10_conceded': _safe_avg(conceded[:min(10, n)], 1.2),
|
||||||
|
'rolling20_goals': _safe_avg(goals[:n], 1.3),
|
||||||
|
'rolling20_conceded': _safe_avg(conceded[:n], 1.2),
|
||||||
|
'rolling5_cs': _safe_avg(clean_sheets[:5], 0.2),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 8. Venue Stats (V27) ──────────────────────────────────────
|
||||||
|
|
||||||
|
def compute_venue_stats(
|
||||||
|
self,
|
||||||
|
cur: RealDictCursor,
|
||||||
|
team_id: str,
|
||||||
|
before_date_ms: int,
|
||||||
|
is_home: bool = True,
|
||||||
|
) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Team goals scored/conceded at specific venue (home or away only).
|
||||||
|
"""
|
||||||
|
if not team_id:
|
||||||
|
return dict(self._DEFAULT_VENUE)
|
||||||
|
venue_col = 'home_team_id' if is_home else 'away_team_id'
|
||||||
|
try:
|
||||||
|
cur.execute(
|
||||||
|
f"""
|
||||||
|
SELECT m.score_home, m.score_away
|
||||||
|
FROM matches m
|
||||||
|
WHERE m.{venue_col} = %s
|
||||||
|
AND m.status = 'FT'
|
||||||
|
AND m.score_home IS NOT NULL
|
||||||
|
AND m.score_away IS NOT NULL
|
||||||
|
AND m.mst_utc < %s
|
||||||
|
ORDER BY m.mst_utc DESC
|
||||||
|
LIMIT 20
|
||||||
|
""",
|
||||||
|
(team_id, before_date_ms),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
except Exception:
|
||||||
|
return dict(self._DEFAULT_VENUE)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return dict(self._DEFAULT_VENUE)
|
||||||
|
|
||||||
|
goals = []
|
||||||
|
conceded_list = []
|
||||||
|
for row in rows:
|
||||||
|
sh = int(row['score_home'])
|
||||||
|
sa = int(row['score_away'])
|
||||||
|
if is_home:
|
||||||
|
goals.append(sh)
|
||||||
|
conceded_list.append(sa)
|
||||||
|
else:
|
||||||
|
goals.append(sa)
|
||||||
|
conceded_list.append(sh)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'venue_goals': _safe_avg(goals, 1.4),
|
||||||
|
'venue_conceded': _safe_avg(conceded_list, 1.1),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 9. Days Rest (V27) ────────────────────────────────────────
|
||||||
|
|
||||||
|
def compute_days_rest(
|
||||||
|
self,
|
||||||
|
cur: RealDictCursor,
|
||||||
|
team_id: str,
|
||||||
|
before_date_ms: int,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Returns number of days since the team's last match.
|
||||||
|
Default: 7.0 (one-week rest).
|
||||||
|
"""
|
||||||
|
if not team_id:
|
||||||
|
return 7.0
|
||||||
|
try:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT m.mst_utc
|
||||||
|
FROM matches m
|
||||||
|
WHERE (m.home_team_id = %s OR m.away_team_id = %s)
|
||||||
|
AND m.status = 'FT'
|
||||||
|
AND m.mst_utc < %s
|
||||||
|
ORDER BY m.mst_utc DESC
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(team_id, team_id, before_date_ms),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
except Exception:
|
||||||
|
return 7.0
|
||||||
|
|
||||||
|
if not row or not row.get('mst_utc'):
|
||||||
|
return 7.0
|
||||||
|
|
||||||
|
last_match_ms = int(row['mst_utc'])
|
||||||
|
diff_days = (before_date_ms - last_match_ms) / (1000 * 86400)
|
||||||
|
return round(max(0.0, min(diff_days, 30.0)), 1)
|
||||||
|
|
||||||
|
|
||||||
# ─── Utility ────────────────────────────────────────────────────────
|
# ─── Utility ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _safe_avg(values: list, default: float) -> float:
|
def _safe_avg(values: list, default: float) -> float:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -27,12 +28,15 @@ from psycopg2.extras import RealDictCursor
|
|||||||
from data.db import get_clean_dsn
|
from data.db import get_clean_dsn
|
||||||
from models.v20_ensemble import FullMatchPrediction
|
from models.v20_ensemble import FullMatchPrediction
|
||||||
from models.v25_ensemble import V25Predictor, get_v25_predictor
|
from models.v25_ensemble import V25Predictor, get_v25_predictor
|
||||||
|
from models.v27_predictor import V27Predictor, compute_divergence, compute_value_edge
|
||||||
|
from features.odds_band_analyzer import OddsBandAnalyzer
|
||||||
from models.basketball_v25 import (
|
from models.basketball_v25 import (
|
||||||
BasketballMatchPrediction,
|
BasketballMatchPrediction,
|
||||||
get_basketball_v25_predictor,
|
get_basketball_v25_predictor,
|
||||||
)
|
)
|
||||||
from core.engines.player_predictor import PlayerPrediction, get_player_predictor
|
from core.engines.player_predictor import PlayerPrediction, get_player_predictor
|
||||||
from services.feature_enrichment import FeatureEnrichmentService
|
from services.feature_enrichment import FeatureEnrichmentService
|
||||||
|
from services.v26_shadow_engine import V26ShadowEngine, get_v26_shadow_engine
|
||||||
from utils.top_leagues import load_top_league_ids
|
from utils.top_leagues import load_top_league_ids
|
||||||
from utils.league_reliability import load_league_reliability
|
from utils.league_reliability import load_league_reliability
|
||||||
|
|
||||||
@@ -137,81 +141,116 @@ class SingleMatchOrchestrator:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.v25_predictor: Optional[V25Predictor] = None
|
self.v25_predictor: Optional[V25Predictor] = None
|
||||||
|
self.v26_shadow_engine: Optional[V26ShadowEngine] = None
|
||||||
self.basketball_predictor: Optional[Any] = None
|
self.basketball_predictor: Optional[Any] = None
|
||||||
self.dsn = get_clean_dsn()
|
self.dsn = get_clean_dsn()
|
||||||
|
self.engine_mode = str(os.getenv("AI_ENGINE_MODE", "v25")).strip().lower()
|
||||||
self.top_league_ids = load_top_league_ids()
|
self.top_league_ids = load_top_league_ids()
|
||||||
self.league_reliability = load_league_reliability()
|
self.league_reliability = load_league_reliability()
|
||||||
self.enrichment = FeatureEnrichmentService()
|
self.enrichment = FeatureEnrichmentService()
|
||||||
# Market calibration multipliers — V31 rebalance
|
self.odds_band_analyzer = OddsBandAnalyzer()
|
||||||
# Previous values created mathematical impossibilities:
|
# ── V32 Calibration Rebalance ──────────────────────────────────
|
||||||
# BTTS: max reachable = 100×0.45 = 45, but min_conf was 55 → NEVER playable
|
# RULE: max_reachable = 100 × calibration MUST be > min_conf + 8
|
||||||
# New approach: calibration = blend(backtest_accuracy, 0.80) to avoid crushing raw signal
|
# Previous values had 5 markets where this was IMPOSSIBLE:
|
||||||
|
# HT(0.42×100=42 < 45), HCAP(0.40×100=40 < 46), HTFT(0.28×100=28 < 32)
|
||||||
|
# HT_OU15(0.46×100=46 < 48), CARDS(0.45×100=45 < 48)
|
||||||
|
# These markets could NEVER become playable → all predictions were PASS.
|
||||||
|
#
|
||||||
|
# New calibration: conservative but mathematically achievable.
|
||||||
|
# Each market's calibration ensures high-confidence model outputs CAN pass.
|
||||||
self.market_calibration: Dict[str, float] = {
|
self.market_calibration: Dict[str, float] = {
|
||||||
"MS": 0.48,
|
"MS": 0.62, # max=62 vs min=42 ✓ (was 0.48→max=48 vs 44 ⚠️)
|
||||||
"DC": 0.82,
|
"DC": 0.82, # max=82 vs min=52 ✓ (unchanged, already good)
|
||||||
"OU15": 0.84,
|
"OU15": 0.84, # max=84 vs min=55 ✓ (unchanged, already good)
|
||||||
"OU25": 0.54,
|
"OU25": 0.68, # max=68 vs min=48 ✓ (was 0.54→max=54 vs 52 ⚠️)
|
||||||
"OU35": 0.44,
|
"OU35": 0.60, # max=60 vs min=48 ✓ (was 0.44→max=44 vs 54 ❌)
|
||||||
"BTTS": 0.50,
|
"BTTS": 0.65, # max=65 vs min=46 ✓ (was 0.50→max=50 vs 50 ⚠️)
|
||||||
"HT": 0.42,
|
"HT": 0.58, # max=58 vs min=40 ✓ (was 0.42→max=42 vs 45 ❌)
|
||||||
"HT_OU05": 0.68,
|
"HT_OU05": 0.68, # max=68 vs min=50 ✓ (unchanged)
|
||||||
"HT_OU15": 0.46,
|
"HT_OU15": 0.60, # max=60 vs min=42 ✓ (was 0.46→max=46 vs 48 ❌)
|
||||||
"OE": 0.58,
|
"OE": 0.62, # max=62 vs min=46 ✓ (was 0.58→max=58 vs 50 ok)
|
||||||
"CARDS": 0.45,
|
"CARDS": 0.58, # max=58 vs min=42 ✓ (was 0.45→max=45 vs 48 ❌)
|
||||||
"HCAP": 0.40,
|
"HCAP": 0.56, # max=56 vs min=40 ✓ (was 0.40→max=40 vs 46 ❌)
|
||||||
"HTFT": 0.28,
|
"HTFT": 0.45, # max=45 vs min=28 ✓ (was 0.28→max=28 vs 32 ❌)
|
||||||
}
|
}
|
||||||
|
# Min confidence: lowered to be achievable (max_reachable - 16 to -20)
|
||||||
self.market_min_conf: Dict[str, float] = {
|
self.market_min_conf: Dict[str, float] = {
|
||||||
"MS": 44.0,
|
"MS": 42.0, # was 44 — 3-way market, hard to get high conf
|
||||||
"DC": 55.0,
|
"DC": 52.0, # was 55 — double chance is easier
|
||||||
"OU15": 58.0,
|
"OU15": 55.0, # was 58 — binary + usually high conf
|
||||||
"OU25": 52.0,
|
"OU25": 48.0, # was 52 — core market, allow more through
|
||||||
"OU35": 54.0,
|
"OU35": 48.0, # was 54 — lowered to let signals pass
|
||||||
"BTTS": 50.0,
|
"BTTS": 46.0, # was 50 — binary market
|
||||||
"HT": 45.0,
|
"HT": 40.0, # was 45 — was ❌ impossible, now achievable
|
||||||
"HT_OU05": 54.0,
|
"HT_OU05": 50.0, # was 54 — binary HT market
|
||||||
"HT_OU15": 48.0,
|
"HT_OU15": 42.0, # was 48 — was ❌ impossible, now achievable
|
||||||
"OE": 50.0,
|
"OE": 46.0, # was 50 — coin-flip market, lower bar
|
||||||
"CARDS": 48.0,
|
"CARDS": 42.0, # was 48 — was ❌ impossible, now achievable
|
||||||
"HCAP": 46.0,
|
"HCAP": 40.0, # was 46 — was ❌ impossible, now achievable
|
||||||
"HTFT": 32.0,
|
"HTFT": 28.0, # was 32 — was ❌ impossible, 9-way market
|
||||||
}
|
}
|
||||||
|
# Min play score: moderate reduction to allow more C-grade bets
|
||||||
self.market_min_play_score: Dict[str, float] = {
|
self.market_min_play_score: Dict[str, float] = {
|
||||||
"MS": 72.0,
|
"MS": 65.0, # was 72 — let more MS through for tracking
|
||||||
"DC": 62.0,
|
"DC": 58.0, # was 62 — DC is high accuracy
|
||||||
"OU15": 64.0,
|
"OU15": 60.0, # was 64 — strong market per backtest
|
||||||
"OU25": 70.0,
|
"OU25": 64.0, # was 70 — core market
|
||||||
"OU35": 76.0,
|
"OU35": 68.0, # was 76 — riskier market
|
||||||
"BTTS": 70.0,
|
"BTTS": 64.0, # was 70 — allow more signals
|
||||||
"HT": 74.0,
|
"HT": 66.0, # was 74 — was never reachable anyway
|
||||||
"HT_OU05": 64.0,
|
"HT_OU05": 60.0, # was 64 — strong backtest market
|
||||||
"HT_OU15": 72.0,
|
"HT_OU15": 64.0, # was 72 — moderate
|
||||||
"OE": 66.0,
|
"OE": 60.0, # was 66 — low priority market
|
||||||
"CARDS": 74.0,
|
"CARDS": 66.0, # was 74 — niche market
|
||||||
"HCAP": 76.0,
|
"HCAP": 68.0, # was 76 — risky
|
||||||
"HTFT": 82.0,
|
"HTFT": 72.0, # was 82 — 9-way, very risky
|
||||||
}
|
}
|
||||||
self.market_min_edge: Dict[str, float] = {
|
self.market_min_edge: Dict[str, float] = {
|
||||||
"MS": 0.03,
|
"MS": 0.02, # was 0.03 — slight relaxation
|
||||||
"DC": 0.01,
|
"DC": 0.01, # unchanged
|
||||||
"OU15": 0.01,
|
"OU15": 0.01, # unchanged
|
||||||
"OU25": 0.02,
|
"OU25": 0.02, # unchanged
|
||||||
"OU35": 0.04,
|
"OU35": 0.03, # was 0.04
|
||||||
"BTTS": 0.03,
|
"BTTS": 0.02, # was 0.03
|
||||||
"HT": 0.04,
|
"HT": 0.03, # was 0.04
|
||||||
"HT_OU05": 0.01,
|
"HT_OU05": 0.01, # unchanged
|
||||||
"HT_OU15": 0.03,
|
"HT_OU15": 0.02, # was 0.03
|
||||||
"OE": 0.02,
|
"OE": 0.02, # unchanged
|
||||||
"CARDS": 0.03,
|
"CARDS": 0.02, # was 0.03
|
||||||
"HCAP": 0.04,
|
"HCAP": 0.03, # was 0.04
|
||||||
"HTFT": 0.06,
|
"HTFT": 0.05, # was 0.06
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_v25_predictor(self) -> V25Predictor:
|
def _get_v25_predictor(self) -> V25Predictor:
|
||||||
if self.v25_predictor is None:
|
if self.v25_predictor is None:
|
||||||
self.v25_predictor = get_v25_predictor()
|
try:
|
||||||
|
self.v25_predictor = get_v25_predictor()
|
||||||
|
print(f"[V25] ✅ Predictor loaded: {len(self.v25_predictor.models)} market models")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[V25] ❌ PREDICTOR LOAD FAILED: {e}")
|
||||||
|
raise
|
||||||
return self.v25_predictor
|
return self.v25_predictor
|
||||||
|
|
||||||
|
def _get_v26_shadow_engine(self) -> V26ShadowEngine:
|
||||||
|
if getattr(self, "v26_shadow_engine", None) is None:
|
||||||
|
self.v26_shadow_engine = get_v26_shadow_engine()
|
||||||
|
return self.v26_shadow_engine
|
||||||
|
|
||||||
|
def _get_v27_predictor(self) -> Optional[V27Predictor]:
|
||||||
|
"""Non-fatal V27 loader — returns None if models can't load."""
|
||||||
|
if getattr(self, "_v27", None) is not None:
|
||||||
|
return self._v27
|
||||||
|
try:
|
||||||
|
pred = V27Predictor()
|
||||||
|
if pred.load_models():
|
||||||
|
self._v27 = pred
|
||||||
|
print(f"[V27] ✅ Predictor loaded: {sum(len(v) for v in pred.models.values())} models")
|
||||||
|
return self._v27
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[V27] ⚠ Load failed (non-fatal): {e}")
|
||||||
|
self._v27 = None
|
||||||
|
return None
|
||||||
|
|
||||||
def _build_v25_features(self, data: MatchData) -> Dict[str, float]:
|
def _build_v25_features(self, data: MatchData) -> Dict[str, float]:
|
||||||
"""
|
"""
|
||||||
Build the single authoritative V25 pre-match feature vector.
|
Build the single authoritative V25 pre-match feature vector.
|
||||||
@@ -272,6 +311,23 @@ class SingleMatchOrchestrator:
|
|||||||
league = enr.compute_league_averages(cur, data.league_id, data.match_date_ms)
|
league = enr.compute_league_averages(cur, data.league_id, data.match_date_ms)
|
||||||
home_momentum = enr.compute_momentum(cur, data.home_team_id, data.match_date_ms)
|
home_momentum = enr.compute_momentum(cur, data.home_team_id, data.match_date_ms)
|
||||||
away_momentum = enr.compute_momentum(cur, data.away_team_id, data.match_date_ms)
|
away_momentum = enr.compute_momentum(cur, data.away_team_id, data.match_date_ms)
|
||||||
|
# V27 enrichment
|
||||||
|
home_rolling = enr.compute_rolling_stats(cur, data.home_team_id, data.match_date_ms)
|
||||||
|
away_rolling = enr.compute_rolling_stats(cur, data.away_team_id, data.match_date_ms)
|
||||||
|
home_venue = enr.compute_venue_stats(cur, data.home_team_id, data.match_date_ms, is_home=True)
|
||||||
|
away_venue = enr.compute_venue_stats(cur, data.away_team_id, data.match_date_ms, is_home=False)
|
||||||
|
home_rest = enr.compute_days_rest(cur, data.home_team_id, data.match_date_ms)
|
||||||
|
away_rest = enr.compute_days_rest(cur, data.away_team_id, data.match_date_ms)
|
||||||
|
# V28 Odds-Band Historical Performance
|
||||||
|
odds_band_features = self.odds_band_analyzer.compute_all(
|
||||||
|
cur=cur,
|
||||||
|
home_team_id=data.home_team_id,
|
||||||
|
away_team_id=data.away_team_id,
|
||||||
|
league_id=data.league_id,
|
||||||
|
odds=odds,
|
||||||
|
before_ts=data.match_date_ms,
|
||||||
|
referee_name=data.referee_name,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Full fallback — use all defaults
|
# Full fallback — use all defaults
|
||||||
home_stats = dict(enr._DEFAULT_TEAM_STATS)
|
home_stats = dict(enr._DEFAULT_TEAM_STATS)
|
||||||
@@ -283,6 +339,14 @@ class SingleMatchOrchestrator:
|
|||||||
league = dict(enr._DEFAULT_LEAGUE)
|
league = dict(enr._DEFAULT_LEAGUE)
|
||||||
home_momentum = 0.0
|
home_momentum = 0.0
|
||||||
away_momentum = 0.0
|
away_momentum = 0.0
|
||||||
|
# V27 fallbacks
|
||||||
|
home_rolling = dict(enr._DEFAULT_ROLLING)
|
||||||
|
away_rolling = dict(enr._DEFAULT_ROLLING)
|
||||||
|
home_venue = dict(enr._DEFAULT_VENUE)
|
||||||
|
away_venue = dict(enr._DEFAULT_VENUE)
|
||||||
|
home_rest = 7.0
|
||||||
|
away_rest = 7.0
|
||||||
|
odds_band_features = {} # V28 fallback
|
||||||
|
|
||||||
odds_presence = {
|
odds_presence = {
|
||||||
'odds_ms_h_present': 1.0 if ms_h > 1.01 else 0.0,
|
'odds_ms_h_present': 1.0 if ms_h > 1.01 else 0.0,
|
||||||
@@ -307,16 +371,38 @@ class SingleMatchOrchestrator:
|
|||||||
'odds_btts_n_present': 1.0 if float(odds.get('btts_n', 0)) > 1.01 else 0.0,
|
'odds_btts_n_present': 1.0 if float(odds.get('btts_n', 0)) > 1.01 else 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Calendar features (V27) ──
|
||||||
|
import datetime
|
||||||
|
match_dt = datetime.datetime.utcfromtimestamp(data.match_date_ms / 1000)
|
||||||
|
match_month = match_dt.month
|
||||||
|
is_season_start = 1.0 if match_month in (7, 8, 9) else 0.0
|
||||||
|
is_season_end = 1.0 if match_month in (5, 6) else 0.0
|
||||||
|
|
||||||
|
# ── Derived / Interaction features (V27) ──
|
||||||
|
elo_diff = home_elo - away_elo
|
||||||
|
form_elo_diff = home_form_elo_val - away_form_elo_val
|
||||||
|
attack_vs_defense_home = data.home_goals_avg - data.away_conceded_avg
|
||||||
|
attack_vs_defense_away = data.away_goals_avg - data.home_conceded_avg
|
||||||
|
xga_home = data.home_conceded_avg
|
||||||
|
xga_away = data.away_conceded_avg
|
||||||
|
xg_diff = xga_home - xga_away
|
||||||
|
mom_diff = home_momentum - away_momentum
|
||||||
|
form_momentum_interaction = mom_diff * form_elo_diff / 1000.0
|
||||||
|
elo_form_consistency = 1.0 - abs(elo_diff - form_elo_diff) / max(abs(elo_diff), 100.0)
|
||||||
|
upset_x_elo_gap = upset_potential * abs(elo_diff) / 500.0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
# META (1)
|
||||||
|
'mst_utc': float(data.match_date_ms),
|
||||||
# ELO (8)
|
# ELO (8)
|
||||||
'home_overall_elo': home_elo,
|
'home_overall_elo': home_elo,
|
||||||
'away_overall_elo': away_elo,
|
'away_overall_elo': away_elo,
|
||||||
'elo_diff': home_elo - away_elo,
|
'elo_diff': elo_diff,
|
||||||
'home_home_elo': home_venue_elo,
|
'home_home_elo': home_venue_elo,
|
||||||
'away_away_elo': away_venue_elo,
|
'away_away_elo': away_venue_elo,
|
||||||
'home_form_elo': home_form_elo_val,
|
'home_form_elo': home_form_elo_val,
|
||||||
'away_form_elo': away_form_elo_val,
|
'away_form_elo': away_form_elo_val,
|
||||||
'form_elo_diff': home_form_elo_val - away_form_elo_val,
|
'form_elo_diff': form_elo_diff,
|
||||||
# Form (12)
|
# Form (12)
|
||||||
'home_goals_avg': data.home_goals_avg,
|
'home_goals_avg': data.home_goals_avg,
|
||||||
'home_conceded_avg': data.home_conceded_avg,
|
'home_conceded_avg': data.home_conceded_avg,
|
||||||
@@ -330,13 +416,17 @@ class SingleMatchOrchestrator:
|
|||||||
'away_winning_streak': away_form['winning_streak'],
|
'away_winning_streak': away_form['winning_streak'],
|
||||||
'home_unbeaten_streak': home_form['unbeaten_streak'],
|
'home_unbeaten_streak': home_form['unbeaten_streak'],
|
||||||
'away_unbeaten_streak': away_form['unbeaten_streak'],
|
'away_unbeaten_streak': away_form['unbeaten_streak'],
|
||||||
# H2H (6)
|
# H2H (10 — original 6 + V27 expanded 4)
|
||||||
'h2h_total_matches': h2h['total_matches'],
|
'h2h_total_matches': h2h['total_matches'],
|
||||||
'h2h_home_win_rate': h2h['home_win_rate'],
|
'h2h_home_win_rate': h2h['home_win_rate'],
|
||||||
'h2h_draw_rate': h2h['draw_rate'],
|
'h2h_draw_rate': h2h['draw_rate'],
|
||||||
'h2h_avg_goals': h2h['avg_goals'],
|
'h2h_avg_goals': h2h['avg_goals'],
|
||||||
'h2h_btts_rate': h2h['btts_rate'],
|
'h2h_btts_rate': h2h['btts_rate'],
|
||||||
'h2h_over25_rate': h2h['over25_rate'],
|
'h2h_over25_rate': h2h['over25_rate'],
|
||||||
|
'h2h_home_goals_avg': h2h['home_goals_avg'],
|
||||||
|
'h2h_away_goals_avg': h2h['away_goals_avg'],
|
||||||
|
'h2h_recent_trend': h2h['recent_trend'],
|
||||||
|
'h2h_venue_advantage': h2h['venue_advantage'],
|
||||||
# Stats (8)
|
# Stats (8)
|
||||||
'home_avg_possession': home_stats['avg_possession'],
|
'home_avg_possession': home_stats['avg_possession'],
|
||||||
'away_avg_possession': away_stats['avg_possession'],
|
'away_avg_possession': away_stats['avg_possession'],
|
||||||
@@ -371,11 +461,16 @@ class SingleMatchOrchestrator:
|
|||||||
'odds_btts_y': float(odds.get('btts_y', 0)),
|
'odds_btts_y': float(odds.get('btts_y', 0)),
|
||||||
'odds_btts_n': float(odds.get('btts_n', 0)),
|
'odds_btts_n': float(odds.get('btts_n', 0)),
|
||||||
**odds_presence,
|
**odds_presence,
|
||||||
# League (4)
|
# League (9 — original 2 + V27 expanded 5 + xga 2)
|
||||||
'home_xga': data.home_conceded_avg,
|
'home_xga': xga_home,
|
||||||
'away_xga': data.away_conceded_avg,
|
'away_xga': xga_away,
|
||||||
'league_avg_goals': league['avg_goals'],
|
'league_avg_goals': league['avg_goals'],
|
||||||
'league_zero_goal_rate': league['zero_goal_rate'],
|
'league_zero_goal_rate': league['zero_goal_rate'],
|
||||||
|
'league_home_win_rate': league['home_win_rate'],
|
||||||
|
'league_draw_rate': league['draw_rate'],
|
||||||
|
'league_btts_rate': league['btts_rate'],
|
||||||
|
'league_ou25_rate': league['ou25_rate'],
|
||||||
|
'league_reliability_score': league['reliability_score'],
|
||||||
# Upset (4)
|
# Upset (4)
|
||||||
'upset_atmosphere': 0.0,
|
'upset_atmosphere': 0.0,
|
||||||
'upset_motivation': 0.0,
|
'upset_motivation': 0.0,
|
||||||
@@ -390,9 +485,45 @@ class SingleMatchOrchestrator:
|
|||||||
# Momentum (3)
|
# Momentum (3)
|
||||||
'home_momentum_score': home_momentum,
|
'home_momentum_score': home_momentum,
|
||||||
'away_momentum_score': away_momentum,
|
'away_momentum_score': away_momentum,
|
||||||
'momentum_diff': home_momentum - away_momentum,
|
'momentum_diff': mom_diff,
|
||||||
|
# ── V27 Rolling Stats (13) ──
|
||||||
|
'home_rolling5_goals': home_rolling['rolling5_goals'],
|
||||||
|
'home_rolling5_conceded': home_rolling['rolling5_conceded'],
|
||||||
|
'home_rolling10_goals': home_rolling['rolling10_goals'],
|
||||||
|
'home_rolling10_conceded': home_rolling['rolling10_conceded'],
|
||||||
|
'home_rolling20_goals': home_rolling['rolling20_goals'],
|
||||||
|
'home_rolling20_conceded': home_rolling['rolling20_conceded'],
|
||||||
|
'away_rolling5_goals': away_rolling['rolling5_goals'],
|
||||||
|
'away_rolling5_conceded': away_rolling['rolling5_conceded'],
|
||||||
|
'away_rolling10_goals': away_rolling['rolling10_goals'],
|
||||||
|
'away_rolling10_conceded': away_rolling['rolling10_conceded'],
|
||||||
|
'home_rolling5_cs': home_rolling['rolling5_cs'],
|
||||||
|
'away_rolling5_cs': away_rolling['rolling5_cs'],
|
||||||
|
# ── V27 Venue Stats (4) ──
|
||||||
|
'home_venue_goals': home_venue['venue_goals'],
|
||||||
|
'home_venue_conceded': home_venue['venue_conceded'],
|
||||||
|
'away_venue_goals': away_venue['venue_goals'],
|
||||||
|
'away_venue_conceded': away_venue['venue_conceded'],
|
||||||
|
# ── V27 Goal Trend (2) ──
|
||||||
|
'home_goal_trend': home_rolling['rolling5_goals'] - home_rolling['rolling10_goals'],
|
||||||
|
'away_goal_trend': away_rolling['rolling5_goals'] - away_rolling['rolling10_goals'],
|
||||||
|
# ── V27 Calendar (4) ──
|
||||||
|
'home_days_rest': home_rest,
|
||||||
|
'away_days_rest': away_rest,
|
||||||
|
'match_month': float(match_month),
|
||||||
|
'is_season_start': is_season_start,
|
||||||
|
'is_season_end': is_season_end,
|
||||||
|
# ── V27 Interaction (6) ──
|
||||||
|
'attack_vs_defense_home': attack_vs_defense_home,
|
||||||
|
'attack_vs_defense_away': attack_vs_defense_away,
|
||||||
|
'xg_diff': xg_diff,
|
||||||
|
'form_momentum_interaction': form_momentum_interaction,
|
||||||
|
'elo_form_consistency': elo_form_consistency,
|
||||||
|
'upset_x_elo_gap': upset_x_elo_gap,
|
||||||
# Squad Features (9) — PlayerPredictorEngine
|
# Squad Features (9) — PlayerPredictorEngine
|
||||||
**self._get_squad_features(data),
|
**self._get_squad_features(data),
|
||||||
|
# V28 Odds-Band Historical Performance Features
|
||||||
|
**odds_band_features,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_squad_features(self, data: MatchData) -> Dict[str, float]:
|
def _get_squad_features(self, data: MatchData) -> Dict[str, float]:
|
||||||
@@ -657,6 +788,17 @@ class SingleMatchOrchestrator:
|
|||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# ── Pre-Match Simulation Mode ────────────────────────────
|
||||||
|
# For finished (FT/postGame) matches, strip live scores so the
|
||||||
|
# entire pipeline treats them as if they haven't kicked off yet.
|
||||||
|
# _is_live_match already returns False for FT, but this adds
|
||||||
|
# defense-in-depth against any code path that reads scores directly.
|
||||||
|
_status_upper = str(data.status or "").upper()
|
||||||
|
_state_upper = str(data.state or "").upper()
|
||||||
|
if _status_upper in {"FT", "FINISHED"} or _state_upper in {"POSTGAME", "POST_GAME"}:
|
||||||
|
data.current_score_home = None
|
||||||
|
data.current_score_away = None
|
||||||
|
|
||||||
sport_key = str(data.sport or "football").lower()
|
sport_key = str(data.sport or "football").lower()
|
||||||
if sport_key == "basketball":
|
if sport_key == "basketball":
|
||||||
prediction = self._get_basketball_predictor().predict(
|
prediction = self._get_basketball_predictor().predict(
|
||||||
@@ -676,7 +818,372 @@ class SingleMatchOrchestrator:
|
|||||||
features = self._build_v25_features(data)
|
features = self._build_v25_features(data)
|
||||||
v25_signal = self._get_v25_signal(data, features)
|
v25_signal = self._get_v25_signal(data, features)
|
||||||
prediction = self._build_v25_prediction(data, features, v25_signal)
|
prediction = self._build_v25_prediction(data, features, v25_signal)
|
||||||
return self._build_prediction_package(data, prediction, v25_signal)
|
base_package = self._build_prediction_package(data, prediction, v25_signal)
|
||||||
|
|
||||||
|
# ── V27 Dual-Engine Divergence ──────────────────────────────
|
||||||
|
v27_predictor = self._get_v27_predictor()
|
||||||
|
if v27_predictor is not None:
|
||||||
|
try:
|
||||||
|
v27_preds = v27_predictor.predict_all(features)
|
||||||
|
|
||||||
|
# MS divergence
|
||||||
|
v27_ms = v27_preds.get("ms")
|
||||||
|
if v27_ms:
|
||||||
|
v25_ms_probs = {
|
||||||
|
"home": prediction.ms_home_prob,
|
||||||
|
"draw": prediction.ms_draw_prob,
|
||||||
|
"away": prediction.ms_away_prob,
|
||||||
|
}
|
||||||
|
ms_divergence = compute_divergence(v25_ms_probs, v27_ms)
|
||||||
|
ms_odds = {
|
||||||
|
"home": float((data.odds_data or {}).get("ms_h", 0)),
|
||||||
|
"draw": float((data.odds_data or {}).get("ms_d", 0)),
|
||||||
|
"away": float((data.odds_data or {}).get("ms_a", 0)),
|
||||||
|
}
|
||||||
|
ms_value = compute_value_edge(v25_ms_probs, v27_ms, ms_odds)
|
||||||
|
else:
|
||||||
|
ms_divergence = {}
|
||||||
|
ms_value = {}
|
||||||
|
|
||||||
|
# OU25 divergence
|
||||||
|
v27_ou25 = v27_preds.get("ou25")
|
||||||
|
if v27_ou25:
|
||||||
|
v25_ou25_probs = {
|
||||||
|
"under": prediction.under_25_prob,
|
||||||
|
"over": prediction.over_25_prob,
|
||||||
|
}
|
||||||
|
ou25_divergence = compute_divergence(v25_ou25_probs, v27_ou25)
|
||||||
|
ou25_odds = {
|
||||||
|
"under": float((data.odds_data or {}).get("ou25_u", 0)),
|
||||||
|
"over": float((data.odds_data or {}).get("ou25_o", 0)),
|
||||||
|
}
|
||||||
|
ou25_value = compute_value_edge(v25_ou25_probs, v27_ou25, ou25_odds)
|
||||||
|
else:
|
||||||
|
ou25_divergence = {}
|
||||||
|
ou25_value = {}
|
||||||
|
|
||||||
|
# ── V28 Odds-Band Historical Performance ─────────────
|
||||||
|
odds_band_ms_home = {
|
||||||
|
"win_rate": features.get("home_band_ms_win_rate", 0.33),
|
||||||
|
"draw_rate": features.get("home_band_ms_draw_rate", 0.33),
|
||||||
|
"loss_rate": features.get("home_band_ms_loss_rate", 0.34),
|
||||||
|
"sample": features.get("home_band_ms_sample", 0),
|
||||||
|
"avg_goals_scored": features.get("home_band_ms_avg_goals_scored", 1.3),
|
||||||
|
"avg_goals_conceded": features.get("home_band_ms_avg_goals_conceded", 1.1),
|
||||||
|
}
|
||||||
|
odds_band_ms_away = {
|
||||||
|
"win_rate": features.get("away_band_ms_win_rate", 0.33),
|
||||||
|
"draw_rate": features.get("away_band_ms_draw_rate", 0.33),
|
||||||
|
"loss_rate": features.get("away_band_ms_loss_rate", 0.34),
|
||||||
|
"sample": features.get("away_band_ms_sample", 0),
|
||||||
|
"avg_goals_scored": features.get("away_band_ms_avg_goals_scored", 1.3),
|
||||||
|
"avg_goals_conceded": features.get("away_band_ms_avg_goals_conceded", 1.1),
|
||||||
|
}
|
||||||
|
odds_band_ou25 = {
|
||||||
|
"over_rate": features.get("band_ou25_over_rate", 0.50),
|
||||||
|
"under_rate": features.get("band_ou25_under_rate", 0.50),
|
||||||
|
"avg_total_goals": features.get("band_ou25_avg_total_goals", 2.5),
|
||||||
|
"sample": features.get("band_ou25_sample", 0),
|
||||||
|
}
|
||||||
|
odds_band_ou15 = {
|
||||||
|
"over_rate": features.get("band_ou15_over_rate", 0.65),
|
||||||
|
"under_rate": features.get("band_ou15_under_rate", 0.35),
|
||||||
|
"avg_total_goals": features.get("band_ou15_avg_total_goals", 2.5),
|
||||||
|
"sample": features.get("band_ou15_sample", 0),
|
||||||
|
}
|
||||||
|
odds_band_ou35 = {
|
||||||
|
"over_rate": features.get("band_ou35_over_rate", 0.35),
|
||||||
|
"under_rate": features.get("band_ou35_under_rate", 0.65),
|
||||||
|
"avg_total_goals": features.get("band_ou35_avg_total_goals", 2.5),
|
||||||
|
"sample": features.get("band_ou35_sample", 0),
|
||||||
|
}
|
||||||
|
odds_band_btts = {
|
||||||
|
"yes_rate": features.get("band_btts_yes_rate", 0.50),
|
||||||
|
"no_rate": features.get("band_btts_no_rate", 0.50),
|
||||||
|
"sample": features.get("band_btts_sample", 0),
|
||||||
|
}
|
||||||
|
odds_band_dc = {
|
||||||
|
"1x_rate": features.get("band_dc_1x_rate", 0.60),
|
||||||
|
"x2_rate": features.get("band_dc_x2_rate", 0.60),
|
||||||
|
"12_rate": features.get("band_dc_12_rate", 0.67),
|
||||||
|
"1x_sample": features.get("band_dc_1x_sample", 0),
|
||||||
|
"x2_sample": features.get("band_dc_x2_sample", 0),
|
||||||
|
"12_sample": features.get("band_dc_12_sample", 0),
|
||||||
|
}
|
||||||
|
odds_band_ht_home = {
|
||||||
|
"win_rate": features.get("home_band_ht_win_rate", 0.33),
|
||||||
|
"draw_rate": features.get("home_band_ht_draw_rate", 0.40),
|
||||||
|
"loss_rate": features.get("home_band_ht_loss_rate", 0.27),
|
||||||
|
"sample": features.get("home_band_ht_sample", 0),
|
||||||
|
}
|
||||||
|
odds_band_ht_away = {
|
||||||
|
"win_rate": features.get("away_band_ht_win_rate", 0.33),
|
||||||
|
"draw_rate": features.get("away_band_ht_draw_rate", 0.40),
|
||||||
|
"loss_rate": features.get("away_band_ht_loss_rate", 0.27),
|
||||||
|
"sample": features.get("away_band_ht_sample", 0),
|
||||||
|
}
|
||||||
|
odds_band_ht_ou05 = {
|
||||||
|
"over_rate": features.get("band_ht_ou05_over_rate", 0.50),
|
||||||
|
"under_rate": features.get("band_ht_ou05_under_rate", 0.50),
|
||||||
|
"sample": features.get("band_ht_ou05_sample", 0),
|
||||||
|
}
|
||||||
|
odds_band_ht_ou15 = {
|
||||||
|
"over_rate": features.get("band_ht_ou15_over_rate", 0.35),
|
||||||
|
"under_rate": features.get("band_ht_ou15_under_rate", 0.65),
|
||||||
|
"sample": features.get("band_ht_ou15_sample", 0),
|
||||||
|
}
|
||||||
|
odds_band_oe = {
|
||||||
|
"odd_rate": features.get("band_oe_odd_rate", 0.50),
|
||||||
|
"even_rate": features.get("band_oe_even_rate", 0.50),
|
||||||
|
"sample": features.get("band_oe_sample", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cards (Kart) band — hakem + takım profili
|
||||||
|
odds_band_cards = {
|
||||||
|
"referee_avg": features.get("band_cards_referee_avg", 0.0),
|
||||||
|
"referee_over_rate": features.get("band_cards_referee_over_rate", 0.50),
|
||||||
|
"referee_sample": features.get("band_cards_referee_sample", 0),
|
||||||
|
"team_avg": features.get("band_cards_team_avg", 0.0),
|
||||||
|
"team_over_rate": features.get("band_cards_team_over_rate", 0.50),
|
||||||
|
"team_sample": features.get("band_cards_team_sample", 0),
|
||||||
|
"combined_over_rate": features.get("band_cards_combined_over_rate", 0.50),
|
||||||
|
"sample": features.get("band_cards_sample", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTFT (İY/MS) 9 combination rates
|
||||||
|
odds_band_htft = {}
|
||||||
|
for combo in ("11", "1x", "12", "x1", "xx", "x2", "21", "2x", "22"):
|
||||||
|
odds_band_htft[combo] = {
|
||||||
|
"rate": features.get(f"band_htft_{combo}_rate", 0.11),
|
||||||
|
"sample": features.get(f"band_htft_{combo}_sample", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Triple Value Detection ────────────────────────────
|
||||||
|
ms_odds = {
|
||||||
|
"home": float((data.odds_data or {}).get("ms_h", 0)),
|
||||||
|
"draw": float((data.odds_data or {}).get("ms_d", 0)),
|
||||||
|
"away": float((data.odds_data or {}).get("ms_a", 0)),
|
||||||
|
}
|
||||||
|
triple_value = {}
|
||||||
|
for outcome_key, band_key, odds_key in [
|
||||||
|
("home", "home", "home"),
|
||||||
|
("away", "away", "away"),
|
||||||
|
]:
|
||||||
|
v27_prob = (v27_ms or {}).get(outcome_key, 0)
|
||||||
|
band_rate = (odds_band_ms_home if band_key == "home"
|
||||||
|
else odds_band_ms_away)["win_rate"]
|
||||||
|
mkt_odds = ms_odds.get(odds_key, 0)
|
||||||
|
implied_prob = (1.0 / mkt_odds) if mkt_odds > 1.0 else 0.33
|
||||||
|
|
||||||
|
combined_prob = (v27_prob + band_rate) / 2.0 if v27_prob > 0 else band_rate
|
||||||
|
edge = combined_prob - implied_prob
|
||||||
|
band_sample = (odds_band_ms_home if band_key == "home"
|
||||||
|
else odds_band_ms_away)["sample"]
|
||||||
|
|
||||||
|
v27_confirms = v27_prob > implied_prob
|
||||||
|
band_confirms = band_rate > implied_prob
|
||||||
|
confirmation_count = sum([v27_confirms, band_confirms])
|
||||||
|
|
||||||
|
triple_value[outcome_key] = {
|
||||||
|
"v27_prob": round(v27_prob, 4),
|
||||||
|
"band_rate": round(band_rate, 4),
|
||||||
|
"implied_prob": round(implied_prob, 4),
|
||||||
|
"combined_prob": round(combined_prob, 4),
|
||||||
|
"edge": round(edge, 4),
|
||||||
|
"band_sample": band_sample,
|
||||||
|
"confirmations": confirmation_count,
|
||||||
|
"is_value": (
|
||||||
|
confirmation_count >= 2
|
||||||
|
and edge > 0.05
|
||||||
|
and band_sample >= 8
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# OU25 triple value
|
||||||
|
ou25_over_odds = float((data.odds_data or {}).get("ou25_o", 0))
|
||||||
|
v27_ou25_over = (v27_ou25 or {}).get("over", 0) if v27_ou25 else 0
|
||||||
|
ou25_band_rate = odds_band_ou25["over_rate"]
|
||||||
|
ou25_implied = (1.0 / ou25_over_odds) if ou25_over_odds > 1.0 else 0.50
|
||||||
|
ou25_combined = (v27_ou25_over + ou25_band_rate) / 2.0 if v27_ou25_over > 0 else ou25_band_rate
|
||||||
|
ou25_edge = ou25_combined - ou25_implied
|
||||||
|
ou25_v27_confirms = v27_ou25_over > ou25_implied
|
||||||
|
ou25_band_confirms = ou25_band_rate > ou25_implied
|
||||||
|
ou25_conf_count = sum([ou25_v27_confirms, ou25_band_confirms])
|
||||||
|
|
||||||
|
triple_value["ou25_over"] = {
|
||||||
|
"v27_prob": round(v27_ou25_over, 4),
|
||||||
|
"band_rate": round(ou25_band_rate, 4),
|
||||||
|
"implied_prob": round(ou25_implied, 4),
|
||||||
|
"combined_prob": round(ou25_combined, 4),
|
||||||
|
"edge": round(ou25_edge, 4),
|
||||||
|
"band_sample": odds_band_ou25["sample"],
|
||||||
|
"confirmations": ou25_conf_count,
|
||||||
|
"is_value": (
|
||||||
|
ou25_conf_count >= 2
|
||||||
|
and ou25_edge > 0.05
|
||||||
|
and odds_band_ou25["sample"] >= 8
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# BTTS triple value
|
||||||
|
btts_yes_odds = float((data.odds_data or {}).get("btts_y", 0))
|
||||||
|
btts_implied = (1.0 / btts_yes_odds) if btts_yes_odds > 1.0 else 0.50
|
||||||
|
btts_band_rate = odds_band_btts["yes_rate"]
|
||||||
|
btts_combined = btts_band_rate
|
||||||
|
btts_edge = btts_combined - btts_implied
|
||||||
|
btts_band_confirms = btts_band_rate > btts_implied
|
||||||
|
|
||||||
|
triple_value["btts_yes"] = {
|
||||||
|
"band_rate": round(btts_band_rate, 4),
|
||||||
|
"implied_prob": round(btts_implied, 4),
|
||||||
|
"combined_prob": round(btts_combined, 4),
|
||||||
|
"edge": round(btts_edge, 4),
|
||||||
|
"band_sample": odds_band_btts["sample"],
|
||||||
|
"confirmations": 1 if btts_band_confirms else 0,
|
||||||
|
"is_value": (
|
||||||
|
btts_band_confirms
|
||||||
|
and btts_edge > 0.05
|
||||||
|
and odds_band_btts["sample"] >= 8
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Band-only value for new markets ───────────────────
|
||||||
|
def _band_value(label, band_rate, odds_key, sample):
|
||||||
|
o = float((data.odds_data or {}).get(odds_key, 0))
|
||||||
|
imp = (1.0 / o) if o > 1.0 else 0.50
|
||||||
|
e = band_rate - imp
|
||||||
|
conf = band_rate > imp
|
||||||
|
return {
|
||||||
|
"band_rate": round(band_rate, 4),
|
||||||
|
"implied_prob": round(imp, 4),
|
||||||
|
"edge": round(e, 4),
|
||||||
|
"band_sample": sample,
|
||||||
|
"is_value": conf and e > 0.05 and sample >= 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
triple_value["ou15_over"] = _band_value(
|
||||||
|
"ou15", odds_band_ou15["over_rate"], "ou15_o", odds_band_ou15["sample"])
|
||||||
|
triple_value["ou35_over"] = _band_value(
|
||||||
|
"ou35", odds_band_ou35["over_rate"], "ou35_o", odds_band_ou35["sample"])
|
||||||
|
triple_value["dc_1x"] = _band_value(
|
||||||
|
"dc1x", odds_band_dc["1x_rate"], "dc_1x", odds_band_dc["1x_sample"])
|
||||||
|
triple_value["dc_x2"] = _band_value(
|
||||||
|
"dcx2", odds_band_dc["x2_rate"], "dc_x2", odds_band_dc["x2_sample"])
|
||||||
|
triple_value["dc_12"] = _band_value(
|
||||||
|
"dc12", odds_band_dc["12_rate"], "dc_12", odds_band_dc["12_sample"])
|
||||||
|
triple_value["ht_home"] = _band_value(
|
||||||
|
"ht_h", odds_band_ht_home["win_rate"], "ht_h", odds_band_ht_home["sample"])
|
||||||
|
triple_value["ht_away"] = _band_value(
|
||||||
|
"ht_a", odds_band_ht_away["win_rate"], "ht_a", odds_band_ht_away["sample"])
|
||||||
|
triple_value["ht_ou05_over"] = _band_value(
|
||||||
|
"htou05", odds_band_ht_ou05["over_rate"], "ht_ou05_o", odds_band_ht_ou05["sample"])
|
||||||
|
triple_value["ht_ou15_over"] = _band_value(
|
||||||
|
"htou15", odds_band_ht_ou15["over_rate"], "ht_ou15_o", odds_band_ht_ou15["sample"])
|
||||||
|
triple_value["oe_odd"] = _band_value(
|
||||||
|
"oe", odds_band_oe["odd_rate"], "oe_odd", odds_band_oe["sample"])
|
||||||
|
|
||||||
|
# Cards triple value — composite (hakem + takım)
|
||||||
|
triple_value["cards_over"] = _band_value(
|
||||||
|
"cards", odds_band_cards["combined_over_rate"], "cards_o",
|
||||||
|
odds_band_cards["sample"])
|
||||||
|
|
||||||
|
# HTFT triple value — 9 combinations
|
||||||
|
for combo in ("11", "1x", "12", "x1", "xx", "x2", "21", "2x", "22"):
|
||||||
|
htft_combo_data = odds_band_htft.get(combo, {})
|
||||||
|
triple_value[f"htft_{combo}"] = _band_value(
|
||||||
|
f"htft_{combo}", htft_combo_data.get("rate", 0.11),
|
||||||
|
f"htft_{combo}", htft_combo_data.get("sample", 0))
|
||||||
|
|
||||||
|
# Attach to package
|
||||||
|
base_package["v27_engine"] = {
|
||||||
|
"version": "v28-pro-max",
|
||||||
|
"approach": "odds-free fundamentals + full odds-band analytics + cards + htft",
|
||||||
|
"predictions": {
|
||||||
|
"ms": v27_ms or {},
|
||||||
|
"ou25": v27_ou25 or {},
|
||||||
|
},
|
||||||
|
"divergence": {
|
||||||
|
"ms": ms_divergence,
|
||||||
|
"ou25": ou25_divergence,
|
||||||
|
},
|
||||||
|
"value_edge": {
|
||||||
|
"ms": ms_value,
|
||||||
|
"ou25": ou25_value,
|
||||||
|
},
|
||||||
|
"odds_band": {
|
||||||
|
"ms_home": odds_band_ms_home,
|
||||||
|
"ms_away": odds_band_ms_away,
|
||||||
|
"ou25": odds_band_ou25,
|
||||||
|
"ou15": odds_band_ou15,
|
||||||
|
"ou35": odds_band_ou35,
|
||||||
|
"btts": odds_band_btts,
|
||||||
|
"dc": odds_band_dc,
|
||||||
|
"ht_home": odds_band_ht_home,
|
||||||
|
"ht_away": odds_band_ht_away,
|
||||||
|
"ht_ou05": odds_band_ht_ou05,
|
||||||
|
"ht_ou15": odds_band_ht_ou15,
|
||||||
|
"oe": odds_band_oe,
|
||||||
|
"cards": odds_band_cards,
|
||||||
|
"htft": odds_band_htft,
|
||||||
|
},
|
||||||
|
"triple_value": triple_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Boost confidence when V27 agrees with V25
|
||||||
|
if v27_ms:
|
||||||
|
v27_best = max(v27_ms, key=v27_ms.get)
|
||||||
|
v25_best_map = {"1": "home", "X": "draw", "2": "away"}
|
||||||
|
v25_best_mapped = v25_best_map.get(prediction.ms_pick, "")
|
||||||
|
if v27_best == v25_best_mapped:
|
||||||
|
# Engines agree → boost confidence by up to 5%
|
||||||
|
boost = min(5.0, abs(ms_divergence.get(v27_best, 0)) * 50)
|
||||||
|
# Additional boost if odds-band also confirms
|
||||||
|
band_val = triple_value.get(v25_best_mapped, {})
|
||||||
|
if band_val.get("is_value"):
|
||||||
|
boost = min(8.0, boost + 3.0) # Triple confirmation extra boost
|
||||||
|
prediction.ms_confidence = min(95.0, prediction.ms_confidence + boost)
|
||||||
|
base_package["prediction"]["ms_confidence"] = prediction.ms_confidence
|
||||||
|
base_package["v27_engine"]["consensus"] = "AGREE"
|
||||||
|
else:
|
||||||
|
base_package["v27_engine"]["consensus"] = "DISAGREE"
|
||||||
|
|
||||||
|
# Update analysis details
|
||||||
|
base_package.setdefault("analysis_details", {})
|
||||||
|
base_package["analysis_details"]["dual_engine"] = True
|
||||||
|
base_package["analysis_details"]["v27_loaded"] = True
|
||||||
|
base_package["analysis_details"]["odds_band_loaded"] = True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[V27] ⚠ Prediction failed (non-fatal): {e}")
|
||||||
|
base_package.setdefault("analysis_details", {})
|
||||||
|
base_package["analysis_details"]["v27_loaded"] = False
|
||||||
|
|
||||||
|
mode = str(getattr(self, "engine_mode", "v25") or "v25").lower()
|
||||||
|
if mode not in {"v25", "v26", "dual"}:
|
||||||
|
mode = "v25"
|
||||||
|
|
||||||
|
quality = base_package.get("data_quality", self._compute_data_quality(data))
|
||||||
|
shadow_package = self._get_v26_shadow_engine().build_package(
|
||||||
|
data=data,
|
||||||
|
prediction=prediction,
|
||||||
|
v25_signal=v25_signal,
|
||||||
|
quality=quality,
|
||||||
|
)
|
||||||
|
|
||||||
|
if mode == "v26":
|
||||||
|
return shadow_package
|
||||||
|
if mode == "dual":
|
||||||
|
merged = dict(base_package)
|
||||||
|
merged.update(
|
||||||
|
{
|
||||||
|
"shadow_engine": shadow_package,
|
||||||
|
"shadow_engine_version": shadow_package.get("model_version"),
|
||||||
|
"calibration_version": shadow_package.get("calibration_version"),
|
||||||
|
"decision_trace_id": shadow_package.get("decision_trace_id"),
|
||||||
|
"market_reliability": shadow_package.get("market_reliability", {}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return merged
|
||||||
|
return base_package
|
||||||
|
|
||||||
def analyze_match_htms(self, match_id: str) -> Optional[Dict[str, Any]]:
|
def analyze_match_htms(self, match_id: str) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -1379,7 +1886,7 @@ class SingleMatchOrchestrator:
|
|||||||
][:safe_count]
|
][:safe_count]
|
||||||
preview = watch_items_all[: min(5, len(watch_items_all))]
|
preview = watch_items_all[: min(5, len(watch_items_all))]
|
||||||
return {
|
return {
|
||||||
"engine": "v25.main",
|
"engine": "v28.main",
|
||||||
"generated_at": __import__("datetime").datetime.utcnow().isoformat() + "Z",
|
"generated_at": __import__("datetime").datetime.utcnow().isoformat() + "Z",
|
||||||
"horizon_hours": safe_horizon,
|
"horizon_hours": safe_horizon,
|
||||||
"min_score": round(safe_min_score, 2),
|
"min_score": round(safe_min_score, 2),
|
||||||
@@ -2426,17 +2933,17 @@ class SingleMatchOrchestrator:
|
|||||||
|
|
||||||
playable_rows = [row for row in market_rows if row.get("playable")]
|
playable_rows = [row for row in market_rows if row.get("playable")]
|
||||||
|
|
||||||
# GUARANTEED PICK LOGIC (Optimized based on backtest results):
|
# GUARANTEED PICK LOGIC (V32 - Calibration-aware):
|
||||||
# Runtime replay insights:
|
# Runtime replay insights:
|
||||||
# - Trust only markets that remain robust after pre-match replay.
|
# - Trust only markets that remain robust after pre-match replay.
|
||||||
# - Current strongest football markets: DC, OU15, HT_OU05.
|
# - Current strongest football markets: DC, OU15, HT_OU05.
|
||||||
#
|
#
|
||||||
# Priority 1: High-accuracy market (DC/OU15/HT_OU05/OU25) + Odds >= 1.30 + Conf >= 40%
|
# Priority 1: High-accuracy market (DC/OU15/HT_OU05/OU25) + Odds >= 1.30 + Conf >= 44%
|
||||||
# Priority 2: Any playable + Odds >= 1.30 + Conf >= 40%
|
# Priority 2: Any playable + Odds >= 1.30 + Conf >= 44%
|
||||||
# Priority 3: Playable + Odds >= 1.30
|
# Priority 3: Playable + Odds >= 1.30
|
||||||
# Priority 4: Best non-playable (fallback)
|
# Priority 4: Best non-playable (fallback)
|
||||||
MIN_ODDS = 1.30
|
MIN_ODDS = 1.30
|
||||||
MIN_CONFIDENCE = 52.0
|
MIN_CONFIDENCE = 44.0 # V32: lowered from 52 to match new calibration
|
||||||
|
|
||||||
# High-accuracy markets from backtest (prioritize these)
|
# High-accuracy markets from backtest (prioritize these)
|
||||||
HIGH_ACCURACY_MARKETS = {"DC", "OU15", "HT_OU05"}
|
HIGH_ACCURACY_MARKETS = {"DC", "OU15", "HT_OU05"}
|
||||||
@@ -2444,7 +2951,7 @@ class SingleMatchOrchestrator:
|
|||||||
# Priority 1: High-accuracy markets with good odds and confidence
|
# Priority 1: High-accuracy markets with good odds and confidence
|
||||||
high_accuracy_picks = [
|
high_accuracy_picks = [
|
||||||
row for row in playable_rows
|
row for row in playable_rows
|
||||||
if row.get("market_type") in HIGH_ACCURACY_MARKETS
|
if row.get("market") in HIGH_ACCURACY_MARKETS
|
||||||
and float(row.get("odds", 0.0)) >= MIN_ODDS
|
and float(row.get("odds", 0.0)) >= MIN_ODDS
|
||||||
and float(row.get("calibrated_confidence", 0.0)) >= MIN_CONFIDENCE
|
and float(row.get("calibrated_confidence", 0.0)) >= MIN_CONFIDENCE
|
||||||
]
|
]
|
||||||
@@ -2649,8 +3156,14 @@ class SingleMatchOrchestrator:
|
|||||||
if market in available_markets
|
if market in available_markets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Determine simulation mode for the response
|
||||||
|
_resp_status = str(data.status or "").upper()
|
||||||
|
_resp_state = str(data.state or "").upper()
|
||||||
|
is_simulation = _resp_status in {"FT", "FINISHED"} or _resp_state in {"POSTGAME", "POST_GAME"}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"model_version": "v25.main",
|
"model_version": "v28-pro-max",
|
||||||
|
"simulation_mode": "pre_match" if is_simulation else None,
|
||||||
"match_info": {
|
"match_info": {
|
||||||
"match_id": data.match_id,
|
"match_id": data.match_id,
|
||||||
"match_name": f"{data.home_team_name} vs {data.away_team_name}",
|
"match_name": f"{data.home_team_name} vs {data.away_team_name}",
|
||||||
@@ -2881,7 +3394,7 @@ class SingleMatchOrchestrator:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"model_version": str(prediction.get("engine_version") or "v25.main.basketball"),
|
"model_version": str(prediction.get("engine_version") or "v28.main.basketball"),
|
||||||
"match_info": {
|
"match_info": {
|
||||||
"match_id": data.match_id,
|
"match_id": data.match_id,
|
||||||
"match_name": f"{data.home_team_name} vs {data.away_team_name}",
|
"match_name": f"{data.home_team_name} vs {data.away_team_name}",
|
||||||
@@ -3779,11 +4292,15 @@ class SingleMatchOrchestrator:
|
|||||||
playable = False
|
playable = False
|
||||||
reasons.append("high_risk_low_data_quality")
|
reasons.append("high_risk_low_data_quality")
|
||||||
if lineup_missing and lineup_sensitive:
|
if lineup_missing and lineup_sensitive:
|
||||||
playable = False
|
# V32: Don't hard-block, apply heavy penalty instead
|
||||||
|
# This allows high-confidence predictions to still surface
|
||||||
|
lineup_penalty += 8.0
|
||||||
reasons.append("lineup_insufficient_for_market")
|
reasons.append("lineup_insufficient_for_market")
|
||||||
if data.lineup_source == "probable_xi" and lineup_sensitive:
|
if data.lineup_source == "probable_xi" and lineup_sensitive:
|
||||||
playable = False
|
# V32: Penalty instead of hard block
|
||||||
reasons.append("lineup_not_confirmed")
|
# Most pre-match predictions use probable_xi — blocking kills all output
|
||||||
|
lineup_penalty += 6.0
|
||||||
|
reasons.append("lineup_probable_xi_penalty")
|
||||||
# V31: negative edge threshold adapts to league reliability
|
# V31: negative edge threshold adapts to league reliability
|
||||||
# Reliable league: stricter (-0.03), unreliable: looser (-0.08)
|
# Reliable league: stricter (-0.03), unreliable: looser (-0.08)
|
||||||
neg_edge_threshold = -0.03 - (1.0 - odds_rel) * 0.05
|
neg_edge_threshold = -0.03 - (1.0 - odds_rel) * 0.05
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
import os, psycopg2
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
load_dotenv('/Users/piton/Documents/Suggest-Bet-BE/.env')
|
|
||||||
conn = psycopg2.connect(os.getenv('DATABASE_URL').split('?')[0])
|
|
||||||
cur = conn.cursor()
|
|
||||||
cur.execute('SELECT mpe.match_id, SUM(CASE WHEN event_type::text LIKE \'%yellow_card%\' THEN 1 WHEN event_type::text LIKE \'%red_card%\' THEN 2 ELSE 1 END) as cards FROM match_player_events mpe WHERE event_type::text LIKE \'%card%\' GROUP BY mpe.match_id LIMIT 5')
|
|
||||||
print(cur.fetchall())
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
"""Quick test: V20+Quant integration — EV Edge, Kelly staking, edge-based grading."""
|
|
||||||
import json
|
|
||||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
|
||||||
|
|
||||||
MATCH_IDS = [
|
|
||||||
"er7n8hqndkhvdsg6an72r7h90", # Def. Justicia vs Atl Lanus
|
|
||||||
"etpay8k4qr3gts3jjidfebaxg", # CA Tigre vs Gymnasia
|
|
||||||
]
|
|
||||||
|
|
||||||
o = SingleMatchOrchestrator()
|
|
||||||
|
|
||||||
for mid in MATCH_IDS:
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"MATCH: {mid}")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
r = o.analyze_match(mid)
|
|
||||||
if not r:
|
|
||||||
print(" Match not found")
|
|
||||||
continue
|
|
||||||
|
|
||||||
info = r.get("match_info", {})
|
|
||||||
print(f" {info.get('match_name', '?')} | {info.get('league', '?')}")
|
|
||||||
|
|
||||||
mp = r.get("main_pick", {})
|
|
||||||
print(f"\n MAIN PICK: {mp.get('market')} {mp.get('pick')}")
|
|
||||||
print(f" probability: {mp.get('probability', 0):.4f}")
|
|
||||||
print(f" odds: {mp.get('odds', 0):.2f}")
|
|
||||||
print(f" ev_edge: {mp.get('ev_edge', mp.get('edge', 0)):+.4f}")
|
|
||||||
print(f" implied_prob: {mp.get('implied_prob', 0):.4f}")
|
|
||||||
print(f" bet_grade: {mp.get('bet_grade', 'N/A')}")
|
|
||||||
print(f" stake_units: {mp.get('stake_units', 0)}")
|
|
||||||
print(f" playable: {mp.get('playable', False)}")
|
|
||||||
print(f" reasons: {mp.get('decision_reasons', [])}")
|
|
||||||
|
|
||||||
print(f"\n ALL MARKETS (with EV Edge + Kelly):")
|
|
||||||
for b in r.get("bet_summary", []):
|
|
||||||
ev = b.get("ev_edge", 0)
|
|
||||||
imp = b.get("implied_prob", 0)
|
|
||||||
flag = ">>>" if b.get("playable") else " "
|
|
||||||
mkt = b["market"]
|
|
||||||
pick = b["pick"]
|
|
||||||
odds = b.get("odds", 0)
|
|
||||||
grade = b["bet_grade"]
|
|
||||||
stake = b["stake_units"]
|
|
||||||
conf = b.get("calibrated_confidence", 0)
|
|
||||||
print(
|
|
||||||
f" {flag} {mkt:8s} {pick:12s} "
|
|
||||||
f"ev_edge={ev:+.3f} "
|
|
||||||
f"odds={odds:.2f} "
|
|
||||||
f"stake={stake:.1f} "
|
|
||||||
f"grade={grade:4s} "
|
|
||||||
f"conf={conf:.1f}% "
|
|
||||||
f"implied={imp:.3f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
print()
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from decimal import Decimal
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
|
||||||
|
|
||||||
from core.engines.odds_predictor import OddsPredictorEngine
|
|
||||||
from features.sidelined_analyzer import SidelinedAnalyzer
|
|
||||||
|
|
||||||
|
|
||||||
class EngineNullSafetyTests(unittest.TestCase):
|
|
||||||
def test_odds_predictor_accepts_decimal_inputs_without_crashing(self):
|
|
||||||
engine = OddsPredictorEngine()
|
|
||||||
|
|
||||||
prediction = engine.predict(
|
|
||||||
odds_data={
|
|
||||||
"ms_h": Decimal("2.10"),
|
|
||||||
"ms_d": Decimal("3.25"),
|
|
||||||
"ms_a": Decimal("3.60"),
|
|
||||||
"ou25_o": Decimal("1.90"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertGreater(prediction.market_home_prob, 0.0)
|
|
||||||
self.assertGreater(prediction.market_draw_prob, 0.0)
|
|
||||||
self.assertGreater(prediction.market_away_prob, 0.0)
|
|
||||||
|
|
||||||
def test_sidelined_analyzer_handles_non_numeric_fields(self):
|
|
||||||
analyzer = SidelinedAnalyzer.__new__(SidelinedAnalyzer)
|
|
||||||
analyzer.position_weights = {"K": 0.35, "D": 0.20, "O": 0.25, "F": 0.30}
|
|
||||||
analyzer.max_rating = 10
|
|
||||||
analyzer.adaptation_threshold = 10
|
|
||||||
analyzer.adaptation_discount = 0.5
|
|
||||||
analyzer.goalkeeper_penalty = 0.15
|
|
||||||
analyzer.confidence_boost = 10
|
|
||||||
analyzer.max_impact = 0.85
|
|
||||||
analyzer.key_player_threshold = 3
|
|
||||||
analyzer.recent_matches_lookback = 15
|
|
||||||
analyzer._fetch_player_stats = MagicMock(return_value={})
|
|
||||||
|
|
||||||
result = analyzer.analyze(
|
|
||||||
{
|
|
||||||
"totalSidelined": 2,
|
|
||||||
"players": [
|
|
||||||
{
|
|
||||||
"playerId": "p1",
|
|
||||||
"playerName": "Player One",
|
|
||||||
"positionShort": "O",
|
|
||||||
"matchesMissed": "N/A",
|
|
||||||
"average": "?",
|
|
||||||
"type": "injury",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"playerId": "p2",
|
|
||||||
"playerName": "Player Two",
|
|
||||||
"positionShort": "K",
|
|
||||||
"matchesMissed": "12",
|
|
||||||
"average": "6.7",
|
|
||||||
"type": "suspension",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(result.total_sidelined, 2)
|
|
||||||
self.assertGreaterEqual(result.impact_score, 0.0)
|
|
||||||
self.assertTrue(len(result.player_details) >= 2)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
"""
|
|
||||||
Unit tests for FeatureEnrichmentService
|
|
||||||
========================================
|
|
||||||
Tests all 6 enrichment methods with mocked DB cursor:
|
|
||||||
1. compute_team_stats
|
|
||||||
2. compute_h2h
|
|
||||||
3. compute_form_streaks
|
|
||||||
4. compute_referee_stats
|
|
||||||
5. compute_league_averages
|
|
||||||
6. compute_momentum
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
|
||||||
|
|
||||||
from services.feature_enrichment import FeatureEnrichmentService, _safe_avg
|
|
||||||
|
|
||||||
|
|
||||||
def _make_cursor(rows=None, side_effect=None):
|
|
||||||
"""Create a mock RealDictCursor."""
|
|
||||||
cur = MagicMock()
|
|
||||||
if side_effect:
|
|
||||||
cur.execute.side_effect = side_effect
|
|
||||||
else:
|
|
||||||
cur.fetchall.return_value = rows or []
|
|
||||||
cur.fetchone.return_value = rows[0] if rows else None
|
|
||||||
return cur
|
|
||||||
|
|
||||||
|
|
||||||
class TestSafeAvg(unittest.TestCase):
|
|
||||||
def test_returns_average(self):
|
|
||||||
self.assertAlmostEqual(_safe_avg([2.0, 4.0, 6.0], 0.0), 4.0)
|
|
||||||
|
|
||||||
def test_returns_default_on_empty(self):
|
|
||||||
self.assertEqual(_safe_avg([], 99.0), 99.0)
|
|
||||||
|
|
||||||
def test_single_value(self):
|
|
||||||
self.assertAlmostEqual(_safe_avg([7.5], 0.0), 7.5)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeTeamStats(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_team_id(self):
|
|
||||||
result = self.svc.compute_team_stats(MagicMock(), '', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_rows(self):
|
|
||||||
cur = _make_cursor(rows=[])
|
|
||||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
|
|
||||||
|
|
||||||
def test_returns_defaults_on_db_error(self):
|
|
||||||
cur = _make_cursor(side_effect=Exception('DB down'))
|
|
||||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
|
|
||||||
|
|
||||||
def test_calculates_averages_correctly(self):
|
|
||||||
rows = [
|
|
||||||
{'possession_percentage': 60.0, 'shots_on_target': 5, 'total_shots': 10, 'corners': 7},
|
|
||||||
{'possession_percentage': 40.0, 'shots_on_target': 3, 'total_shots': 12, 'corners': 3},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
self.assertAlmostEqual(result['avg_possession'], 50.0)
|
|
||||||
self.assertAlmostEqual(result['avg_shots_on_target'], 4.0)
|
|
||||||
self.assertAlmostEqual(result['shot_conversion'], (5 / 10 + 3 / 12) / 2, places=4)
|
|
||||||
self.assertAlmostEqual(result['avg_corners'], 5.0)
|
|
||||||
|
|
||||||
def test_handles_none_subfields_gracefully(self):
|
|
||||||
"""Rows with None values should be skipped, not crash."""
|
|
||||||
rows = [
|
|
||||||
{'possession_percentage': 55.0, 'shots_on_target': None, 'total_shots': None, 'corners': 4},
|
|
||||||
{'possession_percentage': None, 'shots_on_target': 2, 'total_shots': 8, 'corners': None},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
self.assertAlmostEqual(result['avg_possession'], 55.0)
|
|
||||||
self.assertAlmostEqual(result['avg_shots_on_target'], 2.0)
|
|
||||||
self.assertAlmostEqual(result['avg_corners'], 4.0)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeH2H(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_ids(self):
|
|
||||||
result = self.svc.compute_h2h(MagicMock(), '', 'away1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_rows(self):
|
|
||||||
cur = _make_cursor(rows=[])
|
|
||||||
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
|
|
||||||
|
|
||||||
def test_calculates_h2h_stats(self):
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'home1', 'away_team_id': 'away1', 'score_home': 2, 'score_away': 1}, # home win, btts, over25
|
|
||||||
{'home_team_id': 'home1', 'away_team_id': 'away1', 'score_home': 0, 'score_away': 0}, # draw, no btts, no over25
|
|
||||||
{'home_team_id': 'away1', 'away_team_id': 'home1', 'score_home': 1, 'score_away': 3}, # reversed: home wins again, btts, over25
|
|
||||||
{'home_team_id': 'away1', 'away_team_id': 'home1', 'score_home': 2, 'score_away': 0}, # reversed: away(=home1) lost
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
|
|
||||||
|
|
||||||
self.assertEqual(result['total_matches'], 4)
|
|
||||||
self.assertAlmostEqual(result['home_win_rate'], 2 / 4)
|
|
||||||
self.assertAlmostEqual(result['draw_rate'], 1 / 4)
|
|
||||||
self.assertAlmostEqual(result['btts_rate'], 2 / 4)
|
|
||||||
self.assertAlmostEqual(result['over25_rate'], 2 / 4)
|
|
||||||
|
|
||||||
def test_returns_defaults_on_db_error(self):
|
|
||||||
cur = _make_cursor(side_effect=Exception('connection lost'))
|
|
||||||
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeFormStreaks(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_team_id(self):
|
|
||||||
result = self.svc.compute_form_streaks(MagicMock(), '', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_FORM)
|
|
||||||
|
|
||||||
def test_calculates_streaks_correctly(self):
|
|
||||||
"""Most recent first: W, W, D, L → winning_streak=2, unbeaten_streak=3."""
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 2, 'score_away': 0}, # W (clean sheet, scored)
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 1, 'score_away': 0}, # W (clean sheet, scored)
|
|
||||||
{'home_team_id': 'x', 'away_team_id': 'team1', 'score_home': 1, 'score_away': 1}, # D (scored, conceded)
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 2}, # L (not scored, conceded)
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_form_streaks(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
self.assertEqual(result['winning_streak'], 2)
|
|
||||||
self.assertEqual(result['unbeaten_streak'], 3)
|
|
||||||
self.assertAlmostEqual(result['clean_sheet_rate'], 2 / 4)
|
|
||||||
self.assertAlmostEqual(result['scoring_rate'], 3 / 4)
|
|
||||||
|
|
||||||
def test_all_losses(self):
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 1},
|
|
||||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 3},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_form_streaks(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
self.assertEqual(result['winning_streak'], 0)
|
|
||||||
self.assertEqual(result['unbeaten_streak'], 0)
|
|
||||||
self.assertAlmostEqual(result['scoring_rate'], 0.0)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeRefereeStats(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_name(self):
|
|
||||||
result = self.svc.compute_referee_stats(MagicMock(), None, self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_REFEREE)
|
|
||||||
|
|
||||||
def test_calculates_referee_tendencies(self):
|
|
||||||
match_rows = [
|
|
||||||
{'home_team_id': 'h1', 'score_home': 2, 'score_away': 0, 'match_id': 'm1'}, # home win
|
|
||||||
{'home_team_id': 'h2', 'score_home': 1, 'score_away': 1, 'match_id': 'm2'}, # draw
|
|
||||||
]
|
|
||||||
card_row = {'yellows': 6, 'total_cards': 8}
|
|
||||||
|
|
||||||
cur = MagicMock()
|
|
||||||
# First execute (match query) → match_rows
|
|
||||||
# Second execute (card query) → card_row
|
|
||||||
cur.fetchall.return_value = match_rows
|
|
||||||
cur.fetchone.return_value = card_row
|
|
||||||
|
|
||||||
result = self.svc.compute_referee_stats(cur, 'Ref Name', self.ts)
|
|
||||||
|
|
||||||
self.assertEqual(result['experience'], 2)
|
|
||||||
self.assertAlmostEqual(result['avg_goals'], (2 + 0 + 1 + 1) / 2)
|
|
||||||
# home_bias = (1/2) - 0.46 = 0.04
|
|
||||||
self.assertAlmostEqual(result['home_bias'], 0.04, places=4)
|
|
||||||
self.assertAlmostEqual(result['avg_yellow'], 6 / 2)
|
|
||||||
self.assertAlmostEqual(result['cards_total'], 8 / 2)
|
|
||||||
|
|
||||||
def test_returns_defaults_on_db_error(self):
|
|
||||||
cur = _make_cursor(side_effect=Exception('timeout'))
|
|
||||||
result = self.svc.compute_referee_stats(cur, 'Some Ref', self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_REFEREE)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeLeagueAverages(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_defaults_when_no_league_id(self):
|
|
||||||
result = self.svc.compute_league_averages(MagicMock(), None, self.ts)
|
|
||||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_LEAGUE)
|
|
||||||
|
|
||||||
def test_calculates_league_averages(self):
|
|
||||||
rows = [
|
|
||||||
{'score_home': 1, 'score_away': 1}, # 2 goals
|
|
||||||
{'score_home': 0, 'score_away': 0}, # 0 goals (zero-goal match)
|
|
||||||
{'score_home': 3, 'score_away': 2}, # 5 goals
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_league_averages(cur, 'league1', self.ts)
|
|
||||||
|
|
||||||
self.assertAlmostEqual(result['avg_goals'], 7 / 3, places=4)
|
|
||||||
self.assertAlmostEqual(result['zero_goal_rate'], 1 / 3, places=4)
|
|
||||||
|
|
||||||
|
|
||||||
class TestComputeMomentum(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.svc = FeatureEnrichmentService()
|
|
||||||
self.ts = 1700000000000
|
|
||||||
|
|
||||||
def test_returns_zero_when_no_team_id(self):
|
|
||||||
result = self.svc.compute_momentum(MagicMock(), '', self.ts)
|
|
||||||
self.assertEqual(result, 0.0)
|
|
||||||
|
|
||||||
def test_returns_zero_when_no_rows(self):
|
|
||||||
cur = _make_cursor(rows=[])
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
self.assertEqual(result, 0.0)
|
|
||||||
|
|
||||||
def test_all_wins_returns_one(self):
|
|
||||||
"""All wins → momentum = 1.0 (max possible)."""
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'score_home': 3, 'score_away': 0},
|
|
||||||
{'home_team_id': 'team1', 'score_home': 2, 'score_away': 1},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
self.assertAlmostEqual(result, 1.0, places=4)
|
|
||||||
|
|
||||||
def test_all_losses_returns_negative(self):
|
|
||||||
"""All losses → negative momentum."""
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'score_home': 0, 'score_away': 2},
|
|
||||||
{'home_team_id': 'team1', 'score_home': 1, 'score_away': 3},
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
self.assertLess(result, 0.0)
|
|
||||||
|
|
||||||
def test_mixed_results(self):
|
|
||||||
"""W, D, L → weighted score between -1 and 1."""
|
|
||||||
rows = [
|
|
||||||
{'home_team_id': 'team1', 'score_home': 1, 'score_away': 0}, # W (weight=3)
|
|
||||||
{'home_team_id': 'x', 'away_team_id': 'team1', 'score_home': 0, 'score_away': 0}, # D (weight=2)
|
|
||||||
{'home_team_id': 'team1', 'score_home': 0, 'score_away': 1}, # L (weight=1)
|
|
||||||
]
|
|
||||||
cur = _make_cursor(rows)
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
|
|
||||||
# weighted = 3*3 + 1*2 + (-1)*1 = 9+2-1 = 10
|
|
||||||
# max_possible = 3*3 + 3*2 + 3*1 = 18
|
|
||||||
# normalised = 10/18 ≈ 0.5556
|
|
||||||
self.assertAlmostEqual(result, round(10 / 18, 4), places=4)
|
|
||||||
|
|
||||||
def test_returns_zero_on_db_error(self):
|
|
||||||
cur = _make_cursor(side_effect=Exception('broken pipe'))
|
|
||||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
|
||||||
self.assertEqual(result, 0.0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
|
||||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
|
||||||
|
|
||||||
import main as ai_main
|
|
||||||
|
|
||||||
|
|
||||||
def _run(coro):
|
|
||||||
return asyncio.run(coro)
|
|
||||||
|
|
||||||
|
|
||||||
class MainApiFunctionTests(unittest.TestCase):
|
|
||||||
def test_analyze_match_v20plus_returns_payload(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.analyze_match.return_value = {"match_info": {"match_id": "m1"}}
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
result = _run(ai_main.analyze_match_v20plus("m1"))
|
|
||||||
|
|
||||||
self.assertEqual(result["match_info"]["match_id"], "m1")
|
|
||||||
|
|
||||||
def test_analyze_match_v20plus_raises_404(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.analyze_match.return_value = None
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
with self.assertRaises(HTTPException) as ctx:
|
|
||||||
_run(ai_main.analyze_match_v20plus("missing"))
|
|
||||||
|
|
||||||
self.assertEqual(ctx.exception.status_code, 404)
|
|
||||||
|
|
||||||
def test_analyze_match_htms_v20plus_returns_payload(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.analyze_match_htms.return_value = {
|
|
||||||
"status": "ok",
|
|
||||||
"engine_used": "v20plus_top_htms",
|
|
||||||
}
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
result = _run(ai_main.analyze_match_htms_v20plus("m1"))
|
|
||||||
|
|
||||||
self.assertEqual(result["status"], "ok")
|
|
||||||
self.assertEqual(result["engine_used"], "v20plus_top_htms")
|
|
||||||
|
|
||||||
def test_analyze_match_htft_timeout_validation(self):
|
|
||||||
with self.assertRaises(HTTPException) as ctx:
|
|
||||||
_run(ai_main.analyze_match_htft_v20plus("m1", timeout_sec=2))
|
|
||||||
|
|
||||||
self.assertEqual(ctx.exception.status_code, 400)
|
|
||||||
|
|
||||||
def test_generate_coupon_v20plus_forwards_payload(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.build_coupon.return_value = {"bets": []}
|
|
||||||
|
|
||||||
request = ai_main.CouponRequest(
|
|
||||||
match_ids=["m1", "m2"],
|
|
||||||
strategy="SAFE",
|
|
||||||
max_matches=3,
|
|
||||||
min_confidence=70,
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
result = _run(ai_main.generate_coupon_v20plus(request))
|
|
||||||
|
|
||||||
self.assertEqual(result, {"bets": []})
|
|
||||||
orchestrator.build_coupon.assert_called_once_with(
|
|
||||||
match_ids=["m1", "m2"],
|
|
||||||
strategy="SAFE",
|
|
||||||
max_matches=3,
|
|
||||||
min_confidence=70.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_reversal_watchlist_validation(self):
|
|
||||||
with self.assertRaises(HTTPException) as ctx:
|
|
||||||
_run(ai_main.get_reversal_watchlist_v20plus(count=0))
|
|
||||||
self.assertEqual(ctx.exception.status_code, 400)
|
|
||||||
|
|
||||||
def test_reversal_watchlist_forwards_payload(self):
|
|
||||||
orchestrator = MagicMock()
|
|
||||||
orchestrator.get_reversal_watchlist.return_value = {"watchlist": []}
|
|
||||||
|
|
||||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
|
||||||
result = _run(
|
|
||||||
ai_main.get_reversal_watchlist_v20plus(
|
|
||||||
count=12,
|
|
||||||
horizon_hours=48,
|
|
||||||
min_score=50.5,
|
|
||||||
top_leagues_only=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(result, {"watchlist": []})
|
|
||||||
orchestrator.get_reversal_watchlist.assert_called_once_with(
|
|
||||||
count=12,
|
|
||||||
horizon_hours=48,
|
|
||||||
min_score=50.5,
|
|
||||||
top_leagues_only=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,766 +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.basketball_predictor = MagicMock()
|
|
||||||
orchestrator.dsn = "postgresql://unit-test"
|
|
||||||
orchestrator.league_reliability = {}
|
|
||||||
orchestrator.market_calibration = {
|
|
||||||
"MS": 0.82,
|
|
||||||
"DC": 0.93,
|
|
||||||
"OU15": 0.90,
|
|
||||||
"OU25": 0.85,
|
|
||||||
"OU35": 0.88,
|
|
||||||
"BTTS": 0.83,
|
|
||||||
"HT": 0.80,
|
|
||||||
"HT_OU05": 0.88,
|
|
||||||
}
|
|
||||||
orchestrator.market_min_conf = {
|
|
||||||
"MS": 52.0,
|
|
||||||
"DC": 56.0,
|
|
||||||
"OU15": 60.0,
|
|
||||||
"OU25": 58.0,
|
|
||||||
"OU35": 54.0,
|
|
||||||
"BTTS": 57.0,
|
|
||||||
"HT": 53.0,
|
|
||||||
"HT_OU05": 55.0,
|
|
||||||
}
|
|
||||||
orchestrator.market_min_play_score = {
|
|
||||||
"MS": 72.0,
|
|
||||||
"DC": 62.0,
|
|
||||||
"OU15": 64.0,
|
|
||||||
"OU25": 70.0,
|
|
||||||
"OU35": 76.0,
|
|
||||||
"BTTS": 70.0,
|
|
||||||
"HT": 74.0,
|
|
||||||
"HT_OU05": 64.0,
|
|
||||||
}
|
|
||||||
orchestrator.market_min_edge = {
|
|
||||||
"MS": 0.03,
|
|
||||||
"DC": 0.01,
|
|
||||||
"OU15": 0.01,
|
|
||||||
"OU25": 0.02,
|
|
||||||
"OU35": 0.04,
|
|
||||||
"BTTS": 0.03,
|
|
||||||
"HT": 0.04,
|
|
||||||
"HT_OU05": 0.01,
|
|
||||||
}
|
|
||||||
return orchestrator
|
|
||||||
|
|
||||||
|
|
||||||
class SingleMatchOrchestratorTests(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.orchestrator = _build_orchestrator()
|
|
||||||
|
|
||||||
def test_parse_odds_json_uses_exact_market_match_and_ignores_collisions(self):
|
|
||||||
odds_json = {
|
|
||||||
"Maç Sonucu": {"1": "2.15", "X": "3.20", "2": "3.30"},
|
|
||||||
"İlk Yarı/Maç Sonucu": {"1/1": "4.30"},
|
|
||||||
"2,5 Alt/Üst": {"Üst": "1.85", "Alt": "1.95"},
|
|
||||||
"İY 0,5 Alt/Üst": {"Üst": "1.49", "Alt": "2.20"},
|
|
||||||
"1. Yarı Ev Sahibi 0,5 Alt/Üst": {"Üst": "1.99", "Alt": "1.45"},
|
|
||||||
"2,5 Kart Puanı Alt/Üst": {"Üst": "1.33", "Alt": "2.95"},
|
|
||||||
"Karşılıklı Gol": {"Var": "1.75", "Yok": "2.05"},
|
|
||||||
"1. Yarı Karşılıklı Gol": {"Var": "2.10", "Yok": "1.60"},
|
|
||||||
"Çifte Şans": {"1-X": "1.33", "X-2": "1.62", "1-2": "1.30"},
|
|
||||||
"1. Yarı Sonucu": {"1": "2.45", "X": "2.00", "2": "3.80"},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ms_h"], 2.15)
|
|
||||||
self.assertEqual(parsed["ms_d"], 3.20)
|
|
||||||
self.assertEqual(parsed["ms_a"], 3.30)
|
|
||||||
self.assertEqual(parsed["ou25_o"], 1.85)
|
|
||||||
self.assertEqual(parsed["ou25_u"], 1.95)
|
|
||||||
self.assertEqual(parsed["btts_y"], 1.75)
|
|
||||||
self.assertEqual(parsed["btts_n"], 2.05)
|
|
||||||
self.assertEqual(parsed["dc_1x"], 1.33)
|
|
||||||
self.assertEqual(parsed["dc_x2"], 1.62)
|
|
||||||
self.assertEqual(parsed["dc_12"], 1.30)
|
|
||||||
self.assertEqual(parsed["ht_h"], 2.45)
|
|
||||||
self.assertEqual(parsed["ht_d"], 2.00)
|
|
||||||
self.assertEqual(parsed["ht_a"], 3.80)
|
|
||||||
self.assertEqual(parsed["ht_ou05_o"], 1.49)
|
|
||||||
self.assertEqual(parsed["ht_ou05_u"], 2.20)
|
|
||||||
self.assertEqual(parsed["htft_11"], 4.30)
|
|
||||||
|
|
||||||
def test_parse_odds_json_accepts_selection_variants(self):
|
|
||||||
odds_json = {
|
|
||||||
"2,5 Alt/Üst": {"2,5 Üst": "1.91", "2,5 Alt": "1.86"},
|
|
||||||
"Karşılıklı Gol": {"YES": "1.82", "NO": "1.96"},
|
|
||||||
"Çifte Şans": {"1X": "1.28", "X2": "1.44", "12": "1.32"},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ou25_o"], 1.91)
|
|
||||||
self.assertEqual(parsed["ou25_u"], 1.86)
|
|
||||||
self.assertEqual(parsed["btts_y"], 1.82)
|
|
||||||
self.assertEqual(parsed["btts_n"], 1.96)
|
|
||||||
self.assertEqual(parsed["dc_1x"], 1.28)
|
|
||||||
self.assertEqual(parsed["dc_x2"], 1.44)
|
|
||||||
self.assertEqual(parsed["dc_12"], 1.32)
|
|
||||||
|
|
||||||
def test_parse_odds_json_maps_all_football_markets_with_noise(self):
|
|
||||||
odds_json = {
|
|
||||||
"Maç Sonucu": {"1": "2.31", "X": "3.22", "2": "3.05"},
|
|
||||||
"Çifte Şans": {"1-X": "1.34", "X-2": "1.52", "1-2": "1.28"},
|
|
||||||
"1,5 Alt/Üst": {"Üst": "1.29", "Alt": "3.45"},
|
|
||||||
"2,5 Alt/Üst": {"Üst": "1.71", "Alt": "2.05"},
|
|
||||||
"3,5 Alt/Üst": {"Üst": "2.62", "Alt": "1.41"},
|
|
||||||
"Karşılıklı Gol": {"Var": "1.66", "Yok": "2.11"},
|
|
||||||
"1. Yarı Sonucu": {"1": "3.10", "X": "1.95", "2": "4.60"},
|
|
||||||
"1. Yarı 0,5 Alt/Üst": {"Üst": "1.21", "Alt": "2.72"},
|
|
||||||
# noise categories that must not overwrite football main markets
|
|
||||||
"1. Yarı Ev Sahibi 0,5 Alt/Üst": {"Üst": "1.99", "Alt": "1.45"},
|
|
||||||
"1. Yarı Deplasman 0,5 Alt/Üst": {"Üst": "1.73", "Alt": "1.63"},
|
|
||||||
"1.Yarı 3,5 Korner Alt/Üst": {"Üst": "1.26", "Alt": "2.30"},
|
|
||||||
"2,5 Kart Puanı Alt/Üst": {"Üst": "1.40", "Alt": "2.60"},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ms_h"], 2.31)
|
|
||||||
self.assertEqual(parsed["ms_d"], 3.22)
|
|
||||||
self.assertEqual(parsed["ms_a"], 3.05)
|
|
||||||
self.assertEqual(parsed["dc_1x"], 1.34)
|
|
||||||
self.assertEqual(parsed["dc_x2"], 1.52)
|
|
||||||
self.assertEqual(parsed["dc_12"], 1.28)
|
|
||||||
self.assertEqual(parsed["ou15_o"], 1.29)
|
|
||||||
self.assertEqual(parsed["ou15_u"], 3.45)
|
|
||||||
self.assertEqual(parsed["ou25_o"], 1.71)
|
|
||||||
self.assertEqual(parsed["ou25_u"], 2.05)
|
|
||||||
self.assertEqual(parsed["ou35_o"], 2.62)
|
|
||||||
self.assertEqual(parsed["ou35_u"], 1.41)
|
|
||||||
self.assertEqual(parsed["btts_y"], 1.66)
|
|
||||||
self.assertEqual(parsed["btts_n"], 2.11)
|
|
||||||
self.assertEqual(parsed["ht_h"], 3.10)
|
|
||||||
self.assertEqual(parsed["ht_d"], 1.95)
|
|
||||||
self.assertEqual(parsed["ht_a"], 4.60)
|
|
||||||
self.assertEqual(parsed["ht_ou05_o"], 1.21)
|
|
||||||
self.assertEqual(parsed["ht_ou05_u"], 2.72)
|
|
||||||
|
|
||||||
def test_v25_market_odds_ignores_synthetic_default_when_selection_missing(self):
|
|
||||||
odds_json = {
|
|
||||||
"1,5 Alt/Üst": {"Alt": 5.70},
|
|
||||||
"Çifte Şans": {"1-X": 1.30, "X-2": 1.38, "1-2": 1.09},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ou15_o"], 0.0)
|
|
||||||
self.assertEqual(
|
|
||||||
self.orchestrator._v25_market_odds(parsed, "OU15", "Over"),
|
|
||||||
1.0,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.orchestrator._v25_market_odds(parsed, "OU15", "Under"),
|
|
||||||
5.7,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.orchestrator._v25_market_odds(parsed, "DC", "X2"),
|
|
||||||
1.38,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_parse_odds_json_extracts_basketball_ml_total_spread(self):
|
|
||||||
odds_json = {
|
|
||||||
"Maç Sonucu (Uzt. Dahil)": {"1": "1.74", "2": "2.08"},
|
|
||||||
"Alt/Üst (163,5)": {"Üst": "1.86", "Alt": "1.94"},
|
|
||||||
"1. Yarı Alt/Üst (81,5)": {"Üst": "1.89", "Alt": "1.91"},
|
|
||||||
"1. Yarı Alt/Üst (100,5)": {"Üst": "1.83", "Alt": "1.97"},
|
|
||||||
"Hnd. MS (0:5,5)": {"1": "1.91", "+5.5h": "1.87"},
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
|
||||||
|
|
||||||
self.assertEqual(parsed["ml_h"], 1.74)
|
|
||||||
self.assertEqual(parsed["ml_a"], 2.08)
|
|
||||||
self.assertEqual(parsed["tot_line"], 163.5)
|
|
||||||
self.assertEqual(parsed["tot_o"], 1.86)
|
|
||||||
self.assertEqual(parsed["tot_u"], 1.94)
|
|
||||||
self.assertEqual(parsed["spread_home_line"], -5.5)
|
|
||||||
self.assertEqual(parsed["spread_h"], 1.91)
|
|
||||||
self.assertEqual(parsed["spread_a"], 1.87)
|
|
||||||
self.assertNotIn("ht_ou05_o", parsed)
|
|
||||||
self.assertNotIn("ht_ou05_u", parsed)
|
|
||||||
|
|
||||||
def test_extract_odds_merges_relational_when_live_json_is_incomplete(self):
|
|
||||||
row = {
|
|
||||||
"match_id": "m-1",
|
|
||||||
"odds": {"Maç Sonucu": {"1": 2.10, "X": 3.20, "2": 3.35}},
|
|
||||||
}
|
|
||||||
relational_rows = [
|
|
||||||
{"category_name": "Çifte Şans", "selection_name": "1-X", "odd_value": 1.28},
|
|
||||||
{"category_name": "Çifte Şans", "selection_name": "X-2", "odd_value": 1.44},
|
|
||||||
{"category_name": "Çifte Şans", "selection_name": "1-2", "odd_value": 1.31},
|
|
||||||
{"category_name": "2,5 Alt/Üst", "selection_name": "Üst", "odd_value": 1.89},
|
|
||||||
{"category_name": "2,5 Alt/Üst", "selection_name": "Alt", "odd_value": 1.94},
|
|
||||||
{"category_name": "Karşılıklı Gol", "selection_name": "Var", "odd_value": 1.77},
|
|
||||||
{"category_name": "Karşılıklı Gol", "selection_name": "Yok", "odd_value": 2.02},
|
|
||||||
{"category_name": "1. Yarı Sonucu", "selection_name": "1", "odd_value": 2.55},
|
|
||||||
{"category_name": "1. Yarı Sonucu", "selection_name": "X", "odd_value": 1.98},
|
|
||||||
{"category_name": "1. Yarı Sonucu", "selection_name": "2", "odd_value": 3.40},
|
|
||||||
]
|
|
||||||
cur = _StaticFetchAllCursor(relational_rows)
|
|
||||||
|
|
||||||
odds = self.orchestrator._extract_odds(cur, row)
|
|
||||||
|
|
||||||
self.assertEqual(odds["ms_h"], 2.10)
|
|
||||||
self.assertEqual(odds["ms_d"], 3.20)
|
|
||||||
self.assertEqual(odds["ms_a"], 3.35)
|
|
||||||
self.assertEqual(odds["dc_x2"], 1.44)
|
|
||||||
self.assertEqual(odds["ou25_o"], 1.89)
|
|
||||||
self.assertEqual(odds["btts_y"], 1.77)
|
|
||||||
self.assertEqual(odds["ht_d"], 1.98)
|
|
||||||
self.assertEqual(len(cur.executed), 1)
|
|
||||||
|
|
||||||
def test_extract_odds_fills_default_ms_when_no_source_available(self):
|
|
||||||
row = {"match_id": "m-2", "odds": None}
|
|
||||||
cur = _StaticFetchAllCursor([])
|
|
||||||
|
|
||||||
odds = self.orchestrator._extract_odds(cur, row)
|
|
||||||
|
|
||||||
self.assertEqual(odds["ms_h"], SingleMatchOrchestrator.DEFAULT_MS_H)
|
|
||||||
self.assertEqual(odds["ms_d"], SingleMatchOrchestrator.DEFAULT_MS_D)
|
|
||||||
self.assertEqual(odds["ms_a"], SingleMatchOrchestrator.DEFAULT_MS_A)
|
|
||||||
|
|
||||||
def test_parse_lineups_json_supports_id_playerid_personid(self):
|
|
||||||
lineups = {
|
|
||||||
"home": {
|
|
||||||
"xi": [
|
|
||||||
{"id": "11"},
|
|
||||||
{"playerId": "12"},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"away": {
|
|
||||||
"starting": [
|
|
||||||
{"personId": "21"},
|
|
||||||
"22",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
home, away = self.orchestrator._parse_lineups_json(lineups)
|
|
||||||
|
|
||||||
self.assertEqual(home, ["11", "12"])
|
|
||||||
self.assertEqual(away, ["21", "22"])
|
|
||||||
|
|
||||||
def test_extract_lineups_uses_participation_and_probable_xi_fallbacks(self):
|
|
||||||
row = {
|
|
||||||
"match_id": "m-3",
|
|
||||||
"home_team_id": "h1",
|
|
||||||
"away_team_id": "a1",
|
|
||||||
"match_date_ms": 1700000000000,
|
|
||||||
"lineups": {
|
|
||||||
"home": {"xi": [{"personId": "h-live-1"}]},
|
|
||||||
"away": {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
participation = [
|
|
||||||
{"team_id": "a1", "player_id": "a-db-1"},
|
|
||||||
{"team_id": "a1", "player_id": "a-db-2"},
|
|
||||||
]
|
|
||||||
cur = _StaticFetchAllCursor(participation)
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
self.orchestrator,
|
|
||||||
"_build_probable_xi",
|
|
||||||
side_effect=[["h-prob-1"], ["a-prob-1"]],
|
|
||||||
) as probable_xi:
|
|
||||||
home, away, source = self.orchestrator._extract_lineups(cur, row)
|
|
||||||
|
|
||||||
self.assertEqual(home, ["h-live-1"])
|
|
||||||
self.assertEqual(away, ["a-db-1", "a-db-2"])
|
|
||||||
self.assertEqual(source, "none")
|
|
||||||
probable_xi.assert_not_called()
|
|
||||||
|
|
||||||
def test_extract_lineups_falls_back_to_probable_xi_when_live_and_participation_missing(self):
|
|
||||||
row = {
|
|
||||||
"match_id": "m-4",
|
|
||||||
"home_team_id": "h2",
|
|
||||||
"away_team_id": "a2",
|
|
||||||
"match_date_ms": 1700000000000,
|
|
||||||
"lineups": None,
|
|
||||||
}
|
|
||||||
cur = _StaticFetchAllCursor([])
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
self.orchestrator,
|
|
||||||
"_build_probable_xi",
|
|
||||||
side_effect=[["h-prob-1", "h-prob-2"], ["a-prob-1"]],
|
|
||||||
) as probable_xi:
|
|
||||||
home, away, source = self.orchestrator._extract_lineups(cur, row)
|
|
||||||
|
|
||||||
self.assertEqual(home, ["h-prob-1", "h-prob-2"])
|
|
||||||
self.assertEqual(away, ["a-prob-1"])
|
|
||||||
self.assertEqual(source, "probable_xi")
|
|
||||||
self.assertEqual(probable_xi.call_count, 2)
|
|
||||||
|
|
||||||
def test_load_match_data_parses_live_row_json_and_sidelined(self):
|
|
||||||
odds_payload = {
|
|
||||||
"Maç Sonucu": {"1": 2.10, "X": 3.30, "2": 3.50},
|
|
||||||
"Çifte Şans": {"1-X": 1.30, "X-2": 1.52, "1-2": 1.34},
|
|
||||||
"1,5 Alt/Üst": {"Üst": 1.33, "Alt": 2.90},
|
|
||||||
"2,5 Alt/Üst": {"Üst": 1.91, "Alt": 1.85},
|
|
||||||
"3,5 Alt/Üst": {"Üst": 2.95, "Alt": 1.38},
|
|
||||||
"Karşılıklı Gol": {"Var": 1.84, "Yok": 1.92},
|
|
||||||
"1. Yarı Sonucu": {"1": 2.55, "X": 1.97, "2": 3.45},
|
|
||||||
}
|
|
||||||
lineups_payload = {
|
|
||||||
"home": {"xi": [{"personId": "101"}, {"personId": "102"}]},
|
|
||||||
"away": {"xi": [{"personId": "201"}, {"personId": "202"}]},
|
|
||||||
}
|
|
||||||
live_row = {
|
|
||||||
"match_id": "live-101",
|
|
||||||
"home_team_id": "h-101",
|
|
||||||
"away_team_id": "a-101",
|
|
||||||
"league_id": "l-101",
|
|
||||||
"sport": "FOOTBALL",
|
|
||||||
"match_date_ms": 1760000000000,
|
|
||||||
"odds": json.dumps(odds_payload),
|
|
||||||
"lineups": json.dumps(lineups_payload),
|
|
||||||
"sidelined": json.dumps(
|
|
||||||
{
|
|
||||||
"homeTeam": {"totalSidelined": 1, "players": []},
|
|
||||||
"awayTeam": {"totalSidelined": 0, "players": []},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
"referee_name": "John Ref",
|
|
||||||
"home_team_name": "Home FC",
|
|
||||||
"away_team_name": "Away FC",
|
|
||||||
"league_name": "League Name",
|
|
||||||
}
|
|
||||||
cursor = _RouterCursor(live_row=live_row)
|
|
||||||
|
|
||||||
with patch("services.single_match_orchestrator.psycopg2.connect", return_value=_ConnContext(cursor)):
|
|
||||||
data = self.orchestrator._load_match_data("live-101")
|
|
||||||
|
|
||||||
self.assertIsNotNone(data)
|
|
||||||
self.assertEqual(data.match_id, "live-101")
|
|
||||||
self.assertEqual(data.home_team_id, "h-101")
|
|
||||||
self.assertEqual(data.away_team_id, "a-101")
|
|
||||||
self.assertEqual(data.sport, "football")
|
|
||||||
self.assertEqual(data.referee_name, "John Ref")
|
|
||||||
self.assertEqual(data.home_lineup, ["101", "102"])
|
|
||||||
self.assertEqual(data.away_lineup, ["201", "202"])
|
|
||||||
self.assertEqual(data.lineup_source, "none")
|
|
||||||
self.assertEqual(data.sidelined_data["homeTeam"]["totalSidelined"], 1)
|
|
||||||
self.assertEqual(data.odds_data["dc_x2"], 1.52)
|
|
||||||
self.assertEqual(data.odds_data["ht_h"], 2.55)
|
|
||||||
|
|
||||||
def test_analyze_match_forwards_all_core_fields_to_predictor(self):
|
|
||||||
match_data = MatchData(
|
|
||||||
match_id="live-55",
|
|
||||||
home_team_id="home-55",
|
|
||||||
away_team_id="away-55",
|
|
||||||
home_team_name="Home 55",
|
|
||||||
away_team_name="Away 55",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="football",
|
|
||||||
league_id="league-55",
|
|
||||||
league_name="League 55",
|
|
||||||
referee_name="Ref 55",
|
|
||||||
odds_data={"ms_h": 2.4, "ms_d": 3.1, "ms_a": 2.9},
|
|
||||||
home_lineup=["h1", "h2"],
|
|
||||||
away_lineup=["a1", "a2"],
|
|
||||||
sidelined_data={
|
|
||||||
"homeTeam": {"totalSidelined": 2, "players": []},
|
|
||||||
"awayTeam": {"totalSidelined": 1, "players": []},
|
|
||||||
},
|
|
||||||
home_goals_avg=1.6,
|
|
||||||
home_conceded_avg=1.1,
|
|
||||||
away_goals_avg=1.2,
|
|
||||||
away_conceded_avg=1.4,
|
|
||||||
home_position=5,
|
|
||||||
away_position=8,
|
|
||||||
lineup_source="confirmed_live",
|
|
||||||
)
|
|
||||||
prediction = FullMatchPrediction(match_id="live-55", home_team="Home 55", away_team="Away 55")
|
|
||||||
|
|
||||||
self.orchestrator._load_match_data = MagicMock(return_value=match_data)
|
|
||||||
self.orchestrator.v25_predictor.predict_market_bundle = MagicMock(return_value={"MS": {"pick": "1"}})
|
|
||||||
self.orchestrator._build_v25_features = MagicMock(return_value={})
|
|
||||||
self.orchestrator._get_v25_signal = MagicMock(return_value={"MS": {"pick": "1"}})
|
|
||||||
self.orchestrator._build_v25_prediction = MagicMock(return_value=prediction)
|
|
||||||
self.orchestrator._build_prediction_package = MagicMock(return_value={"ok": True})
|
|
||||||
|
|
||||||
result = self.orchestrator.analyze_match("live-55")
|
|
||||||
|
|
||||||
self.assertEqual(result, {"ok": True})
|
|
||||||
self.orchestrator._build_v25_features.assert_called_once_with(match_data)
|
|
||||||
self.orchestrator._get_v25_signal.assert_called_once_with(match_data, {})
|
|
||||||
self.orchestrator._build_v25_prediction.assert_called_once_with(
|
|
||||||
match_data,
|
|
||||||
{},
|
|
||||||
{"MS": {"pick": "1"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_analyze_match_routes_basketball_to_basketball_predictor(self):
|
|
||||||
match_data = MatchData(
|
|
||||||
match_id="b-live-1",
|
|
||||||
home_team_id="bh",
|
|
||||||
away_team_id="ba",
|
|
||||||
home_team_name="Home B",
|
|
||||||
away_team_name="Away B",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="basketball",
|
|
||||||
league_id="bleague",
|
|
||||||
league_name="B League",
|
|
||||||
referee_name=None,
|
|
||||||
odds_data={"ml_h": 1.75, "ml_a": 2.05, "tot_line": 161.5, "tot_o": 1.88, "tot_u": 1.92},
|
|
||||||
home_lineup=None,
|
|
||||||
away_lineup=None,
|
|
||||||
sidelined_data={"homeTeam": {"totalSidelined": 1}, "awayTeam": {"totalSidelined": 0}},
|
|
||||||
home_goals_avg=85.0,
|
|
||||||
home_conceded_avg=79.0,
|
|
||||||
away_goals_avg=82.0,
|
|
||||||
away_conceded_avg=81.0,
|
|
||||||
home_position=4,
|
|
||||||
away_position=7,
|
|
||||||
lineup_source="none",
|
|
||||||
)
|
|
||||||
prediction = BasketballMatchPrediction(
|
|
||||||
match_id="b-live-1",
|
|
||||||
home_team_name="Home B",
|
|
||||||
away_team_name="Away B",
|
|
||||||
league_name="B League",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.orchestrator._load_match_data = MagicMock(return_value=match_data)
|
|
||||||
self.orchestrator.basketball_predictor.predict = MagicMock(return_value=prediction)
|
|
||||||
self.orchestrator._build_basketball_prediction_package = MagicMock(
|
|
||||||
return_value={"sport": "basketball", "ok": True}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = self.orchestrator.analyze_match("b-live-1")
|
|
||||||
|
|
||||||
self.assertEqual(result, {"sport": "basketball", "ok": True})
|
|
||||||
self.orchestrator.basketball_predictor.predict.assert_called_once()
|
|
||||||
kwargs = self.orchestrator.basketball_predictor.predict.call_args.kwargs
|
|
||||||
self.assertEqual(kwargs["match_id"], "b-live-1")
|
|
||||||
self.assertEqual(kwargs["home_team_id"], "bh")
|
|
||||||
self.assertEqual(kwargs["away_team_id"], "ba")
|
|
||||||
self.assertEqual(kwargs["league_id"], "bleague")
|
|
||||||
self.assertEqual(kwargs["odds_data"]["ml_h"], 1.75)
|
|
||||||
self.orchestrator.v25_predictor.predict_market_bundle.assert_not_called()
|
|
||||||
|
|
||||||
def test_build_market_rows_maps_odds_keys_correctly(self):
|
|
||||||
data = MatchData(
|
|
||||||
match_id="m-rows",
|
|
||||||
home_team_id="h",
|
|
||||||
away_team_id="a",
|
|
||||||
home_team_name="Home",
|
|
||||||
away_team_name="Away",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="football",
|
|
||||||
league_id=None,
|
|
||||||
league_name="",
|
|
||||||
referee_name=None,
|
|
||||||
odds_data={
|
|
||||||
"ms_h": 2.3,
|
|
||||||
"ms_d": 3.2,
|
|
||||||
"ms_a": 3.1,
|
|
||||||
"dc_x2": 1.45,
|
|
||||||
"ou15_o": 1.36,
|
|
||||||
"ou25_u": 1.92,
|
|
||||||
"ou35_o": 2.85,
|
|
||||||
"btts_y": 1.88,
|
|
||||||
"ht_h": 2.55,
|
|
||||||
"ht_ou05_o": 1.47,
|
|
||||||
},
|
|
||||||
home_lineup=None,
|
|
||||||
away_lineup=None,
|
|
||||||
sidelined_data=None,
|
|
||||||
home_goals_avg=1.5,
|
|
||||||
home_conceded_avg=1.2,
|
|
||||||
away_goals_avg=1.2,
|
|
||||||
away_conceded_avg=1.4,
|
|
||||||
home_position=10,
|
|
||||||
away_position=10,
|
|
||||||
lineup_source="none",
|
|
||||||
)
|
|
||||||
pred = FullMatchPrediction(
|
|
||||||
match_id="m-rows",
|
|
||||||
home_team="Home",
|
|
||||||
away_team="Away",
|
|
||||||
ms_home_prob=0.25,
|
|
||||||
ms_draw_prob=0.30,
|
|
||||||
ms_away_prob=0.45,
|
|
||||||
ms_pick="2",
|
|
||||||
ms_confidence=69.0,
|
|
||||||
dc_1x_prob=0.60,
|
|
||||||
dc_x2_prob=0.72,
|
|
||||||
dc_12_prob=0.68,
|
|
||||||
dc_pick="X2",
|
|
||||||
dc_confidence=67.0,
|
|
||||||
over_15_prob=0.74,
|
|
||||||
under_15_prob=0.26,
|
|
||||||
ou15_pick="1.5 Üst",
|
|
||||||
ou15_confidence=72.0,
|
|
||||||
over_25_prob=0.44,
|
|
||||||
under_25_prob=0.56,
|
|
||||||
ou25_pick="2.5 Alt",
|
|
||||||
ou25_confidence=61.0,
|
|
||||||
over_35_prob=0.39,
|
|
||||||
under_35_prob=0.61,
|
|
||||||
ou35_pick="3.5 Over",
|
|
||||||
ou35_confidence=58.0,
|
|
||||||
btts_yes_prob=0.57,
|
|
||||||
btts_no_prob=0.43,
|
|
||||||
btts_pick="Yes",
|
|
||||||
btts_confidence=63.0,
|
|
||||||
ht_home_prob=0.41,
|
|
||||||
ht_draw_prob=0.39,
|
|
||||||
ht_away_prob=0.20,
|
|
||||||
ht_pick="1",
|
|
||||||
ht_confidence=60.0,
|
|
||||||
ht_over_05_prob=0.64,
|
|
||||||
ht_under_05_prob=0.36,
|
|
||||||
ht_ou_pick="Over 0.5",
|
|
||||||
)
|
|
||||||
|
|
||||||
rows = self.orchestrator._build_market_rows(data, pred)
|
|
||||||
by_market = {row["market"]: row for row in rows}
|
|
||||||
|
|
||||||
self.assertEqual(by_market["MS"]["odds"], 3.1)
|
|
||||||
self.assertEqual(by_market["DC"]["odds"], 1.45)
|
|
||||||
self.assertEqual(by_market["OU15"]["odds"], 1.36)
|
|
||||||
self.assertEqual(by_market["OU25"]["odds"], 1.92)
|
|
||||||
self.assertEqual(by_market["OU35"]["odds"], 2.85)
|
|
||||||
self.assertEqual(by_market["BTTS"]["odds"], 1.88)
|
|
||||||
self.assertEqual(by_market["HT"]["odds"], 2.55)
|
|
||||||
self.assertEqual(by_market["HT_OU05"]["odds"], 1.47)
|
|
||||||
|
|
||||||
def test_build_basketball_market_rows_maps_odds_keys_correctly(self):
|
|
||||||
data = MatchData(
|
|
||||||
match_id="b-rows",
|
|
||||||
home_team_id="bh",
|
|
||||||
away_team_id="ba",
|
|
||||||
home_team_name="Home B",
|
|
||||||
away_team_name="Away B",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="basketball",
|
|
||||||
league_id="bl",
|
|
||||||
league_name="Basketball League",
|
|
||||||
referee_name=None,
|
|
||||||
odds_data={
|
|
||||||
"ml_h": 1.73,
|
|
||||||
"ml_a": 2.10,
|
|
||||||
"tot_line": 162.5,
|
|
||||||
"tot_o": 1.89,
|
|
||||||
"tot_u": 1.93,
|
|
||||||
"spread_home_line": -4.5,
|
|
||||||
"spread_h": 1.91,
|
|
||||||
"spread_a": 1.88,
|
|
||||||
},
|
|
||||||
home_lineup=None,
|
|
||||||
away_lineup=None,
|
|
||||||
sidelined_data=None,
|
|
||||||
home_goals_avg=84.0,
|
|
||||||
home_conceded_avg=80.0,
|
|
||||||
away_goals_avg=82.0,
|
|
||||||
away_conceded_avg=81.0,
|
|
||||||
home_position=5,
|
|
||||||
away_position=8,
|
|
||||||
lineup_source="none",
|
|
||||||
)
|
|
||||||
pred = {
|
|
||||||
"match_id": "b-rows",
|
|
||||||
"market_board": {
|
|
||||||
"ML": {"1": "62%", "2": "38%"},
|
|
||||||
"Totals": {"Under 162.5": "43%", "Over 162.5": "57%"},
|
|
||||||
"Spread": {"Away +4.5": "46%", "Home -4.5": "54%"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows = self.orchestrator._build_basketball_market_rows(data, pred)
|
|
||||||
by_market = {row["market"]: row for row in rows}
|
|
||||||
|
|
||||||
self.assertEqual(by_market["ML"]["odds"], 1.73)
|
|
||||||
self.assertEqual(by_market["TOTAL"]["odds"], 1.89)
|
|
||||||
self.assertEqual(by_market["SPREAD"]["odds"], 1.91)
|
|
||||||
|
|
||||||
def test_compute_data_quality_flags_missing_referee_and_lineup(self):
|
|
||||||
data = MatchData(
|
|
||||||
match_id="dq-1",
|
|
||||||
home_team_id="h",
|
|
||||||
away_team_id="a",
|
|
||||||
home_team_name="Home",
|
|
||||||
away_team_name="Away",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="football",
|
|
||||||
league_id=None,
|
|
||||||
league_name="",
|
|
||||||
referee_name=None,
|
|
||||||
odds_data={"ms_h": 2.5, "ms_d": 3.2, "ms_a": 2.9},
|
|
||||||
home_lineup=["h1", "h2"],
|
|
||||||
away_lineup=["a1"],
|
|
||||||
sidelined_data=None,
|
|
||||||
home_goals_avg=1.5,
|
|
||||||
home_conceded_avg=1.2,
|
|
||||||
away_goals_avg=1.2,
|
|
||||||
away_conceded_avg=1.4,
|
|
||||||
home_position=10,
|
|
||||||
away_position=10,
|
|
||||||
lineup_source="none",
|
|
||||||
)
|
|
||||||
|
|
||||||
quality = self.orchestrator._compute_data_quality(data)
|
|
||||||
|
|
||||||
self.assertIn("lineup_incomplete", quality["flags"])
|
|
||||||
self.assertIn("missing_referee", quality["flags"])
|
|
||||||
self.assertEqual(quality["label"], "MEDIUM")
|
|
||||||
|
|
||||||
def test_load_match_data_returns_none_when_team_ids_missing(self):
|
|
||||||
live_row = {
|
|
||||||
"match_id": "live-missing-ids",
|
|
||||||
"home_team_id": None,
|
|
||||||
"away_team_id": None,
|
|
||||||
"league_id": "l-1",
|
|
||||||
"sport": "football",
|
|
||||||
"match_date_ms": 1760000000000,
|
|
||||||
"odds": None,
|
|
||||||
"lineups": None,
|
|
||||||
"sidelined": None,
|
|
||||||
"referee_name": None,
|
|
||||||
"home_team_name": "Home",
|
|
||||||
"away_team_name": "Away",
|
|
||||||
"league_name": "League",
|
|
||||||
}
|
|
||||||
cursor = _RouterCursor(live_row=live_row)
|
|
||||||
|
|
||||||
with patch("services.single_match_orchestrator.psycopg2.connect", return_value=_ConnContext(cursor)):
|
|
||||||
data = self.orchestrator._load_match_data("live-missing-ids")
|
|
||||||
|
|
||||||
self.assertIsNone(data)
|
|
||||||
|
|
||||||
def test_decorate_market_row_blocks_required_market_when_odds_missing(self):
|
|
||||||
data = MatchData(
|
|
||||||
match_id="dq-odds",
|
|
||||||
home_team_id="h",
|
|
||||||
away_team_id="a",
|
|
||||||
home_team_name="Home",
|
|
||||||
away_team_name="Away",
|
|
||||||
match_date_ms=1760000000000,
|
|
||||||
sport="football",
|
|
||||||
league_id="l1",
|
|
||||||
league_name="League",
|
|
||||||
referee_name="Ref",
|
|
||||||
odds_data={"ms_h": 2.2, "ms_d": 3.2, "ms_a": 3.0},
|
|
||||||
home_lineup=["h"] * 11,
|
|
||||||
away_lineup=["a"] * 11,
|
|
||||||
sidelined_data=None,
|
|
||||||
home_goals_avg=1.5,
|
|
||||||
home_conceded_avg=1.2,
|
|
||||||
away_goals_avg=1.2,
|
|
||||||
away_conceded_avg=1.4,
|
|
||||||
home_position=7,
|
|
||||||
away_position=9,
|
|
||||||
lineup_source="confirmed_live",
|
|
||||||
)
|
|
||||||
prediction = FullMatchPrediction(match_id="dq-odds", home_team="Home", away_team="Away")
|
|
||||||
quality = self.orchestrator._compute_data_quality(data)
|
|
||||||
row = {
|
|
||||||
"market": "HT_OU05",
|
|
||||||
"pick": "İY 0.5 Üst",
|
|
||||||
"probability": 0.65,
|
|
||||||
"confidence": 66.0,
|
|
||||||
"odds": 0.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
out = self.orchestrator._decorate_market_row(data, prediction, quality, row)
|
|
||||||
self.assertFalse(out["playable"])
|
|
||||||
self.assertIn("market_odds_missing", out["decision_reasons"])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
"""
|
|
||||||
Unit Test for NEW Skip Logic in BetRecommender
|
|
||||||
==============================================
|
|
||||||
Run with: python ai-engine/tests/test_skip_logic.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
# Add paths
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
|
||||||
|
|
||||||
from core.calculators.bet_recommender import BetRecommender, RecommendationResult, MarketPredictionDTO
|
|
||||||
from core.calculators.risk_assessor import RiskAnalysis
|
|
||||||
from core.calculators.match_result_calculator import MatchResultPrediction
|
|
||||||
from core.calculators.over_under_calculator import OverUnderPrediction
|
|
||||||
from config.config_loader import get_config
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DummyContext:
|
|
||||||
"""Minimal mock for CalculationContext"""
|
|
||||||
odds_data: dict
|
|
||||||
|
|
||||||
class TestSkipLogic(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# Mock config to pass into BetRecommender
|
|
||||||
self.mock_config = {
|
|
||||||
"recommendations.market_weights": {"MS": 1.0, "ÇŞ": 0.9, "BTTS": 0.9, "2.5 Üst/Alt": 0.9},
|
|
||||||
"recommendations.safe_markets": ["ÇŞ", "1.5 Üst/Alt"],
|
|
||||||
"recommendations.market_accuracy": {"MS": 65, "ÇŞ": 75, "BTTS": 60, "2.5 Üst/Alt": 65},
|
|
||||||
"recommendations.baseline_accuracy": 65.0,
|
|
||||||
"recommendations.confidence_threshold": 60,
|
|
||||||
"recommendations.value_confidence_min": 45,
|
|
||||||
"recommendations.value_confidence_max": 60,
|
|
||||||
"recommendations.value_edge_margin": 0.03,
|
|
||||||
"recommendations.value_upgrade_edge": 5.0,
|
|
||||||
"recommendations.risk_safe_boost": 1.2,
|
|
||||||
"recommendations.risk_ms_penalty_high": 0.5,
|
|
||||||
"recommendations.risk_other_penalty": 0.7,
|
|
||||||
"recommendations.risk_ms_penalty_medium": 0.8,
|
|
||||||
}
|
|
||||||
self.recommender = BetRecommender(self.mock_config)
|
|
||||||
|
|
||||||
def _make_risk(self, level="MEDIUM", is_surprise=False):
|
|
||||||
return RiskAnalysis(risk_level=level, is_surprise_risk=is_surprise, risk_score=0.5)
|
|
||||||
|
|
||||||
def _make_ms_pred(self, pick, conf):
|
|
||||||
# pick: "1", "X", "2"
|
|
||||||
probs = {"1": {"ms_home_prob": 0.5, "ms_draw_prob": 0.3, "ms_away_prob": 0.2},
|
|
||||||
"X": {"ms_home_prob": 0.2, "ms_draw_prob": 0.5, "ms_away_prob": 0.3},
|
|
||||||
"2": {"ms_home_prob": 0.2, "ms_draw_prob": 0.3, "ms_away_prob": 0.5}}
|
|
||||||
p = probs.get(pick, probs["1"])
|
|
||||||
return MatchResultPrediction(
|
|
||||||
ms_pick=pick, ms_confidence=conf,
|
|
||||||
dc_pick="1X", dc_confidence=0,
|
|
||||||
dc_1x_prob=0.7, dc_x2_prob=0.7, dc_12_prob=0.7,
|
|
||||||
**p
|
|
||||||
)
|
|
||||||
|
|
||||||
def _make_ou_pred(self):
|
|
||||||
return OverUnderPrediction(
|
|
||||||
ou25_pick="2.5 Üst", ou25_confidence=50.0,
|
|
||||||
over_25_prob=0.55, under_25_prob=0.45,
|
|
||||||
|
|
||||||
btts_pick="Var", btts_confidence=50.0,
|
|
||||||
btts_yes_prob=0.55, btts_no_prob=0.45,
|
|
||||||
|
|
||||||
ou15_pick="1.5 Üst", ou15_confidence=60.0, over_15_prob=0.7, under_15_prob=0.3,
|
|
||||||
ou35_pick="3.5 Alt", ou35_confidence=50.0, over_35_prob=0.3, under_35_prob=0.7
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_low_confidence_should_skip(self):
|
|
||||||
"""Confidence < 45% should be SKIPPED"""
|
|
||||||
ms_pred = self._make_ms_pred(pick="2", conf=40.0)
|
|
||||||
ou_pred = self._make_ou_pred()
|
|
||||||
risk = self._make_risk("MEDIUM")
|
|
||||||
ctx = DummyContext(odds_data={"ms_2": 2.5})
|
|
||||||
|
|
||||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
|
||||||
|
|
||||||
# Check if MS bet is skipped
|
|
||||||
ms_bet = next((b for b in res.skipped_bets if b.market_type == "MS"), None)
|
|
||||||
self.assertIsNotNone(ms_bet, "MS bet with 40% conf should be skipped!")
|
|
||||||
self.assertTrue(ms_bet.is_skip)
|
|
||||||
|
|
||||||
def test_good_confidence_should_recommend(self):
|
|
||||||
"""Confidence > 60% and Good Odds should be RECOMMENDED"""
|
|
||||||
ms_pred = self._make_ms_pred(pick="1", conf=70.0)
|
|
||||||
ou_pred = self._make_ou_pred()
|
|
||||||
risk = self._make_risk("MEDIUM")
|
|
||||||
# Odds 1.80 for 70% prob = Good Value (Need real odds for MS to pass)
|
|
||||||
ctx = DummyContext(odds_data={"ms_1": 1.80, "ou15_o": 1.50}) # Added ou15 odds
|
|
||||||
|
|
||||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
|
||||||
|
|
||||||
# Check if ANY bet is recommended (doesn't have to be MS, but usually is)
|
|
||||||
self.assertGreater(len(res.recommended_bets), 0, "At least one bet should be recommended!")
|
|
||||||
# Check that MS bet is NOT skipped
|
|
||||||
ms_bet = next((b for b in res.recommended_bets if b.market_type == "MS"), None)
|
|
||||||
if ms_bet:
|
|
||||||
self.assertFalse(ms_bet.is_skip)
|
|
||||||
|
|
||||||
def test_negative_edge_should_skip(self):
|
|
||||||
"""Even with high confidence, if Odds are too low (Bad Value), SKIP"""
|
|
||||||
ms_pred = self._make_ms_pred(pick="1", conf=70.0) # 70% prob
|
|
||||||
ou_pred = self._make_ou_pred()
|
|
||||||
risk = self._make_risk("MEDIUM")
|
|
||||||
# Odds 1.10 -> Implied 90%. Our prob is 70%. Edge is -20% -> SKIP
|
|
||||||
ctx = DummyContext(odds_data={"ms_1": 1.10})
|
|
||||||
|
|
||||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
|
||||||
|
|
||||||
ms_bet = next((b for b in res.skipped_bets if b.market_type == "MS"), None)
|
|
||||||
self.assertIsNotNone(ms_bet, "MS bet with terrible odds (Negative Edge) should be skipped!")
|
|
||||||
self.assertTrue(ms_bet.is_skip)
|
|
||||||
|
|
||||||
def test_no_bets_recommendation(self):
|
|
||||||
"""If all bets are low confidence, best_bet should be None"""
|
|
||||||
ms_pred = self._make_ms_pred(pick="1", conf=30.0) # Very low conf
|
|
||||||
ou_pred = self._make_ou_pred()
|
|
||||||
# Reset ALL OU confs to low
|
|
||||||
ou_pred.ou25_confidence = 30.0
|
|
||||||
ou_pred.btts_confidence = 30.0
|
|
||||||
ou_pred.ou15_confidence = 30.0 # This was 60 in setUp, causing the fail!
|
|
||||||
ou_pred.ou35_confidence = 30.0
|
|
||||||
|
|
||||||
risk = self._make_risk("MEDIUM")
|
|
||||||
ctx = DummyContext(odds_data={"ms_1": 2.0})
|
|
||||||
|
|
||||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
|
||||||
|
|
||||||
self.assertIsNone(res.best_bet, "If everything is skipped, there should be no best_bet.")
|
|
||||||
self.assertEqual(len(res.recommended_bets), 0, "No bets should be recommended!")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print("🧪 Running Skip Logic Unit Tests...")
|
|
||||||
print("="*50)
|
|
||||||
unittest.main(verbosity=2)
|
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Changelog - 2026-04-22
|
||||||
|
|
||||||
|
Bu doküman, 22 Nisan 2026 tarihinde `iddaai-fe` ve `iddaai-be` üzerinde yapılan Frekans Motoru (Conditional Frequency Engine) frontend entegrasyonunu özetler.
|
||||||
|
|
||||||
|
## 1. Frekans Motoru — Backend Recap
|
||||||
|
|
||||||
|
- `POST /coupon/frequency-coupon` endpoint'i önceki oturumda tamamlanmıştı.
|
||||||
|
- `SmartCouponService.generateFrequencyBasedCoupon()` metodu aktif ve çalışır durumda.
|
||||||
|
- `FrequencyEngineService` → raw SQL ile `matches` tablosundaki tarihsel veriyi tarayarak oran bandı bazlı sinyal üretiyor.
|
||||||
|
- Strateji: Her takımın ev/deplasman performansını, karşılaştığı oran bandına göre filtreleyip, kombine sinyal (combined_signal) hesaplıyor.
|
||||||
|
|
||||||
|
## 2. Frontend Tip Tanımları
|
||||||
|
|
||||||
|
- `iddaai-fe/src/lib/api/coupons/types.ts` güncellendi.
|
||||||
|
- Eklenen tipler:
|
||||||
|
|
||||||
|
### FrequencyCouponRequestDto
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
maxMatches?: number; // 2-5 arası, varsayılan 3
|
||||||
|
minSignal?: number; // 0.50-0.99, kombine sinyal eşiği
|
||||||
|
markets?: string[]; // OU1.5, OU2.5, OU3.5, BTTS, MS
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FrequencyCouponBetDto
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
match_id: string;
|
||||||
|
match_name: string;
|
||||||
|
league: string;
|
||||||
|
market: string;
|
||||||
|
pick: string;
|
||||||
|
odds: number;
|
||||||
|
home_signal: number;
|
||||||
|
away_signal: number;
|
||||||
|
combined_signal: number;
|
||||||
|
home_odds_band: string;
|
||||||
|
away_odds_band: string;
|
||||||
|
home_match_count: number;
|
||||||
|
away_match_count: number;
|
||||||
|
league_profile: string; // GOLCU | DEFANSIF | NORMAL
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FrequencyCouponRejectedDto
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
match_name: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FrequencyCouponResultDto
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
bets: FrequencyCouponBetDto[];
|
||||||
|
rejected_matches: FrequencyCouponRejectedDto[];
|
||||||
|
reasoning: string[];
|
||||||
|
total_odds: number;
|
||||||
|
expected_hit_rate: number;
|
||||||
|
expected_value: number;
|
||||||
|
ev_positive: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. API Service Katmanı
|
||||||
|
|
||||||
|
- `iddaai-fe/src/lib/api/coupons/service.ts` güncellendi.
|
||||||
|
- `generateFrequencyCoupon(dto)` metodu eklendi.
|
||||||
|
- Endpoint: `POST /coupon/frequency-coupon`
|
||||||
|
|
||||||
|
## 4. React Hook
|
||||||
|
|
||||||
|
- `iddaai-fe/src/lib/api/coupons/use-hooks.ts` güncellendi.
|
||||||
|
- `useGenerateFrequencyCoupon()` TanStack Query mutation hook'u eklendi.
|
||||||
|
- `FrequencyCouponRequestDto` import edildi.
|
||||||
|
|
||||||
|
## 5. Çeviri Dosyaları (i18n)
|
||||||
|
|
||||||
|
- `messages/tr.json` ve `messages/en.json` güncellendi.
|
||||||
|
- `coupons` namespace'ine 30+ yeni anahtar eklendi:
|
||||||
|
|
||||||
|
| Anahtar | TR | EN |
|
||||||
|
|---|---|---|
|
||||||
|
| `freq-engine-title` | Frekans Motoru | Frequency Engine |
|
||||||
|
| `freq-engine-subtitle` | Takımların oran bandına göre tarihsel performansını analiz eder... | Analyzes teams' historical performance by odds band... |
|
||||||
|
| `freq-suggest` | Frekans Kuponu Oluştur | Generate Frequency Coupon |
|
||||||
|
| `freq-min-signal` | Minimum Sinyal | Minimum Signal |
|
||||||
|
| `freq-ev-label` | Beklenen Değer (EV) | Expected Value (EV) |
|
||||||
|
| `freq-hit-rate` | Tahmini İsabet | Est. Hit Rate |
|
||||||
|
| `freq-ev-positive` | +EV Pozitif | +EV Positive |
|
||||||
|
| `freq-combined-signal` | Kombine Sinyal | Combined Signal |
|
||||||
|
| `freq-league-golcu` | Golcü | High-Scoring |
|
||||||
|
| `freq-league-defansif` | Defansif | Defensive |
|
||||||
|
| `engine-mode-label` | Motor Seçimi | Engine Mode |
|
||||||
|
| `engine-mode-help` | AI: Gemini tabanlı yapay zeka tahmini. Frekans: Veritabanı tabanlı istatistiksel analiz. | AI: Gemini-based AI prediction. Frequency: Database-driven statistical analysis. |
|
||||||
|
| `freq-mode-active` | Frekans Motoru aktif | Frequency Engine active |
|
||||||
|
| `ai-mode-active` | AI Motoru aktif | AI Engine active |
|
||||||
|
|
||||||
|
## 6. FrequencyPanel Bileşeni (Yeni Dosya)
|
||||||
|
|
||||||
|
- `iddaai-fe/src/components/coupons/frequency-panel.tsx` oluşturuldu.
|
||||||
|
- Bağımsız (standalone) bileşen, kendi state ve mutation yönetimini içerir.
|
||||||
|
|
||||||
|
### Bileşen Özellikleri:
|
||||||
|
1. **Min Signal Slider** — 50%-95% arası, kombine sinyal eşiği kontrolü
|
||||||
|
2. **Max Matches Slider** — 2-5 arası, kupon boyutu kontrolü
|
||||||
|
3. **Market Filtre Badge'leri** — OU1.5, OU2.5, OU3.5, BTTS, MS (çoklu seçim)
|
||||||
|
4. **Generate Butonu** → `useGenerateFrequencyCoupon` mutation'ını tetikler
|
||||||
|
5. **Sonuç Paneli**:
|
||||||
|
- EV / Hit Rate / Toplam Oran istatistik kartları
|
||||||
|
- Her bahis için ev sinyali, deplasman sinyali, kombine sinyal gösterimi
|
||||||
|
- Oran bandı bilgisi (ör. "1.30-1.50")
|
||||||
|
- Lig profili badge'i (Golcü/Defansif/Normal)
|
||||||
|
- Geçmiş maç sayısı gösterimi
|
||||||
|
- Analiz detayları (reasoning listesi)
|
||||||
|
- Elenen maçlar (rejected_matches)
|
||||||
|
6. **Kupon Store Senkronizasyonu** — Sonuç geldiğinde bahisler otomatik olarak `useCouponStore`'a eklenir
|
||||||
|
|
||||||
|
## 7. Coupon Builder Engine Toggle
|
||||||
|
|
||||||
|
- `iddaai-fe/src/components/coupons/coupon-builder-content.tsx` güncellendi.
|
||||||
|
- Değişiklikler:
|
||||||
|
- `LuDatabase` icon import edildi
|
||||||
|
- `FrequencyPanel` import edildi
|
||||||
|
- `engineMode` state eklendi: `"ai" | "frequency"`
|
||||||
|
- Sidebar'a **Motor Seçimi** toggle eklendi (Badge tabanlı)
|
||||||
|
- `engineMode === "frequency"` olduğunda strateji/AI suggest bölümü gizlenir, yerine `FrequencyPanel` render edilir
|
||||||
|
- `engineMode === "ai"` olduğunda mevcut AI akışı aynen korunur
|
||||||
|
|
||||||
|
### Veri Akışı:
|
||||||
|
```
|
||||||
|
Kullanıcı → "Frekans" badge'ine tıklar → FrequencyPanel açılır
|
||||||
|
→ Sinyal/market/boyut ayarı yapar → "Frekans Kuponu Oluştur" butonuna basar
|
||||||
|
→ POST /coupon/frequency-coupon { maxMatches, minSignal, markets }
|
||||||
|
→ Backend: SmartCouponService → FrequencyEngineService (raw SQL)
|
||||||
|
→ Response: FrequencyCouponResultDto
|
||||||
|
→ UI: Sinyal kartları, EV istatistikleri, reasoning render edilir
|
||||||
|
→ Bahisler otomatik olarak CouponStore'a sync edilir
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Derleme ve Doğrulama Notları
|
||||||
|
|
||||||
|
- `node_modules` kullanıcının makinesinde yüklü olmadığı için `npm run build` çalıştırılamadı.
|
||||||
|
- Kod yapısal olarak doğru, TypeScript tipleri backend DTO'ları ile birebir eşleşiyor.
|
||||||
|
- Doğrulama için: `npm install && npm run build` çalıştırılmalı.
|
||||||
|
|
||||||
|
## 9. Açık Kalan / Sonraki Adımlar
|
||||||
|
|
||||||
|
- `npm install && npm run build` ile frontend build doğrulanmalı.
|
||||||
|
- Frekans kuponu uçtan uca test edilmeli (backend Docker ayakta iken).
|
||||||
|
- FrequencyPanel içindeki market badge'lerine `HT_OU05` ve `DC` gibi ek marketler eklenebilir.
|
||||||
|
- Frekans sonuçlarındaki `league_profile` badge renkleri dark mode için ince ayar gerektirebilir.
|
||||||
|
- Kupon geçmişinde AI vs Frekans ayrımını gösteren bir etiket eklenebilir.
|
||||||
@@ -29,6 +29,13 @@
|
|||||||
"cleanup:live": "ts-node -r tsconfig-paths/register src/scripts/cleanup-live-matches.ts",
|
"cleanup:live": "ts-node -r tsconfig-paths/register src/scripts/cleanup-live-matches.ts",
|
||||||
"swagger:summary": "ts-node -r tsconfig-paths/register src/scripts/export-swagger-endpoints-summary.ts",
|
"swagger:summary": "ts-node -r tsconfig-paths/register src/scripts/export-swagger-endpoints-summary.ts",
|
||||||
"postman:export": "ts-node -r tsconfig-paths/register src/scripts/export-postman-collection.ts"
|
"postman:export": "ts-node -r tsconfig-paths/register src/scripts/export-postman-collection.ts"
|
||||||
|
,
|
||||||
|
"ai:extract:v26": "python3 ai-engine/scripts/extract_training_data_v26.py",
|
||||||
|
"ai:train:v26": "python3 ai-engine/scripts/train_v26_shadow.py",
|
||||||
|
"ai:backtest:v26": "python3 ai-engine/scripts/backtest_v26_shadow.py",
|
||||||
|
"ai:backtest:v26:roi": "python3 ai-engine/scripts/backtest_v26_shadow_roi_detail.py",
|
||||||
|
"ai:backtest:v26:htft": "python3 ai-engine/scripts/backtest_v26_shadow_htft_upset.py",
|
||||||
|
"ai:test": "python3 -m pytest ai-engine/tests/test_main_api.py ai-engine/tests/test_single_match_orchestrator.py ai-engine/tests/test_v26_shadow_engine.py"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.964.0",
|
"@aws-sdk/client-s3": "^3.964.0",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
CREATE TABLE "prediction_runs" (
|
||||||
|
"id" BIGSERIAL NOT NULL,
|
||||||
|
"match_id" TEXT NOT NULL,
|
||||||
|
"engine_version" TEXT NOT NULL,
|
||||||
|
"decision_trace_id" TEXT,
|
||||||
|
"generated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"odds_snapshot" JSONB,
|
||||||
|
"payload_summary" JSONB NOT NULL,
|
||||||
|
"eventual_outcome" TEXT,
|
||||||
|
"unit_profit" DOUBLE PRECISION,
|
||||||
|
|
||||||
|
CONSTRAINT "prediction_runs_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "prediction_runs_match_id_generated_at_idx"
|
||||||
|
ON "prediction_runs"("match_id", "generated_at" DESC);
|
||||||
|
|
||||||
|
CREATE INDEX "prediction_runs_engine_version_generated_at_idx"
|
||||||
|
ON "prediction_runs"("engine_version", "generated_at" DESC);
|
||||||
@@ -489,6 +489,22 @@ model Prediction {
|
|||||||
@@map("predictions")
|
@@map("predictions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model PredictionRun {
|
||||||
|
id BigInt @id @default(autoincrement())
|
||||||
|
matchId String @map("match_id")
|
||||||
|
engineVersion String @map("engine_version")
|
||||||
|
decisionTraceId String? @map("decision_trace_id")
|
||||||
|
generatedAt DateTime @default(now()) @map("generated_at")
|
||||||
|
oddsSnapshot Json? @map("odds_snapshot")
|
||||||
|
payloadSummary Json @map("payload_summary")
|
||||||
|
eventualOutcome String? @map("eventual_outcome")
|
||||||
|
unitProfit Float? @map("unit_profit")
|
||||||
|
|
||||||
|
@@index([matchId, generatedAt(sort: Desc)])
|
||||||
|
@@index([engineVersion, generatedAt(sort: Desc)])
|
||||||
|
@@map("prediction_runs")
|
||||||
|
}
|
||||||
|
|
||||||
model AiPredictionsLog {
|
model AiPredictionsLog {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
matchId String @map("match_id")
|
matchId String @map("match_id")
|
||||||
|
|||||||
+330
-695
File diff suppressed because it is too large
Load Diff
@@ -1,109 +0,0 @@
|
|||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import torch
|
|
||||||
import torch.nn.functional as F
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# Path alignment
|
|
||||||
sys.path.append(os.getcwd())
|
|
||||||
sys.path.append(os.path.join(os.getcwd(), 'ai-engine'))
|
|
||||||
|
|
||||||
from pipeline.tiered_loader import TieredDataLoader
|
|
||||||
from pipeline.sequence_builder import SequenceBuilder
|
|
||||||
from models.hybrid_v11 import HybridDeepModel
|
|
||||||
from features.odds_history import OddsHistoryEngine
|
|
||||||
from features.synthetic_xg import SyntheticXGModel
|
|
||||||
|
|
||||||
DEVICE = 'cpu'
|
|
||||||
MODEL_PATH = 'ai-engine/models/v11_hybrid_model.pth'
|
|
||||||
TARGET_ID = 'en78ih6ec7exnpxcku3xc3das'
|
|
||||||
|
|
||||||
def audit():
|
|
||||||
print(f"🕵️ Auditing Match: {TARGET_ID}")
|
|
||||||
|
|
||||||
# 1. Pipeline Data
|
|
||||||
builder = SequenceBuilder()
|
|
||||||
X, y, meta = builder.build_sequences()
|
|
||||||
|
|
||||||
# Check if target is in dataset
|
|
||||||
idx_list = meta.index[meta['match_id'] == TARGET_ID].tolist()
|
|
||||||
if not idx_list:
|
|
||||||
print("❌ Match not found in generated sequences. Is it too old or too new?")
|
|
||||||
return
|
|
||||||
|
|
||||||
idx = idx_list[0]
|
|
||||||
row_meta = meta.iloc[idx]
|
|
||||||
|
|
||||||
# 2. Features
|
|
||||||
loader = TieredDataLoader()
|
|
||||||
odds_df = loader.load_gold_data([TARGET_ID])
|
|
||||||
eng = OddsHistoryEngine()
|
|
||||||
xg_model = SyntheticXGModel()
|
|
||||||
|
|
||||||
# Team Mapping
|
|
||||||
unique_teams = meta['team_id'].unique()
|
|
||||||
team_map = {tid: i for i, tid in enumerate(unique_teams)}
|
|
||||||
|
|
||||||
# 3. Predict exactly like Backtest
|
|
||||||
state = torch.load(MODEL_PATH, map_location=DEVICE)
|
|
||||||
emb_key = 'entity_emb.weight' if 'entity_emb.weight' in state else 'team_embedding.weight'
|
|
||||||
saved_vocab_size = state[emb_key].shape[0]
|
|
||||||
|
|
||||||
model = HybridDeepModel(num_teams=saved_vocab_size)
|
|
||||||
new_state = {k.replace('team_embedding', 'entity_emb'): v for k, v in state.items()}
|
|
||||||
model.load_state_dict(new_state, strict=False)
|
|
||||||
model.eval()
|
|
||||||
|
|
||||||
# Data components
|
|
||||||
team_idx = team_map.get(row_meta['team_id'], 0)
|
|
||||||
entities = torch.LongTensor([team_idx, 0]).unsqueeze(0)
|
|
||||||
seq = torch.FloatTensor(X[idx]).unsqueeze(0)
|
|
||||||
|
|
||||||
# Context (Odds + xG)
|
|
||||||
odds_lookup = {}
|
|
||||||
for _, r in odds_df.iterrows():
|
|
||||||
mid = r['match_id']
|
|
||||||
if mid not in odds_lookup: odds_lookup[mid] = {}
|
|
||||||
if r['category'] == 'Maç Sonucu': odds_lookup[mid][r['selection']] = r['odd_value']
|
|
||||||
elif r['category'] == '2,5 Alt/Üst':
|
|
||||||
if 'Üst' in r['selection']: odds_lookup[mid]['Over'] = r['odd_value']
|
|
||||||
else: odds_lookup[mid]['Under'] = r['odd_value']
|
|
||||||
|
|
||||||
odds = odds_lookup.get(TARGET_ID, {'1': 1.0, 'X': 1.0, '2': 1.0, 'Over': 1.0, 'Under': 1.0})
|
|
||||||
syn_xg = 1.35 # Placeholder in trainer for xG component if used
|
|
||||||
hist_win_rate = eng.get_feature(row_meta['team_id'], float(odds.get('1', 1.0)))
|
|
||||||
|
|
||||||
ctx = torch.FloatTensor([
|
|
||||||
float(odds.get('1', 1.0)), float(odds.get('X', 1.0)), float(odds.get('2', 1.0)),
|
|
||||||
float(odds.get('Over', 1.0)), float(odds.get('Under', 1.0)),
|
|
||||||
syn_xg, syn_xg,
|
|
||||||
hist_win_rate
|
|
||||||
]).unsqueeze(0)
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
logits_res, pred_goals, logits_btts, logits_ht_ft = model(entities, seq, ctx)
|
|
||||||
probs = F.softmax(logits_res, dim=1).numpy()[0]
|
|
||||||
prob_btts = torch.sigmoid(logits_btts).item()
|
|
||||||
probs_ht = F.softmax(logits_ht_ft, dim=1).numpy()[0]
|
|
||||||
|
|
||||||
print("\n📊 INTERNAL PIPELINE PREDICTION:")
|
|
||||||
print(f"Target Team: {row_meta['team_id']}")
|
|
||||||
print(f"1X2 Probs: Home:{probs[0]:.4f} Draw:{probs[1]:.4f} Away:{probs[2]:.4f}")
|
|
||||||
print(f"BTTS Prob: {prob_btts:.4f}")
|
|
||||||
|
|
||||||
ht_map = ["1/1", "1/X", "1/2", "X/1", "X/X", "X/2", "2/1", "2/X", "2/2"]
|
|
||||||
top3_ht = np.argsort(probs_ht)[-3:][::-1]
|
|
||||||
print("Top 3 HT/FT:")
|
|
||||||
for idx_ht in top3_ht:
|
|
||||||
print(f" {ht_map[idx_ht]}: {probs_ht[idx_ht]:.4f}")
|
|
||||||
|
|
||||||
actual_res = y[idx][0]
|
|
||||||
actual_ht_idx = int(y[idx][3])
|
|
||||||
print(f"\n✅ ACTUAL REALITY:")
|
|
||||||
print(f"Result (Y): {actual_res} (0.0=Away)")
|
|
||||||
print(f"HT/FT Class: {actual_ht_idx} ({ht_map[actual_ht_idx]})")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
audit()
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Test surprise detection on known surprise matches."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, 'ai-engine')
|
|
||||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Test Bayern vs Augsburg (24 Jan 2026) - 1/2 Reversal
|
|
||||||
match_id = 'en78ih6ec7exnpxcku3xc3das'
|
|
||||||
|
|
||||||
orch = SingleMatchOrchestrator()
|
|
||||||
result = orch.analyze_match(match_id)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
print('=== Bayern Munch vs Augsburg (24 Jan 2026) ===')
|
|
||||||
print('Actual: HT 1-0, FT 1-2 (1/2 Reversal!)')
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check risk
|
|
||||||
risk = result.get('risk', {})
|
|
||||||
print(f"Risk Level: {risk.get('level', 'N/A')}")
|
|
||||||
print(f"Is Surprise Risk: {risk.get('is_surprise_risk', False)}")
|
|
||||||
print(f"Surprise Type: {risk.get('surprise_type', 'N/A')}")
|
|
||||||
print(f"Risk Score: {risk.get('score', 'N/A')}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check HT/FT probabilities from market_board
|
|
||||||
htft = result.get('market_board', {}).get('HTFT', {}).get('probs', {})
|
|
||||||
print('HT/FT Probabilities:')
|
|
||||||
if htft:
|
|
||||||
for k, v in sorted(htft.items(), key=lambda x: x[1], reverse=True):
|
|
||||||
print(f" {k}: {v*100:.1f}%")
|
|
||||||
else:
|
|
||||||
print(" EMPTY!")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check main pick
|
|
||||||
main = result.get('main_pick', {})
|
|
||||||
print(f"Main Pick: {main.get('market', 'N/A')} - {main.get('pick', 'N/A')}")
|
|
||||||
print(f"Confidence: {main.get('calibrated_confidence', 'N/A')}%")
|
|
||||||
print(f"Is Guaranteed: {main.get('is_guaranteed', False)}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check aggressive pick
|
|
||||||
agg = result.get('aggressive_pick', {})
|
|
||||||
if agg:
|
|
||||||
print(f"Aggressive Pick: {agg.get('market', 'N/A')} - {agg.get('pick', 'N/A')}")
|
|
||||||
print(f"Odds: {agg.get('odds', 'N/A')}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check bet_summary for HTFT
|
|
||||||
bet_summary = result.get('bet_summary', [])
|
|
||||||
for bet in bet_summary:
|
|
||||||
if bet.get('market') == 'HTFT':
|
|
||||||
print(f"HTFT Bet: {bet}")
|
|
||||||
else:
|
|
||||||
print('Match not found')
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Test the improved surprise detection logic"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, 'ai-engine')
|
|
||||||
|
|
||||||
from core.calculators.risk_assessor import RiskAssessor
|
|
||||||
from config.config_loader import get_config
|
|
||||||
|
|
||||||
def test_surprise_detection():
|
|
||||||
config = get_config()
|
|
||||||
assessor = RiskAssessor(config)
|
|
||||||
|
|
||||||
# Test cases based on real scenarios
|
|
||||||
test_cases = [
|
|
||||||
{
|
|
||||||
'name': 'Bayern vs Augsburg (1.30 odds, 2% 1/2 prob)',
|
|
||||||
'odds': {'ms_h': 1.30, 'ms_d': 5.00, 'ms_a': 8.00},
|
|
||||||
'ht_ft': {'1/1': 0.30, '1/X': 0.07, '1/2': 0.02, 'X/1': 0.15, 'X/X': 0.16, 'X/2': 0.09, '2/1': 0.03, '2/X': 0.04, '2/2': 0.14},
|
|
||||||
'expected_surprise': True,
|
|
||||||
'expected_type': '1/2 Potential Upset'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Strong favorite (1.20 odds, 1.5% 1/2 prob)',
|
|
||||||
'odds': {'ms_h': 1.20, 'ms_d': 6.00, 'ms_a': 12.00},
|
|
||||||
'ht_ft': {'1/1': 0.35, '1/X': 0.05, '1/2': 0.015, 'X/1': 0.20, 'X/X': 0.15, 'X/2': 0.05, '2/1': 0.02, '2/X': 0.03, '2/2': 0.10},
|
|
||||||
'expected_surprise': True,
|
|
||||||
'expected_type': '1/2 Potential Upset'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Moderate favorite (1.50 odds, 3% 1/2 prob)',
|
|
||||||
'odds': {'ms_h': 1.50, 'ms_d': 4.00, 'ms_a': 6.00},
|
|
||||||
'ht_ft': {'1/1': 0.28, '1/X': 0.08, '1/2': 0.03, 'X/1': 0.18, 'X/X': 0.15, 'X/2': 0.08, '2/1': 0.04, '2/X': 0.05, '2/2': 0.11},
|
|
||||||
'expected_surprise': True,
|
|
||||||
'expected_type': '1/2 Potential Upset'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Even match (2.00 odds, 5% 1/2 prob)',
|
|
||||||
'odds': {'ms_h': 2.00, 'ms_d': 3.30, 'ms_a': 3.30},
|
|
||||||
'ht_ft': {'1/1': 0.20, '1/X': 0.10, '1/2': 0.05, 'X/1': 0.15, 'X/X': 0.15, 'X/2': 0.10, '2/1': 0.05, '2/X': 0.10, '2/2': 0.10},
|
|
||||||
'expected_surprise': False, # No clear favorite
|
|
||||||
'expected_type': None
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Away favorite (1.40 away odds, 2% 2/1 prob)',
|
|
||||||
'odds': {'ms_h': 6.00, 'ms_d': 4.00, 'ms_a': 1.40},
|
|
||||||
'ht_ft': {'1/1': 0.10, '1/X': 0.05, '1/2': 0.04, 'X/1': 0.08, 'X/X': 0.15, 'X/2': 0.20, '2/1': 0.02, '2/X': 0.06, '2/2': 0.30},
|
|
||||||
'expected_surprise': True,
|
|
||||||
'expected_type': '2/1 Potential Upset'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
print("=" * 70)
|
|
||||||
print("SURPRISE DETECTION TEST RESULTS")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
passed = 0
|
|
||||||
failed = 0
|
|
||||||
|
|
||||||
for tc in test_cases:
|
|
||||||
class MockCtx:
|
|
||||||
is_surprise = False
|
|
||||||
is_top_league = True
|
|
||||||
sport = 'football'
|
|
||||||
xgboost_preds = {'ht_ft': tc['ht_ft']}
|
|
||||||
odds_data = tc['odds']
|
|
||||||
|
|
||||||
result = assessor.assess_risk(MockCtx())
|
|
||||||
|
|
||||||
# Check if result matches expectation
|
|
||||||
is_correct = result.is_surprise_risk == tc['expected_surprise']
|
|
||||||
if tc['expected_type'] and result.surprise_type != tc['expected_type']:
|
|
||||||
is_correct = False
|
|
||||||
|
|
||||||
status = "✅ PASS" if is_correct else "❌ FAIL"
|
|
||||||
if is_correct:
|
|
||||||
passed += 1
|
|
||||||
else:
|
|
||||||
failed += 1
|
|
||||||
|
|
||||||
print(f"\n{status} - {tc['name']}")
|
|
||||||
print(f" Expected: surprise={tc['expected_surprise']}, type={tc['expected_type']}")
|
|
||||||
print(f" Got: surprise={result.is_surprise_risk}, type={result.surprise_type}")
|
|
||||||
if result.reasons:
|
|
||||||
print(f" Reasons: {result.reasons}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
print(f"SUMMARY: {passed} passed, {failed} failed")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
return failed == 0
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
success = test_surprise_detection()
|
|
||||||
sys.exit(0 if success else 1)
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Test UpsetEngine on Bayern vs Augsburg match."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, 'ai-engine')
|
|
||||||
from features.upset_engine import get_upset_engine
|
|
||||||
from data.db import get_clean_dsn
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Get match data
|
|
||||||
conn = psycopg2.connect(get_clean_dsn())
|
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
|
||||||
m.ht_score_home, m.ht_score_away, m.mst_utc,
|
|
||||||
th.name as home_name, ta.name as away_name, l.name as league
|
|
||||||
FROM matches m
|
|
||||||
JOIN teams th ON m.home_team_id = th.id
|
|
||||||
JOIN teams ta ON m.away_team_id = ta.id
|
|
||||||
JOIN leagues l ON m.league_id = l.id
|
|
||||||
WHERE m.id = 'en78ih6ec7exnpxcku3xc3das'
|
|
||||||
""")
|
|
||||||
match = cur.fetchone()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if match:
|
|
||||||
print('=== Bayern Munch vs Augsburg (24 Jan 2026) ===')
|
|
||||||
print(f"Actual: HT {match['ht_score_home']}-{match['ht_score_away']}, FT {match['score_home']}-{match['score_away']} (1/2 Reversal!)")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Test UpsetEngine
|
|
||||||
engine = get_upset_engine()
|
|
||||||
|
|
||||||
# Calculate upset potential using get_features
|
|
||||||
result = engine.get_features(
|
|
||||||
home_team_name=match['home_name'],
|
|
||||||
home_team_id=match['home_team_id'],
|
|
||||||
away_team_name=match['away_name'],
|
|
||||||
league_name=match['league'],
|
|
||||||
home_position=1, # Bayern is typically top
|
|
||||||
away_position=15, # Augsburg is typically lower
|
|
||||||
match_date_ms=match['mst_utc'],
|
|
||||||
total_teams=18,
|
|
||||||
)
|
|
||||||
|
|
||||||
print('UpsetEngine Results:')
|
|
||||||
print(f" Atmosphere Score: {result.get('upset_atmosphere', 0):.2f}")
|
|
||||||
print(f" Motivation Score: {result.get('upset_motivation', 0):.2f}")
|
|
||||||
print(f" Fatigue Score: {result.get('upset_fatigue', 0):.2f}")
|
|
||||||
print(f" Historical Score: {result.get('upset_historical', 0):.2f}")
|
|
||||||
print(f" TOTAL UPSET POTENTIAL: {result.get('upset_potential', 0):.2f}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check if upset was detected
|
|
||||||
if result.get('upset_potential', 0) > 0.5:
|
|
||||||
print("🔥 HIGH UPSET POTENTIAL DETECTED!")
|
|
||||||
elif result.get('upset_potential', 0) > 0.3:
|
|
||||||
print("⚠️ MEDIUM UPSET POTENTIAL")
|
|
||||||
else:
|
|
||||||
print("❌ LOW UPSET POTENTIAL - Model did not detect this as upset")
|
|
||||||
else:
|
|
||||||
print('Match not found')
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import { AppController } from "./app.controller";
|
|
||||||
import { AppService } from "./app.service";
|
|
||||||
|
|
||||||
describe("AppController", () => {
|
|
||||||
let appController: AppController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const app: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [AppController],
|
|
||||||
providers: [AppService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
appController = app.get<AppController>(AppController);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("root", () => {
|
|
||||||
it('should return "Hello World!"', () => {
|
|
||||||
expect(appController.getHello()).toBe("Hello World!");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { UserRole } from "@prisma/client";
|
||||||
|
|
||||||
|
export const APP_ROLES = {
|
||||||
|
user: UserRole.user,
|
||||||
|
superadmin: UserRole.superadmin,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ADMIN_ROLES = [APP_ROLES.superadmin] as const;
|
||||||
|
|
||||||
|
export function normalizeRole(role: string | null | undefined): string {
|
||||||
|
return role?.trim().toLowerCase() ?? "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
import axios, {
|
||||||
|
AxiosError,
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
AxiosResponse,
|
||||||
|
} from "axios";
|
||||||
|
import { Logger } from "@nestjs/common";
|
||||||
|
|
||||||
|
export type AiCircuitState = "closed" | "open";
|
||||||
|
|
||||||
|
export interface AiEngineClientOptions {
|
||||||
|
baseUrl: string;
|
||||||
|
logger: Logger;
|
||||||
|
serviceName: string;
|
||||||
|
timeoutMs?: number;
|
||||||
|
maxRetries?: number;
|
||||||
|
retryDelayMs?: number;
|
||||||
|
circuitBreakerThreshold?: number;
|
||||||
|
circuitBreakerCooldownMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AiEngineRequestConfig extends AxiosRequestConfig {
|
||||||
|
retryCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiEngineClientSnapshot {
|
||||||
|
state: AiCircuitState;
|
||||||
|
consecutiveFailures: number;
|
||||||
|
openedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AiEngineRequestError extends Error {
|
||||||
|
status?: number;
|
||||||
|
detail?: unknown;
|
||||||
|
isCircuitOpen: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
options: {
|
||||||
|
status?: number;
|
||||||
|
detail?: unknown;
|
||||||
|
isCircuitOpen?: boolean;
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AiEngineRequestError";
|
||||||
|
this.status = options.status;
|
||||||
|
this.detail = options.detail;
|
||||||
|
this.isCircuitOpen = options.isCircuitOpen ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AiEngineClient {
|
||||||
|
private readonly axiosClient: AxiosInstance;
|
||||||
|
private readonly logger: Logger;
|
||||||
|
private readonly serviceName: string;
|
||||||
|
private readonly defaultTimeoutMs: number;
|
||||||
|
private readonly maxRetries: number;
|
||||||
|
private readonly retryDelayMs: number;
|
||||||
|
private readonly circuitBreakerThreshold: number;
|
||||||
|
private readonly circuitBreakerCooldownMs: number;
|
||||||
|
|
||||||
|
private consecutiveFailures = 0;
|
||||||
|
private circuitOpenedAt: number | null = null;
|
||||||
|
|
||||||
|
constructor(options: AiEngineClientOptions) {
|
||||||
|
this.logger = options.logger;
|
||||||
|
this.serviceName = options.serviceName;
|
||||||
|
this.defaultTimeoutMs = options.timeoutMs ?? 30000;
|
||||||
|
this.maxRetries = options.maxRetries ?? 2;
|
||||||
|
this.retryDelayMs = options.retryDelayMs ?? 750;
|
||||||
|
this.circuitBreakerThreshold = options.circuitBreakerThreshold ?? 3;
|
||||||
|
this.circuitBreakerCooldownMs =
|
||||||
|
options.circuitBreakerCooldownMs ?? 30000;
|
||||||
|
|
||||||
|
this.axiosClient = axios.create({
|
||||||
|
baseURL: options.baseUrl,
|
||||||
|
timeout: this.defaultTimeoutMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(
|
||||||
|
path: string,
|
||||||
|
config?: AiEngineRequestConfig,
|
||||||
|
): Promise<AxiosResponse<T>> {
|
||||||
|
return this.request<T>({
|
||||||
|
method: "get",
|
||||||
|
url: path,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async post<T>(
|
||||||
|
path: string,
|
||||||
|
data?: unknown,
|
||||||
|
config?: AiEngineRequestConfig,
|
||||||
|
): Promise<AxiosResponse<T>> {
|
||||||
|
return this.request<T>({
|
||||||
|
method: "post",
|
||||||
|
url: path,
|
||||||
|
data,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshot(): AiEngineClientSnapshot {
|
||||||
|
return {
|
||||||
|
state: this.isCircuitOpen() ? "open" : "closed",
|
||||||
|
consecutiveFailures: this.consecutiveFailures,
|
||||||
|
openedAt: this.circuitOpenedAt
|
||||||
|
? new Date(this.circuitOpenedAt).toISOString()
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(config: AiEngineRequestConfig): Promise<AxiosResponse<T>> {
|
||||||
|
this.ensureCircuitAvailable();
|
||||||
|
|
||||||
|
const retries = this.resolveRetryCount(config);
|
||||||
|
let lastError: unknown;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
||||||
|
try {
|
||||||
|
const response = await this.axiosClient.request<T>({
|
||||||
|
timeout: this.defaultTimeoutMs,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resetFailures();
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
const shouldRetry = attempt < retries && this.isRetriableError(error);
|
||||||
|
|
||||||
|
if (!shouldRetry) {
|
||||||
|
this.registerFailure(error);
|
||||||
|
throw this.toRequestError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(
|
||||||
|
`[${this.serviceName}] AI request retry ${attempt + 1}/${retries} for ${config.method?.toUpperCase()} ${config.url}`,
|
||||||
|
);
|
||||||
|
await this.delay(this.retryDelayMs * (attempt + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerFailure(lastError);
|
||||||
|
throw this.toRequestError(lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveRetryCount(config: AiEngineRequestConfig): number {
|
||||||
|
if (typeof config.retryCount === "number" && config.retryCount >= 0) {
|
||||||
|
return config.retryCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.maxRetries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureCircuitAvailable() {
|
||||||
|
if (!this.isCircuitOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingCooldown =
|
||||||
|
this.circuitBreakerCooldownMs - (Date.now() - (this.circuitOpenedAt ?? 0));
|
||||||
|
|
||||||
|
if (remainingCooldown > 0) {
|
||||||
|
throw new AiEngineRequestError("AI engine circuit breaker is open", {
|
||||||
|
status: 503,
|
||||||
|
detail: {
|
||||||
|
cooldownRemainingMs: remainingCooldown,
|
||||||
|
},
|
||||||
|
isCircuitOpen: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(
|
||||||
|
`[${this.serviceName}] AI circuit breaker cooldown elapsed, allowing a recovery attempt`,
|
||||||
|
);
|
||||||
|
this.circuitOpenedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isCircuitOpen(): boolean {
|
||||||
|
return this.circuitOpenedAt !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetFailures() {
|
||||||
|
this.consecutiveFailures = 0;
|
||||||
|
this.circuitOpenedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerFailure(error: unknown) {
|
||||||
|
this.consecutiveFailures += 1;
|
||||||
|
|
||||||
|
const normalizedError = this.toRequestError(error);
|
||||||
|
this.logger.warn(
|
||||||
|
`[${this.serviceName}] AI request failed (${this.consecutiveFailures}/${this.circuitBreakerThreshold}): ${normalizedError.message}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.consecutiveFailures >= this.circuitBreakerThreshold) {
|
||||||
|
this.circuitOpenedAt = Date.now();
|
||||||
|
this.logger.error(
|
||||||
|
`[${this.serviceName}] AI circuit breaker opened after ${this.consecutiveFailures} consecutive failures`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isRetriableError(error: unknown): boolean {
|
||||||
|
if (!axios.isAxiosError(error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error.response) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = error.response.status;
|
||||||
|
return status >= 500 || status === 429 || error.code === "ECONNABORTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
private toRequestError(error: unknown): AiEngineRequestError {
|
||||||
|
if (error instanceof AiEngineRequestError) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const detail = error.response?.data ?? error.message;
|
||||||
|
const status = error.response?.status;
|
||||||
|
const message = this.buildAxiosErrorMessage(error);
|
||||||
|
|
||||||
|
return new AiEngineRequestError(message, {
|
||||||
|
status,
|
||||||
|
detail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return new AiEngineRequestError(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AiEngineRequestError("Unknown AI engine error", {
|
||||||
|
detail: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildAxiosErrorMessage(error: AxiosError): string {
|
||||||
|
if (error.code === "ECONNABORTED") {
|
||||||
|
return "AI engine request timed out";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error.response) {
|
||||||
|
return "AI engine is unreachable";
|
||||||
|
}
|
||||||
|
|
||||||
|
const detail =
|
||||||
|
(error.response.data as Record<string, unknown> | undefined)?.detail ??
|
||||||
|
error.message;
|
||||||
|
|
||||||
|
return typeof detail === "string"
|
||||||
|
? detail
|
||||||
|
: `AI engine request failed with status ${error.response.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async delay(ms: number) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
type ScoreLikeValue = number | string | null | undefined;
|
||||||
|
|
||||||
|
type ScoreLike = {
|
||||||
|
home?: ScoreLikeValue;
|
||||||
|
away?: ScoreLikeValue;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
export interface MatchStatusLike {
|
||||||
|
state?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
substate?: string | null;
|
||||||
|
statusBoxContent?: string | null;
|
||||||
|
scoreHome?: ScoreLikeValue;
|
||||||
|
scoreAway?: ScoreLikeValue;
|
||||||
|
score?: ScoreLike;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LIVE_STATUS_TOKENS = [
|
||||||
|
"live",
|
||||||
|
"livegame",
|
||||||
|
"playing",
|
||||||
|
"half time",
|
||||||
|
"halftime",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"ht",
|
||||||
|
"1q",
|
||||||
|
"2q",
|
||||||
|
"3q",
|
||||||
|
"4q",
|
||||||
|
];
|
||||||
|
|
||||||
|
const LIVE_STATE_TOKENS = [
|
||||||
|
"live",
|
||||||
|
"livegame",
|
||||||
|
"firsthalf",
|
||||||
|
"secondhalf",
|
||||||
|
"halftime",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"ht",
|
||||||
|
"1q",
|
||||||
|
"2q",
|
||||||
|
"3q",
|
||||||
|
"4q",
|
||||||
|
];
|
||||||
|
|
||||||
|
const FINISHED_STATUS_TOKENS = [
|
||||||
|
"finished",
|
||||||
|
"played",
|
||||||
|
"ft",
|
||||||
|
"aet",
|
||||||
|
"pen",
|
||||||
|
"penalties",
|
||||||
|
"afterpenalties",
|
||||||
|
"ended",
|
||||||
|
"post",
|
||||||
|
"postgame",
|
||||||
|
"posted",
|
||||||
|
];
|
||||||
|
|
||||||
|
const FINISHED_STATE_TOKENS = [
|
||||||
|
"finished",
|
||||||
|
"post",
|
||||||
|
"postgame",
|
||||||
|
"posted",
|
||||||
|
"ft",
|
||||||
|
"ended",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LIVE_STATUS_VALUES_FOR_DB = [
|
||||||
|
"LIVE",
|
||||||
|
"live",
|
||||||
|
"1H",
|
||||||
|
"2H",
|
||||||
|
"HT",
|
||||||
|
"1Q",
|
||||||
|
"2Q",
|
||||||
|
"3Q",
|
||||||
|
"4Q",
|
||||||
|
"Playing",
|
||||||
|
"Half Time",
|
||||||
|
"liveGame",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LIVE_STATE_VALUES_FOR_DB = [
|
||||||
|
"live",
|
||||||
|
"liveGame",
|
||||||
|
"firsthalf",
|
||||||
|
"secondhalf",
|
||||||
|
"halfTime",
|
||||||
|
"1H",
|
||||||
|
"2H",
|
||||||
|
"HT",
|
||||||
|
"1Q",
|
||||||
|
"2Q",
|
||||||
|
"3Q",
|
||||||
|
"4Q",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FINISHED_STATUS_VALUES_FOR_DB = [
|
||||||
|
"Finished",
|
||||||
|
"Played",
|
||||||
|
"FT",
|
||||||
|
"AET",
|
||||||
|
"PEN",
|
||||||
|
"Ended",
|
||||||
|
"post",
|
||||||
|
"postGame",
|
||||||
|
"posted",
|
||||||
|
"Posted",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FINISHED_STATE_VALUES_FOR_DB = [
|
||||||
|
"Finished",
|
||||||
|
"post",
|
||||||
|
"postGame",
|
||||||
|
"postgame",
|
||||||
|
"posted",
|
||||||
|
"FT",
|
||||||
|
"Ended",
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeToken(value: unknown): string {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScoreValue(value: ScoreLikeValue): number | null {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasResolvedScore(match: MatchStatusLike): boolean {
|
||||||
|
const homeScore = parseScoreValue(match.score?.home ?? match.scoreHome);
|
||||||
|
const awayScore = parseScoreValue(match.score?.away ?? match.scoreAway);
|
||||||
|
return homeScore !== null && awayScore !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMatchLive(match: MatchStatusLike): boolean {
|
||||||
|
const state = normalizeToken(match.state);
|
||||||
|
const status = normalizeToken(match.status);
|
||||||
|
const substate = normalizeToken(match.substate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
LIVE_STATE_TOKENS.includes(state) ||
|
||||||
|
LIVE_STATUS_TOKENS.includes(status) ||
|
||||||
|
LIVE_STATE_TOKENS.includes(substate)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMatchCompleted(match: MatchStatusLike): boolean {
|
||||||
|
if (normalizeToken(match.statusBoxContent) === "ert") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = normalizeToken(match.state);
|
||||||
|
const status = normalizeToken(match.status);
|
||||||
|
const substate = normalizeToken(match.substate);
|
||||||
|
|
||||||
|
if (
|
||||||
|
FINISHED_STATE_TOKENS.includes(state) ||
|
||||||
|
FINISHED_STATUS_TOKENS.includes(status) ||
|
||||||
|
FINISHED_STATE_TOKENS.includes(substate)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasResolvedScore(match) && !isMatchLive(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveStoredMatchStatus(match: MatchStatusLike): string {
|
||||||
|
if (normalizeToken(match.statusBoxContent) === "ert") {
|
||||||
|
return "POSTPONED";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatchLive(match)) {
|
||||||
|
return "LIVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatchCompleted(match)) {
|
||||||
|
return "FT";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "NS";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDisplayMatchStatus(match: MatchStatusLike): string {
|
||||||
|
if (isMatchLive(match)) {
|
||||||
|
return "LIVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMatchCompleted(match)) {
|
||||||
|
return "Finished";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(match.status || match.state || "NS");
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
function extractDateParts(date: Date, timeZone: string) {
|
||||||
|
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||||
|
timeZone,
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
const parts = formatter.formatToParts(date);
|
||||||
|
const year = Number(parts.find((part) => part.type === "year")?.value);
|
||||||
|
const month = Number(parts.find((part) => part.type === "month")?.value);
|
||||||
|
const day = Number(parts.find((part) => part.type === "day")?.value);
|
||||||
|
|
||||||
|
return { year, month, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDateStringInTimeZone(
|
||||||
|
date: Date,
|
||||||
|
timeZone: string,
|
||||||
|
): string {
|
||||||
|
const { year, month, day } = extractDateParts(date, timeZone);
|
||||||
|
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShiftedDateStringInTimeZone(
|
||||||
|
daysOffset: number,
|
||||||
|
timeZone: string,
|
||||||
|
baseDate: Date = new Date(),
|
||||||
|
): string {
|
||||||
|
const { year, month, day } = extractDateParts(baseDate, timeZone);
|
||||||
|
const shifted = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
shifted.setUTCDate(shifted.getUTCDate() + daysOffset);
|
||||||
|
return shifted.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeZoneOffsetMs(date: Date, timeZone: string): number {
|
||||||
|
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone,
|
||||||
|
timeZoneName: "shortOffset",
|
||||||
|
});
|
||||||
|
|
||||||
|
const offsetLabel =
|
||||||
|
formatter.formatToParts(date).find((part) => part.type === "timeZoneName")
|
||||||
|
?.value || "GMT+0";
|
||||||
|
|
||||||
|
const match = offsetLabel.match(/GMT([+-])(\d{1,2})(?::?(\d{2}))?/);
|
||||||
|
if (!match) return 0;
|
||||||
|
|
||||||
|
const sign = match[1] === "-" ? -1 : 1;
|
||||||
|
const hours = Number(match[2] || "0");
|
||||||
|
const minutes = Number(match[3] || "0");
|
||||||
|
|
||||||
|
return sign * (hours * 60 + minutes) * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDayBoundsForTimeZone(
|
||||||
|
dateString: string,
|
||||||
|
timeZone: string,
|
||||||
|
): { startMs: number; endMs: number } {
|
||||||
|
const [year, month, day] = dateString.split("-").map(Number);
|
||||||
|
const startGuess = new Date(Date.UTC(year, month - 1, day, 0, 0, 0));
|
||||||
|
const nextDayGuess = new Date(Date.UTC(year, month - 1, day + 1, 0, 0, 0));
|
||||||
|
|
||||||
|
const startOffsetMs = getTimeZoneOffsetMs(startGuess, timeZone);
|
||||||
|
const nextDayOffsetMs = getTimeZoneOffsetMs(nextDayGuess, timeZone);
|
||||||
|
|
||||||
|
const startMs = Date.UTC(year, month - 1, day, 0, 0, 0) - startOffsetMs;
|
||||||
|
const nextDayStartMs =
|
||||||
|
Date.UTC(year, month - 1, day + 1, 0, 0, 0) - nextDayOffsetMs;
|
||||||
|
|
||||||
|
return {
|
||||||
|
startMs,
|
||||||
|
endMs: nextDayStartMs - 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDateOnlyValueForTimeZone(
|
||||||
|
timeZone: string,
|
||||||
|
date: Date = new Date(),
|
||||||
|
): Date {
|
||||||
|
return new Date(`${getDateStringInTimeZone(date, timeZone)}T00:00:00.000Z`);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ export const envSchema = z.object({
|
|||||||
DATABASE_URL: z.string().url(),
|
DATABASE_URL: z.string().url(),
|
||||||
// AI Engine
|
// AI Engine
|
||||||
AI_ENGINE_URL: z.string().url().default("http://localhost:8000"),
|
AI_ENGINE_URL: z.string().url().default("http://localhost:8000"),
|
||||||
|
AI_ENGINE_MODE: z.enum(["v28-pro-max", "dual"]).default("v28-pro-max"),
|
||||||
|
|
||||||
// JWT
|
// JWT
|
||||||
JWT_SECRET: z.string().min(32),
|
JWT_SECRET: z.string().min(32),
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ async function bootstrap() {
|
|||||||
"https://suggestbet.bilgich.com",
|
"https://suggestbet.bilgich.com",
|
||||||
"https://iddaai.com",
|
"https://iddaai.com",
|
||||||
"https://www.iddaai.com",
|
"https://www.iddaai.com",
|
||||||
|
"http://localhost:6195",
|
||||||
]
|
]
|
||||||
: true,
|
: true,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|||||||
@@ -259,15 +259,21 @@ export class AdminController {
|
|||||||
premiumUsers,
|
premiumUsers,
|
||||||
totalMatches,
|
totalMatches,
|
||||||
totalPredictions,
|
totalPredictions,
|
||||||
|
totalCoupons,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.prisma.user.count(),
|
this.prisma.user.count(),
|
||||||
this.prisma.user.count({ where: { isActive: true } }),
|
this.prisma.user.count({ where: { isActive: true } }),
|
||||||
this.prisma.user.count({ where: { subscriptionStatus: "active" } }),
|
this.prisma.user.count({ where: { subscriptionStatus: "active" } }),
|
||||||
this.prisma.match.count(),
|
this.prisma.match.count(),
|
||||||
this.prisma.prediction.count(),
|
this.prisma.prediction.count(),
|
||||||
|
this.prisma.userCoupon.count(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return createSuccessResponse({
|
return createSuccessResponse({
|
||||||
|
totalUsers,
|
||||||
|
activeUsers,
|
||||||
|
totalPredictions,
|
||||||
|
totalCoupons,
|
||||||
users: {
|
users: {
|
||||||
total: totalUsers,
|
total: totalUsers,
|
||||||
active: activeUsers,
|
active: activeUsers,
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import {
|
|||||||
ROLES_KEY,
|
ROLES_KEY,
|
||||||
PERMISSIONS_KEY,
|
PERMISSIONS_KEY,
|
||||||
} from "../../../common/decorators";
|
} from "../../../common/decorators";
|
||||||
|
import { normalizeRole } from "../../../common/constants/roles";
|
||||||
|
|
||||||
interface AuthenticatedUser {
|
interface AuthenticatedUser {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
|
role?: string;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,11 +90,28 @@ export class RolesGuard implements CanActivate {
|
|||||||
|
|
||||||
const user = req.user as AuthenticatedUser | undefined;
|
const user = req.user as AuthenticatedUser | undefined;
|
||||||
|
|
||||||
if (!user || !user.roles) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasRole = requiredRoles.some((role) => user.roles.includes(role));
|
const normalizedUserRoles = (user.roles?.length
|
||||||
|
? user.roles
|
||||||
|
: user.role
|
||||||
|
? [user.role]
|
||||||
|
: []
|
||||||
|
).map((role) => normalizeRole(role));
|
||||||
|
|
||||||
|
const normalizedRequiredRoles = requiredRoles.map((role) =>
|
||||||
|
normalizeRole(role),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (normalizedUserRoles.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasRole = normalizedRequiredRoles.some((role) =>
|
||||||
|
normalizedUserRoles.includes(role),
|
||||||
|
);
|
||||||
if (!hasRole) {
|
if (!hasRole) {
|
||||||
throw new ForbiddenException("PERMISSION_DENIED");
|
throw new ForbiddenException("PERMISSION_DENIED");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PassportStrategy } from "@nestjs/passport";
|
|||||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { AuthService, JwtPayload } from "../auth.service";
|
import { AuthService, JwtPayload } from "../auth.service";
|
||||||
|
import { normalizeRole } from "../../../common/constants/roles";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
@@ -29,9 +30,13 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedRole = normalizeRole(payload.role);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
role: payload.role,
|
role: normalizedRole,
|
||||||
|
roles: normalizedRole ? [normalizedRole] : [],
|
||||||
|
permissions: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
AnalyzeMatchDto,
|
AnalyzeMatchDto,
|
||||||
DailyBankoDto,
|
DailyBankoDto,
|
||||||
SuggestCouponDto,
|
SuggestCouponDto,
|
||||||
|
FrequencyCouponDto,
|
||||||
} from "./dto/coupons-request.dto";
|
} from "./dto/coupons-request.dto";
|
||||||
import { Public } from "../../common/decorators";
|
import { Public } from "../../common/decorators";
|
||||||
import { JwtAuthGuard } from "../auth/guards/auth.guards"; // Assuming standard guard
|
import { JwtAuthGuard } from "../auth/guards/auth.guards"; // Assuming standard guard
|
||||||
@@ -188,8 +189,43 @@ export class CouponsController {
|
|||||||
return { success: true, data: coupon };
|
return { success: true, data: coupon };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /coupon/frequency-coupon
|
||||||
|
* Generate a frequency-based parlay coupon (Conditional Frequency Engine)
|
||||||
|
*/
|
||||||
|
@Post("frequency-coupon")
|
||||||
|
@Public()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Generate frequency-based parlay coupon",
|
||||||
|
description:
|
||||||
|
"Scans upcoming matches, applies conditional frequency analysis " +
|
||||||
|
"(team odds-band performance), and builds 2-5 match combos with +EV calculation.",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "Frequency coupon generated" })
|
||||||
|
async getFrequencyCoupon(@Body() dto: FrequencyCouponDto) {
|
||||||
|
const coupon = await this.smartCouponService.generateFrequencyBasedCoupon({
|
||||||
|
matchIds: dto.matchIds,
|
||||||
|
maxMatches: dto.maxMatches,
|
||||||
|
minSignal: dto.minSignal,
|
||||||
|
markets: dto.markets,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!coupon || coupon.bets.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Frekans analizine uygun yeterli maç bulunamadı. " +
|
||||||
|
"minSignal değerini düşürmeyi veya daha fazla maç beklemeyi deneyin.",
|
||||||
|
data: coupon,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: coupon };
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// USER COUPON ENDPOINTS (NEW)
|
// USER COUPON ENDPOINTS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Module } from "@nestjs/common";
|
|||||||
import { CouponsController } from "./coupons.controller";
|
import { CouponsController } from "./coupons.controller";
|
||||||
import { SmartCouponService } from "./services/smart-coupon.service";
|
import { SmartCouponService } from "./services/smart-coupon.service";
|
||||||
import { UserCouponService } from "./services/user-coupon.service";
|
import { UserCouponService } from "./services/user-coupon.service";
|
||||||
|
import { FrequencyEngineService } from "./services/frequency-engine.service";
|
||||||
import { CouponsService } from "./coupons.service";
|
import { CouponsService } from "./coupons.service";
|
||||||
import { DatabaseModule } from "../../database/database.module";
|
import { DatabaseModule } from "../../database/database.module";
|
||||||
import { ServicesModule } from "../../services/services.module";
|
import { ServicesModule } from "../../services/services.module";
|
||||||
@@ -10,7 +11,18 @@ import { MatchesModule } from "../matches/matches.module";
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [DatabaseModule, ServicesModule, MatchesModule],
|
imports: [DatabaseModule, ServicesModule, MatchesModule],
|
||||||
controllers: [CouponsController],
|
controllers: [CouponsController],
|
||||||
providers: [CouponsService, SmartCouponService, UserCouponService],
|
providers: [
|
||||||
exports: [CouponsService, SmartCouponService, UserCouponService],
|
CouponsService,
|
||||||
|
SmartCouponService,
|
||||||
|
UserCouponService,
|
||||||
|
FrequencyEngineService,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CouponsService,
|
||||||
|
SmartCouponService,
|
||||||
|
UserCouponService,
|
||||||
|
FrequencyEngineService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class CouponsModule {}
|
export class CouponsModule {}
|
||||||
|
|
||||||
|
|||||||
@@ -74,3 +74,47 @@ export class SuggestCouponDto {
|
|||||||
@Max(100)
|
@Max(100)
|
||||||
minConfidence?: number;
|
minConfidence?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FrequencyCouponDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Optional match IDs — system auto-fetches if empty",
|
||||||
|
example: ["match-1", "match-2"],
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@ArrayMaxSize(50)
|
||||||
|
matchIds?: string[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Maximum matches in parlay (2-5)",
|
||||||
|
example: 3,
|
||||||
|
default: 3,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(2)
|
||||||
|
@Max(5)
|
||||||
|
maxMatches?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Minimum combined signal threshold (0.50-0.99)",
|
||||||
|
example: 0.7,
|
||||||
|
default: 0.7,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0.5)
|
||||||
|
@Max(0.99)
|
||||||
|
minSignal?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
"Filter markets: OU1.5, OU2.5, OU3.5, BTTS, MS (default: all)",
|
||||||
|
example: ["OU2.5", "BTTS"],
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
markets?: string[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,584 @@
|
|||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "../../../database/prisma.service";
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Types
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface FrequencySignal {
|
||||||
|
market: string;
|
||||||
|
pick: string;
|
||||||
|
homeSignal: number;
|
||||||
|
awaySignal: number;
|
||||||
|
combinedSignal: number;
|
||||||
|
homeMatchCount: number;
|
||||||
|
awayMatchCount: number;
|
||||||
|
leagueBonus: number;
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchCandidate {
|
||||||
|
matchId: string;
|
||||||
|
homeTeamId: string;
|
||||||
|
awayTeamId: string;
|
||||||
|
homeTeamName: string;
|
||||||
|
awayTeamName: string;
|
||||||
|
leagueId: string;
|
||||||
|
leagueName: string;
|
||||||
|
homeOdds: number;
|
||||||
|
awayOdds: number;
|
||||||
|
drawOdds: number;
|
||||||
|
signals: FrequencySignal[];
|
||||||
|
bestSignal: FrequencySignal | null;
|
||||||
|
matchTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamFrequencyRow {
|
||||||
|
team_id: string;
|
||||||
|
venue: "home" | "away";
|
||||||
|
odds_band: string;
|
||||||
|
total_matches: number;
|
||||||
|
ou15_rate: number;
|
||||||
|
ou25_rate: number;
|
||||||
|
ou35_rate: number;
|
||||||
|
btts_rate: number;
|
||||||
|
win_rate: number;
|
||||||
|
avg_goals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeagueProfileRow {
|
||||||
|
league_id: string;
|
||||||
|
league_name: string;
|
||||||
|
total_matches: number;
|
||||||
|
ou25_rate: number;
|
||||||
|
btts_rate: number;
|
||||||
|
avg_goals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpcomingMatchRow {
|
||||||
|
match_id: string;
|
||||||
|
home_team_id: string;
|
||||||
|
away_team_id: string;
|
||||||
|
home_team_name: string;
|
||||||
|
away_team_name: string;
|
||||||
|
league_id: string;
|
||||||
|
league_name: string;
|
||||||
|
mst_utc: bigint;
|
||||||
|
ms1_odds: number | null;
|
||||||
|
ms2_odds: number | null;
|
||||||
|
msx_odds: number | null;
|
||||||
|
ou25_over_odds: number | null;
|
||||||
|
ou25_under_odds: number | null;
|
||||||
|
btts_yes_odds: number | null;
|
||||||
|
btts_no_odds: number | null;
|
||||||
|
ou15_over_odds: number | null;
|
||||||
|
ou35_over_odds: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Constants
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MIN_MATCHES = 3;
|
||||||
|
|
||||||
|
const GOLCU_LEAGUES = new Set([
|
||||||
|
// Strategy generator'dan türetilen yüksek golcü ligler
|
||||||
|
// Lig isimleri veritabanındaki gibi
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DEFANSIF_LEAGUES = new Set([
|
||||||
|
// Düşük golcü ligler
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Service
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FrequencyEngineService {
|
||||||
|
private readonly logger = new Logger(FrequencyEngineService.name);
|
||||||
|
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Belirli bir takımın ev/deplasman + oran bandı koşullu frekanslarını döndürür.
|
||||||
|
*/
|
||||||
|
async getTeamFrequency(
|
||||||
|
teamId: string,
|
||||||
|
venue: "home" | "away",
|
||||||
|
oddsBand: string,
|
||||||
|
): Promise<TeamFrequencyRow | null> {
|
||||||
|
const venueColumn =
|
||||||
|
venue === "home" ? "m.home_team_id" : "m.away_team_id";
|
||||||
|
const oddsSelection = venue === "home" ? "'1'" : "'2'";
|
||||||
|
const bandRange = this.parseBandRange(oddsBand);
|
||||||
|
|
||||||
|
if (!bandRange) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await this.prisma.$queryRawUnsafe<TeamFrequencyRow[]>(
|
||||||
|
`
|
||||||
|
WITH team_matches AS (
|
||||||
|
SELECT
|
||||||
|
m.id AS match_id,
|
||||||
|
m.score_home,
|
||||||
|
m.score_away,
|
||||||
|
(m.score_home + m.score_away) AS total_goals,
|
||||||
|
CAST(os.odd_value AS DECIMAL) AS team_odds
|
||||||
|
FROM matches m
|
||||||
|
JOIN odd_categories oc ON oc.match_id = m.id AND oc.name = 'Maç Sonucu'
|
||||||
|
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id AND os.name = ${oddsSelection}
|
||||||
|
WHERE m.status = 'FT'
|
||||||
|
AND m.score_home IS NOT NULL
|
||||||
|
AND ${venueColumn} = $1
|
||||||
|
AND CAST(os.odd_value AS DECIMAL) >= $2
|
||||||
|
AND CAST(os.odd_value AS DECIMAL) < $3
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
$1::text AS team_id,
|
||||||
|
$4::text AS venue,
|
||||||
|
$5::text AS odds_band,
|
||||||
|
COUNT(*)::int AS total_matches,
|
||||||
|
COALESCE(AVG(CASE WHEN total_goals > 1 THEN 1.0 ELSE 0.0 END), 0)::float AS ou15_rate,
|
||||||
|
COALESCE(AVG(CASE WHEN total_goals > 2 THEN 1.0 ELSE 0.0 END), 0)::float AS ou25_rate,
|
||||||
|
COALESCE(AVG(CASE WHEN total_goals > 3 THEN 1.0 ELSE 0.0 END), 0)::float AS ou35_rate,
|
||||||
|
COALESCE(AVG(CASE WHEN score_home > 0 AND score_away > 0 THEN 1.0 ELSE 0.0 END), 0)::float AS btts_rate,
|
||||||
|
COALESCE(AVG(CASE WHEN ${venue === "home" ? "score_home > score_away" : "score_away > score_home"} THEN 1.0 ELSE 0.0 END), 0)::float AS win_rate,
|
||||||
|
COALESCE(AVG(total_goals), 0)::float AS avg_goals
|
||||||
|
FROM team_matches
|
||||||
|
`,
|
||||||
|
teamId,
|
||||||
|
bandRange.min,
|
||||||
|
bandRange.max,
|
||||||
|
venue,
|
||||||
|
oddsBand,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rows.length || rows[0].total_matches < MIN_MATCHES) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* İki takımın oran bandı geçmişlerini çapraz kontrol eder.
|
||||||
|
* Tüm marketler için kombine sinyal üretir.
|
||||||
|
*/
|
||||||
|
async getMatchFrequencySignals(
|
||||||
|
homeTeamId: string,
|
||||||
|
awayTeamId: string,
|
||||||
|
homeOdds: number,
|
||||||
|
awayOdds: number,
|
||||||
|
leagueId?: string,
|
||||||
|
): Promise<FrequencySignal[]> {
|
||||||
|
const homeBand = this.getOddsBand(homeOdds);
|
||||||
|
const awayBand = this.getOddsBand(awayOdds);
|
||||||
|
|
||||||
|
const [homeFreq, awayFreq, leagueProfile] = await Promise.all([
|
||||||
|
this.getTeamFrequency(homeTeamId, "home", homeBand),
|
||||||
|
this.getTeamFrequency(awayTeamId, "away", awayBand),
|
||||||
|
leagueId ? this.getLeagueProfile(leagueId) : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!homeFreq || !awayFreq) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const leagueBonus = this.calculateLeagueBonus(leagueProfile);
|
||||||
|
const signals: FrequencySignal[] = [];
|
||||||
|
|
||||||
|
// OU 1.5 OVER
|
||||||
|
const ou15Combined = (homeFreq.ou15_rate + awayFreq.ou15_rate) / 2;
|
||||||
|
if (ou15Combined >= 0.80) {
|
||||||
|
signals.push({
|
||||||
|
market: "OU1.5_OVER",
|
||||||
|
pick: "1.5 UST",
|
||||||
|
homeSignal: homeFreq.ou15_rate,
|
||||||
|
awaySignal: awayFreq.ou15_rate,
|
||||||
|
combinedSignal: ou15Combined,
|
||||||
|
homeMatchCount: homeFreq.total_matches,
|
||||||
|
awayMatchCount: awayFreq.total_matches,
|
||||||
|
leagueBonus,
|
||||||
|
confidence: this.calculateConfidence(
|
||||||
|
ou15Combined,
|
||||||
|
homeFreq.total_matches,
|
||||||
|
awayFreq.total_matches,
|
||||||
|
leagueBonus,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OU 2.5 OVER
|
||||||
|
const ou25Combined = (homeFreq.ou25_rate + awayFreq.ou25_rate) / 2;
|
||||||
|
if (ou25Combined >= 0.60) {
|
||||||
|
signals.push({
|
||||||
|
market: "OU2.5_OVER",
|
||||||
|
pick: "2.5 UST",
|
||||||
|
homeSignal: homeFreq.ou25_rate,
|
||||||
|
awaySignal: awayFreq.ou25_rate,
|
||||||
|
combinedSignal: ou25Combined,
|
||||||
|
homeMatchCount: homeFreq.total_matches,
|
||||||
|
awayMatchCount: awayFreq.total_matches,
|
||||||
|
leagueBonus,
|
||||||
|
confidence: this.calculateConfidence(
|
||||||
|
ou25Combined,
|
||||||
|
homeFreq.total_matches,
|
||||||
|
awayFreq.total_matches,
|
||||||
|
leagueBonus,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OU 3.5 OVER
|
||||||
|
const ou35Combined = (homeFreq.ou35_rate + awayFreq.ou35_rate) / 2;
|
||||||
|
if (ou35Combined >= 0.50) {
|
||||||
|
signals.push({
|
||||||
|
market: "OU3.5_OVER",
|
||||||
|
pick: "3.5 UST",
|
||||||
|
homeSignal: homeFreq.ou35_rate,
|
||||||
|
awaySignal: awayFreq.ou35_rate,
|
||||||
|
combinedSignal: ou35Combined,
|
||||||
|
homeMatchCount: homeFreq.total_matches,
|
||||||
|
awayMatchCount: awayFreq.total_matches,
|
||||||
|
leagueBonus,
|
||||||
|
confidence: this.calculateConfidence(
|
||||||
|
ou35Combined,
|
||||||
|
homeFreq.total_matches,
|
||||||
|
awayFreq.total_matches,
|
||||||
|
leagueBonus,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// BTTS YES
|
||||||
|
const bttsCombined = (homeFreq.btts_rate + awayFreq.btts_rate) / 2;
|
||||||
|
if (bttsCombined >= 0.60) {
|
||||||
|
signals.push({
|
||||||
|
market: "BTTS_YES",
|
||||||
|
pick: "KG VAR",
|
||||||
|
homeSignal: homeFreq.btts_rate,
|
||||||
|
awaySignal: awayFreq.btts_rate,
|
||||||
|
combinedSignal: bttsCombined,
|
||||||
|
homeMatchCount: homeFreq.total_matches,
|
||||||
|
awayMatchCount: awayFreq.total_matches,
|
||||||
|
leagueBonus,
|
||||||
|
confidence: this.calculateConfidence(
|
||||||
|
bttsCombined,
|
||||||
|
homeFreq.total_matches,
|
||||||
|
awayFreq.total_matches,
|
||||||
|
leagueBonus,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OU 2.5 UNDER (düşük gol beklentisi)
|
||||||
|
const ou25UnderCombined =
|
||||||
|
(1 - homeFreq.ou25_rate + (1 - awayFreq.ou25_rate)) / 2;
|
||||||
|
if (ou25UnderCombined >= 0.65) {
|
||||||
|
signals.push({
|
||||||
|
market: "OU2.5_UNDER",
|
||||||
|
pick: "2.5 ALT",
|
||||||
|
homeSignal: 1 - homeFreq.ou25_rate,
|
||||||
|
awaySignal: 1 - awayFreq.ou25_rate,
|
||||||
|
combinedSignal: ou25UnderCombined,
|
||||||
|
homeMatchCount: homeFreq.total_matches,
|
||||||
|
awayMatchCount: awayFreq.total_matches,
|
||||||
|
leagueBonus: -leagueBonus, // golcü lig bonusu ters çevrilir
|
||||||
|
confidence: this.calculateConfidence(
|
||||||
|
ou25UnderCombined,
|
||||||
|
homeFreq.total_matches,
|
||||||
|
awayFreq.total_matches,
|
||||||
|
-leagueBonus,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// MS HOME WIN (ev sahibi kazanma)
|
||||||
|
const hwCombined = (homeFreq.win_rate + awayFreq.win_rate) / 2;
|
||||||
|
// awayFreq.win_rate aslında deplasman takımının KAYBETme oranı
|
||||||
|
// (away takımı o bandda maçları kazanma değil, kaybetme olarak bak)
|
||||||
|
if (hwCombined >= 0.70 && homeOdds > 1.10 && homeOdds < 3.50) {
|
||||||
|
signals.push({
|
||||||
|
market: "MS_HOME",
|
||||||
|
pick: "MS 1",
|
||||||
|
homeSignal: homeFreq.win_rate,
|
||||||
|
awaySignal: awayFreq.win_rate,
|
||||||
|
combinedSignal: hwCombined,
|
||||||
|
homeMatchCount: homeFreq.total_matches,
|
||||||
|
awayMatchCount: awayFreq.total_matches,
|
||||||
|
leagueBonus: 0,
|
||||||
|
confidence: this.calculateConfidence(
|
||||||
|
hwCombined,
|
||||||
|
homeFreq.total_matches,
|
||||||
|
awayFreq.total_matches,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Güvene göre sırala (en güçlü sinyal önce)
|
||||||
|
signals.sort((a, b) => b.confidence - a.confidence);
|
||||||
|
|
||||||
|
return signals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yaklaşan maçları oranlarıyla birlikte getirir.
|
||||||
|
* LiveMatch tablosundan JSON odds parse eder.
|
||||||
|
*/
|
||||||
|
async getUpcomingMatchesWithOdds(
|
||||||
|
matchIds?: string[],
|
||||||
|
limit: number = 50,
|
||||||
|
): Promise<UpcomingMatchRow[]> {
|
||||||
|
const nowMs = Date.now();
|
||||||
|
|
||||||
|
if (matchIds && matchIds.length > 0) {
|
||||||
|
// Belirli maçlar istendi
|
||||||
|
return this.prisma.$queryRawUnsafe<UpcomingMatchRow[]>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
lm.id AS match_id,
|
||||||
|
lm.home_team_id,
|
||||||
|
lm.away_team_id,
|
||||||
|
COALESCE(ht.name, 'Unknown') AS home_team_name,
|
||||||
|
COALESCE(at.name, 'Unknown') AS away_team_name,
|
||||||
|
COALESCE(lm.league_id, '') AS league_id,
|
||||||
|
COALESCE(l.name, 'Unknown') AS league_name,
|
||||||
|
lm.mst_utc,
|
||||||
|
(lm.odds->'Maç Sonucu'->>'1')::decimal AS ms1_odds,
|
||||||
|
(lm.odds->'Maç Sonucu'->>'2')::decimal AS ms2_odds,
|
||||||
|
(lm.odds->'Maç Sonucu'->>'0')::decimal AS msx_odds,
|
||||||
|
(lm.odds->'2,5 Alt/Üst'->>'Üst')::decimal AS ou25_over_odds,
|
||||||
|
(lm.odds->'2,5 Alt/Üst'->>'Alt')::decimal AS ou25_under_odds,
|
||||||
|
(lm.odds->'Karşılıklı Gol'->>'Var')::decimal AS btts_yes_odds,
|
||||||
|
(lm.odds->'Karşılıklı Gol'->>'Yok')::decimal AS btts_no_odds,
|
||||||
|
(lm.odds->'1,5 Alt/Üst'->>'Üst')::decimal AS ou15_over_odds,
|
||||||
|
(lm.odds->'3,5 Alt/Üst'->>'Üst')::decimal AS ou35_over_odds
|
||||||
|
FROM live_matches lm
|
||||||
|
LEFT JOIN teams ht ON lm.home_team_id = ht.id
|
||||||
|
LEFT JOIN teams at ON lm.away_team_id = at.id
|
||||||
|
LEFT JOIN leagues l ON lm.league_id = l.id
|
||||||
|
WHERE lm.id = ANY($1)
|
||||||
|
AND lm.odds IS NOT NULL
|
||||||
|
AND lm.odds != 'null'::jsonb
|
||||||
|
ORDER BY lm.mst_utc ASC
|
||||||
|
`,
|
||||||
|
matchIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otomatik: yaklaşan tüm maçlar
|
||||||
|
return this.prisma.$queryRawUnsafe<UpcomingMatchRow[]>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
lm.id AS match_id,
|
||||||
|
lm.home_team_id,
|
||||||
|
lm.away_team_id,
|
||||||
|
COALESCE(ht.name, 'Unknown') AS home_team_name,
|
||||||
|
COALESCE(at.name, 'Unknown') AS away_team_name,
|
||||||
|
COALESCE(lm.league_id, '') AS league_id,
|
||||||
|
COALESCE(l.name, 'Unknown') AS league_name,
|
||||||
|
lm.mst_utc,
|
||||||
|
(lm.odds->'Maç Sonucu'->>'1')::decimal AS ms1_odds,
|
||||||
|
(lm.odds->'Maç Sonucu'->>'2')::decimal AS ms2_odds,
|
||||||
|
(lm.odds->'Maç Sonucu'->>'0')::decimal AS msx_odds,
|
||||||
|
(lm.odds->'2,5 Alt/Üst'->>'Üst')::decimal AS ou25_over_odds,
|
||||||
|
(lm.odds->'2,5 Alt/Üst'->>'Alt')::decimal AS ou25_under_odds,
|
||||||
|
(lm.odds->'Karşılıklı Gol'->>'Var')::decimal AS btts_yes_odds,
|
||||||
|
(lm.odds->'Karşılıklı Gol'->>'Yok')::decimal AS btts_no_odds,
|
||||||
|
(lm.odds->'1,5 Alt/Üst'->>'Üst')::decimal AS ou15_over_odds,
|
||||||
|
(lm.odds->'3,5 Alt/Üst'->>'Üst')::decimal AS ou35_over_odds
|
||||||
|
FROM live_matches lm
|
||||||
|
LEFT JOIN teams ht ON lm.home_team_id = ht.id
|
||||||
|
LEFT JOIN teams at ON lm.away_team_id = at.id
|
||||||
|
LEFT JOIN leagues l ON lm.league_id = l.id
|
||||||
|
WHERE lm.mst_utc >= $1
|
||||||
|
AND lm.sport = 'football'
|
||||||
|
AND lm.odds IS NOT NULL
|
||||||
|
AND lm.odds != 'null'::jsonb
|
||||||
|
AND (lm.status IS NULL OR lm.status NOT IN ('FT', 'AET', 'PEN', 'ABD', 'CANC', 'PST', 'SUSP', 'INT', 'AWD', 'WO'))
|
||||||
|
AND (lm.state IS NULL OR lm.state NOT IN ('after', 'postponed', 'cancelled', 'abandoned'))
|
||||||
|
ORDER BY lm.mst_utc ASC
|
||||||
|
LIMIT $2
|
||||||
|
`,
|
||||||
|
BigInt(nowMs),
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lig bazlı gol profili.
|
||||||
|
*/
|
||||||
|
async getLeagueProfile(
|
||||||
|
leagueId: string,
|
||||||
|
): Promise<LeagueProfileRow | null> {
|
||||||
|
const rows = await this.prisma.$queryRawUnsafe<LeagueProfileRow[]>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
m.league_id,
|
||||||
|
l.name AS league_name,
|
||||||
|
COUNT(*)::int AS total_matches,
|
||||||
|
AVG(CASE WHEN (m.score_home + m.score_away) > 2 THEN 1.0 ELSE 0.0 END)::float AS ou25_rate,
|
||||||
|
AVG(CASE WHEN m.score_home > 0 AND m.score_away > 0 THEN 1.0 ELSE 0.0 END)::float AS btts_rate,
|
||||||
|
AVG(m.score_home + m.score_away)::float AS avg_goals
|
||||||
|
FROM matches m
|
||||||
|
JOIN leagues l ON m.league_id = l.id
|
||||||
|
WHERE m.status = 'FT'
|
||||||
|
AND m.score_home IS NOT NULL
|
||||||
|
AND m.league_id = $1
|
||||||
|
GROUP BY m.league_id, l.name
|
||||||
|
HAVING COUNT(*) >= 20
|
||||||
|
`,
|
||||||
|
leagueId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.length > 0 ? rows[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bir upcoming match row'unu MatchCandidate'e dönüştürür
|
||||||
|
* ve frekans sinyallerini hesaplar.
|
||||||
|
*/
|
||||||
|
async buildMatchCandidate(
|
||||||
|
row: UpcomingMatchRow,
|
||||||
|
): Promise<MatchCandidate | null> {
|
||||||
|
const homeOdds = row.ms1_odds ? Number(row.ms1_odds) : 0;
|
||||||
|
const awayOdds = row.ms2_odds ? Number(row.ms2_odds) : 0;
|
||||||
|
const drawOdds = row.msx_odds ? Number(row.msx_odds) : 0;
|
||||||
|
|
||||||
|
if (homeOdds <= 0 || awayOdds <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signals = await this.getMatchFrequencySignals(
|
||||||
|
row.home_team_id,
|
||||||
|
row.away_team_id,
|
||||||
|
homeOdds,
|
||||||
|
awayOdds,
|
||||||
|
row.league_id || undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (signals.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
matchId: row.match_id,
|
||||||
|
homeTeamId: row.home_team_id,
|
||||||
|
awayTeamId: row.away_team_id,
|
||||||
|
homeTeamName: row.home_team_name,
|
||||||
|
awayTeamName: row.away_team_name,
|
||||||
|
leagueId: row.league_id,
|
||||||
|
leagueName: row.league_name,
|
||||||
|
homeOdds,
|
||||||
|
awayOdds,
|
||||||
|
drawOdds,
|
||||||
|
signals,
|
||||||
|
bestSignal: signals[0] || null,
|
||||||
|
matchTime: Number(row.mst_utc),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bir market pick'ine karşılık gelen odds'u UpcomingMatchRow'dan çeker.
|
||||||
|
*/
|
||||||
|
getMarketOdds(row: UpcomingMatchRow, market: string): number {
|
||||||
|
switch (market) {
|
||||||
|
case "OU1.5_OVER":
|
||||||
|
return row.ou15_over_odds ? Number(row.ou15_over_odds) : 0;
|
||||||
|
case "OU2.5_OVER":
|
||||||
|
return row.ou25_over_odds ? Number(row.ou25_over_odds) : 0;
|
||||||
|
case "OU2.5_UNDER":
|
||||||
|
return row.ou25_under_odds ? Number(row.ou25_under_odds) : 0;
|
||||||
|
case "OU3.5_OVER":
|
||||||
|
return row.ou35_over_odds ? Number(row.ou35_over_odds) : 0;
|
||||||
|
case "BTTS_YES":
|
||||||
|
return row.btts_yes_odds ? Number(row.btts_yes_odds) : 0;
|
||||||
|
case "MS_HOME":
|
||||||
|
return row.ms1_odds ? Number(row.ms1_odds) : 0;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Private Helpers
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oran bandı fonksiyonu — strategy_generator.py ile aynı mantık.
|
||||||
|
*/
|
||||||
|
getOddsBand(odds: number): string {
|
||||||
|
if (odds < 1.3) return "1.00-1.30";
|
||||||
|
if (odds < 1.5) return "1.30-1.50";
|
||||||
|
if (odds < 1.8) return "1.50-1.80";
|
||||||
|
if (odds < 2.2) return "1.80-2.20";
|
||||||
|
if (odds < 2.8) return "2.20-2.80";
|
||||||
|
if (odds < 4.0) return "2.80-4.00";
|
||||||
|
if (odds < 6.0) return "4.00-6.00";
|
||||||
|
return "6.00+";
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseBandRange(
|
||||||
|
band: string,
|
||||||
|
): { min: number; max: number } | null {
|
||||||
|
const map: Record<string, { min: number; max: number }> = {
|
||||||
|
"1.00-1.30": { min: 1.0, max: 1.3 },
|
||||||
|
"1.30-1.50": { min: 1.3, max: 1.5 },
|
||||||
|
"1.50-1.80": { min: 1.5, max: 1.8 },
|
||||||
|
"1.80-2.20": { min: 1.8, max: 2.2 },
|
||||||
|
"2.20-2.80": { min: 2.2, max: 2.8 },
|
||||||
|
"2.80-4.00": { min: 2.8, max: 4.0 },
|
||||||
|
"4.00-6.00": { min: 4.0, max: 6.0 },
|
||||||
|
"6.00+": { min: 6.0, max: 999.0 },
|
||||||
|
};
|
||||||
|
return map[band] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateLeagueBonus(
|
||||||
|
profile: LeagueProfileRow | null,
|
||||||
|
): number {
|
||||||
|
if (!profile || profile.total_matches < 20) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OU2.5 > %60 ise golcü lig bonusu
|
||||||
|
if (profile.ou25_rate > 0.6) {
|
||||||
|
return Math.min((profile.ou25_rate - 0.5) * 0.2, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OU2.5 < %40 ise defansif lig bonusu (negatif)
|
||||||
|
if (profile.ou25_rate < 0.4) {
|
||||||
|
return Math.max((profile.ou25_rate - 0.5) * 0.2, -0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateConfidence(
|
||||||
|
combinedSignal: number,
|
||||||
|
homeN: number,
|
||||||
|
awayN: number,
|
||||||
|
leagueBonus: number,
|
||||||
|
): number {
|
||||||
|
// Base confidence: kombine sinyal * 100
|
||||||
|
let confidence = combinedSignal * 100;
|
||||||
|
|
||||||
|
// Sample size bonus: daha fazla veri = daha güvenilir
|
||||||
|
const minN = Math.min(homeN, awayN);
|
||||||
|
if (minN >= 20) {
|
||||||
|
confidence += 5;
|
||||||
|
} else if (minN >= 10) {
|
||||||
|
confidence += 2;
|
||||||
|
} else if (minN < 5) {
|
||||||
|
confidence -= 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liga bonusu
|
||||||
|
confidence += leagueBonus * 100;
|
||||||
|
|
||||||
|
return Math.max(0, Math.min(100, parseFloat(confidence.toFixed(1))));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
||||||
import axios from "axios";
|
|
||||||
import { GeminiService } from "../../gemini/gemini.service";
|
import { GeminiService } from "../../gemini/gemini.service";
|
||||||
|
import {
|
||||||
|
AiEngineClient,
|
||||||
|
AiEngineRequestError,
|
||||||
|
} from "../../../common/utils/ai-engine-client";
|
||||||
|
import {
|
||||||
|
FrequencyEngineService,
|
||||||
|
type MatchCandidate,
|
||||||
|
type FrequencySignal,
|
||||||
|
} from "./frequency-engine.service";
|
||||||
|
|
||||||
export type PredictionRiskLevel = "LOW" | "MEDIUM" | "HIGH" | "EXTREME";
|
export type PredictionRiskLevel = "LOW" | "MEDIUM" | "HIGH" | "EXTREME";
|
||||||
export type PredictionDataQuality = "HIGH" | "MEDIUM" | "LOW";
|
export type PredictionDataQuality = "HIGH" | "MEDIUM" | "LOW";
|
||||||
@@ -126,24 +134,37 @@ export interface SmartCouponResult {
|
|||||||
export class SmartCouponService {
|
export class SmartCouponService {
|
||||||
private readonly logger = new Logger(SmartCouponService.name);
|
private readonly logger = new Logger(SmartCouponService.name);
|
||||||
private readonly aiEngineUrl: string;
|
private readonly aiEngineUrl: string;
|
||||||
|
private readonly aiEngineClient: AiEngineClient;
|
||||||
|
|
||||||
constructor(private readonly geminiService: GeminiService) {
|
constructor(
|
||||||
|
private readonly geminiService: GeminiService,
|
||||||
|
private readonly frequencyEngine: FrequencyEngineService,
|
||||||
|
) {
|
||||||
this.aiEngineUrl = process.env.AI_ENGINE_URL || "http://ai-engine:8000";
|
this.aiEngineUrl = process.env.AI_ENGINE_URL || "http://ai-engine:8000";
|
||||||
|
this.aiEngineClient = new AiEngineClient({
|
||||||
|
baseUrl: this.aiEngineUrl,
|
||||||
|
logger: this.logger,
|
||||||
|
serviceName: SmartCouponService.name,
|
||||||
|
timeoutMs: 60000,
|
||||||
|
maxRetries: 2,
|
||||||
|
retryDelayMs: 750,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async analyzeMatch(matchId: string): Promise<SingleMatchPredictionPackage> {
|
async analyzeMatch(matchId: string): Promise<SingleMatchPredictionPackage> {
|
||||||
let prediction: SingleMatchPredictionPackage;
|
let prediction: SingleMatchPredictionPackage;
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<SingleMatchPredictionPackage>(
|
const response = await this.aiEngineClient.post<SingleMatchPredictionPackage>(
|
||||||
`${this.aiEngineUrl}/v20plus/analyze/${matchId}`,
|
`/v20plus/analyze/${matchId}`,
|
||||||
);
|
);
|
||||||
prediction = response.data;
|
prediction = response.data;
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
if (axios.isAxiosError(error)) {
|
if (error instanceof AiEngineRequestError) {
|
||||||
const detail = error.response?.data?.detail || error.message;
|
const detail =
|
||||||
|
typeof error.detail === "string" ? error.detail : error.message;
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
`AI analyze failed: ${detail}`,
|
`AI analyze failed: ${detail}`,
|
||||||
error.response?.status || HttpStatus.SERVICE_UNAVAILABLE,
|
error.status || HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
@@ -205,8 +226,8 @@ export class SmartCouponService {
|
|||||||
options: { maxMatches?: number; minConfidence?: number } = {},
|
options: { maxMatches?: number; minConfidence?: number } = {},
|
||||||
): Promise<SmartCouponResult> {
|
): Promise<SmartCouponResult> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<SmartCouponResult>(
|
const response = await this.aiEngineClient.post<SmartCouponResult>(
|
||||||
`${this.aiEngineUrl}/v20plus/coupon`,
|
"/v20plus/coupon",
|
||||||
{
|
{
|
||||||
match_ids: matchIds,
|
match_ids: matchIds,
|
||||||
strategy,
|
strategy,
|
||||||
@@ -215,13 +236,14 @@ export class SmartCouponService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
this.logger.error("Failed to generate smart coupon", error);
|
this.logger.error("Failed to generate smart coupon", error);
|
||||||
if (axios.isAxiosError(error)) {
|
if (error instanceof AiEngineRequestError) {
|
||||||
const detail = error.response?.data?.detail || error.message;
|
const detail =
|
||||||
|
typeof error.detail === "string" ? error.detail : error.message;
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
`Coupon generation failed: ${detail}`,
|
`Coupon generation failed: ${detail}`,
|
||||||
error.response?.status || HttpStatus.SERVICE_UNAVAILABLE,
|
error.status || HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
@@ -230,6 +252,235 @@ export class SmartCouponService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// FREQUENCY-BASED COUPON ENGINE
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async generateFrequencyBasedCoupon(options: {
|
||||||
|
matchIds?: string[];
|
||||||
|
maxMatches?: number;
|
||||||
|
minSignal?: number;
|
||||||
|
markets?: string[];
|
||||||
|
}): Promise<FrequencyCouponResult> {
|
||||||
|
const maxMatches = options.maxMatches ?? 3;
|
||||||
|
const minSignal = options.minSignal ?? 0.70;
|
||||||
|
const allowedMarkets = options.markets?.map((m) => m.toUpperCase()) || null;
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`[FrequencyCoupon] Starting — max=${maxMatches}, minSignal=${minSignal}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Yaklaşan maçları oranlarıyla getir
|
||||||
|
const upcomingRows = await this.frequencyEngine.getUpcomingMatchesWithOdds(
|
||||||
|
options.matchIds,
|
||||||
|
80,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`[FrequencyCoupon] Found ${upcomingRows.length} upcoming matches with odds`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (upcomingRows.length === 0) {
|
||||||
|
return {
|
||||||
|
strategy: "FREQUENCY",
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
bets: [],
|
||||||
|
total_odds: 0,
|
||||||
|
expected_hit_rate: 0,
|
||||||
|
expected_value: 0,
|
||||||
|
ev_positive: false,
|
||||||
|
reasoning: ["Bültende uygun maç bulunamadı."],
|
||||||
|
rejected_matches: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Her maç için frekans sinyallerini hesapla (paralel)
|
||||||
|
const candidatePromises = upcomingRows.map((row) =>
|
||||||
|
this.frequencyEngine.buildMatchCandidate(row).then((candidate) => ({
|
||||||
|
candidate,
|
||||||
|
row,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const candidateResults = await Promise.all(candidatePromises);
|
||||||
|
|
||||||
|
// 3. Sinyali olan adayları filtrele
|
||||||
|
const allCandidates: Array<{
|
||||||
|
candidate: MatchCandidate;
|
||||||
|
row: (typeof upcomingRows)[0];
|
||||||
|
}> = [];
|
||||||
|
const rejected: FrequencyCouponResult["rejected_matches"] = [];
|
||||||
|
|
||||||
|
for (const { candidate, row } of candidateResults) {
|
||||||
|
if (!candidate) {
|
||||||
|
rejected.push({
|
||||||
|
match_id: row.match_id,
|
||||||
|
match_name: `${row.home_team_name} vs ${row.away_team_name}`,
|
||||||
|
reason: `Yetersiz geçmiş veri (min ${3} maç gerekli)`,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Market filtresi uygula
|
||||||
|
let filteredSignals = candidate.signals;
|
||||||
|
if (allowedMarkets) {
|
||||||
|
filteredSignals = filteredSignals.filter((s) =>
|
||||||
|
allowedMarkets.some((m) => s.market.includes(m)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min signal filtresi
|
||||||
|
filteredSignals = filteredSignals.filter(
|
||||||
|
(s) => s.combinedSignal >= minSignal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredSignals.length === 0) {
|
||||||
|
rejected.push({
|
||||||
|
match_id: row.match_id,
|
||||||
|
match_name: `${row.home_team_name} vs ${row.away_team_name}`,
|
||||||
|
reason: `Kombinasyon sinyali ${(minSignal * 100).toFixed(0)}% eşiğinin altında`,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// En güçlü sinyali seç
|
||||||
|
candidate.signals = filteredSignals;
|
||||||
|
candidate.bestSignal = filteredSignals[0];
|
||||||
|
allCandidates.push({ candidate, row });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`[FrequencyCoupon] ${allCandidates.length} candidates passed filters, ${rejected.length} rejected`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. En güçlü sinyale göre sırala
|
||||||
|
allCandidates.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(b.candidate.bestSignal?.confidence ?? 0) -
|
||||||
|
(a.candidate.bestSignal?.confidence ?? 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Çeşitlilik: aynı ligden max 2 maç
|
||||||
|
const selected: typeof allCandidates = [];
|
||||||
|
const leagueCount = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const entry of allCandidates) {
|
||||||
|
if (selected.length >= maxMatches) break;
|
||||||
|
|
||||||
|
const lid = entry.candidate.leagueId;
|
||||||
|
const currentCount = leagueCount.get(lid) || 0;
|
||||||
|
if (currentCount >= 2) {
|
||||||
|
rejected.push({
|
||||||
|
match_id: entry.candidate.matchId,
|
||||||
|
match_name: `${entry.candidate.homeTeamName} vs ${entry.candidate.awayTeamName}`,
|
||||||
|
reason: `Aynı ligden zaten 2 maç seçildi (${entry.candidate.leagueName})`,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
selected.push(entry);
|
||||||
|
leagueCount.set(lid, currentCount + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Sonucu oluştur
|
||||||
|
const bets: FrequencyCouponResult["bets"] = [];
|
||||||
|
let totalOdds = 1;
|
||||||
|
let combinedHitRate = 1;
|
||||||
|
const reasoning: string[] = [];
|
||||||
|
|
||||||
|
for (const { candidate, row } of selected) {
|
||||||
|
const signal = candidate.bestSignal!;
|
||||||
|
const betOdds = this.frequencyEngine.getMarketOdds(row, signal.market);
|
||||||
|
|
||||||
|
if (betOdds <= 0) continue;
|
||||||
|
|
||||||
|
const homeBand = this.frequencyEngine.getOddsBand(candidate.homeOdds);
|
||||||
|
const awayBand = this.frequencyEngine.getOddsBand(candidate.awayOdds);
|
||||||
|
|
||||||
|
// Lig profili belirle
|
||||||
|
let leagueProfile = "NORMAL";
|
||||||
|
if (signal.leagueBonus > 0.02) leagueProfile = "GOLCU";
|
||||||
|
else if (signal.leagueBonus < -0.02) leagueProfile = "DEFANSIF";
|
||||||
|
|
||||||
|
bets.push({
|
||||||
|
match_id: candidate.matchId,
|
||||||
|
match_name: `${candidate.homeTeamName} vs ${candidate.awayTeamName}`,
|
||||||
|
league: candidate.leagueName,
|
||||||
|
market: signal.market,
|
||||||
|
pick: signal.pick,
|
||||||
|
home_signal: parseFloat(signal.homeSignal.toFixed(3)),
|
||||||
|
away_signal: parseFloat(signal.awaySignal.toFixed(3)),
|
||||||
|
combined_signal: parseFloat(signal.combinedSignal.toFixed(3)),
|
||||||
|
league_profile: leagueProfile,
|
||||||
|
historical_hit_rate: parseFloat(signal.combinedSignal.toFixed(3)),
|
||||||
|
odds: betOdds,
|
||||||
|
home_odds_band: homeBand,
|
||||||
|
away_odds_band: awayBand,
|
||||||
|
home_match_count: signal.homeMatchCount,
|
||||||
|
away_match_count: signal.awayMatchCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
totalOdds *= betOdds;
|
||||||
|
combinedHitRate *= signal.combinedSignal;
|
||||||
|
|
||||||
|
reasoning.push(
|
||||||
|
`${candidate.homeTeamName} vs ${candidate.awayTeamName}: ` +
|
||||||
|
`${signal.pick} — Ev(${homeBand}): ${(signal.homeSignal * 100).toFixed(0)}% (${signal.homeMatchCount} maç), ` +
|
||||||
|
`Dep(${awayBand}): ${(signal.awaySignal * 100).toFixed(0)}% (${signal.awayMatchCount} maç)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalOdds = parseFloat(totalOdds.toFixed(2));
|
||||||
|
const expectedValue = parseFloat((combinedHitRate * totalOdds).toFixed(3));
|
||||||
|
|
||||||
|
return {
|
||||||
|
strategy: "FREQUENCY",
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
bets,
|
||||||
|
total_odds: totalOdds,
|
||||||
|
expected_hit_rate: parseFloat(combinedHitRate.toFixed(4)),
|
||||||
|
expected_value: expectedValue,
|
||||||
|
ev_positive: expectedValue > 1.0,
|
||||||
|
reasoning,
|
||||||
|
rejected_matches: rejected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Frequency Coupon Result Interface
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface FrequencyCouponResult {
|
||||||
|
strategy: "FREQUENCY";
|
||||||
|
generated_at: string;
|
||||||
|
bets: Array<{
|
||||||
|
match_id: string;
|
||||||
|
match_name: string;
|
||||||
|
league: string;
|
||||||
|
market: string;
|
||||||
|
pick: string;
|
||||||
|
home_signal: number;
|
||||||
|
away_signal: number;
|
||||||
|
combined_signal: number;
|
||||||
|
league_profile: string;
|
||||||
|
historical_hit_rate: number;
|
||||||
|
odds: number;
|
||||||
|
home_odds_band: string;
|
||||||
|
away_odds_band: string;
|
||||||
|
home_match_count: number;
|
||||||
|
away_match_count: number;
|
||||||
|
}>;
|
||||||
|
total_odds: number;
|
||||||
|
expected_hit_rate: number;
|
||||||
|
expected_value: number;
|
||||||
|
ev_positive: boolean;
|
||||||
|
reasoning: string[];
|
||||||
|
rejected_matches: Array<{
|
||||||
|
match_id: string;
|
||||||
|
match_name: string;
|
||||||
|
reason: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MATCH_COMMENTARY_SYSTEM_PROMPT = `Sen uzman bir futbol bahis analistisin. Sana verilen model çıktısını analiz edip kısa, net ve aksiyon odaklı Türkçe bir yorum yaz.
|
const MATCH_COMMENTARY_SYSTEM_PROMPT = `Sen uzman bir futbol bahis analistisin. Sana verilen model çıktısını analiz edip kısa, net ve aksiyon odaklı Türkçe bir yorum yaz.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
BasketballTeamStats,
|
BasketballTeamStats,
|
||||||
} from "./feeder.types";
|
} from "./feeder.types";
|
||||||
import { ImageUtils } from "../../common/utils/image.util";
|
import { ImageUtils } from "../../common/utils/image.util";
|
||||||
|
import { deriveStoredMatchStatus } from "../../common/utils/match-status.util";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FeederPersistenceService {
|
export class FeederPersistenceService {
|
||||||
@@ -311,33 +312,15 @@ export class FeederPersistenceService {
|
|||||||
headerData?.htScoreAway ??
|
headerData?.htScoreAway ??
|
||||||
this.safeInt(matchSummary.score?.ht?.away);
|
this.safeInt(matchSummary.score?.ht?.away);
|
||||||
|
|
||||||
let status = "NS";
|
const status = deriveStoredMatchStatus({
|
||||||
if (headerData?.matchStatus) {
|
state: headerData?.matchStatus ?? matchSummary.state,
|
||||||
if (
|
status: matchSummary.status,
|
||||||
headerData.matchStatus === "postGame" ||
|
substate: matchSummary.substate,
|
||||||
headerData.matchStatus === "post"
|
statusBoxContent: matchSummary.statusBoxContent,
|
||||||
) {
|
scoreHome: finalScoreHome,
|
||||||
status = "FT";
|
scoreAway: finalScoreAway,
|
||||||
} else if (
|
score: matchSummary.score,
|
||||||
headerData.matchStatus === "live" ||
|
});
|
||||||
headerData.matchStatus === "liveGame"
|
|
||||||
) {
|
|
||||||
status = "LIVE";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Postponed Matches (ERT)
|
|
||||||
if (matchSummary.statusBoxContent === "ERT") {
|
|
||||||
status = "POSTPONED";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
status === "NS" &&
|
|
||||||
finalScoreHome !== null &&
|
|
||||||
finalScoreAway !== null
|
|
||||||
) {
|
|
||||||
status = "FT";
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx.match.upsert({
|
await tx.match.upsert({
|
||||||
where: { id: matchId },
|
where: { id: matchId },
|
||||||
@@ -870,15 +853,11 @@ export class FeederPersistenceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getExistingMatchIds(matchIds: string[]): Promise<string[]> {
|
async getExistingMatchIds(matchIds: string[]): Promise<string[]> {
|
||||||
// Only consider matches "existing" if they have ALL key data points
|
|
||||||
// This allows re-fetching matches that exist but have missing data
|
|
||||||
const matches = await this.prisma.match.findMany({
|
const matches = await this.prisma.match.findMany({
|
||||||
where: {
|
where: {
|
||||||
id: { in: matchIds },
|
id: { in: matchIds },
|
||||||
AND: [
|
AND: [
|
||||||
{ oddCategories: { some: {} } },
|
{ oddCategories: { some: {} } },
|
||||||
{ playerEvents: { some: {} } },
|
|
||||||
{ officials: { some: {} } },
|
|
||||||
{
|
{
|
||||||
OR: [
|
OR: [
|
||||||
{ footballTeamStats: { some: {} } },
|
{ footballTeamStats: { some: {} } },
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
DbEventPayload,
|
DbEventPayload,
|
||||||
DbMarketPayload,
|
DbMarketPayload,
|
||||||
} from "./feeder.types";
|
} from "./feeder.types";
|
||||||
|
import { isMatchCompleted } from "../../common/utils/match-status.util";
|
||||||
|
|
||||||
interface ProcessDateOptions {
|
interface ProcessDateOptions {
|
||||||
onlyCompletedMatches?: boolean;
|
onlyCompletedMatches?: boolean;
|
||||||
@@ -113,51 +114,16 @@ export class FeederService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseScoreValue(value: unknown): number | null {
|
|
||||||
if (value === null || value === undefined || value === "") return null;
|
|
||||||
const parsed = Number(value);
|
|
||||||
return Number.isFinite(parsed) ? parsed : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isCompletedMatchSummary(match: MatchSummary): boolean {
|
private isCompletedMatchSummary(match: MatchSummary): boolean {
|
||||||
if (match.statusBoxContent === "ERT") return false;
|
return isMatchCompleted({
|
||||||
|
state: match.state,
|
||||||
const normalizedState = String(match.state || "")
|
status: match.status,
|
||||||
.trim()
|
substate: match.substate,
|
||||||
.toLowerCase();
|
statusBoxContent: match.statusBoxContent,
|
||||||
const normalizedStatus = String(match.status || "")
|
score: match.score,
|
||||||
.trim()
|
scoreHome: match.homeScore,
|
||||||
.toLowerCase();
|
scoreAway: match.awayScore,
|
||||||
const normalizedSubstate = String(match.substate || "")
|
});
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
if (["postgame", "post"].includes(normalizedState)) return true;
|
|
||||||
|
|
||||||
if (
|
|
||||||
["played", "finished", "ft", "afterpenalties", "penalties"].includes(
|
|
||||||
normalizedStatus,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
["postgame", "post", "played", "finished", "ft"].includes(
|
|
||||||
normalizedSubstate,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const homeScore = this.parseScoreValue(
|
|
||||||
match.score?.home ?? match.homeScore,
|
|
||||||
);
|
|
||||||
const awayScore = this.parseScoreValue(
|
|
||||||
match.score?.away ?? match.awayScore,
|
|
||||||
);
|
|
||||||
|
|
||||||
return homeScore !== null && awayScore !== null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async runPreviousDayCompletedMatchesScan(
|
async runPreviousDayCompletedMatchesScan(
|
||||||
@@ -957,15 +923,30 @@ export class FeederService {
|
|||||||
*/
|
*/
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
if (saved && hasCriticalError) {
|
const completedMatch = isMatchCompleted({
|
||||||
// Collect missing components
|
state: headerData?.matchStatus ?? matchSummary.state,
|
||||||
const missingParts: string[] = [];
|
status: matchSummary.status,
|
||||||
if (!stats) missingParts.push("Stats");
|
substate: matchSummary.substate,
|
||||||
|
statusBoxContent: matchSummary.statusBoxContent,
|
||||||
|
scoreHome: headerData?.scoreHome ?? matchSummary.score?.home,
|
||||||
|
scoreAway: headerData?.scoreAway ?? matchSummary.score?.away,
|
||||||
|
});
|
||||||
|
|
||||||
|
const missingParts: string[] = [];
|
||||||
|
if (scope === "all" && completedMatch) {
|
||||||
|
if (sport === "football" && !stats) missingParts.push("Stats");
|
||||||
|
if (sport === "basketball" && !basketballTeamStats)
|
||||||
|
missingParts.push("BoxScore");
|
||||||
if (oddsArray.length === 0) missingParts.push("Odds");
|
if (oddsArray.length === 0) missingParts.push("Odds");
|
||||||
if (officialsData.length === 0) missingParts.push("Officials");
|
}
|
||||||
|
|
||||||
|
if (saved && (hasCriticalError || missingParts.length > 0)) {
|
||||||
|
const reason = hasCriticalError
|
||||||
|
? "missing data after upstream errors"
|
||||||
|
: "incomplete completed-match payload";
|
||||||
|
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`[${matchId}] Saved with MISSING DATA (502). Missing: [${missingParts.join(", ")}]. Scheduled for retry.`,
|
`[${matchId}] Saved with ${reason}. Missing: [${missingParts.join(", ")}]. Scheduled for retry.`,
|
||||||
);
|
);
|
||||||
return { success: false, retryable: true };
|
return { success: false, retryable: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,90 @@
|
|||||||
import { Controller, Get } from "@nestjs/common";
|
import { Controller, Get, Res } from "@nestjs/common";
|
||||||
import { ApiTags, ApiOperation } from "@nestjs/swagger";
|
import { ApiTags, ApiOperation } from "@nestjs/swagger";
|
||||||
import {
|
import type { Response } from "express";
|
||||||
HealthCheck,
|
|
||||||
HealthCheckService,
|
|
||||||
PrismaHealthIndicator,
|
|
||||||
} from "@nestjs/terminus";
|
|
||||||
import { Public } from "../../common/decorators";
|
import { Public } from "../../common/decorators";
|
||||||
import { PrismaService } from "../../database/prisma.service";
|
import { PrismaService } from "../../database/prisma.service";
|
||||||
|
import { PredictionsService } from "../predictions/predictions.service";
|
||||||
|
|
||||||
@ApiTags("Health")
|
@ApiTags("Health")
|
||||||
@Controller("health")
|
@Controller("health")
|
||||||
export class HealthController {
|
export class HealthController {
|
||||||
constructor(
|
constructor(
|
||||||
private health: HealthCheckService,
|
|
||||||
private prismaHealth: PrismaHealthIndicator,
|
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
|
private readonly predictionsService: PredictionsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Public()
|
@Public()
|
||||||
@HealthCheck()
|
|
||||||
@ApiOperation({ summary: "Basic health check" })
|
@ApiOperation({ summary: "Basic health check" })
|
||||||
check() {
|
async check(@Res() response: Response) {
|
||||||
return this.health.check([]);
|
const database = await this.getDatabaseHealth();
|
||||||
|
const aiEngine = await this.predictionsService.checkHealth();
|
||||||
|
const ok = database.status === "up" && aiEngine.predictionServiceReady;
|
||||||
|
|
||||||
|
return response.status(ok ? 200 : 503).json({
|
||||||
|
status: ok ? "ok" : "degraded",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
checks: {
|
||||||
|
database,
|
||||||
|
aiEngine,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("ready")
|
@Get("ready")
|
||||||
@Public()
|
@Public()
|
||||||
@HealthCheck()
|
|
||||||
@ApiOperation({ summary: "Readiness check (includes database)" })
|
@ApiOperation({ summary: "Readiness check (includes database)" })
|
||||||
readiness() {
|
async readiness(@Res() response: Response) {
|
||||||
return this.health.check([
|
const database = await this.getDatabaseHealth();
|
||||||
() => this.prismaHealth.pingCheck("database", this.prisma),
|
const aiEngine = await this.predictionsService.checkHealth();
|
||||||
]);
|
const ready = database.status === "up" && aiEngine.predictionServiceReady;
|
||||||
|
|
||||||
|
return response.status(ready ? 200 : 503).json({
|
||||||
|
status: ready ? "ready" : "not_ready",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
checks: {
|
||||||
|
database,
|
||||||
|
aiEngine,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("live")
|
@Get("live")
|
||||||
@Public()
|
@Public()
|
||||||
@ApiOperation({ summary: "Liveness check" })
|
@ApiOperation({ summary: "Liveness check" })
|
||||||
liveness() {
|
liveness(@Res() response: Response) {
|
||||||
return { status: "ok", timestamp: new Date().toISOString() };
|
return response
|
||||||
|
.status(200)
|
||||||
|
.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("dependencies")
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: "Dependency-level health details" })
|
||||||
|
async dependencies(@Res() response: Response) {
|
||||||
|
const database = await this.getDatabaseHealth();
|
||||||
|
const aiEngine = await this.predictionsService.checkHealth();
|
||||||
|
|
||||||
|
return response.status(200).json({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
checks: {
|
||||||
|
database,
|
||||||
|
aiEngine,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getDatabaseHealth() {
|
||||||
|
try {
|
||||||
|
await this.prisma.$queryRaw`SELECT 1`;
|
||||||
|
return {
|
||||||
|
status: "up",
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
status: "down",
|
||||||
|
detail: error instanceof Error ? error.message : "Unknown database error",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { TerminusModule } from "@nestjs/terminus";
|
|
||||||
import { PrismaHealthIndicator } from "@nestjs/terminus";
|
|
||||||
import { HealthController } from "./health.controller";
|
import { HealthController } from "./health.controller";
|
||||||
|
import { PredictionsModule } from "../predictions/predictions.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TerminusModule],
|
imports: [PredictionsModule],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
providers: [PrismaHealthIndicator],
|
|
||||||
})
|
})
|
||||||
export class HealthModule {}
|
export class HealthModule {}
|
||||||
|
|||||||
@@ -119,20 +119,26 @@ export class LeaguesController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /leagues/teams/:id/matches
|
* GET /leagues/teams/:id/matches
|
||||||
* Get team's recent matches
|
* Get team's recent matches (paginated)
|
||||||
*/
|
*/
|
||||||
@Get("teams/:id/matches")
|
@Get("teams/:id/matches")
|
||||||
@Public()
|
@Public()
|
||||||
@ApiOperation({ summary: "Get team's recent matches" })
|
@ApiOperation({ summary: "Get team's recent matches (paginated)" })
|
||||||
@ApiParam({ name: "id", description: "Team ID" })
|
@ApiParam({ name: "id", description: "Team ID" })
|
||||||
@ApiQuery({ name: "limit", required: false, type: Number })
|
@ApiQuery({ name: "page", required: false, type: Number, description: "Page number (default: 1)" })
|
||||||
|
@ApiQuery({ name: "limit", required: false, type: Number, description: "Items per page (default: 20)" })
|
||||||
|
@ApiQuery({ name: "season", required: false, type: String, description: "Season (e.g. 2024-2025)" })
|
||||||
async getTeamMatches(
|
async getTeamMatches(
|
||||||
@Param("id") id: string,
|
@Param("id") id: string,
|
||||||
|
@Query("page") page?: string,
|
||||||
@Query("limit") limit?: string,
|
@Query("limit") limit?: string,
|
||||||
|
@Query("season") season?: string,
|
||||||
) {
|
) {
|
||||||
return this.leaguesService.getTeamRecentMatches(
|
return this.leaguesService.getTeamRecentMatches(
|
||||||
id,
|
id,
|
||||||
parseInt(limit || "10", 10),
|
parseInt(page || "1", 10),
|
||||||
|
parseInt(limit || "20", 10),
|
||||||
|
season
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,21 +99,81 @@ export class LeaguesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get team's matches (past + upcoming)
|
* Get team's matches (past + upcoming) with pagination
|
||||||
*/
|
*/
|
||||||
async getTeamRecentMatches(teamId: string, limit: number = 50) {
|
async getTeamRecentMatches(
|
||||||
return this.prisma.match.findMany({
|
teamId: string,
|
||||||
where: {
|
page: number = 1,
|
||||||
OR: [{ homeTeamId: teamId }, { awayTeamId: teamId }],
|
limit: number = 20,
|
||||||
},
|
season?: string
|
||||||
include: {
|
) {
|
||||||
homeTeam: true,
|
const skip = (page - 1) * limit;
|
||||||
awayTeam: true,
|
const where: any = {
|
||||||
league: { include: { country: true } },
|
OR: [{ homeTeamId: teamId }, { awayTeamId: teamId }],
|
||||||
},
|
};
|
||||||
orderBy: { mstUtc: "desc" },
|
|
||||||
take: limit,
|
if (season) {
|
||||||
});
|
// season format expected: "2024-2025"
|
||||||
|
const parts = season.split("-");
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const startYear = parseInt(parts[0], 10);
|
||||||
|
const endYear = parseInt(parts[1], 10);
|
||||||
|
|
||||||
|
if (!isNaN(startYear) && !isNaN(endYear)) {
|
||||||
|
// Season starts August 1st of startYear
|
||||||
|
const startDate = new Date(Date.UTC(startYear, 7, 1)).getTime();
|
||||||
|
// Season ends July 31st of endYear
|
||||||
|
const endDate = new Date(Date.UTC(endYear, 6, 31, 23, 59, 59, 999)).getTime();
|
||||||
|
|
||||||
|
where.mstUtc = {
|
||||||
|
gte: startDate,
|
||||||
|
lte: endDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await this.prisma.$transaction([
|
||||||
|
this.prisma.match.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
homeTeam: true,
|
||||||
|
awayTeam: true,
|
||||||
|
league: { include: { country: true } },
|
||||||
|
},
|
||||||
|
orderBy: { mstUtc: "desc" },
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
}),
|
||||||
|
this.prisma.match.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
matchName: m.matchName,
|
||||||
|
matchSlug: m.matchSlug,
|
||||||
|
mstUtc: Number(m.mstUtc),
|
||||||
|
scoreHome: m.scoreHome,
|
||||||
|
scoreAway: m.scoreAway,
|
||||||
|
status: m.status,
|
||||||
|
state: m.state,
|
||||||
|
homeTeamName: m.homeTeam?.name,
|
||||||
|
homeTeamLogo: m.homeTeamId
|
||||||
|
? `https://file.mackolikfeeds.com/teams/${m.homeTeamId}`
|
||||||
|
: null,
|
||||||
|
awayTeamName: m.awayTeam?.name,
|
||||||
|
awayTeamLogo: m.awayTeamId
|
||||||
|
? `https://file.mackolikfeeds.com/teams/${m.awayTeamId}`
|
||||||
|
: null,
|
||||||
|
leagueName: m.league?.name,
|
||||||
|
countryName: m.league?.country?.name,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ import {
|
|||||||
ActiveLeagueDto,
|
ActiveLeagueDto,
|
||||||
} from "./dto";
|
} from "./dto";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import {
|
||||||
|
FINISHED_STATE_VALUES_FOR_DB,
|
||||||
|
FINISHED_STATUS_VALUES_FOR_DB,
|
||||||
|
LIVE_STATE_VALUES_FOR_DB,
|
||||||
|
LIVE_STATUS_VALUES_FOR_DB,
|
||||||
|
getDisplayMatchStatus,
|
||||||
|
} from "../../common/utils/match-status.util";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MatchesService {
|
export class MatchesService {
|
||||||
@@ -38,23 +45,12 @@ export class MatchesService {
|
|||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
status: {
|
status: {
|
||||||
in: [
|
in: LIVE_STATUS_VALUES_FOR_DB,
|
||||||
"LIVE",
|
|
||||||
"1H",
|
|
||||||
"2H",
|
|
||||||
"HT",
|
|
||||||
"1Q",
|
|
||||||
"2Q",
|
|
||||||
"3Q",
|
|
||||||
"4Q",
|
|
||||||
"Playing",
|
|
||||||
"Half Time",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: {
|
state: {
|
||||||
in: ["live", "firsthalf", "secondhalf"],
|
in: LIVE_STATE_VALUES_FOR_DB,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -66,14 +62,23 @@ export class MatchesService {
|
|||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
status: {
|
status: {
|
||||||
in: ["Finished", "Played", "FT", "AET", "PEN", "Ended"],
|
in: FINISHED_STATUS_VALUES_FOR_DB,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: {
|
state: {
|
||||||
in: ["Finished", "post", "FT", "postGame"],
|
in: FINISHED_STATE_VALUES_FOR_DB,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ scoreHome: { not: null } },
|
||||||
|
{ scoreAway: { not: null } },
|
||||||
|
{
|
||||||
|
NOT: this.getLiveFilter(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -325,16 +330,13 @@ export class MatchesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map status for frontend
|
// Map status for frontend
|
||||||
let displayStatus = match.status || "NS";
|
const displayStatus = getDisplayMatchStatus({
|
||||||
if (match.state === "live") {
|
state: match.state,
|
||||||
displayStatus = "LIVE";
|
status: match.status,
|
||||||
} else if (
|
substate: match.substate,
|
||||||
match.state === "post" ||
|
scoreHome: match.scoreHome,
|
||||||
match.state === "FT" ||
|
scoreAway: match.scoreAway,
|
||||||
match.status === "Finished"
|
});
|
||||||
) {
|
|
||||||
displayStatus = "Finished";
|
|
||||||
}
|
|
||||||
|
|
||||||
league.matches.push({
|
league.matches.push({
|
||||||
id: match.id,
|
id: match.id,
|
||||||
@@ -562,16 +564,13 @@ export class MatchesService {
|
|||||||
|
|
||||||
if (liveMatch) {
|
if (liveMatch) {
|
||||||
// Map liveMatch status
|
// Map liveMatch status
|
||||||
let displayStatus = liveMatch.status || "NS";
|
const displayStatus = getDisplayMatchStatus({
|
||||||
if (liveMatch.state === "live") {
|
state: liveMatch.state,
|
||||||
displayStatus = "LIVE";
|
status: liveMatch.status,
|
||||||
} else if (
|
substate: liveMatch.substate,
|
||||||
liveMatch.state === "post" ||
|
scoreHome: liveMatch.scoreHome,
|
||||||
liveMatch.state === "FT" ||
|
scoreAway: liveMatch.scoreAway,
|
||||||
liveMatch.status === "Finished"
|
});
|
||||||
) {
|
|
||||||
displayStatus = "Finished";
|
|
||||||
}
|
|
||||||
|
|
||||||
match = {
|
match = {
|
||||||
...liveMatch,
|
...liveMatch,
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ export class MatchPickDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
market: string;
|
market: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: "standard" })
|
||||||
|
strategy_channel?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
pick: string;
|
pick: string;
|
||||||
|
|
||||||
@@ -350,6 +353,15 @@ export class MatchPredictionDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
model_version: string;
|
model_version: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
calibration_version?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
shadow_engine_version?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
decision_trace_id?: string | null;
|
||||||
|
|
||||||
@ApiProperty({ type: MatchInfoDto })
|
@ApiProperty({ type: MatchInfoDto })
|
||||||
match_info: MatchInfoDto;
|
match_info: MatchInfoDto;
|
||||||
|
|
||||||
@@ -368,6 +380,9 @@ export class MatchPredictionDto {
|
|||||||
@ApiProperty({ type: MatchPickDto, nullable: true })
|
@ApiProperty({ type: MatchPickDto, nullable: true })
|
||||||
value_pick: MatchPickDto | null;
|
value_pick: MatchPickDto | null;
|
||||||
|
|
||||||
|
@ApiProperty({ type: MatchPickDto, nullable: true, required: false })
|
||||||
|
surprise_pick?: MatchPickDto | null;
|
||||||
|
|
||||||
@ApiProperty({ type: MatchBetAdviceDto })
|
@ApiProperty({ type: MatchBetAdviceDto })
|
||||||
bet_advice: MatchBetAdviceDto;
|
bet_advice: MatchBetAdviceDto;
|
||||||
|
|
||||||
@@ -394,6 +409,23 @@ export class MatchPredictionDto {
|
|||||||
|
|
||||||
@ApiProperty({ type: [String] })
|
@ApiProperty({ type: [String] })
|
||||||
reasoning_factors: string[];
|
reasoning_factors: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: Object, required: false })
|
||||||
|
market_reliability?: Record<string, number>;
|
||||||
|
|
||||||
|
@ApiProperty({ type: Object, required: false })
|
||||||
|
shadow_engine?: Record<string, unknown>;
|
||||||
|
|
||||||
|
@ApiProperty({ type: Object, required: false })
|
||||||
|
surprise_hunter?: Record<string, unknown>;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
description:
|
||||||
|
"V28 Odds-Band engine output: historical band analytics, triple-value detection, cards profiling, and HTFT 9-combo analysis",
|
||||||
|
})
|
||||||
|
v27_engine?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ValueBetDto {
|
export class ValueBetDto {
|
||||||
@@ -461,6 +493,24 @@ export class AIHealthDto {
|
|||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
predictionServiceReady: boolean;
|
predictionServiceReady: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: true })
|
||||||
|
aiEngineReachable?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, enum: ["closed", "open"] })
|
||||||
|
circuitState?: "closed" | "open";
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: 0 })
|
||||||
|
consecutiveFailures?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
endpoint?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
detail?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
mode?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from "./smart-coupon.dto";
|
export * from "./smart-coupon.dto";
|
||||||
|
|||||||
@@ -96,11 +96,11 @@ export class PredictionsController {
|
|||||||
async getPrediction(
|
async getPrediction(
|
||||||
@Param("matchId") matchId: string,
|
@Param("matchId") matchId: string,
|
||||||
): Promise<MatchPredictionDto> {
|
): Promise<MatchPredictionDto> {
|
||||||
// Check cache first
|
// Check cache first - DISABLED per user request to always fetch from scratch
|
||||||
const cached = await this.predictionsService.getCachedPrediction(matchId);
|
// const cached = await this.predictionsService.getCachedPrediction(matchId);
|
||||||
if (cached) {
|
// if (cached) {
|
||||||
return cached;
|
// return cached;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Get from AI Engine
|
// Get from AI Engine
|
||||||
const prediction = await this.predictionsService.getPredictionById(matchId);
|
const prediction = await this.predictionsService.getPredictionById(matchId);
|
||||||
@@ -109,9 +109,6 @@ export class PredictionsController {
|
|||||||
throw new NotFoundException(`Match not found: ${matchId}`);
|
throw new NotFoundException(`Match not found: ${matchId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
await this.predictionsService.cachePrediction(matchId, prediction);
|
|
||||||
|
|
||||||
return prediction;
|
return prediction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,14 @@ import {
|
|||||||
ValueBetDto,
|
ValueBetDto,
|
||||||
AIHealthDto,
|
AIHealthDto,
|
||||||
} from "./dto";
|
} from "./dto";
|
||||||
import axios, { AxiosError } from "axios";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { FeederService } from "../feeder/feeder.service";
|
import { FeederService } from "../feeder/feeder.service";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
|
import {
|
||||||
|
AiEngineClient,
|
||||||
|
AiEngineRequestError,
|
||||||
|
} from "../../common/utils/ai-engine-client";
|
||||||
|
|
||||||
type ConfidenceBand = "HIGH" | "MEDIUM" | "LOW";
|
type ConfidenceBand = "HIGH" | "MEDIUM" | "LOW";
|
||||||
|
|
||||||
@@ -45,6 +48,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
private readonly logger = new Logger(PredictionsService.name);
|
private readonly logger = new Logger(PredictionsService.name);
|
||||||
private queueEvents: QueueEvents | null = null;
|
private queueEvents: QueueEvents | null = null;
|
||||||
private readonly aiEngineUrl: string;
|
private readonly aiEngineUrl: string;
|
||||||
|
private readonly aiEngineClient: AiEngineClient;
|
||||||
private readonly topLeagueIds = new Set<string>();
|
private readonly topLeagueIds = new Set<string>();
|
||||||
private readonly reasonTranslations: Record<string, string> = {
|
private readonly reasonTranslations: Record<string, string> = {
|
||||||
confidence_below_threshold: "Güven eşiğin altında",
|
confidence_below_threshold: "Güven eşiğin altında",
|
||||||
@@ -125,6 +129,14 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
"AI_ENGINE_URL",
|
"AI_ENGINE_URL",
|
||||||
"http://localhost:8000",
|
"http://localhost:8000",
|
||||||
);
|
);
|
||||||
|
this.aiEngineClient = new AiEngineClient({
|
||||||
|
baseUrl: this.aiEngineUrl,
|
||||||
|
logger: this.logger,
|
||||||
|
serviceName: PredictionsService.name,
|
||||||
|
timeoutMs: 60000,
|
||||||
|
maxRetries: 2,
|
||||||
|
retryDelayMs: 750,
|
||||||
|
});
|
||||||
this.topLeagueIds = this.loadTopLeagueIds();
|
this.topLeagueIds = this.loadTopLeagueIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,55 +161,80 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkHealth(): Promise<AIHealthDto> {
|
async checkHealth(): Promise<AIHealthDto> {
|
||||||
return Promise.resolve({
|
const circuit = this.aiEngineClient.getSnapshot();
|
||||||
status: "healthy",
|
|
||||||
modelLoaded: true,
|
try {
|
||||||
predictionServiceReady: true,
|
const response = await this.aiEngineClient.get<{
|
||||||
});
|
status?: string;
|
||||||
|
model_loaded?: boolean;
|
||||||
|
prediction_service_ready?: boolean;
|
||||||
|
}>("/health", {
|
||||||
|
timeout: 5000,
|
||||||
|
retryCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.data?.status || "healthy",
|
||||||
|
modelLoaded: response.data?.model_loaded ?? true,
|
||||||
|
predictionServiceReady:
|
||||||
|
response.data?.prediction_service_ready ?? true,
|
||||||
|
aiEngineReachable: true,
|
||||||
|
circuitState: circuit.state,
|
||||||
|
consecutiveFailures: circuit.consecutiveFailures,
|
||||||
|
endpoint: this.aiEngineUrl,
|
||||||
|
mode:
|
||||||
|
typeof (response.data as Record<string, unknown>)?.mode === "string"
|
||||||
|
? String((response.data as Record<string, unknown>).mode)
|
||||||
|
: this.configService.get("AI_ENGINE_MODE", "v28-pro-max"),
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const requestError =
|
||||||
|
error instanceof AiEngineRequestError
|
||||||
|
? error
|
||||||
|
: new AiEngineRequestError("AI health check failed");
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: requestError.isCircuitOpen ? "circuit_open" : "unhealthy",
|
||||||
|
modelLoaded: false,
|
||||||
|
predictionServiceReady: false,
|
||||||
|
aiEngineReachable: false,
|
||||||
|
circuitState: this.aiEngineClient.getSnapshot().state,
|
||||||
|
consecutiveFailures:
|
||||||
|
this.aiEngineClient.getSnapshot().consecutiveFailures,
|
||||||
|
endpoint: this.aiEngineUrl,
|
||||||
|
detail:
|
||||||
|
typeof requestError.detail === "string"
|
||||||
|
? requestError.detail
|
||||||
|
: requestError.message,
|
||||||
|
mode: this.configService.get("AI_ENGINE_MODE", "v28-pro-max"),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPredictionById(matchId: string): Promise<MatchPredictionDto | null> {
|
async getPredictionById(matchId: string): Promise<MatchPredictionDto | null> {
|
||||||
await this.ensurePredictionDataReady(matchId);
|
await this.ensurePredictionDataReady(matchId);
|
||||||
const matchContext = await this.getMatchContext(matchId);
|
const matchContext = await this.getMatchContext(matchId);
|
||||||
|
|
||||||
// Queue mode (Redis enabled)
|
// Queue mode (Redis enabled) - REMOVED per user request to always fetch from scratch
|
||||||
if (this.predictionsQueue && this.queueEvents) {
|
|
||||||
try {
|
|
||||||
const job = await this.predictionsQueue.addPredictMatchJob({ matchId });
|
|
||||||
const data = await job.waitUntilFinished(this.queueEvents, 30000);
|
|
||||||
if (!data || data.error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.enrichPredictionResponse(
|
|
||||||
data as MatchPredictionDto,
|
|
||||||
matchContext,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
this.logger.error(`Prediction queue failed for ${matchId}: ${message}`);
|
|
||||||
this.throwAiError(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct HTTP mode (no Redis)
|
// Direct HTTP mode (no Redis)
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await this.aiEngineClient.post<MatchPredictionDto>(
|
||||||
`${this.aiEngineUrl}/v20plus/analyze/${matchId}`,
|
`/v20plus/analyze/${matchId}`,
|
||||||
{},
|
{ simulate: true, is_simulation: true, pre_match_only: true },
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
);
|
||||||
|
await this.recordPredictionRun(matchId, response.data);
|
||||||
return this.enrichPredictionResponse(
|
return this.enrichPredictionResponse(
|
||||||
response.data as MatchPredictionDto,
|
response.data as MatchPredictionDto,
|
||||||
matchContext,
|
matchContext,
|
||||||
);
|
);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const error = e as AxiosError<Record<string, unknown>>;
|
const requestError =
|
||||||
const status = error?.response?.status;
|
e instanceof AiEngineRequestError
|
||||||
const detail =
|
? e
|
||||||
error?.response?.data?.detail ||
|
: new AiEngineRequestError("AI Engine request failed");
|
||||||
error?.response?.data ||
|
const status = requestError.status;
|
||||||
error?.message;
|
const detail = requestError.detail || requestError.message;
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Direct AI Engine call failed for ${matchId}: status=${status}, detail=${JSON.stringify(detail)}`,
|
`Direct AI Engine call failed for ${matchId}: status=${status}, detail=${JSON.stringify(detail)}`,
|
||||||
);
|
);
|
||||||
@@ -265,7 +302,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
count: upcoming.length,
|
count: upcoming.length,
|
||||||
modelVersion: "v25-v30-ensemble",
|
modelVersion: "v28-pro-max",
|
||||||
matches: upcoming.map((p) => {
|
matches: upcoming.map((p) => {
|
||||||
const out = p.predictionJson as Record<string, unknown>;
|
const out = p.predictionJson as Record<string, unknown>;
|
||||||
const matchInfo = (out?.match_info || {}) as Record<string, unknown>;
|
const matchInfo = (out?.match_info || {}) as Record<string, unknown>;
|
||||||
@@ -504,6 +541,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
bet_advice: betAdvice as MatchPredictionDto["bet_advice"],
|
bet_advice: betAdvice as MatchPredictionDto["bet_advice"],
|
||||||
market_board: enrichedMarketBoard,
|
market_board: enrichedMarketBoard,
|
||||||
reasoning_factors: reasoningFactors,
|
reasoning_factors: reasoningFactors,
|
||||||
|
model_version: "v28-pro-max",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,14 +1026,18 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
|
|
||||||
// Direct HTTP mode
|
// Direct HTTP mode
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await this.aiEngineClient.post(
|
||||||
`${this.aiEngineUrl}/smart-coupon`,
|
"/smart-coupon",
|
||||||
{ match_ids: matchIds, strategy, ...options },
|
{ match_ids: matchIds, strategy, ...options },
|
||||||
{ timeout: 60000 },
|
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message =
|
||||||
|
error instanceof AiEngineRequestError
|
||||||
|
? error.message
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: String(error);
|
||||||
this.logger.error(`Direct smart coupon call failed: ${message}`);
|
this.logger.error(`Direct smart coupon call failed: ${message}`);
|
||||||
this.throwAiError(message);
|
this.throwAiError(message);
|
||||||
}
|
}
|
||||||
@@ -1018,6 +1060,12 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
HttpStatus.BAD_GATEWAY,
|
HttpStatus.BAD_GATEWAY,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (message.includes("circuit breaker is open")) {
|
||||||
|
throw new HttpException(
|
||||||
|
"AI Engine is temporarily unavailable",
|
||||||
|
HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
|
);
|
||||||
|
}
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
"Failed to get prediction from AI Engine",
|
"Failed to get prediction from AI Engine",
|
||||||
HttpStatus.SERVICE_UNAVAILABLE,
|
HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
@@ -1077,7 +1125,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!modelVersion.startsWith("v25")) {
|
if (!modelVersion.startsWith("v28-pro-max")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,4 +1217,124 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async recordPredictionRun(
|
||||||
|
matchId: string,
|
||||||
|
payload: MatchPredictionDto,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const oddsSnapshot = await this.getPredictionOddsSnapshot(matchId);
|
||||||
|
const payloadSummary = this.buildPredictionPayloadSummary(payload);
|
||||||
|
await this.prisma.$executeRawUnsafe(
|
||||||
|
`
|
||||||
|
INSERT INTO prediction_runs (
|
||||||
|
match_id,
|
||||||
|
engine_version,
|
||||||
|
decision_trace_id,
|
||||||
|
odds_snapshot,
|
||||||
|
payload_summary
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb)
|
||||||
|
`,
|
||||||
|
matchId,
|
||||||
|
String(payload.model_version || "unknown"),
|
||||||
|
typeof payload.decision_trace_id === "string"
|
||||||
|
? payload.decision_trace_id
|
||||||
|
: null,
|
||||||
|
JSON.stringify(oddsSnapshot),
|
||||||
|
JSON.stringify(payloadSummary),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
this.logger.warn(`Prediction run audit skipped for ${matchId}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPredictionOddsSnapshot(
|
||||||
|
matchId: string,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const liveMatch = await this.prisma.liveMatch.findUnique({
|
||||||
|
where: { id: matchId },
|
||||||
|
select: {
|
||||||
|
odds: true,
|
||||||
|
oddsUpdatedAt: true,
|
||||||
|
state: true,
|
||||||
|
status: true,
|
||||||
|
scoreHome: true,
|
||||||
|
scoreAway: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (liveMatch) {
|
||||||
|
return {
|
||||||
|
source: "live_match",
|
||||||
|
odds: liveMatch.odds ?? {},
|
||||||
|
odds_updated_at: liveMatch.oddsUpdatedAt?.toISOString() ?? null,
|
||||||
|
state: liveMatch.state ?? null,
|
||||||
|
status: liveMatch.status ?? null,
|
||||||
|
score_home: liveMatch.scoreHome ?? null,
|
||||||
|
score_away: liveMatch.scoreAway ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const oddCategoryCount = await this.prisma.oddCategory.count({
|
||||||
|
where: { matchId },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
source: "historical_match",
|
||||||
|
odd_category_count: oddCategoryCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPredictionPayloadSummary(
|
||||||
|
payload: MatchPredictionDto,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const topSummary = Array.isArray(payload.bet_summary)
|
||||||
|
? payload.bet_summary.slice(0, 5).map((item) => ({
|
||||||
|
market: item.market,
|
||||||
|
pick: item.pick,
|
||||||
|
playable: item.playable,
|
||||||
|
bet_grade: item.bet_grade,
|
||||||
|
calibrated_confidence: item.calibrated_confidence,
|
||||||
|
ev_edge: item.ev_edge ?? 0,
|
||||||
|
stake_units: item.stake_units,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
model_version: payload.model_version,
|
||||||
|
calibration_version: payload.calibration_version ?? null,
|
||||||
|
shadow_engine_version: payload.shadow_engine_version ?? null,
|
||||||
|
decision_trace_id: payload.decision_trace_id ?? null,
|
||||||
|
main_pick: payload.main_pick
|
||||||
|
? {
|
||||||
|
market: payload.main_pick.market,
|
||||||
|
pick: payload.main_pick.pick,
|
||||||
|
playable: payload.main_pick.playable,
|
||||||
|
bet_grade: payload.main_pick.bet_grade,
|
||||||
|
calibrated_confidence: payload.main_pick.calibrated_confidence,
|
||||||
|
ev_edge: payload.main_pick.ev_edge ?? 0,
|
||||||
|
stake_units: payload.main_pick.stake_units,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
value_pick: payload.value_pick
|
||||||
|
? {
|
||||||
|
market: payload.value_pick.market,
|
||||||
|
pick: payload.value_pick.pick,
|
||||||
|
playable: payload.value_pick.playable,
|
||||||
|
bet_grade: payload.value_pick.bet_grade,
|
||||||
|
calibrated_confidence: payload.value_pick.calibrated_confidence,
|
||||||
|
ev_edge: payload.value_pick.ev_edge ?? 0,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
bet_advice: {
|
||||||
|
playable: payload.bet_advice?.playable ?? false,
|
||||||
|
suggested_stake_units:
|
||||||
|
payload.bet_advice?.suggested_stake_units ?? 0,
|
||||||
|
reason: payload.bet_advice?.reason ?? null,
|
||||||
|
},
|
||||||
|
top_summary: topSummary,
|
||||||
|
market_reliability: payload.market_reliability ?? {},
|
||||||
|
shadow_engine: payload.shadow_engine ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -51,7 +51,11 @@ export class PredictionsProcessor extends WorkerHost {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${this.aiEngineUrl}/v20plus/analyze/${matchId}`,
|
`${this.aiEngineUrl}/v20plus/analyze/${matchId}`,
|
||||||
{},
|
{
|
||||||
|
simulate: data.simulate,
|
||||||
|
is_simulation: data.is_simulation,
|
||||||
|
pre_match_only: data.pre_match_only,
|
||||||
|
},
|
||||||
{ timeout: 30000 },
|
{ timeout: 30000 },
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export enum PredictionJobType {
|
|||||||
export interface PredictMatchJobData {
|
export interface PredictMatchJobData {
|
||||||
matchId: string;
|
matchId: string;
|
||||||
forceUpdate?: boolean;
|
forceUpdate?: boolean;
|
||||||
|
simulate?: boolean;
|
||||||
|
is_simulation?: boolean;
|
||||||
|
pre_match_only?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SmartCouponJobData {
|
export interface SmartCouponJobData {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { RolesGuard } from "../auth/guards/auth.guards";
|
|||||||
@ApiTags("Social Poster")
|
@ApiTags("Social Poster")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(RolesGuard)
|
@UseGuards(RolesGuard)
|
||||||
@Roles("admin")
|
@Roles("superadmin")
|
||||||
@Controller("social-poster")
|
@Controller("social-poster")
|
||||||
export class SocialPosterController {
|
export class SocialPosterController {
|
||||||
constructor(private readonly socialPosterService: SocialPosterService) {}
|
constructor(private readonly socialPosterService: SocialPosterService) {}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class SporTotoController {
|
|||||||
|
|
||||||
@Post("sync")
|
@Post("sync")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Roles("admin")
|
@Roles("superadmin")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@@ -114,7 +114,7 @@ export class SporTotoController {
|
|||||||
|
|
||||||
@Post("bulletins")
|
@Post("bulletins")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Roles("admin")
|
@Roles("superadmin")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@@ -135,7 +135,7 @@ export class SporTotoController {
|
|||||||
|
|
||||||
@Patch("bulletins/:id/results")
|
@Patch("bulletins/:id/results")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Roles("admin")
|
@Roles("superadmin")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export class UsersController extends BaseController<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Override create to require admin role
|
// Override create to require admin role
|
||||||
@Roles("admin")
|
@Roles("superadmin")
|
||||||
async create(
|
async create(
|
||||||
...args: Parameters<
|
...args: Parameters<
|
||||||
BaseController<User, CreateUserDto, UpdateUserDto>["create"]
|
BaseController<User, CreateUserDto, UpdateUserDto>["create"]
|
||||||
@@ -94,7 +94,7 @@ export class UsersController extends BaseController<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Override delete to require admin role
|
// Override delete to require admin role
|
||||||
@Roles("admin")
|
@Roles("superadmin")
|
||||||
async delete(
|
async delete(
|
||||||
...args: Parameters<
|
...args: Parameters<
|
||||||
BaseController<User, CreateUserDto, UpdateUserDto>["delete"]
|
BaseController<User, CreateUserDto, UpdateUserDto>["delete"]
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
+34
-21
@@ -1,7 +1,9 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { HttpService } from "@nestjs/axios";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { firstValueFrom } from "rxjs";
|
import {
|
||||||
|
AiEngineClient,
|
||||||
|
AiEngineRequestError,
|
||||||
|
} from "../common/utils/ai-engine-client";
|
||||||
|
|
||||||
export interface AIPredictionResult {
|
export interface AIPredictionResult {
|
||||||
matchId: string;
|
matchId: string;
|
||||||
@@ -40,13 +42,21 @@ export interface AIPredictionResult {
|
|||||||
export class AiService {
|
export class AiService {
|
||||||
private readonly logger = new Logger(AiService.name);
|
private readonly logger = new Logger(AiService.name);
|
||||||
private readonly pythonEngineUrl: string;
|
private readonly pythonEngineUrl: string;
|
||||||
|
private readonly aiEngineClient: AiEngineClient;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.pythonEngineUrl =
|
this.pythonEngineUrl =
|
||||||
this.configService.get("AI_ENGINE_URL") || "http://127.0.0.1:8000";
|
this.configService.get("AI_ENGINE_URL") || "http://127.0.0.1:8000";
|
||||||
|
this.aiEngineClient = new AiEngineClient({
|
||||||
|
baseUrl: this.pythonEngineUrl,
|
||||||
|
logger: this.logger,
|
||||||
|
serviceName: AiService.name,
|
||||||
|
timeoutMs: 30000,
|
||||||
|
maxRetries: 2,
|
||||||
|
retryDelayMs: 500,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,17 +78,12 @@ export class AiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Calling Python V25 Engine for ${matchDetails.homeTeam} vs ${matchDetails.awayTeam}`,
|
`Calling Python V28 Pro Max Engine for ${matchDetails.homeTeam} vs ${matchDetails.awayTeam}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await firstValueFrom(
|
const response = await this.aiEngineClient.post(
|
||||||
this.httpService.post(
|
`/v20plus/analyze/${matchId}`,
|
||||||
`${this.pythonEngineUrl}/v20plus/analyze/${matchId}`,
|
{},
|
||||||
{},
|
|
||||||
{
|
|
||||||
timeout: 30000,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
@@ -86,8 +91,14 @@ export class AiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn(`Python Engine error: ${error.message}`);
|
const message =
|
||||||
|
error instanceof AiEngineRequestError
|
||||||
|
? error.message
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Unknown AI engine error";
|
||||||
|
this.logger.warn(`Python Engine error: ${message}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +150,7 @@ export class AiService {
|
|||||||
homeAnalysis: undefined,
|
homeAnalysis: undefined,
|
||||||
awayAnalysis: undefined,
|
awayAnalysis: undefined,
|
||||||
expertComment: data.ai_commentary || data.expert_comment || "",
|
expertComment: data.ai_commentary || data.expert_comment || "",
|
||||||
modelVersion: data.model_version || "v25.main",
|
modelVersion: "v28-pro-max",
|
||||||
confidenceScore:
|
confidenceScore:
|
||||||
confidenceScore > 1 ? confidenceScore : confidenceScore * 100,
|
confidenceScore > 1 ? confidenceScore : confidenceScore * 100,
|
||||||
expectedGoals: data?.score_prediction?.xg_total,
|
expectedGoals: data?.score_prediction?.xg_total,
|
||||||
@@ -181,10 +192,10 @@ export class AiService {
|
|||||||
scorePrediction: pyData.score_prediction?.ft || "-",
|
scorePrediction: pyData.score_prediction?.ft || "-",
|
||||||
confidenceScore:
|
confidenceScore:
|
||||||
typeof firstPick?.confidence === "number" ? firstPick.confidence : 0,
|
typeof firstPick?.confidence === "number" ? firstPick.confidence : 0,
|
||||||
modelVersion: pyData.model_version || "v25.main",
|
modelVersion: "v28-pro-max",
|
||||||
expectedGoals: pyData.score_prediction?.xg_total || 0,
|
expectedGoals: pyData.score_prediction?.xg_total || 0,
|
||||||
keyInsights: [
|
keyInsights: [
|
||||||
`Model: ${pyData.model_version || "v25.main"}`,
|
`Model: v28-pro-max`,
|
||||||
`Risk: ${pyData.risk?.level || "N/A"} (${pyData.risk?.score ?? 0})`,
|
`Risk: ${pyData.risk?.level || "N/A"} (${pyData.risk?.score ?? 0})`,
|
||||||
`Data Quality: ${pyData.data_quality?.label || "N/A"}`,
|
`Data Quality: ${pyData.data_quality?.label || "N/A"}`,
|
||||||
`xG Beklentisi: ${
|
`xG Beklentisi: ${
|
||||||
@@ -286,10 +297,12 @@ export class AiService {
|
|||||||
*/
|
*/
|
||||||
async checkHealth(): Promise<boolean> {
|
async checkHealth(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(
|
const response = await this.aiEngineClient.get<{ status?: string }>(
|
||||||
this.httpService.get(`${this.pythonEngineUrl}/health`, {
|
"/health",
|
||||||
|
{
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
}),
|
retryCount: 0,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return response.data?.status === "healthy";
|
return response.data?.status === "healthy";
|
||||||
} catch {
|
} catch {
|
||||||
@@ -311,7 +324,7 @@ export class AiService {
|
|||||||
winnerPrediction: "N/A",
|
winnerPrediction: "N/A",
|
||||||
scorePrediction: "-",
|
scorePrediction: "-",
|
||||||
confidenceScore: 0,
|
confidenceScore: 0,
|
||||||
modelVersion: "v25.main",
|
modelVersion: "v28-pro-max",
|
||||||
expectedGoals: 0,
|
expectedGoals: 0,
|
||||||
keyInsights: [],
|
keyInsights: [],
|
||||||
};
|
};
|
||||||
|
|||||||
+177
-98
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { Cron } from "@nestjs/schedule";
|
import { Cron } from "@nestjs/schedule";
|
||||||
import { HttpService } from "@nestjs/axios";
|
import { HttpService } from "@nestjs/axios";
|
||||||
import { PrismaService } from "../database/prisma.service";
|
import { PrismaService } from "../database/prisma.service";
|
||||||
@@ -8,10 +8,22 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { SidelinedResponse } from "../modules/feeder/feeder.types";
|
import { SidelinedResponse } from "../modules/feeder/feeder.types";
|
||||||
|
import {
|
||||||
|
FINISHED_STATE_VALUES_FOR_DB,
|
||||||
|
FINISHED_STATUS_VALUES_FOR_DB,
|
||||||
|
LIVE_STATE_VALUES_FOR_DB,
|
||||||
|
LIVE_STATUS_VALUES_FOR_DB,
|
||||||
|
} from "../common/utils/match-status.util";
|
||||||
|
import {
|
||||||
|
getDateStringInTimeZone,
|
||||||
|
getDayBoundsForTimeZone,
|
||||||
|
getShiftedDateStringInTimeZone,
|
||||||
|
} from "../common/utils/timezone.util";
|
||||||
|
import { TaskLockService } from "./task-lock.service";
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────
|
||||||
// Types
|
// Types
|
||||||
// ────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface LiveScoreTeamPayload {
|
interface LiveScoreTeamPayload {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -64,75 +76,119 @@ interface LiveLineupsJson {
|
|||||||
|
|
||||||
type SportType = "football" | "basketball";
|
type SportType = "football" | "basketball";
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────
|
||||||
// Service
|
// Service
|
||||||
// ────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataFetcherTask {
|
export class DataFetcherTask {
|
||||||
private readonly logger = new Logger(DataFetcherTask.name);
|
private readonly logger = new Logger(DataFetcherTask.name);
|
||||||
|
private readonly timeZone = "Europe/Istanbul";
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly scraper: FeederScraperService,
|
private readonly scraper: FeederScraperService,
|
||||||
|
private readonly taskLock: TaskLockService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// CRON 1: Main sync — every 15 minutes
|
// CRON 1: Main sync — every 15 minutes
|
||||||
// Phases: match list → live scores → odds → lineups
|
// Phases: match list → live scores → odds → lineups
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Cron("*/15 * * * *")
|
@Cron("*/15 * * * *")
|
||||||
async syncLiveMatches(): Promise<void> {
|
async syncLiveMatches(): Promise<void> {
|
||||||
if (this.shouldSkipInHistoricalMode("syncLiveMatches")) return;
|
if (this.shouldSkipInHistoricalMode("syncLiveMatches")) return;
|
||||||
this.logger.log("━━━ syncLiveMatches START ━━━");
|
await this.taskLock.runWithLease(
|
||||||
|
"syncLiveMatches",
|
||||||
const today = new Date().toISOString().split("T")[0];
|
30 * 60 * 1000,
|
||||||
|
async () => {
|
||||||
// Phase 1: Match list (football + basketball)
|
await this.runLiveSync();
|
||||||
await this.syncMatchList(today);
|
},
|
||||||
|
this.logger,
|
||||||
// Phase 2: Live score updates
|
);
|
||||||
await this.updateLiveScores();
|
|
||||||
|
|
||||||
// Phase 3: Odds + referee + lineups + sidelined (via processMatchOdds)
|
|
||||||
await this.fetchOddsForMatches();
|
|
||||||
|
|
||||||
// Phase 4: Fill missing lineups (backup for edge cases)
|
|
||||||
await this.fillMissingLineups();
|
|
||||||
|
|
||||||
this.logger.log("━━━ syncLiveMatches END ━━━");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// CRON 2: Daily cleanup + full sync — 07:00 Istanbul
|
// CRON 2: Daily cleanup + full sync — 07:00 Istanbul
|
||||||
// Truncates live_matches, then runs full sync
|
// Preserve yesterday as a fallback until the 08:00 archive job completes.
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Cron("0 7 * * *", { timeZone: "Europe/Istanbul" })
|
@Cron("0 7 * * *", { timeZone: "Europe/Istanbul" })
|
||||||
async cleanAndFullSync(): Promise<void> {
|
async cleanAndFullSync(): Promise<void> {
|
||||||
if (this.shouldSkipInHistoricalMode("cleanAndFullSync")) return;
|
if (this.shouldSkipInHistoricalMode("cleanAndFullSync")) return;
|
||||||
this.logger.log("🧹 cleanAndFullSync: Truncating live_matches...");
|
await this.taskLock.runWithLease(
|
||||||
|
"cleanAndFullSync",
|
||||||
|
2 * 60 * 60 * 1000,
|
||||||
|
async () => {
|
||||||
|
this.logger.log(
|
||||||
|
"cleanAndFullSync: Pruning stale live_matches while preserving yesterday for archive fallback...",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deleted = await this.prisma.liveMatch.deleteMany({});
|
const yesterdayDate = getShiftedDateStringInTimeZone(
|
||||||
this.logger.log(
|
-1,
|
||||||
`🧹 Deleted ${deleted.count} live matches. Starting full sync...`,
|
this.timeZone,
|
||||||
);
|
);
|
||||||
} catch (error: unknown) {
|
const { startMs: yesterdayStartMs } = getDayBoundsForTimeZone(
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
yesterdayDate,
|
||||||
this.logger.error(`Truncate failed: ${message}`);
|
this.timeZone,
|
||||||
return;
|
);
|
||||||
}
|
const cutoffDate = new Date(yesterdayStartMs);
|
||||||
|
|
||||||
// Run full sync immediately after cleanup
|
const deleted = await this.prisma.liveMatch.deleteMany({
|
||||||
await this.syncLiveMatches();
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ mstUtc: { lt: BigInt(yesterdayStartMs) } },
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ mstUtc: null },
|
||||||
|
{ updatedAt: { lt: cutoffDate } },
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{ status: { in: FINISHED_STATUS_VALUES_FOR_DB } },
|
||||||
|
{ state: { in: FINISHED_STATE_VALUES_FOR_DB } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Pruned ${deleted.count} stale live matches. Starting full sync...`,
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
this.logger.error(`Stale live_match cleanup failed: ${message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.runLiveSync();
|
||||||
|
},
|
||||||
|
this.logger,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// Phase 1: Fetch match list for all sports
|
// Phase 1: Fetch match list for all sports
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async runLiveSync(): Promise<void> {
|
||||||
|
if (this.shouldSkipInHistoricalMode("syncLiveMatches")) return;
|
||||||
|
|
||||||
|
this.logger.log("syncLiveMatches START");
|
||||||
|
|
||||||
|
const today = getDateStringInTimeZone(new Date(), this.timeZone);
|
||||||
|
await this.syncMatchList(today);
|
||||||
|
await this.updateLiveScores();
|
||||||
|
await this.fetchOddsForMatches();
|
||||||
|
await this.fillMissingLineups();
|
||||||
|
|
||||||
|
this.logger.log("syncLiveMatches END");
|
||||||
|
}
|
||||||
|
|
||||||
private async syncMatchList(date: string): Promise<void> {
|
private async syncMatchList(date: string): Promise<void> {
|
||||||
// Football
|
// Football
|
||||||
@@ -141,7 +197,7 @@ export class DataFetcherTask {
|
|||||||
await this.fetchMatchesForSport("football", date, footballLeagues);
|
await this.fetchMatchesForSport("football", date, footballLeagues);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
"top_leagues.json is missing/empty — writing ALL football matches",
|
"top_leagues.json is missing/empty — writing ALL football matches",
|
||||||
);
|
);
|
||||||
await this.fetchMatchesForSport("football", date, new Set());
|
await this.fetchMatchesForSport("football", date, new Set());
|
||||||
}
|
}
|
||||||
@@ -170,17 +226,18 @@ export class DataFetcherTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// Phase 2: Live score updates (merged from live-updater.task)
|
// Phase 2: Live score updates (merged from live-updater.task)
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async updateLiveScores(): Promise<void> {
|
private async updateLiveScores(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const liveMatches = await this.prisma.liveMatch.findMany({
|
const liveMatches = await this.prisma.liveMatch.findMany({
|
||||||
where: {
|
where: {
|
||||||
state: {
|
OR: [
|
||||||
in: ["live", "firsthalf", "secondhalf", "1H", "2H", "HT", "LIVE"],
|
{ state: { in: LIVE_STATE_VALUES_FOR_DB } },
|
||||||
},
|
{ status: { in: LIVE_STATUS_VALUES_FOR_DB } },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
select: { id: true, matchSlug: true },
|
select: { id: true, matchSlug: true },
|
||||||
});
|
});
|
||||||
@@ -191,7 +248,7 @@ export class DataFetcherTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`📡 Updating scores for ${liveMatches.length} live matches`,
|
`📡 Updating scores for ${liveMatches.length} live matches`,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const match of liveMatches) {
|
for (const match of liveMatches) {
|
||||||
@@ -219,19 +276,19 @@ export class DataFetcherTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log("📡 Live score update complete");
|
this.logger.log("📡 Live score update complete");
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
this.logger.error(`Live score update failed: ${message}`);
|
this.logger.error(`Live score update failed: ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// Phase 3: Odds + referee + lineups + sidelined
|
// Phase 3: Odds + referee + lineups + sidelined
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async fetchOddsForMatches(): Promise<void> {
|
private async fetchOddsForMatches(): Promise<void> {
|
||||||
this.logger.log("💰 Fetching odds for live matches...");
|
this.logger.log("💰 Fetching odds for live matches...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load both league filters
|
// Load both league filters
|
||||||
@@ -266,11 +323,11 @@ export class DataFetcherTask {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (matchesToFetch.length === 0) {
|
if (matchesToFetch.length === 0) {
|
||||||
this.logger.log("💰 No matches to fetch odds for");
|
this.logger.log("💰 No matches to fetch odds for");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`💰 Fetching odds for ${matchesToFetch.length} matches`);
|
this.logger.log(`💰 Fetching odds for ${matchesToFetch.length} matches`);
|
||||||
|
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
@@ -299,7 +356,7 @@ export class DataFetcherTask {
|
|||||||
// Retry failed matches (502/Timeout)
|
// Retry failed matches (502/Timeout)
|
||||||
if (failedMatches.length > 0) {
|
if (failedMatches.length > 0) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`⚠️ Retrying ${failedMatches.length} failed matches (502/Timeout)...`,
|
`âš ï¸ Retrying ${failedMatches.length} failed matches (502/Timeout)...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const match of failedMatches) {
|
for (const match of failedMatches) {
|
||||||
@@ -307,19 +364,19 @@ export class DataFetcherTask {
|
|||||||
try {
|
try {
|
||||||
await this.processMatchOdds(match);
|
await this.processMatchOdds(match);
|
||||||
successCount++;
|
successCount++;
|
||||||
this.logger.log(`✅ Retry successful for match ${match.id}`);
|
this.logger.log(`✅ Retry successful for match ${match.id}`);
|
||||||
} catch (retryErr: unknown) {
|
} catch (retryErr: unknown) {
|
||||||
const message =
|
const message =
|
||||||
retryErr instanceof Error ? retryErr.message : String(retryErr);
|
retryErr instanceof Error ? retryErr.message : String(retryErr);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`❌ Retry failed for match ${match.id}: ${message}`,
|
`⌠Retry failed for match ${match.id}: ${message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`💰 Odds complete: ${successCount} success, ${errorCount} errors (initially)`,
|
`💰 Odds complete: ${successCount} success, ${errorCount} errors (initially)`,
|
||||||
);
|
);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
@@ -327,14 +384,36 @@ export class DataFetcherTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// Phase 4: Fill missing lineups (backup)
|
// Phase 4: Fill missing lineups (backup)
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async fillMissingLineups(): Promise<void> {
|
private async fillMissingLineups(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const matchesToUpdate = await this.prisma.liveMatch.findMany({
|
const matchesToUpdate = await this.prisma.liveMatch.findMany({
|
||||||
where: { status: { notIn: ["FT", "post", "postGame"] } },
|
where: {
|
||||||
|
sport: "football",
|
||||||
|
NOT: {
|
||||||
|
OR: [
|
||||||
|
{ status: { in: FINISHED_STATUS_VALUES_FOR_DB } },
|
||||||
|
{ state: { in: FINISHED_STATE_VALUES_FOR_DB } },
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ scoreHome: { not: null } },
|
||||||
|
{ scoreAway: { not: null } },
|
||||||
|
{
|
||||||
|
NOT: {
|
||||||
|
OR: [
|
||||||
|
{ status: { in: LIVE_STATUS_VALUES_FOR_DB } },
|
||||||
|
{ state: { in: LIVE_STATE_VALUES_FOR_DB } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
select: { id: true, matchSlug: true, lineups: true, sport: true },
|
select: { id: true, matchSlug: true, lineups: true, sport: true },
|
||||||
take: 30,
|
take: 30,
|
||||||
});
|
});
|
||||||
@@ -345,11 +424,11 @@ export class DataFetcherTask {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (toUpdate.length === 0) {
|
if (toUpdate.length === 0) {
|
||||||
this.logger.debug("👕 All lineups already filled");
|
this.logger.debug("👕 All lineups already filled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`👕 Filling lineups for ${toUpdate.length} matches...`);
|
this.logger.log(`👕 Filling lineups for ${toUpdate.length} matches...`);
|
||||||
|
|
||||||
for (const match of toUpdate) {
|
for (const match of toUpdate) {
|
||||||
try {
|
try {
|
||||||
@@ -374,7 +453,7 @@ export class DataFetcherTask {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`👕 Lineups filled for match ${match.id}`);
|
this.logger.log(`👕 Lineups filled for match ${match.id}`);
|
||||||
await this.delay(500);
|
await this.delay(500);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
@@ -387,9 +466,9 @@ export class DataFetcherTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// Unified match fetcher — DRY for football + basketball
|
// Unified match fetcher — DRY for football + basketball
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async fetchMatchesForSport(
|
private async fetchMatchesForSport(
|
||||||
sport: SportType,
|
sport: SportType,
|
||||||
@@ -650,7 +729,7 @@ export class DataFetcherTask {
|
|||||||
upsertCount + skippedCount === targetMatches.length
|
upsertCount + skippedCount === targetMatches.length
|
||||||
) {
|
) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[${sport}] ⏳ Progress: ${upsertCount + skippedCount}/${targetMatches.length} (Saved: ${upsertCount}, Skipped: ${skippedCount})`,
|
`[${sport}] â³ Progress: ${upsertCount + skippedCount}/${targetMatches.length} (Saved: ${upsertCount}, Skipped: ${skippedCount})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -668,10 +747,10 @@ export class DataFetcherTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// processMatchOdds — odds + referee + lineups + sidelined
|
// processMatchOdds — odds + referee + lineups + sidelined
|
||||||
// (Preserved from original — no logic changes)
|
// (Preserved from original — no logic changes)
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async processMatchOdds(match: LiveMatchOddsTarget): Promise<void> {
|
private async processMatchOdds(match: LiveMatchOddsTarget): Promise<void> {
|
||||||
const matchSlug = match.matchSlug || "match";
|
const matchSlug = match.matchSlug || "match";
|
||||||
@@ -687,7 +766,7 @@ export class DataFetcherTask {
|
|||||||
let lineups: LiveLineupsJson | null = null;
|
let lineups: LiveLineupsJson | null = null;
|
||||||
let sidelined: SidelinedResponse | null = null;
|
let sidelined: SidelinedResponse | null = null;
|
||||||
|
|
||||||
// 1. Fetch Odds from İddaa page
|
// 1. Fetch Odds from İddaa page
|
||||||
const oddsUrl = `https://www.mackolik.com/${sportPath}/${matchSlug}/iddaa/${match.id}`;
|
const oddsUrl = `https://www.mackolik.com/${sportPath}/${matchSlug}/iddaa/${match.id}`;
|
||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
@@ -722,7 +801,7 @@ export class DataFetcherTask {
|
|||||||
typeof mainResp.data === "string" ? mainResp.data : "",
|
typeof mainResp.data === "string" ? mainResp.data : "",
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// Non-critical — referee is optional
|
// Non-critical — referee is optional
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +830,7 @@ export class DataFetcherTask {
|
|||||||
subs: substitutions?.stats?.away || [],
|
subs: substitutions?.stats?.away || [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.logger.log(`👥 Lineups found for ${match.matchName}`);
|
this.logger.log(`👥 Lineups found for ${match.matchName}`);
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(`No lineups (yet) for ${match.matchName}`);
|
this.logger.debug(`No lineups (yet) for ${match.matchName}`);
|
||||||
}
|
}
|
||||||
@@ -779,7 +858,7 @@ export class DataFetcherTask {
|
|||||||
sidelined.awayTeam?.totalSidelined > 0
|
sidelined.awayTeam?.totalSidelined > 0
|
||||||
) {
|
) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`🚑 Sidelined: ${sidelined.homeTeam.totalSidelined}(H) - ${sidelined.awayTeam.totalSidelined}(A) for ${match.matchName}`,
|
`🚑 Sidelined: ${sidelined.homeTeam.totalSidelined}(H) - ${sidelined.awayTeam.totalSidelined}(A) for ${match.matchName}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -813,22 +892,22 @@ export class DataFetcherTask {
|
|||||||
sidelined.awayTeam.totalSidelined > 0))
|
sidelined.awayTeam.totalSidelined > 0))
|
||||||
) {
|
) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`✅ Loop update: ${match.matchName} | Odds: ${Object.keys(odds).length} | Ref: ${refereeName || "N/A"} | Lineups: ${lineups ? "Yes" : "No"} | Sidelined: ${sidelined ? "Yes" : "No"}`,
|
`✅ Loop update: ${match.matchName} | Odds: ${Object.keys(odds).length} | Ref: ${refereeName || "N/A"} | Lineups: ${lineups ? "Yes" : "No"} | Sidelined: ${sidelined ? "Yes" : "No"}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`❕ No detailed data for ${match.matchName}, marked check.`,
|
`â• No detailed data for ${match.matchName}, marked check.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// HTML Extraction Helpers (preserved — no logic changes)
|
// HTML Extraction Helpers (preserved — no logic changes)
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract odds from Mackolik HTML page
|
* Extract odds from Mackolik HTML page
|
||||||
* Returns structured odds object: { "MS": {"1": 2.10, "X": 3.40}, "AU25": {"Alt": 2.05, "Üst": 1.75} }
|
* Returns structured odds object: { "MS": {"1": 2.10, "X": 3.40}, "AU25": {"Alt": 2.05, "Üst": 1.75} }
|
||||||
*/
|
*/
|
||||||
private extractOddsFromHtml(
|
private extractOddsFromHtml(
|
||||||
html: string,
|
html: string,
|
||||||
@@ -914,17 +993,17 @@ export class DataFetcherTask {
|
|||||||
const lower = name.toLowerCase();
|
const lower = name.toLowerCase();
|
||||||
|
|
||||||
// Specific & Compound names FIRST
|
// Specific & Compound names FIRST
|
||||||
if (lower.includes("ilk yarı/maç sonucu")) return "HTFT";
|
if (lower.includes("ilk yarı/maç sonucu")) return "HTFT";
|
||||||
if (lower.includes("1. yarı sonucu")) return "HT";
|
if (lower.includes("1. yarı sonucu")) return "HT";
|
||||||
if (lower.includes("çifte şans")) return "CS";
|
if (lower.includes("çifte şans")) return "CS";
|
||||||
|
|
||||||
// General names LATER
|
// General names LATER
|
||||||
if (lower.includes("maç sonucu") && !lower.includes("handikap"))
|
if (lower.includes("maç sonucu") && !lower.includes("handikap"))
|
||||||
return "MS";
|
return "MS";
|
||||||
if (lower.includes("karşılıklı gol")) return "KG";
|
if (lower.includes("karşılıklı gol")) return "KG";
|
||||||
if (lower.includes("2,5 alt/üst") || lower.includes("2.5")) return "AU25";
|
if (lower.includes("2,5 alt/üst") || lower.includes("2.5")) return "AU25";
|
||||||
if (lower.includes("1,5 alt/üst") || lower.includes("1.5")) return "AU15";
|
if (lower.includes("1,5 alt/üst") || lower.includes("1.5")) return "AU15";
|
||||||
if (lower.includes("3,5 alt/üst") || lower.includes("3.5")) return "AU35";
|
if (lower.includes("3,5 alt/üst") || lower.includes("3.5")) return "AU35";
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -934,7 +1013,7 @@ export class DataFetcherTask {
|
|||||||
*/
|
*/
|
||||||
private extractRefereeFromHtml(html: string): string | null {
|
private extractRefereeFromHtml(html: string): string | null {
|
||||||
try {
|
try {
|
||||||
// Strategy 1: Mackolik officials section — head referee in '--main' list item
|
// Strategy 1: Mackolik officials section — head referee in '--main' list item
|
||||||
const mainOfficialPattern =
|
const mainOfficialPattern =
|
||||||
/official-list-item--main[^>]*>\s*(?:<[^>]*>\s*)*?<span[^>]*official-name[^>]*>\s*([^<]+)/i;
|
/official-list-item--main[^>]*>\s*(?:<[^>]*>\s*)*?<span[^>]*official-name[^>]*>\s*([^<]+)/i;
|
||||||
const mainMatch = mainOfficialPattern.exec(html);
|
const mainMatch = mainOfficialPattern.exec(html);
|
||||||
@@ -970,9 +1049,9 @@ export class DataFetcherTask {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// Low-level Helpers (preserved — no logic changes)
|
// Low-level Helpers (preserved — no logic changes)
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private shouldSkipInHistoricalMode(jobName: string): boolean {
|
private shouldSkipInHistoricalMode(jobName: string): boolean {
|
||||||
if (process.env.FEEDER_MODE === "historical") {
|
if (process.env.FEEDER_MODE === "historical") {
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { FeederService } from "../modules/feeder/feeder.service";
|
|
||||||
import { HistoricalResultsSyncTask } from "./historical-results-sync.task";
|
|
||||||
|
|
||||||
describe("HistoricalResultsSyncTask", () => {
|
|
||||||
const runPreviousDayCompletedMatchesScan = jest.fn();
|
|
||||||
let task: HistoricalResultsSyncTask;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
delete process.env.FEEDER_MODE;
|
|
||||||
|
|
||||||
task = new HistoricalResultsSyncTask({
|
|
||||||
runPreviousDayCompletedMatchesScan,
|
|
||||||
} as unknown as FeederService);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
delete process.env.FEEDER_MODE;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls feeder service in normal mode", async () => {
|
|
||||||
await task.syncPreviousDayCompletedMatches();
|
|
||||||
|
|
||||||
expect(runPreviousDayCompletedMatchesScan).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips execution in historical feeder mode", async () => {
|
|
||||||
process.env.FEEDER_MODE = "historical";
|
|
||||||
|
|
||||||
await task.syncPreviousDayCompletedMatches();
|
|
||||||
|
|
||||||
expect(runPreviousDayCompletedMatchesScan).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { Cron } from "@nestjs/schedule";
|
import { Cron } from "@nestjs/schedule";
|
||||||
import { FeederService } from "../modules/feeder/feeder.service";
|
import { FeederService } from "../modules/feeder/feeder.service";
|
||||||
|
import { TaskLockService } from "./task-lock.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HistoricalResultsSyncTask {
|
export class HistoricalResultsSyncTask {
|
||||||
private readonly logger = new Logger(HistoricalResultsSyncTask.name);
|
private readonly logger = new Logger(HistoricalResultsSyncTask.name);
|
||||||
|
|
||||||
constructor(private readonly feederService: FeederService) {}
|
constructor(
|
||||||
|
private readonly feederService: FeederService,
|
||||||
|
private readonly taskLock: TaskLockService,
|
||||||
|
) {}
|
||||||
|
|
||||||
private shouldSkipInHistoricalMode(jobName: string): boolean {
|
private shouldSkipInHistoricalMode(jobName: string): boolean {
|
||||||
if (process.env.FEEDER_MODE === "historical") {
|
if (process.env.FEEDER_MODE === "historical") {
|
||||||
@@ -25,17 +29,24 @@ export class HistoricalResultsSyncTask {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(
|
await this.taskLock.runWithLease(
|
||||||
"Starting previous-day completed match sync for football and basketball...",
|
"syncPreviousDayCompletedMatches",
|
||||||
);
|
6 * 60 * 60 * 1000,
|
||||||
|
async () => {
|
||||||
|
this.logger.log(
|
||||||
|
"Starting previous-day completed match sync for football and basketball...",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.feederService.runPreviousDayCompletedMatchesScan();
|
await this.feederService.runPreviousDayCompletedMatchesScan();
|
||||||
this.logger.log("Previous-day completed match sync finished");
|
this.logger.log("Previous-day completed match sync finished");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Previous-day completed match sync failed: ${error.message}`,
|
`Previous-day completed match sync failed: ${error.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
this.logger,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { Cron } from "@nestjs/schedule";
|
import { Cron } from "@nestjs/schedule";
|
||||||
import { PrismaService } from "../database/prisma.service";
|
import { PrismaService } from "../database/prisma.service";
|
||||||
|
import {
|
||||||
|
FINISHED_STATE_VALUES_FOR_DB,
|
||||||
|
FINISHED_STATUS_VALUES_FOR_DB,
|
||||||
|
LIVE_STATE_VALUES_FOR_DB,
|
||||||
|
LIVE_STATUS_VALUES_FOR_DB,
|
||||||
|
} from "../common/utils/match-status.util";
|
||||||
|
import {
|
||||||
|
getDateOnlyValueForTimeZone,
|
||||||
|
getShiftedDateStringInTimeZone,
|
||||||
|
getDayBoundsForTimeZone,
|
||||||
|
} from "../common/utils/timezone.util";
|
||||||
|
import { TaskLockService } from "./task-lock.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LimitResetterTask {
|
export class LimitResetterTask {
|
||||||
private readonly logger = new Logger(LimitResetterTask.name);
|
private readonly logger = new Logger(LimitResetterTask.name);
|
||||||
|
private readonly timeZone = "Europe/Istanbul";
|
||||||
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly taskLock: TaskLockService,
|
||||||
|
) {}
|
||||||
|
|
||||||
private shouldSkipInHistoricalMode(jobName: string): boolean {
|
private shouldSkipInHistoricalMode(jobName: string): boolean {
|
||||||
if (process.env.FEEDER_MODE === "historical") {
|
if (process.env.FEEDER_MODE === "historical") {
|
||||||
@@ -22,34 +38,39 @@ export class LimitResetterTask {
|
|||||||
@Cron("0 3 * * *", { timeZone: "Europe/Istanbul" })
|
@Cron("0 3 * * *", { timeZone: "Europe/Istanbul" })
|
||||||
async resetUsageLimits() {
|
async resetUsageLimits() {
|
||||||
if (this.shouldSkipInHistoricalMode("resetUsageLimits")) return;
|
if (this.shouldSkipInHistoricalMode("resetUsageLimits")) return;
|
||||||
this.logger.log("Starting daily usage limit reset job...");
|
await this.taskLock.runWithLease(
|
||||||
|
"resetUsageLimits",
|
||||||
|
30 * 60 * 1000,
|
||||||
|
async () => {
|
||||||
|
this.logger.log("Starting daily usage limit reset job...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const today = new Date();
|
const today = getDateOnlyValueForTimeZone(this.timeZone);
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
// Reset all limits that were last reset before today
|
const result = await this.prisma.usageLimit.updateMany({
|
||||||
const result = await this.prisma.usageLimit.updateMany({
|
where: {
|
||||||
where: {
|
lastResetDate: { lt: today },
|
||||||
lastResetDate: { lt: today },
|
},
|
||||||
},
|
data: {
|
||||||
data: {
|
analysisCount: 0,
|
||||||
analysisCount: 0,
|
couponCount: 0,
|
||||||
couponCount: 0,
|
lastResetDate: today,
|
||||||
lastResetDate: today,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (result.count > 0) {
|
if (result.count > 0) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Usage limits for ${result.count} users have been reset`,
|
`Usage limits for ${result.count} users have been reset`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.logger.log("No user limits needed resetting");
|
this.logger.log("No user limits needed resetting");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Limit reset job failed: ${error.message}`);
|
this.logger.error(`Limit reset job failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
this.logger,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,37 +79,65 @@ export class LimitResetterTask {
|
|||||||
@Cron("0 4 * * *", { timeZone: "Europe/Istanbul" })
|
@Cron("0 4 * * *", { timeZone: "Europe/Istanbul" })
|
||||||
async cleanupOldData() {
|
async cleanupOldData() {
|
||||||
if (this.shouldSkipInHistoricalMode("cleanupOldData")) return;
|
if (this.shouldSkipInHistoricalMode("cleanupOldData")) return;
|
||||||
this.logger.log("Starting data cleanup job...");
|
await this.taskLock.runWithLease(
|
||||||
|
"cleanupOldData",
|
||||||
|
60 * 60 * 1000,
|
||||||
|
async () => {
|
||||||
|
this.logger.log("Starting data cleanup job...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const thirtyDaysAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
|
||||||
// Delete old AI prediction logs
|
const deletedLogs = await this.prisma.aiPredictionsLog.deleteMany({
|
||||||
const deletedLogs = await this.prisma.aiPredictionsLog.deleteMany({
|
where: {
|
||||||
where: {
|
createdAt: { lt: thirtyDaysAgo },
|
||||||
createdAt: { lt: thirtyDaysAgo },
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Delete old live matches (finished more than 1 day ago)
|
const yesterdayDate = getShiftedDateStringInTimeZone(
|
||||||
// Historical data is already persisted in the 'matches' table
|
-1,
|
||||||
const oneDayAgo = new Date();
|
this.timeZone,
|
||||||
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
|
);
|
||||||
|
const { startMs: yesterdayStartMs } = getDayBoundsForTimeZone(
|
||||||
|
yesterdayDate,
|
||||||
|
this.timeZone,
|
||||||
|
);
|
||||||
|
const liveMatchCutoff = new Date(yesterdayStartMs);
|
||||||
|
|
||||||
const deletedLiveMatches = await this.prisma.liveMatch.deleteMany({
|
const deletedLiveMatches = await this.prisma.liveMatch.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
state: "Finished",
|
updatedAt: { lt: liveMatchCutoff },
|
||||||
updatedAt: { lt: oneDayAgo },
|
OR: [
|
||||||
},
|
{ status: { in: FINISHED_STATUS_VALUES_FOR_DB } },
|
||||||
});
|
{ state: { in: FINISHED_STATE_VALUES_FOR_DB } },
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ scoreHome: { not: null } },
|
||||||
|
{ scoreAway: { not: null } },
|
||||||
|
{
|
||||||
|
NOT: {
|
||||||
|
OR: [
|
||||||
|
{ status: { in: LIVE_STATUS_VALUES_FOR_DB } },
|
||||||
|
{ state: { in: LIVE_STATE_VALUES_FOR_DB } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Cleanup complete: ${deletedLogs.count} old logs, ${deletedLiveMatches.count} old live matches`,
|
`Cleanup complete: ${deletedLogs.count} old logs, ${deletedLiveMatches.count} old live matches`,
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Cleanup job failed: ${error.message}`);
|
this.logger.error(`Cleanup job failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
this.logger,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,26 +146,33 @@ export class LimitResetterTask {
|
|||||||
@Cron("0 0 * * *", { timeZone: "Europe/Istanbul" })
|
@Cron("0 0 * * *", { timeZone: "Europe/Istanbul" })
|
||||||
async checkSubscriptions() {
|
async checkSubscriptions() {
|
||||||
if (this.shouldSkipInHistoricalMode("checkSubscriptions")) return;
|
if (this.shouldSkipInHistoricalMode("checkSubscriptions")) return;
|
||||||
this.logger.log("Checking expired subscriptions...");
|
await this.taskLock.runWithLease(
|
||||||
|
"checkSubscriptions",
|
||||||
|
30 * 60 * 1000,
|
||||||
|
async () => {
|
||||||
|
this.logger.log("Checking expired subscriptions...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const result = await this.prisma.user.updateMany({
|
const result = await this.prisma.user.updateMany({
|
||||||
where: {
|
where: {
|
||||||
subscriptionStatus: "active",
|
subscriptionStatus: "active",
|
||||||
subscriptionExpiresAt: { lt: now },
|
subscriptionExpiresAt: { lt: now },
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
subscriptionStatus: "expired",
|
subscriptionStatus: "expired",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.count > 0) {
|
if (result.count > 0) {
|
||||||
this.logger.log(`${result.count} subscriptions marked as expired`);
|
this.logger.log(`${result.count} subscriptions marked as expired`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Subscription check failed: ${error.message}`);
|
this.logger.error(`Subscription check failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
this.logger,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { PrismaService } from "../database/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TaskLockService {
|
||||||
|
private readonly logger = new Logger(TaskLockService.name);
|
||||||
|
private readonly activeTasks = new Set<string>();
|
||||||
|
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async runWithLease<T>(
|
||||||
|
key: string,
|
||||||
|
ttlMs: number,
|
||||||
|
task: () => Promise<T>,
|
||||||
|
logger: Logger,
|
||||||
|
): Promise<T | null> {
|
||||||
|
if (this.activeTasks.has(key)) {
|
||||||
|
logger.warn(`Skipping ${key}: task is already running in this process`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
const acquired = await this.acquireLease(key, owner, ttlMs);
|
||||||
|
|
||||||
|
if (!acquired) {
|
||||||
|
logger.warn(`Skipping ${key}: lease is already held by another instance`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeTasks.add(key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await task();
|
||||||
|
} finally {
|
||||||
|
this.activeTasks.delete(key);
|
||||||
|
await this.releaseLease(key, owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async acquireLease(
|
||||||
|
key: string,
|
||||||
|
owner: string,
|
||||||
|
ttlMs: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const rows = await this.prisma.$queryRaw<{ key: string }[]>(
|
||||||
|
Prisma.sql`
|
||||||
|
INSERT INTO app_settings (key, value, updated_at)
|
||||||
|
VALUES (${this.getDbKey(key)}, ${owner}, NOW() + (${ttlMs} * INTERVAL '1 millisecond'))
|
||||||
|
ON CONFLICT (key) DO UPDATE
|
||||||
|
SET value = EXCLUDED.value,
|
||||||
|
updated_at = EXCLUDED.updated_at
|
||||||
|
WHERE app_settings.updated_at < NOW()
|
||||||
|
OR app_settings.value = ${owner}
|
||||||
|
RETURNING key
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async releaseLease(key: string, owner: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.prisma.$executeRaw(
|
||||||
|
Prisma.sql`
|
||||||
|
DELETE FROM app_settings
|
||||||
|
WHERE key = ${this.getDbKey(key)}
|
||||||
|
AND value = ${owner}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
this.logger.warn(`Failed to release task lease ${key}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDbKey(key: string): string {
|
||||||
|
return `task_lock:${key}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ScheduleModule } from "@nestjs/schedule";
|
|
||||||
import { HttpModule } from "@nestjs/axios";
|
import { HttpModule } from "@nestjs/axios";
|
||||||
import { DataFetcherTask } from "./data-fetcher.task";
|
import { DataFetcherTask } from "./data-fetcher.task";
|
||||||
import { HistoricalResultsSyncTask } from "./historical-results-sync.task";
|
import { HistoricalResultsSyncTask } from "./historical-results-sync.task";
|
||||||
import { LimitResetterTask } from "./limit-resetter.task";
|
import { LimitResetterTask } from "./limit-resetter.task";
|
||||||
|
import { TaskLockService } from "./task-lock.service";
|
||||||
import { DatabaseModule } from "../database/database.module";
|
import { DatabaseModule } from "../database/database.module";
|
||||||
import { FeederModule } from "../modules/feeder/feeder.module";
|
import { FeederModule } from "../modules/feeder/feeder.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ScheduleModule.forRoot(),
|
|
||||||
HttpModule.register({
|
HttpModule.register({
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -20,7 +19,12 @@ import { FeederModule } from "../modules/feeder/feeder.module";
|
|||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
FeederModule,
|
FeederModule,
|
||||||
],
|
],
|
||||||
providers: [DataFetcherTask, HistoricalResultsSyncTask, LimitResetterTask],
|
providers: [
|
||||||
|
TaskLockService,
|
||||||
|
DataFetcherTask,
|
||||||
|
HistoricalResultsSyncTask,
|
||||||
|
LimitResetterTask,
|
||||||
|
],
|
||||||
exports: [DataFetcherTask, HistoricalResultsSyncTask, LimitResetterTask],
|
exports: [DataFetcherTask, HistoricalResultsSyncTask, LimitResetterTask],
|
||||||
})
|
})
|
||||||
export class TasksModule {}
|
export class TasksModule {}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
match_id = '7cnm7h7qbsq2bbaxngusojh90'
|
|
||||||
url = f'http://localhost:8007/v20plus/analyze/{match_id}'
|
|
||||||
|
|
||||||
print(f"🔮 Sending prediction request for: {match_id}")
|
|
||||||
print(f"URL: {url}\n")
|
|
||||||
|
|
||||||
response = requests.post(url)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
print("📊 DATA QUALITY:")
|
|
||||||
print(json.dumps(data.get('data_quality', {}), indent=2))
|
|
||||||
|
|
||||||
print("\n🎯 MAIN PICK:")
|
|
||||||
print(json.dumps(data.get('main_pick', {}), indent=2))
|
|
||||||
|
|
||||||
print("\n⚽ SCORE PREDICTION:")
|
|
||||||
print(json.dumps(data.get('score_prediction', {}), indent=2))
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import { INestApplication } from "@nestjs/common";
|
|
||||||
import request from "supertest";
|
|
||||||
import { App } from "supertest/types";
|
|
||||||
import { AppModule } from "./../src/app.module";
|
|
||||||
|
|
||||||
describe("AppController (e2e)", () => {
|
|
||||||
let app: INestApplication<App>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AppModule],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
|
||||||
await app.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("/ (GET)", () => {
|
|
||||||
return request(app.getHttpServer())
|
|
||||||
.get("/")
|
|
||||||
.expect(200)
|
|
||||||
.expect("Hello World!");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
|
||||||
"rootDir": ".",
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testRegex": ".e2e-spec.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user