v26-shadow #2
@@ -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
|
||||
|
Binary file not shown.
@@ -0,0 +1,871 @@
|
||||
iter Logloss
|
||||
0 0.6924099937
|
||||
1 0.6916660956
|
||||
2 0.691108145
|
||||
3 0.6904585078
|
||||
4 0.689812816
|
||||
5 0.689192261
|
||||
6 0.6886032715
|
||||
7 0.6880706742
|
||||
8 0.6876192378
|
||||
9 0.6870868859
|
||||
10 0.6865493528
|
||||
11 0.686105086
|
||||
12 0.6856345086
|
||||
13 0.6852027185
|
||||
14 0.6848238481
|
||||
15 0.6844045699
|
||||
16 0.6840077621
|
||||
17 0.6836197496
|
||||
18 0.6832475033
|
||||
19 0.6829012069
|
||||
20 0.6825880966
|
||||
21 0.6822424968
|
||||
22 0.6819180513
|
||||
23 0.6816384467
|
||||
24 0.6813262593
|
||||
25 0.6810353411
|
||||
26 0.6808138172
|
||||
27 0.6805550049
|
||||
28 0.680347991
|
||||
29 0.680089679
|
||||
30 0.6798451919
|
||||
31 0.6796090443
|
||||
32 0.6793890865
|
||||
33 0.6791683772
|
||||
34 0.6789766369
|
||||
35 0.6787930242
|
||||
36 0.6786087714
|
||||
37 0.6784161299
|
||||
38 0.6782227897
|
||||
39 0.6780242369
|
||||
40 0.6778499631
|
||||
41 0.6776975784
|
||||
42 0.6775231674
|
||||
43 0.6773582124
|
||||
44 0.6772234666
|
||||
45 0.6770659843
|
||||
46 0.6769049529
|
||||
47 0.6767664194
|
||||
48 0.6766584917
|
||||
49 0.6765507257
|
||||
50 0.6764489911
|
||||
51 0.6763947956
|
||||
52 0.6762778712
|
||||
53 0.6761865366
|
||||
54 0.6760679685
|
||||
55 0.6759774874
|
||||
56 0.6758500622
|
||||
57 0.6757625065
|
||||
58 0.6756876412
|
||||
59 0.6756151069
|
||||
60 0.6755303655
|
||||
61 0.6754565036
|
||||
62 0.6753738983
|
||||
63 0.6752897299
|
||||
64 0.6752115539
|
||||
65 0.6751595431
|
||||
66 0.6750764658
|
||||
67 0.6750179194
|
||||
68 0.6749408803
|
||||
69 0.6748795802
|
||||
70 0.674790372
|
||||
71 0.6747239773
|
||||
72 0.6746701254
|
||||
73 0.6746120937
|
||||
74 0.6745550085
|
||||
75 0.6744855074
|
||||
76 0.6744264172
|
||||
77 0.674381715
|
||||
78 0.6743331681
|
||||
79 0.67428564
|
||||
80 0.6742202413
|
||||
81 0.6741620971
|
||||
82 0.6741109453
|
||||
83 0.6740556003
|
||||
84 0.6740146772
|
||||
85 0.673983295
|
||||
86 0.6739595301
|
||||
87 0.6739336659
|
||||
88 0.673890361
|
||||
89 0.673863586
|
||||
90 0.6738190616
|
||||
91 0.6737799295
|
||||
92 0.6737364374
|
||||
93 0.6737093719
|
||||
94 0.6736630475
|
||||
95 0.67364367
|
||||
96 0.6735998081
|
||||
97 0.6735526984
|
||||
98 0.6735012924
|
||||
99 0.6734818024
|
||||
100 0.6734379341
|
||||
101 0.6734059869
|
||||
102 0.6733740852
|
||||
103 0.6733330971
|
||||
104 0.6733060254
|
||||
105 0.6732755898
|
||||
106 0.6732294722
|
||||
107 0.6732035176
|
||||
108 0.673196437
|
||||
109 0.6731652709
|
||||
110 0.673138808
|
||||
111 0.6731062725
|
||||
112 0.6730726625
|
||||
113 0.6730285927
|
||||
114 0.6729872702
|
||||
115 0.6729721425
|
||||
116 0.6729564624
|
||||
117 0.6729312424
|
||||
118 0.6729354345
|
||||
119 0.6729085401
|
||||
120 0.6728898322
|
||||
121 0.6728773638
|
||||
122 0.6728618874
|
||||
123 0.6728540413
|
||||
124 0.6728441291
|
||||
125 0.672815631
|
||||
126 0.6728082021
|
||||
127 0.6727900064
|
||||
128 0.6727649552
|
||||
129 0.6727467657
|
||||
130 0.6727396032
|
||||
131 0.6727245271
|
||||
132 0.6726955143
|
||||
133 0.67269209
|
||||
134 0.672677932
|
||||
135 0.6726540285
|
||||
136 0.6726288583
|
||||
137 0.6725863431
|
||||
138 0.6725837967
|
||||
139 0.6725772977
|
||||
140 0.6725685594
|
||||
141 0.6725553829
|
||||
142 0.6725484347
|
||||
143 0.6725306172
|
||||
144 0.672543149
|
||||
145 0.6725196247
|
||||
146 0.6725226452
|
||||
147 0.6725056913
|
||||
148 0.6724771476
|
||||
149 0.6724439435
|
||||
150 0.672442532
|
||||
151 0.6724303064
|
||||
152 0.6724235788
|
||||
153 0.6724294499
|
||||
154 0.6724285935
|
||||
155 0.6724172017
|
||||
156 0.6724130745
|
||||
157 0.6723860878
|
||||
158 0.6723707604
|
||||
159 0.6723566111
|
||||
160 0.6723469906
|
||||
161 0.6723287161
|
||||
162 0.6723155898
|
||||
163 0.6722970834
|
||||
164 0.6722872244
|
||||
165 0.6722800481
|
||||
166 0.6722550973
|
||||
167 0.6722394313
|
||||
168 0.6722204135
|
||||
169 0.6721982148
|
||||
170 0.6721971176
|
||||
171 0.6721880705
|
||||
172 0.672179176
|
||||
173 0.6721769709
|
||||
174 0.6721693215
|
||||
175 0.6721581386
|
||||
176 0.6721638661
|
||||
177 0.6721598475
|
||||
178 0.6721433342
|
||||
179 0.6721335599
|
||||
180 0.6721300594
|
||||
181 0.6721153533
|
||||
182 0.6721076397
|
||||
183 0.6721009911
|
||||
184 0.6720999252
|
||||
185 0.6720953028
|
||||
186 0.6720942505
|
||||
187 0.6720856237
|
||||
188 0.6720876136
|
||||
189 0.6720880182
|
||||
190 0.6720743856
|
||||
191 0.6720598415
|
||||
192 0.6720563492
|
||||
193 0.6720389527
|
||||
194 0.6720317324
|
||||
195 0.672000736
|
||||
196 0.6719895017
|
||||
197 0.6719725302
|
||||
198 0.6719770493
|
||||
199 0.6719667172
|
||||
200 0.6719511616
|
||||
201 0.6719427289
|
||||
202 0.6719299116
|
||||
203 0.6719106583
|
||||
204 0.6718967065
|
||||
205 0.671890967
|
||||
206 0.6718896293
|
||||
207 0.6718883534
|
||||
208 0.6718827289
|
||||
209 0.6718763224
|
||||
210 0.67187262
|
||||
211 0.6718590402
|
||||
212 0.6718455115
|
||||
213 0.6718253747
|
||||
214 0.671794877
|
||||
215 0.6717873786
|
||||
216 0.6717765089
|
||||
217 0.6717616726
|
||||
218 0.6717499215
|
||||
219 0.6717326052
|
||||
220 0.6717161937
|
||||
221 0.6717056951
|
||||
222 0.6717021438
|
||||
223 0.6716868488
|
||||
224 0.6716751909
|
||||
225 0.671670116
|
||||
226 0.6716558757
|
||||
227 0.6716559962
|
||||
228 0.6716487875
|
||||
229 0.6716427451
|
||||
230 0.6716323255
|
||||
231 0.6716303547
|
||||
232 0.6716309509
|
||||
233 0.6716215401
|
||||
234 0.6716162103
|
||||
235 0.6716135097
|
||||
236 0.6716156696
|
||||
237 0.6716020054
|
||||
238 0.6715921704
|
||||
239 0.6715804466
|
||||
240 0.6715882966
|
||||
241 0.6715753942
|
||||
242 0.6715752261
|
||||
243 0.6715625509
|
||||
244 0.6715628214
|
||||
245 0.6715601629
|
||||
246 0.6715576255
|
||||
247 0.6715550274
|
||||
248 0.6715448645
|
||||
249 0.6715308166
|
||||
250 0.671519334
|
||||
251 0.6715184071
|
||||
252 0.6715163019
|
||||
253 0.6715096094
|
||||
254 0.6714992963
|
||||
255 0.6714917256
|
||||
256 0.671477406
|
||||
257 0.6714741542
|
||||
258 0.6714576155
|
||||
259 0.6714473645
|
||||
260 0.6714427232
|
||||
261 0.6714364275
|
||||
262 0.6714339587
|
||||
263 0.6714336287
|
||||
264 0.6714283568
|
||||
265 0.6714271895
|
||||
266 0.671413471
|
||||
267 0.6714072396
|
||||
268 0.6714002677
|
||||
269 0.6714001163
|
||||
270 0.6713933952
|
||||
271 0.6713926761
|
||||
272 0.6713836619
|
||||
273 0.6713772112
|
||||
274 0.6713603715
|
||||
275 0.6713560246
|
||||
276 0.6713837913
|
||||
277 0.6713684274
|
||||
278 0.6713619356
|
||||
279 0.6713584836
|
||||
280 0.6713673572
|
||||
281 0.6713625568
|
||||
282 0.6713542652
|
||||
283 0.6713512017
|
||||
284 0.671342038
|
||||
285 0.6713279798
|
||||
286 0.6713123285
|
||||
287 0.6713035326
|
||||
288 0.6713022203
|
||||
289 0.671296041
|
||||
290 0.6712829551
|
||||
291 0.6712769751
|
||||
292 0.6712702915
|
||||
293 0.6712379343
|
||||
294 0.6712192006
|
||||
295 0.6712074061
|
||||
296 0.6711953324
|
||||
297 0.6711891001
|
||||
298 0.6711870526
|
||||
299 0.6711812809
|
||||
300 0.6711768946
|
||||
301 0.6711845012
|
||||
302 0.6711869636
|
||||
303 0.671186884
|
||||
304 0.6711890401
|
||||
305 0.6711868603
|
||||
306 0.6711900892
|
||||
307 0.6711884242
|
||||
308 0.6711837119
|
||||
309 0.6711766645
|
||||
310 0.671172959
|
||||
311 0.6711740433
|
||||
312 0.6711715069
|
||||
313 0.6711589843
|
||||
314 0.6711446402
|
||||
315 0.6711415366
|
||||
316 0.6711359351
|
||||
317 0.671143361
|
||||
318 0.6711353638
|
||||
319 0.6711444387
|
||||
320 0.6711487352
|
||||
321 0.67114436
|
||||
322 0.6711444722
|
||||
323 0.6711325635
|
||||
324 0.6711269403
|
||||
325 0.6711154078
|
||||
326 0.6711203043
|
||||
327 0.6711241333
|
||||
328 0.6711213497
|
||||
329 0.6711231641
|
||||
330 0.6711049215
|
||||
331 0.6711031963
|
||||
332 0.6710996314
|
||||
333 0.6710867309
|
||||
334 0.6710914578
|
||||
335 0.6710929585
|
||||
336 0.6710984779
|
||||
337 0.6710923199
|
||||
338 0.6710893917
|
||||
339 0.6710923306
|
||||
340 0.6710927901
|
||||
341 0.6711092802
|
||||
342 0.6711012995
|
||||
343 0.6711015305
|
||||
344 0.6710975574
|
||||
345 0.6710899474
|
||||
346 0.671085152
|
||||
347 0.6710814533
|
||||
348 0.6710701892
|
||||
349 0.67105503
|
||||
350 0.6710527861
|
||||
351 0.6710508715
|
||||
352 0.6710560803
|
||||
353 0.6710465693
|
||||
354 0.6710440741
|
||||
355 0.6710496913
|
||||
356 0.6710404659
|
||||
357 0.6710293986
|
||||
358 0.6710353817
|
||||
359 0.6710271815
|
||||
360 0.6710288077
|
||||
361 0.6710169894
|
||||
362 0.6710119848
|
||||
363 0.6710114775
|
||||
364 0.6710013614
|
||||
365 0.6709985657
|
||||
366 0.6709948954
|
||||
367 0.6709970591
|
||||
368 0.6709739289
|
||||
369 0.6709754911
|
||||
370 0.6709717066
|
||||
371 0.67096845
|
||||
372 0.6709739445
|
||||
373 0.6709728881
|
||||
374 0.6709694284
|
||||
375 0.6709604166
|
||||
376 0.6709605025
|
||||
377 0.6709603727
|
||||
378 0.670944339
|
||||
379 0.6709447187
|
||||
380 0.6709538679
|
||||
381 0.6709640912
|
||||
382 0.6709534847
|
||||
383 0.6709471555
|
||||
384 0.6709506783
|
||||
385 0.6709546729
|
||||
386 0.670930774
|
||||
387 0.6709287322
|
||||
388 0.6709198643
|
||||
389 0.6709220389
|
||||
390 0.6709230923
|
||||
391 0.670930414
|
||||
392 0.6709354296
|
||||
393 0.6709351544
|
||||
394 0.6709414935
|
||||
395 0.6709445943
|
||||
396 0.6709475685
|
||||
397 0.6709533591
|
||||
398 0.6709592222
|
||||
399 0.6709508704
|
||||
400 0.6709479912
|
||||
401 0.6709417519
|
||||
402 0.6709476082
|
||||
403 0.6709480979
|
||||
404 0.6709448724
|
||||
405 0.6709421934
|
||||
406 0.6709386261
|
||||
407 0.6709461564
|
||||
408 0.670934384
|
||||
409 0.6709312987
|
||||
410 0.670931806
|
||||
411 0.6709286111
|
||||
412 0.6709224729
|
||||
413 0.6709236504
|
||||
414 0.6709245901
|
||||
415 0.6709463437
|
||||
416 0.6709567049
|
||||
417 0.670945606
|
||||
418 0.6709479298
|
||||
419 0.6709464351
|
||||
420 0.6709414048
|
||||
421 0.6709414427
|
||||
422 0.6709296343
|
||||
423 0.670924721
|
||||
424 0.670906284
|
||||
425 0.6708996826
|
||||
426 0.6708987677
|
||||
427 0.670909526
|
||||
428 0.6709033226
|
||||
429 0.6708750209
|
||||
430 0.6708752079
|
||||
431 0.6708776566
|
||||
432 0.6708736133
|
||||
433 0.6708754298
|
||||
434 0.6708751084
|
||||
435 0.6708642042
|
||||
436 0.6708610465
|
||||
437 0.6708574768
|
||||
438 0.6708557953
|
||||
439 0.670871378
|
||||
440 0.6708640187
|
||||
441 0.6708700565
|
||||
442 0.6708667534
|
||||
443 0.6708675383
|
||||
444 0.6708740175
|
||||
445 0.6708774523
|
||||
446 0.6708697231
|
||||
447 0.6708614971
|
||||
448 0.6708607946
|
||||
449 0.6708740865
|
||||
450 0.6708729562
|
||||
451 0.6708674017
|
||||
452 0.6708693088
|
||||
453 0.6708712037
|
||||
454 0.6708703905
|
||||
455 0.6708577595
|
||||
456 0.6708493546
|
||||
457 0.6708523777
|
||||
458 0.6708454134
|
||||
459 0.6708404483
|
||||
460 0.6708274771
|
||||
461 0.6708244992
|
||||
462 0.6708344314
|
||||
463 0.6708279081
|
||||
464 0.6708258106
|
||||
465 0.6708049714
|
||||
466 0.670810989
|
||||
467 0.6708212237
|
||||
468 0.6708221741
|
||||
469 0.6708259658
|
||||
470 0.6708159692
|
||||
471 0.6708136212
|
||||
472 0.6708224942
|
||||
473 0.6708363084
|
||||
474 0.670850875
|
||||
475 0.6708527236
|
||||
476 0.6708453401
|
||||
477 0.6708413844
|
||||
478 0.6708364569
|
||||
479 0.6708251774
|
||||
480 0.6708154393
|
||||
481 0.6708111613
|
||||
482 0.6708102339
|
||||
483 0.6707929623
|
||||
484 0.6707900226
|
||||
485 0.6707832384
|
||||
486 0.6707739118
|
||||
487 0.6707737538
|
||||
488 0.6707730234
|
||||
489 0.6707796291
|
||||
490 0.670791408
|
||||
491 0.6707944906
|
||||
492 0.6707835635
|
||||
493 0.6707908928
|
||||
494 0.670796262
|
||||
495 0.6707877825
|
||||
496 0.6707854132
|
||||
497 0.6707756206
|
||||
498 0.6707707899
|
||||
499 0.6707704386
|
||||
500 0.6707621465
|
||||
501 0.6707661931
|
||||
502 0.6707651988
|
||||
503 0.6707607827
|
||||
504 0.670760242
|
||||
505 0.6707506008
|
||||
506 0.6707452886
|
||||
507 0.6707355189
|
||||
508 0.6707312551
|
||||
509 0.6707199485
|
||||
510 0.6707131947
|
||||
511 0.6707154112
|
||||
512 0.6706982346
|
||||
513 0.6706988941
|
||||
514 0.6706989098
|
||||
515 0.670693306
|
||||
516 0.6706944515
|
||||
517 0.6706899688
|
||||
518 0.6706909374
|
||||
519 0.6706855074
|
||||
520 0.6706787779
|
||||
521 0.6706737082
|
||||
522 0.6706761225
|
||||
523 0.670685455
|
||||
524 0.6706693855
|
||||
525 0.6706647216
|
||||
526 0.6706569188
|
||||
527 0.6706549134
|
||||
528 0.6706547978
|
||||
529 0.6706564214
|
||||
530 0.6706559196
|
||||
531 0.6706515072
|
||||
532 0.6706474616
|
||||
533 0.6706424204
|
||||
534 0.6706520008
|
||||
535 0.6706448306
|
||||
536 0.6706415789
|
||||
537 0.6706305359
|
||||
538 0.6706152774
|
||||
539 0.670616585
|
||||
540 0.6705963243
|
||||
541 0.6706027368
|
||||
542 0.6706003522
|
||||
543 0.6706044301
|
||||
544 0.6706047241
|
||||
545 0.6706038235
|
||||
546 0.6706026913
|
||||
547 0.6705845786
|
||||
548 0.6705873967
|
||||
549 0.6705755426
|
||||
550 0.6705715731
|
||||
551 0.6705757153
|
||||
552 0.6705516814
|
||||
553 0.6705530864
|
||||
554 0.6705552479
|
||||
555 0.6705563336
|
||||
556 0.6705718544
|
||||
557 0.6705688384
|
||||
558 0.6705641528
|
||||
559 0.6705628467
|
||||
560 0.670558488
|
||||
561 0.6705544404
|
||||
562 0.6705617451
|
||||
563 0.6705631717
|
||||
564 0.6705636201
|
||||
565 0.6705537522
|
||||
566 0.670555083
|
||||
567 0.6705524541
|
||||
568 0.6705503132
|
||||
569 0.6705354602
|
||||
570 0.6705387012
|
||||
571 0.6705411923
|
||||
572 0.6705390018
|
||||
573 0.6705354939
|
||||
574 0.670531296
|
||||
575 0.6705377163
|
||||
576 0.6705248875
|
||||
577 0.6705252902
|
||||
578 0.6705181562
|
||||
579 0.6705123446
|
||||
580 0.6705128345
|
||||
581 0.6705173712
|
||||
582 0.670541941
|
||||
583 0.6705463243
|
||||
584 0.6705513215
|
||||
585 0.6705455889
|
||||
586 0.6705408087
|
||||
587 0.6705510193
|
||||
588 0.6705456751
|
||||
589 0.6705402427
|
||||
590 0.6705443402
|
||||
591 0.67054441
|
||||
592 0.6705441955
|
||||
593 0.6705319356
|
||||
594 0.6705358843
|
||||
595 0.6705334396
|
||||
596 0.6705320462
|
||||
597 0.6705332043
|
||||
598 0.6705328363
|
||||
599 0.6705315638
|
||||
600 0.6705274435
|
||||
601 0.670509808
|
||||
602 0.6705077789
|
||||
603 0.6705212132
|
||||
604 0.6705098442
|
||||
605 0.6705061509
|
||||
606 0.6705003071
|
||||
607 0.6705045031
|
||||
608 0.6705083194
|
||||
609 0.6705329997
|
||||
610 0.6705269987
|
||||
611 0.6705315607
|
||||
612 0.6705142835
|
||||
613 0.6705165015
|
||||
614 0.6705001061
|
||||
615 0.6705013916
|
||||
616 0.6705037253
|
||||
617 0.67049647
|
||||
618 0.6705005632
|
||||
619 0.6704957943
|
||||
620 0.6704955333
|
||||
621 0.6704961207
|
||||
622 0.6704921459
|
||||
623 0.6704751713
|
||||
624 0.6704753101
|
||||
625 0.6704620888
|
||||
626 0.6704604282
|
||||
627 0.6704663192
|
||||
628 0.6704680085
|
||||
629 0.670453228
|
||||
630 0.6704577785
|
||||
631 0.67046675
|
||||
632 0.6704731863
|
||||
633 0.6704811116
|
||||
634 0.6704839644
|
||||
635 0.6704854798
|
||||
636 0.6704835837
|
||||
637 0.6704736198
|
||||
638 0.6704640242
|
||||
639 0.670465663
|
||||
640 0.6704646829
|
||||
641 0.6704600961
|
||||
642 0.6704643207
|
||||
643 0.6704600533
|
||||
644 0.6704614691
|
||||
645 0.6704728212
|
||||
646 0.6704758731
|
||||
647 0.6704833026
|
||||
648 0.6704767664
|
||||
649 0.6704702727
|
||||
650 0.6704671372
|
||||
651 0.6704699936
|
||||
652 0.6704587989
|
||||
653 0.6704637668
|
||||
654 0.6704653717
|
||||
655 0.6704598273
|
||||
656 0.6704522865
|
||||
657 0.6704558586
|
||||
658 0.6704466331
|
||||
659 0.6704405886
|
||||
660 0.6704463767
|
||||
661 0.6704475216
|
||||
662 0.6704572386
|
||||
663 0.6704658153
|
||||
664 0.6704600945
|
||||
665 0.6704561998
|
||||
666 0.6704535154
|
||||
667 0.6704413781
|
||||
668 0.6704450013
|
||||
669 0.6704422199
|
||||
670 0.67044342
|
||||
671 0.6704415341
|
||||
672 0.6704439539
|
||||
673 0.6704498197
|
||||
674 0.6704452194
|
||||
675 0.6704366524
|
||||
676 0.6704427124
|
||||
677 0.6704395579
|
||||
678 0.6704401246
|
||||
679 0.6704415621
|
||||
680 0.6704341343
|
||||
681 0.6704369615
|
||||
682 0.6704357425
|
||||
683 0.6704294622
|
||||
684 0.6704289794
|
||||
685 0.6704272409
|
||||
686 0.6704101162
|
||||
687 0.6704069439
|
||||
688 0.6704100747
|
||||
689 0.6704122261
|
||||
690 0.6704137826
|
||||
691 0.6704207952
|
||||
692 0.6704154834
|
||||
693 0.6704253514
|
||||
694 0.6704155636
|
||||
695 0.6704141298
|
||||
696 0.6704207635
|
||||
697 0.6704268341
|
||||
698 0.6704243126
|
||||
699 0.6704235165
|
||||
700 0.6704257736
|
||||
701 0.6704247758
|
||||
702 0.6704331799
|
||||
703 0.6704252722
|
||||
704 0.6704146644
|
||||
705 0.6704164122
|
||||
706 0.6704118954
|
||||
707 0.6704043129
|
||||
708 0.6703978198
|
||||
709 0.6703935976
|
||||
710 0.6703839683
|
||||
711 0.6703843723
|
||||
712 0.6703879502
|
||||
713 0.6703895978
|
||||
714 0.6703894359
|
||||
715 0.6703928777
|
||||
716 0.6703933128
|
||||
717 0.6703844355
|
||||
718 0.6703825151
|
||||
719 0.6703983542
|
||||
720 0.670399556
|
||||
721 0.6703931808
|
||||
722 0.6703886918
|
||||
723 0.6703847574
|
||||
724 0.6703885941
|
||||
725 0.6703788615
|
||||
726 0.6703799906
|
||||
727 0.6703774518
|
||||
728 0.6703783496
|
||||
729 0.6703648854
|
||||
730 0.6703716654
|
||||
731 0.6703550938
|
||||
732 0.6703467057
|
||||
733 0.6703484503
|
||||
734 0.6703549183
|
||||
735 0.6703501504
|
||||
736 0.6703672622
|
||||
737 0.6703560249
|
||||
738 0.6703547155
|
||||
739 0.6703593236
|
||||
740 0.6703606827
|
||||
741 0.6703511404
|
||||
742 0.6703431646
|
||||
743 0.6703475116
|
||||
744 0.6703483634
|
||||
745 0.6703475713
|
||||
746 0.670360457
|
||||
747 0.6703664352
|
||||
748 0.6703617612
|
||||
749 0.6703669926
|
||||
750 0.6703670837
|
||||
751 0.6703706628
|
||||
752 0.670369618
|
||||
753 0.6703692351
|
||||
754 0.6703624433
|
||||
755 0.6703686285
|
||||
756 0.6703598432
|
||||
757 0.6703618766
|
||||
758 0.6703694148
|
||||
759 0.6703683652
|
||||
760 0.6703604855
|
||||
761 0.6703758987
|
||||
762 0.6703773302
|
||||
763 0.6703641028
|
||||
764 0.6703649602
|
||||
765 0.6703567811
|
||||
766 0.6703544688
|
||||
767 0.6703611821
|
||||
768 0.6703527821
|
||||
769 0.6703523616
|
||||
770 0.6703616298
|
||||
771 0.6703603551
|
||||
772 0.6703675655
|
||||
773 0.6703582411
|
||||
774 0.6703581437
|
||||
775 0.6703551885
|
||||
776 0.6703608491
|
||||
777 0.6703674554
|
||||
778 0.6703679619
|
||||
779 0.6703701757
|
||||
780 0.6703603462
|
||||
781 0.670359801
|
||||
782 0.6703523669
|
||||
783 0.6703365674
|
||||
784 0.6703486118
|
||||
785 0.6703450011
|
||||
786 0.6703473135
|
||||
787 0.670350998
|
||||
788 0.6703417767
|
||||
789 0.6703349821
|
||||
790 0.6703457717
|
||||
791 0.6703506266
|
||||
792 0.6703596395
|
||||
793 0.6703799895
|
||||
794 0.6703687687
|
||||
795 0.6703780675
|
||||
796 0.670374835
|
||||
797 0.6703831387
|
||||
798 0.670377656
|
||||
799 0.6703689741
|
||||
800 0.6703709756
|
||||
801 0.6703737517
|
||||
802 0.6703818964
|
||||
803 0.6703812173
|
||||
804 0.6703960068
|
||||
805 0.6703976729
|
||||
806 0.6704024604
|
||||
807 0.6704085008
|
||||
808 0.6704076633
|
||||
809 0.6704111719
|
||||
810 0.6704220803
|
||||
811 0.6704265011
|
||||
812 0.6704251162
|
||||
813 0.6704375472
|
||||
814 0.6704319336
|
||||
815 0.670437614
|
||||
816 0.6704554331
|
||||
817 0.6704603317
|
||||
818 0.6704548042
|
||||
819 0.6704502654
|
||||
820 0.6704512072
|
||||
821 0.6704433481
|
||||
822 0.6704095112
|
||||
823 0.6704086019
|
||||
824 0.6703987131
|
||||
825 0.6704019708
|
||||
826 0.6704046556
|
||||
827 0.6704091961
|
||||
828 0.6704103204
|
||||
829 0.6704074257
|
||||
830 0.6704115335
|
||||
831 0.6704041275
|
||||
832 0.6704004556
|
||||
833 0.6704037097
|
||||
834 0.6704035477
|
||||
835 0.6704025281
|
||||
836 0.6704024549
|
||||
837 0.670405721
|
||||
838 0.6703983189
|
||||
839 0.6704035256
|
||||
840 0.6704047613
|
||||
841 0.6704139617
|
||||
842 0.6704066193
|
||||
843 0.670402892
|
||||
844 0.6704081961
|
||||
845 0.6704029862
|
||||
846 0.6704014281
|
||||
847 0.6704036115
|
||||
848 0.6704016777
|
||||
849 0.6704085392
|
||||
850 0.6704042952
|
||||
851 0.670394789
|
||||
852 0.6703885644
|
||||
853 0.6703946813
|
||||
854 0.6704042137
|
||||
855 0.6704077517
|
||||
856 0.6704118698
|
||||
857 0.6704114259
|
||||
858 0.6704157567
|
||||
859 0.6703837005
|
||||
860 0.6703829482
|
||||
861 0.6703818491
|
||||
862 0.6703826129
|
||||
863 0.6703834487
|
||||
864 0.6703868275
|
||||
865 0.6703853357
|
||||
866 0.670450644
|
||||
867 0.6704556991
|
||||
868 0.6704535742
|
||||
869 0.6704495915
|
||||
|
@@ -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),
|
||||
}
|
||||
+10
-1
@@ -16,6 +16,7 @@ from pydantic import BaseModel
|
||||
|
||||
from models.basketball_v25 import get_basketball_v25_predictor
|
||||
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
|
||||
|
||||
load_dotenv()
|
||||
@@ -38,6 +39,7 @@ async def lifespan(_: FastAPI):
|
||||
try:
|
||||
print("🚀 Initializing V25 orchestrator...", flush=True)
|
||||
get_single_match_orchestrator()
|
||||
get_v26_shadow_engine()
|
||||
print("✅ V25 orchestrator ready", flush=True)
|
||||
except Exception as error:
|
||||
print(f"❌ Failed to initialize orchestrator: {error}", flush=True)
|
||||
@@ -104,6 +106,7 @@ def read_root() -> dict[str, Any]:
|
||||
return {
|
||||
"status": "Suggest-Bet AI Engine v25",
|
||||
"engine": "V25 Single Match Orchestrator",
|
||||
"mode": os.getenv("AI_ENGINE_MODE", "v25"),
|
||||
"routes": [
|
||||
"POST /v20plus/analyze/{match_id}",
|
||||
"GET /v20plus/analyze-htms/{match_id}",
|
||||
@@ -118,15 +121,21 @@ def read_root() -> dict[str, Any]:
|
||||
@app.get("/health")
|
||||
def health_check() -> dict[str, Any]:
|
||||
try:
|
||||
get_single_match_orchestrator()
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
shadow_engine = get_v26_shadow_engine()
|
||||
basketball_predictor = get_basketball_v25_predictor()
|
||||
basketball_readiness = basketball_predictor.readiness_summary()
|
||||
ready = bool(basketball_readiness["fully_loaded"])
|
||||
return {
|
||||
"status": "healthy" if ready else "degraded",
|
||||
"engine": "v25.main",
|
||||
"mode": os.getenv("AI_ENGINE_MODE", "v25"),
|
||||
"ready": ready,
|
||||
"basketball_v25": basketball_readiness,
|
||||
"v26_shadow": shadow_engine.readiness_summary(),
|
||||
"prediction_service_ready": True,
|
||||
"model_loaded": ready,
|
||||
"orchestrator_mode": getattr(orchestrator, "engine_mode", "v25"),
|
||||
}
|
||||
except Exception as error:
|
||||
return {"status": "unhealthy", "ready": False, "error": str(error)}
|
||||
|
||||
@@ -0,0 +1,902 @@
|
||||
[
|
||||
{
|
||||
"match_id": "2b1jyd72hogojec5j50fd9gr8",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "1b2chhfsohmulm85sb95y189g",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "X"
|
||||
},
|
||||
{
|
||||
"match_id": "dg84sd1wkmtfrtdm9od7wy7f8",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "Alt",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "dydrdtrxi3dsomph1at54jaxg",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.0264,
|
||||
"avg_confidence": 69.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 2.0,
|
||||
"avg_edge": 0.1559,
|
||||
"avg_confidence": 71.4
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "2.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "b6uzz042mizu0dqpci538z4lw",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.1515,
|
||||
"avg_confidence": 66.3
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 2.0,
|
||||
"avg_edge": 0.1103,
|
||||
"avg_confidence": 64.45
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "dmp0q35bpbb7rt11opg5mwzkk",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.0756,
|
||||
"avg_confidence": 67.5
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "6opr8muwfdoosfpnkm4gbb190",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 2.0,
|
||||
"avg_edge": 0.384,
|
||||
"avg_confidence": 80.8
|
||||
},
|
||||
"v25_main": "Tek",
|
||||
"v26_main": "KG Var"
|
||||
},
|
||||
{
|
||||
"match_id": "a6dxspn0akrnf19mno8z83yms",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 1.001,
|
||||
"avg_confidence": 72.5
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "9qrr3sya0mlfusmqlushjask4",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "2",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "ytsc38rm4j22govgwo3as6j8",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 1.1251,
|
||||
"avg_confidence": 67.6
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "X2",
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "68oi5t06w15a9b8wt8rsl8gk",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "KG Yok",
|
||||
"v26_main": "2"
|
||||
},
|
||||
{
|
||||
"match_id": "bsw4axalza4idsop3qio295hw",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.0113,
|
||||
"avg_confidence": 69.6
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.9893,
|
||||
"avg_confidence": 59.1
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "btsq93obda94w3be5bogr0etw",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "ajgyxy5iqiprjkr2l98np982c",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.0166,
|
||||
"avg_confidence": 69.5
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "4ocwqbbw37kikqj38hf4txes4",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.0755,
|
||||
"avg_confidence": 67.5
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 2.0,
|
||||
"avg_edge": 0.0585,
|
||||
"avg_confidence": 74.65
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "dogkmluzn54lg0q6yuom1l53o",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 1.2566,
|
||||
"avg_confidence": 81.7
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "2"
|
||||
},
|
||||
{
|
||||
"match_id": "cfar57gsu6hy770n7e58u8duc",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "cbg4zpl58ahr6r22d6syo0wt0",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "1X",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "5psd7wioj63day6cylflbzf2s",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "3.5 Alt"
|
||||
},
|
||||
{
|
||||
"match_id": "570ybzbhwkgvef82h49c27eac",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00c7ift",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "5vaa5dl47mw2t6xf3t3qjhvro",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "9pqea93iqumda82hj1sfxd7v8",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "cjs180a7sqxkurwo18rp9aeqc",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "2"
|
||||
},
|
||||
{
|
||||
"match_id": "3j2wsty5be0d432ozvuj5w2l0",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 1.1947,
|
||||
"avg_confidence": 71.4
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.091,
|
||||
"avg_confidence": 68.6
|
||||
},
|
||||
"v25_main": "1X",
|
||||
"v26_main": "3.5 Alt"
|
||||
},
|
||||
{
|
||||
"match_id": "3toy2ctfu4kce9ojkngofzf2s",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 2.0,
|
||||
"avg_edge": 0.0557,
|
||||
"avg_confidence": 67.25
|
||||
},
|
||||
"v25_main": "\u00c7ift",
|
||||
"v26_main": "KG Yok"
|
||||
},
|
||||
{
|
||||
"match_id": "5uvgaveseimkwkkj5lf0iv9xw",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "X"
|
||||
},
|
||||
{
|
||||
"match_id": "8i2hjmknzgjkhfhn9pehrhy50",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00c7ift",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "rfhmiaml1h9taxebpvmaijo4",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00c7ift",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "25sjaorwswwaeu3p2hd3p747o",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "X"
|
||||
},
|
||||
{
|
||||
"match_id": "d8e65nskkka2b6s1cr3rks1ec",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.7042,
|
||||
"avg_confidence": 61.7
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "2"
|
||||
},
|
||||
{
|
||||
"match_id": "fbxbk21jlzgmf0dc385mu6fo",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.5759,
|
||||
"avg_confidence": 57.1
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "9i8ikoed9f94nt90w8uy6y710",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.8784,
|
||||
"avg_confidence": 56.4
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "X"
|
||||
},
|
||||
{
|
||||
"match_id": "2iw74m76pj2ibdu9igq7u6ql0",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.6571,
|
||||
"avg_confidence": 60.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "3yt7t944i6ftaok5b799fa784",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "X"
|
||||
},
|
||||
{
|
||||
"match_id": "99r0d7ggi1169dhklo4eroopg",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.0266,
|
||||
"avg_confidence": 69.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 4.0,
|
||||
"avg_edge": 0.2509,
|
||||
"avg_confidence": 76.65
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "2.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "63lonant3zu1u7xmpomocetw",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 2.0,
|
||||
"avg_edge": 0.179,
|
||||
"avg_confidence": 66.6
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "2"
|
||||
},
|
||||
{
|
||||
"match_id": "790qnaweqoyffb5ndxnb4hlas",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.1067,
|
||||
"avg_confidence": 74.5
|
||||
},
|
||||
"v25_main": "2/2",
|
||||
"v26_main": "2.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "55bckv97w88qeqymhqgwg9fdg",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "1X",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "ir507rn2anyknjvm1xua1c7o",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "Alt",
|
||||
"v26_main": "2.5 Alt"
|
||||
},
|
||||
{
|
||||
"match_id": "d9h5hdhwum2s04ma722emljx0",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 1.2061,
|
||||
"avg_confidence": 79.9
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "3riltqclv2llihnklpcw6u784",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "8808y1x2hmz52k3mr598orqc4",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.2221,
|
||||
"avg_confidence": 67.9
|
||||
},
|
||||
"v25_main": "KG Var",
|
||||
"v26_main": "KG Var"
|
||||
},
|
||||
{
|
||||
"match_id": "5qypw3tl5qkx16ihhbifv9jis",
|
||||
"v25": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.0979,
|
||||
"avg_confidence": 66.9
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 3.0,
|
||||
"avg_edge": 0.1071,
|
||||
"avg_confidence": 66.3
|
||||
},
|
||||
"v25_main": "\u00dcst",
|
||||
"v26_main": "2"
|
||||
},
|
||||
{
|
||||
"match_id": "710vie8tgz5ccs7sq9xwrjmz8",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 1.2892,
|
||||
"avg_confidence": 82.9
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "2vkbdwkxyjj1invl5877bz0us",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "X"
|
||||
},
|
||||
{
|
||||
"match_id": "7caglruhdiegg9xrc20fzhrtg",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00c7ift",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "gl3nvw4hprtny0aby1z40duc",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00c7ift",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "f87f0jsazmdhlq3wzz7ngp3o",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "X2",
|
||||
"v26_main": "2.5 Alt"
|
||||
},
|
||||
{
|
||||
"match_id": "chb58495fc1o6ek9t9kdfbyfo",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.0572,
|
||||
"avg_confidence": 75.5
|
||||
},
|
||||
"v25_main": "1",
|
||||
"v26_main": "KG Var"
|
||||
},
|
||||
{
|
||||
"match_id": "57h5qfr5mr4qoxix7vhiwd2j8",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 1.1927,
|
||||
"avg_confidence": 79.4
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "2"
|
||||
},
|
||||
{
|
||||
"match_id": "de76ipnpgaznxvl9tk9ewquqc",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "X"
|
||||
},
|
||||
{
|
||||
"match_id": "5pjdtn5bb9v64cj4ceh4ei1hw",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": "\u00c7ift",
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "6orrk7jpn3ucpybdieh6oin84",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "3shwt6aecvnu9utm2go9diq6s",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.674,
|
||||
"avg_confidence": 60.6
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "5kukkba6u6wq1xj8oi118q1hw",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.8906,
|
||||
"avg_confidence": 56.7
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "X"
|
||||
},
|
||||
{
|
||||
"match_id": "9fzu7f8aybsqy88r3nam79p1w",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "2"
|
||||
},
|
||||
{
|
||||
"match_id": "bxcnq91mnr9f6bhicdxgzbims",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1.5 \u00dcst"
|
||||
},
|
||||
{
|
||||
"match_id": "dueticp36nbckn2pnpnt6az8",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "X"
|
||||
},
|
||||
{
|
||||
"match_id": "5m9do9pcggnl0tgmeolvnph5g",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 0.6914,
|
||||
"avg_confidence": 61.3
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1"
|
||||
},
|
||||
{
|
||||
"match_id": "avu5vzpdi1wy7mrneodb3gw7o",
|
||||
"v25": {
|
||||
"playable_count": 0.0,
|
||||
"avg_edge": 0.0,
|
||||
"avg_confidence": 0.0
|
||||
},
|
||||
"v26": {
|
||||
"playable_count": 1.0,
|
||||
"avg_edge": 1.1608,
|
||||
"avg_confidence": 78.3
|
||||
},
|
||||
"v25_main": null,
|
||||
"v26_main": "1"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
match_id,date,league,match,ht_score,final_score,strategy,market,pick,odds,playable,confidence,result,counted_in_roi,profit_flat,resolution_note,source,reversal_pick,reversal_prob,favorite_gap,favorite_odd,support_score,odds_band_score,odds_band_label,league_reversal_rate,league_strict_rev_rate,referee_strict_rev_rate,surprise_score,reason_codes,pick_reason
|
||||
b6uzz042mizu0dqpci538z4lw,2025-09-06,Süper Kupa,V. Sarsfield vs C. Cordoba,0-0,2-0,v25_aggressive,HTFT,2/1,34.5,True,21.0,LOST,True,-1.0,actual=X/1,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
||||
b6uzz042mizu0dqpci538z4lw,2025-09-06,Süper Kupa,V. Sarsfield vs C. Cordoba,0-0,2-0,v26_aggressive,HTFT,1/1,3.26,True,16.0,LOST,True,-1.0,actual=X/1,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
||||
ytsc38rm4j22govgwo3as6j8,2025-09-06,DK Elemeler,Avusturya vs G. Kıbrıs Rum Kesimi,0-0,1-0,v25_aggressive,HTFT,X/1,4.01,True,11.4,WON,True,3.01,actual=X/1,v25.aggressive_pick,X/1,,,,,,,,,,,,
|
||||
cfar57gsu6hy770n7e58u8duc,2025-09-06,Eerste Divisie,Cambuur vs Willem II,0-1,2-2,v25_aggressive,HTFT,2/1,20.7,True,15.6,LOST,True,-1.0,actual=2/X,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
||||
cfar57gsu6hy770n7e58u8duc,2025-09-06,Eerste Divisie,Cambuur vs Willem II,0-1,2-2,v26_aggressive,HTFT,1/1,2.13,True,14.4,LOST,True,-1.0,actual=2/X,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
||||
99r0d7ggi1169dhklo4eroopg,2025-09-06,2. Lig,Bromley vs Gillingham,2-0,2-2,v25_aggressive,HTFT,2/1,30.5,True,25.6,LOST,True,-1.0,actual=1/X,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
||||
99r0d7ggi1169dhklo4eroopg,2025-09-06,2. Lig,Bromley vs Gillingham,2-0,2-2,v26_surprise,HTFT,1/2,34.5,False,4.9,LOST,False,0.0,actual=1/X,v26.surprise_pick,1/2,0.0679,1.02,1.92,73.5,0.642,,0.04,0.0,0.0,62.7,"favorite_gap_large,favorite_price_supported,reversal_prob_warm,quality_supports_reversal,favorite_odds_band_reversal_window,favorite_streak_break_window",
|
||||
99r0d7ggi1169dhklo4eroopg,2025-09-06,2. Lig,Bromley vs Gillingham,2-0,2-2,v26_aggressive,HTFT,1/1,3.16,True,16.1,LOST,True,-1.0,actual=1/X,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
||||
790qnaweqoyffb5ndxnb4hlas,2025-09-06,Ulusal Lig,Aldershot vs Brackley,1-0,2-2,v25_aggressive,HTFT,X/2,6.07,True,9.0,LOST,True,-1.0,actual=1/X,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
||||
790qnaweqoyffb5ndxnb4hlas,2025-09-06,Ulusal Lig,Aldershot vs Brackley,1-0,2-2,v26_aggressive,HTFT,2/2,3.8,True,37.1,LOST,True,-1.0,actual=1/X,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
||||
8808y1x2hmz52k3mr598orqc4,2025-09-06,DK Elemeler,Ermenistan vs Portekiz,0-3,0-5,v25_aggressive,HTFT,X/2,4.03,True,11.6,LOST,True,-1.0,actual=2/2,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
||||
7gfnwoxqz5o2clin40a85uqdw,2025-09-06,1. Lig,Port Vale vs Leyton Orient,1-2,2-3,v25_aggressive,HTFT,X/1,4.84,True,12.3,LOST,True,-1.0,actual=2/2,v25.aggressive_pick,X/1,,,,,,,,,,,,
|
||||
7gfnwoxqz5o2clin40a85uqdw,2025-09-06,1. Lig,Port Vale vs Leyton Orient,1-2,2-3,v26_surprise,HTFT,1/2,33.0,False,5.4,LOST,False,0.0,actual=2/2,v26.surprise_pick,1/2,0.0749,0.62,1.97,52.1,0.642,,0.044,0.0,0.0,57.0,"favorite_gap_large,favorite_price_supported,reversal_prob_warm,draw_swing_support,quality_supports_reversal,favorite_odds_band_reversal_window",
|
||||
7gfnwoxqz5o2clin40a85uqdw,2025-09-06,1. Lig,Port Vale vs Leyton Orient,1-2,2-3,v26_aggressive,HTFT,2/2,4.51,True,17.3,WON,True,3.51,actual=2/2,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
||||
7fld91ykj1kfuoc8wn4r2frbo,2025-09-06,1. Lig,Lincoln City vs Wigan Ath,2-1,2-2,v25_aggressive,HTFT,X/1,4.7,True,19.2,LOST,True,-1.0,actual=1/X,v25.aggressive_pick,X/1,,,,,,,,,,,,
|
||||
7fld91ykj1kfuoc8wn4r2frbo,2025-09-06,1. Lig,Lincoln City vs Wigan Ath,2-1,2-2,v26_surprise,HTFT,1/2,34.5,False,7.1,LOST,False,0.0,actual=1/X,v26.surprise_pick,1/2,0.0984,0.71,2.0,61.5,0.642,,0.044,0.0,0.0,61.7,"favorite_gap_large,favorite_price_supported,reversal_prob_hot,upset_risk_detected,quality_supports_reversal,favorite_odds_band_reversal_window",
|
||||
7fld91ykj1kfuoc8wn4r2frbo,2025-09-06,1. Lig,Lincoln City vs Wigan Ath,2-1,2-2,v26_aggressive,HTFT,1/1,3.36,True,19.5,LOST,True,-1.0,actual=1/X,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
||||
7i4nhkex1qssyp3x6rsgj03ro,2025-09-06,1. Lig,Wycombe vs Mansfield,1-0,2-0,v25_aggressive,HTFT,X/2,6.92,True,11.1,LOST,True,-1.0,actual=1/1,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
||||
7i4nhkex1qssyp3x6rsgj03ro,2025-09-06,1. Lig,Wycombe vs Mansfield,1-0,2-0,v26_main_htft,HTFT,2/2,5.22,False,39.9,LOST,False,0.0,actual=1/1,v26.main_pick,,,,,,,,,,,0.0,,
|
||||
7hagai8xazmsj5exw7idwhhck,2025-09-06,1. Lig,Rotherham vs Exeter City,1-0,1-0,v25_aggressive,HTFT,X/2,6.28,True,6.7,LOST,True,-1.0,actual=1/1,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
||||
7hagai8xazmsj5exw7idwhhck,2025-09-06,1. Lig,Rotherham vs Exeter City,1-0,1-0,v26_aggressive,HTFT,2/2,4.47,True,29.6,LOST,True,-1.0,actual=1/1,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
||||
6e37x17qvk0snpokd1698lkb8,2025-09-06,Premiership,Crusaders vs Coleraine,0-3,0-4,v25_aggressive,HTFT,X/2,4.24,True,11.5,LOST,True,-1.0,actual=2/2,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
||||
6e37x17qvk0snpokd1698lkb8,2025-09-06,Premiership,Crusaders vs Coleraine,0-3,0-4,v26_aggressive,HTFT,2/2,2.32,True,37.7,WON,True,1.32,actual=2/2,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
||||
6f0fqlafaei9oj8yd9hdi6rdg,2025-09-06,Premiership,Linfield vs Portadown,0-0,3-0,v25_aggressive,HTFT,2/1,23.2,True,18.6,LOST,True,-1.0,actual=X/1,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
||||
6f0fqlafaei9oj8yd9hdi6rdg,2025-09-06,Premiership,Linfield vs Portadown,0-0,3-0,v26_aggressive,HTFT,1/1,1.74,True,20.4,LOST,True,-1.0,actual=X/1,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
||||
6dny1bmxcj6shc5382dno2al0,2025-09-06,Premiership,Carrick vs Cliftonville,0-1,1-2,v25_aggressive,HTFT,2/1,30.5,True,14.4,LOST,True,-1.0,actual=2/2,v25.aggressive_pick,2/1,,,,,,,,,,,,
|
||||
6dny1bmxcj6shc5382dno2al0,2025-09-06,Premiership,Carrick vs Cliftonville,0-1,1-2,v26_surprise,HTFT,2/1,30.5,False,10.1,LOST,False,0.0,actual=2/2,v26.surprise_pick,2/1,0.1401,0.59,1.97,62.8,0.642,,0.0667,0.0,0.0,66.2,"favorite_price_supported,reversal_prob_hot,quality_supports_reversal,favorite_odds_band_reversal_window,league_strict_reversal_prior,draw_pressure_supports_swing",
|
||||
6dny1bmxcj6shc5382dno2al0,2025-09-06,Premiership,Carrick vs Cliftonville,0-1,1-2,v26_aggressive,HTFT,1/1,4.36,True,14.6,LOST,True,-1.0,actual=2/2,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
||||
9d9nwo82prx06riy5odgnr190,2025-09-06,2. Lig,Walsall vs Chesterfield,1-0,1-0,v25_aggressive,HTFT,X/2,5.38,True,10.3,LOST,True,-1.0,actual=1/1,v25.aggressive_pick,X/2,,,,,,,,,,,,
|
||||
9d9nwo82prx06riy5odgnr190,2025-09-06,2. Lig,Walsall vs Chesterfield,1-0,1-0,v26_aggressive,HTFT,2/2,3.78,True,35.1,LOST,True,-1.0,actual=1/1,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
||||
7dwtx5tr7g66bsqkoc1wos9as,2025-09-06,1. Lig,Bolton vs Wimbledon,1-0,3-0,v25_aggressive,HTFT,X/1,3.76,True,12.1,LOST,True,-1.0,actual=1/1,v25.aggressive_pick,X/1,,,,,,,,,,,,
|
||||
7dwtx5tr7g66bsqkoc1wos9as,2025-09-06,1. Lig,Bolton vs Wimbledon,1-0,3-0,v26_aggressive,HTFT,1/1,1.81,True,33.3,WON,True,0.81,actual=1/1,v26.aggressive_pick,1/1,,,,,,,,,,,,
|
||||
7c85gguekhbhadtm7qdbgir6c,2025-09-06,Ulusal Lig,Tamworth vs Eastleigh,0-0,1-0,v25_aggressive,HTFT,1/2,34.5,True,14.1,LOST,True,-1.0,actual=X/1,v25.aggressive_pick,1/2,,,,,,,,,,,,
|
||||
7c85gguekhbhadtm7qdbgir6c,2025-09-06,Ulusal Lig,Tamworth vs Eastleigh,0-0,1-0,v26_aggressive,HTFT,2/2,5.53,True,14.8,LOST,True,-1.0,actual=X/1,v26.aggressive_pick,2/2,,,,,,,,,,,,
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
# HT/FT + Upset Backtest
|
||||
|
||||
- Sample: last 120 finished football matches
|
||||
- Scope: only HT/FT reversal and upset-oriented picks
|
||||
- ROI: flat `1 unit` per played pick
|
||||
- Generated at: 2026-04-21T12:32:14.419378+00:00
|
||||
|
||||
## Strategy Summary
|
||||
|
||||
| Strategy | Candidates | Played | Won | Lost | Hit Rate | Profit | ROI |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| v25_aggressive | 16 | 16 | 1 | 15 | 6.25% | -11.99 | -74.94% |
|
||||
| v26_surprise | 4 | 0 | 0 | 0 | 0.0% | +0.00 | +0.00% |
|
||||
| v26_aggressive | 13 | 13 | 3 | 10 | 23.08% | -4.36 | -33.54% |
|
||||
| v26_main_htft | 1 | 0 | 0 | 0 | 0.0% | +0.00 | +0.00% |
|
||||
|
||||
## v26 Surprise By Reversal Type
|
||||
|
||||
| Reversal | Candidates | Played | Won | Lost | Profit | ROI |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| 1/2 | 3 | 0 | 0 | 0 | +0.00 | +0.00% |
|
||||
| 2/1 | 1 | 0 | 0 | 0 | +0.00 | +0.00% |
|
||||
| X/1 | 0 | 0 | 0 | 0 | +0.00 | +0.00% |
|
||||
| X/2 | 0 | 0 | 0 | 0 | +0.00 | +0.00% |
|
||||
|
||||
## Match Detail
|
||||
|
||||
| Date | Match | HT | FT | v25 aggressive | v26 surprise | v26 aggressive | v26 main HTFT |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 2025-09-06 | Forge FC vs Hfx Wan | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Estoril vs Santa Clara | 0-0 | 0-1 | - | - | - | - |
|
||||
| 2025-09-06 | Tenerife vs Merida AD | 1-0 | 3-0 | - | - | - | - |
|
||||
| 2025-09-06 | Ibiza vs Hercules | 2-1 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | V. Sarsfield vs C. Cordoba | 0-0 | 2-0 | 2/1 (LOST, played, -1.00) | - | 1/1 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Botafogo vs Paranaense | 0-1 | 1-3 | - | - | - | - |
|
||||
| 2025-09-06 | San Felipe vs Curico Unido | 2-0 | 4-2 | - | - | - | - |
|
||||
| 2025-09-06 | Nacional Potosi vs Independiente | 0-1 | 0-3 | - | - | - | - |
|
||||
| 2025-09-06 | Avusturya vs G. Kıbrıs Rum Kesimi | 0-0 | 1-0 | X/1 (WON, played, +3.01) | - | - | - |
|
||||
| 2025-09-06 | CD Estepona FS vs Linares | 1-0 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Pergolettese vs Cittadella | 2-1 | 3-1 | - | - | - | - |
|
||||
| 2025-09-06 | Union Brescia vs Pro Vercelli | 2-0 | 5-0 | - | - | - | - |
|
||||
| 2025-09-06 | Casertana vs Potenza | 1-1 | 3-2 | - | - | - | - |
|
||||
| 2025-09-06 | Montevideo vs Danubio | 0-2 | 0-2 | - | - | - | - |
|
||||
| 2025-09-06 | Hoogstraten vs W. Beveren | 0-0 | 1-3 | - | - | - | - |
|
||||
| 2025-09-06 | Cambuur vs Willem II | 0-1 | 2-2 | 2/1 (LOST, played, -1.00) | - | 1/1 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Tlaxcala vs A. Oaxaca | 1-0 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | JS Kabylie vs Olympique Akbou | 0-0 | 0-0 | - | - | - | - |
|
||||
| 2025-09-06 | CD A. Baleares vs Castellon II | 3-0 | 3-0 | - | - | - | - |
|
||||
| 2025-09-06 | Karlovac 1919 vs Dugopolje | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | San Sebastian R. vs RSD Alcala | 1-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Hindistan U23 vs Katar U23 | 0-1 | 1-2 | - | - | - | - |
|
||||
| 2025-09-06 | Estradense vs Boiro | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Yeclano II vs El Palmar | 1-0 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | Montlouis vs La Roche | 0-3 | 1-4 | - | - | - | - |
|
||||
| 2025-09-06 | L'Hospitalet vs San Cristobal | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Marchamalo vs Guadalajara II | 1-1 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Real Zaragoza vs R. Valladolid | 0-0 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Bromley vs Gillingham | 2-0 | 2-2 | 2/1 (LOST, played, -1.00) | 1/2 (LOST, not played, +0.00) | 1/1 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Ajman Club vs Al Wahda | 2-1 | 2-4 | - | - | - | - |
|
||||
| 2025-09-06 | Aldershot vs Brackley | 1-0 | 2-2 | X/2 (LOST, played, -1.00) | - | 2/2 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Barbastro vs UE Olot | 1-0 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | CA Atlanta vs Guemes | 1-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Pulpileno vs UCAM Murcia II | 0-0 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Mojados vs Santa Marta | 0-2 | 1-3 | - | - | - | - |
|
||||
| 2025-09-06 | Ermenistan vs Portekiz | 0-3 | 0-5 | X/2 (LOST, played, -1.00) | - | - | - |
|
||||
| 2025-09-06 | USM Khenchela vs CR Belouizdad | 1-0 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Nafta vs Gorica | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | CS Cerrito vs Atenas | 0-0 | 0-3 | - | - | - | - |
|
||||
| 2025-09-06 | VfB Oldenburg vs Hannoverscher | 4-1 | 6-1 | - | - | - | - |
|
||||
| 2025-09-06 | Lealtad vs Numancia | 1-1 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Astorga vs Real Avila | 0-0 | 1-3 | - | - | - | - |
|
||||
| 2025-09-06 | Grindavik vs IR Reykjavik | 2-1 | 3-1 | - | - | - | - |
|
||||
| 2025-09-06 | Andratx vs Sant Andreu | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Arenas Getxo vs Cacereno | 2-1 | 2-2 | - | - | - | - |
|
||||
| 2025-09-06 | Aegir vs Dalvik / Reynir | 1-1 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | Laval II vs Cesson | 0-0 | 0-1 | - | - | - | - |
|
||||
| 2025-09-06 | Palencia CF vs Arandina | 3-0 | 4-0 | - | - | - | - |
|
||||
| 2025-09-06 | Leioa vs Pasaia | 0-0 | 0-1 | - | - | - | - |
|
||||
| 2025-09-06 | Vic vs UE Cornella | 0-0 | 0-1 | - | - | - | - |
|
||||
| 2025-09-06 | Voltigeurs vs Granville | 1-2 | 3-2 | - | - | - | - |
|
||||
| 2025-09-06 | Bobigny vs Creteil | 0-1 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Beauvais vs Furiani Agliani | 0-0 | 2-2 | - | - | - | - |
|
||||
| 2025-09-06 | Oissel vs Caen II | 0-1 | 0-2 | - | - | - | - |
|
||||
| 2025-09-06 | Cartagena LU II vs Un. Molinense | 0-2 | 1-2 | - | - | - | - |
|
||||
| 2025-09-06 | Hercules II vs Atzeneta | 0-1 | 0-3 | - | - | - | - |
|
||||
| 2025-09-06 | Sassari vs Pianese | 0-0 | 0-2 | - | - | - | - |
|
||||
| 2025-09-06 | Giugliano vs Foggia | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Sorrento vs Trapani 1905 | 0-0 | 1-2 | - | - | - | - |
|
||||
| 2025-09-06 | Ascoli vs Juventus U23 | 0-0 | 0-0 | - | - | - | - |
|
||||
| 2025-09-06 | Arezzo vs Vis Pesaro | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Ath. Carpi vs Campobasso | 0-0 | 2-2 | - | - | - | - |
|
||||
| 2025-09-06 | Oviedo II vs Sarriana | 2-0 | 3-2 | - | - | - | - |
|
||||
| 2025-09-06 | Llosetense vs Platges | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Le Havre (K) vs Strasbourg (K) | 2-2 | 2-2 | - | - | - | - |
|
||||
| 2025-09-06 | Montpellier (K) vs Fleury 91 (K) | 0-1 | 1-2 | - | - | - | - |
|
||||
| 2025-09-06 | Bistrica vs NK Krsko | 3-0 | 6-0 | - | - | - | - |
|
||||
| 2025-09-06 | Bilje vs Dravinja | 1-0 | 2-0 | - | - | - | - |
|
||||
| 2025-09-06 | Nantes (K) vs Saint-Etienne (K) | 2-1 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | Jadran Dekani vs NK Ilirija | 3-1 | 3-1 | - | - | - | - |
|
||||
| 2025-09-06 | Tabor Sezana vs Jesenice | 0-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Alaves II vs Logrones | 0-0 | 2-0 | - | - | - | - |
|
||||
| 2025-09-06 | Unionistas II vs Tordesillas | 1-1 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Leganes II vs Alcorcon II | 0-0 | 0-0 | - | - | - | - |
|
||||
| 2025-09-06 | Volna Pinsk vs Bumprom | 0-1 | 0-1 | - | - | - | - |
|
||||
| 2025-09-06 | L. Mikulas vs S. Bratislava II | 0-0 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | Dubrava vs Hrvace | 0-1 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | Plymouth vs Stockport | 2-1 | 4-2 | - | - | - | - |
|
||||
| 2025-09-06 | Port Vale vs Leyton Orient | 1-2 | 2-3 | X/1 (LOST, played, -1.00) | 1/2 (LOST, not played, +0.00) | 2/2 (WON, played, +3.51) | - |
|
||||
| 2025-09-06 | Huddersfield vs Peterborough | 0-0 | 3-2 | - | - | - | - |
|
||||
| 2025-09-06 | Lincoln City vs Wigan Ath | 2-1 | 2-2 | X/1 (LOST, played, -1.00) | 1/2 (LOST, not played, +0.00) | 1/1 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Wycombe vs Mansfield | 1-0 | 2-0 | X/2 (LOST, played, -1.00) | - | - | 2/2 (LOST, not played, +0.00) |
|
||||
| 2025-09-06 | Rotherham vs Exeter City | 1-0 | 1-0 | X/2 (LOST, played, -1.00) | - | 2/2 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Ballymena Utd vs Glentoran | 0-2 | 0-2 | - | - | - | - |
|
||||
| 2025-09-06 | Crusaders vs Coleraine | 0-3 | 0-4 | X/2 (LOST, played, -1.00) | - | 2/2 (WON, played, +1.32) | - |
|
||||
| 2025-09-06 | Glenavon vs Dungannon | 0-2 | 0-2 | - | - | - | - |
|
||||
| 2025-09-06 | Linfield vs Portadown | 0-0 | 3-0 | 2/1 (LOST, played, -1.00) | - | 1/1 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Colchester vs Crewe | 0-1 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | G. Morton vs Raith Rovers | 0-0 | 0-1 | - | - | - | - |
|
||||
| 2025-09-06 | Puchov vs Pohronie | 0-1 | 3-1 | - | - | - | - |
|
||||
| 2025-09-06 | Carrick vs Cliftonville | 0-1 | 1-2 | 2/1 (LOST, played, -1.00) | 2/1 (LOST, not played, +0.00) | 1/1 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Harrogate vs Crawley Town | 0-1 | 0-1 | - | - | - | - |
|
||||
| 2025-09-06 | Walsall vs Chesterfield | 1-0 | 1-0 | X/2 (LOST, played, -1.00) | - | 2/2 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Banik Lehota vs Samorin | 1-0 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | Bolton vs Wimbledon | 1-0 | 3-0 | X/1 (LOST, played, -1.00) | - | 1/1 (WON, played, +0.81) | - |
|
||||
| 2025-09-06 | Edinburgh vs Hearts II | 4-1 | 4-2 | - | - | - | - |
|
||||
| 2025-09-06 | Kelty Hearts vs Stranraer | 0-0 | 3-0 | - | - | - | - |
|
||||
| 2025-09-06 | Forfar vs Dundee II | 2-1 | 4-1 | - | - | - | - |
|
||||
| 2025-09-06 | Spartans vs Hibernian II | 2-0 | 5-1 | - | - | - | - |
|
||||
| 2025-09-06 | East Kilbride vs Hamilton | 0-2 | 2-4 | - | - | - | - |
|
||||
| 2025-09-06 | Annan Ath vs St. Mirren II | 0-1 | 2-2 | - | - | - | - |
|
||||
| 2025-09-06 | East Fife vs Dumbarton | 1-0 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Clyde vs Motherwell II | 3-0 | 4-0 | - | - | - | - |
|
||||
| 2025-09-06 | Peterhead vs Dundee United II | 2-0 | 3-0 | - | - | - | - |
|
||||
| 2025-09-06 | Montrose vs Aberdeen II | 2-1 | 3-1 | - | - | - | - |
|
||||
| 2025-09-06 | Elgin City vs Cove Rangers | 1-0 | 1-0 | - | - | - | - |
|
||||
| 2025-09-06 | Queen Of S. vs Rangers II | 0-0 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | Boston Utd vs Solihull | 1-1 | 1-2 | - | - | - | - |
|
||||
| 2025-09-06 | Carlisle vs Truro | 2-0 | 3-0 | - | - | - | - |
|
||||
| 2025-09-06 | Southend vs Halifax | 1-0 | 3-0 | - | - | - | - |
|
||||
| 2025-09-06 | Woking vs Gateshead | 2-0 | 5-0 | - | - | - | - |
|
||||
| 2025-09-06 | Tamworth vs Eastleigh | 0-0 | 1-0 | 1/2 (LOST, played, -1.00) | - | 2/2 (LOST, played, -1.00) | - |
|
||||
| 2025-09-06 | Alcorcon vs Teruel | 1-1 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | Merthyr vs Worksop | 0-0 | 2-0 | - | - | - | - |
|
||||
| 2025-09-06 | Chesham Utd vs Bath | 0-0 | 0-0 | - | - | - | - |
|
||||
| 2025-09-06 | Southport vs South Shields | 0-0 | 0-0 | - | - | - | - |
|
||||
| 2025-09-06 | Kidderminster vs Macclesfield | 0-0 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Throttur Vogar vs Höttur / Huginn | 0-0 | 2-1 | - | - | - | - |
|
||||
| 2025-09-06 | Alfreton vs Kings Lynn | 0-0 | 1-1 | - | - | - | - |
|
||||
| 2025-09-06 | Buxton vs Oxford City | 1-0 | 2-1 | - | - | - | - |
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
asyncpg>=0.29.0
|
||||
pydantic>=2.5.0
|
||||
pytest>=8.0.0
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
V27 FINAL BACKTEST — Conservative Flat Bet
|
||||
Only the strongest validated edges. No Kelly compounding.
|
||||
"""
|
||||
import pandas as pd, numpy as np
|
||||
|
||||
df = pd.read_csv('data/training_data_v27.csv', low_memory=False)
|
||||
for c in df.columns:
|
||||
if c not in ['match_id','league_name','home_team','away_team']:
|
||||
df[c] = pd.to_numeric(df[c], errors='coerce')
|
||||
df = df.dropna(subset=['odds_ms_h','odds_ms_d','odds_ms_a'])
|
||||
df = df[(df.odds_ms_h>1.01)&(df.odds_ms_d>1.01)&(df.odds_ms_a>1.01)]
|
||||
|
||||
n = len(df)
|
||||
# 5-fold walk-forward: train on 60%, validate patterns, test on remaining
|
||||
folds = 5
|
||||
fold_size = n // folds
|
||||
all_results = []
|
||||
|
||||
print("="*65)
|
||||
print(" V27 WALK-FORWARD FLAT-BET BACKTEST")
|
||||
print("="*65)
|
||||
|
||||
for fold in range(2, folds): # start from fold 2 so we have enough training data
|
||||
train_end = fold * fold_size
|
||||
test_start = train_end
|
||||
test_end = (fold+1)*fold_size if fold < folds-1 else n
|
||||
|
||||
train_df = df.iloc[:train_end]
|
||||
test_df = df.iloc[test_start:test_end]
|
||||
|
||||
print(f"\n --- Fold {fold}: train={len(train_df)}, test={len(test_df)} ---")
|
||||
|
||||
# Discover REST edges from training data
|
||||
strategies = []
|
||||
|
||||
for hr in [5, 7, 10, 14]:
|
||||
for ar in [3, 4, 5]:
|
||||
for cls, col in [(0,'odds_ms_h'), (2,'odds_ms_a')]:
|
||||
idx = (train_df.home_days_rest > hr) & (train_df.away_days_rest < ar)
|
||||
sub = train_df[idx]
|
||||
if len(sub) < 50:
|
||||
continue
|
||||
rate = (sub.label_ms == cls).mean()
|
||||
avg_odds = sub[col].mean()
|
||||
ev = rate * avg_odds
|
||||
if ev > 1.02: # only strong edges (>2% edge)
|
||||
strategies.append((hr, ar, cls, rate, avg_odds, ev, len(sub)))
|
||||
|
||||
if not strategies:
|
||||
print(" No strong edges found in training data")
|
||||
continue
|
||||
|
||||
# Apply best strategies to test
|
||||
strategies.sort(key=lambda x: x[5], reverse=True)
|
||||
best = strategies[:3] # top 3 only
|
||||
|
||||
fold_bets = 0
|
||||
fold_wins = 0
|
||||
fold_pnl = 0
|
||||
stake = 10 # flat 10 units
|
||||
|
||||
for _, row in test_df.iterrows():
|
||||
for hr, ar, cls, est_p, _, _, _ in best:
|
||||
if pd.isna(row.home_days_rest) or pd.isna(row.away_days_rest):
|
||||
continue
|
||||
if row.home_days_rest <= hr or row.away_days_rest >= ar:
|
||||
continue
|
||||
odds_col = ['odds_ms_h','odds_ms_d','odds_ms_a'][cls]
|
||||
odds_val = row[odds_col]
|
||||
if pd.isna(odds_val) or odds_val < 1.50 or odds_val > 5.0:
|
||||
continue
|
||||
# Additional filter: only bet when odds give reasonable EV
|
||||
if est_p * odds_val < 1.0:
|
||||
continue
|
||||
|
||||
won = (row.label_ms == cls)
|
||||
pnl = stake * (odds_val - 1) if won else -stake
|
||||
fold_bets += 1
|
||||
if won:
|
||||
fold_wins += 1
|
||||
fold_pnl += pnl
|
||||
all_results.append({'fold': fold, 'won': won, 'pnl': pnl,
|
||||
'odds': odds_val, 'stake': stake,
|
||||
'cls': ['H','D','A'][cls]})
|
||||
|
||||
if fold_bets > 0:
|
||||
roi = fold_pnl / (fold_bets * stake) * 100
|
||||
print(f" Best strategies: {[(h,a,['H','D','A'][c],f'EV={e:.3f}') for h,a,c,_,_,e,_ in best]}")
|
||||
print(f" Bets: {fold_bets}, Wins: {fold_wins} ({fold_wins/fold_bets*100:.1f}%), "
|
||||
f"ROI: {roi:+.1f}%, PnL: {fold_pnl:+.0f}")
|
||||
|
||||
# Overall
|
||||
print("\n" + "="*65)
|
||||
print(" OVERALL RESULTS")
|
||||
print("="*65)
|
||||
if all_results:
|
||||
total = len(all_results)
|
||||
wins = sum(1 for r in all_results if r['won'])
|
||||
total_pnl = sum(r['pnl'] for r in all_results)
|
||||
total_staked = sum(r['stake'] for r in all_results)
|
||||
roi = total_pnl / total_staked * 100
|
||||
|
||||
print(f" Total bets: {total}")
|
||||
print(f" Wins: {wins} ({wins/total*100:.1f}%)")
|
||||
print(f" Total staked: {total_staked:.0f}")
|
||||
print(f" PnL: {total_pnl:+.0f}")
|
||||
print(f" ROI: {roi:+.1f}%")
|
||||
print(f" Avg odds: {np.mean([r['odds'] for r in all_results]):.2f}")
|
||||
|
||||
# By class
|
||||
print("\n --- By Bet Type ---")
|
||||
for cls in ['H','A']:
|
||||
cb = [r for r in all_results if r['cls'] == cls]
|
||||
if cb:
|
||||
cw = sum(1 for r in cb if r['won'])
|
||||
cp = sum(r['pnl'] for r in cb)
|
||||
cs = sum(r['stake'] for r in cb)
|
||||
print(f" {cls}: {len(cb)} bets, hit={cw/len(cb)*100:.1f}%, ROI={cp/cs*100:+.1f}%")
|
||||
|
||||
# Cumulative PnL curve
|
||||
print("\n --- Cumulative PnL ---")
|
||||
cum = 0
|
||||
step = max(1, total // 15)
|
||||
for j in range(0, total, step):
|
||||
cum = sum(r['pnl'] for r in all_results[:j+1])
|
||||
print(f" After bet {j+1:4d}: PnL={cum:+.0f}")
|
||||
cum = sum(r['pnl'] for r in all_results)
|
||||
print(f" After bet {total:4d}: PnL={cum:+.0f} (FINAL)")
|
||||
else:
|
||||
print(" No bets placed!")
|
||||
|
||||
# ── Now combine with MODEL for smarter filtering ──
|
||||
print("\n" + "="*65)
|
||||
print(" COMBINED: Rest Rules + Fundamentals Model")
|
||||
print("="*65)
|
||||
|
||||
import pickle, json
|
||||
from pathlib import Path
|
||||
MODELS_DIR = Path("models/v27")
|
||||
|
||||
feat_cols = json.load(open(MODELS_DIR / "v27_feature_cols.json"))
|
||||
ms_models = {}
|
||||
for name in ['xgb','lgb','cb']:
|
||||
p = MODELS_DIR / f"v27_ms_{name}.pkl"
|
||||
if p.exists():
|
||||
with open(p,'rb') as f:
|
||||
ms_models[name] = pickle.load(f)
|
||||
|
||||
if ms_models:
|
||||
test_df = df.iloc[int(n*0.8):].copy()
|
||||
X_test = test_df[feat_cols].values
|
||||
|
||||
# Get model predictions
|
||||
preds = []
|
||||
for name, m in ms_models.items():
|
||||
if name == 'xgb':
|
||||
import xgboost as xgb
|
||||
dm = xgb.DMatrix(X_test, feature_names=feat_cols)
|
||||
preds.append(m.predict(dm))
|
||||
elif name == 'lgb':
|
||||
preds.append(m.predict(X_test))
|
||||
elif name == 'cb':
|
||||
preds.append(m.predict_proba(X_test))
|
||||
model_probs = np.mean(preds, axis=0) # (n, 3)
|
||||
|
||||
# Now apply rest rules + model agreement
|
||||
margin = 1/test_df.odds_ms_h.values + 1/test_df.odds_ms_d.values + 1/test_df.odds_ms_a.values
|
||||
impl = np.column_stack([
|
||||
(1/test_df.odds_ms_h.values)/margin,
|
||||
(1/test_df.odds_ms_d.values)/margin,
|
||||
(1/test_df.odds_ms_a.values)/margin,
|
||||
])
|
||||
|
||||
combo_bets = 0
|
||||
combo_wins = 0
|
||||
combo_pnl = 0
|
||||
|
||||
for j in range(len(test_df)):
|
||||
row = test_df.iloc[j]
|
||||
for hr, ar in [(14,5),(10,5),(7,5),(5,5)]:
|
||||
if pd.isna(row.home_days_rest) or pd.isna(row.away_days_rest):
|
||||
continue
|
||||
if row.home_days_rest <= hr or row.away_days_rest >= ar:
|
||||
continue
|
||||
for cls in [0, 2]:
|
||||
odds_val = [row.odds_ms_h, row.odds_ms_d, row.odds_ms_a][cls]
|
||||
if pd.isna(odds_val) or odds_val < 1.50 or odds_val > 5.0:
|
||||
continue
|
||||
|
||||
model_p = model_probs[j, cls]
|
||||
impl_p = impl[j, cls]
|
||||
|
||||
# DOUBLE FILTER: rest rule + model agrees (model_prob > implied)
|
||||
if model_p <= impl_p:
|
||||
continue # model disagrees, skip
|
||||
edge = model_p - impl_p
|
||||
if edge < 0.03:
|
||||
continue # too small
|
||||
|
||||
won = (row.label_ms == cls)
|
||||
pnl = 10 * (odds_val - 1) if won else -10
|
||||
combo_bets += 1
|
||||
if won:
|
||||
combo_wins += 1
|
||||
combo_pnl += pnl
|
||||
|
||||
if combo_bets > 0:
|
||||
roi = combo_pnl / (combo_bets * 10) * 100
|
||||
print(f" Bets: {combo_bets}")
|
||||
print(f" Wins: {combo_wins} ({combo_wins/combo_bets*100:.1f}%)")
|
||||
print(f" PnL: {combo_pnl:+.0f}")
|
||||
print(f" ROI: {roi:+.1f}%")
|
||||
else:
|
||||
print(" No combined bets triggered")
|
||||
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
|
||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_DIR))
|
||||
|
||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
||||
|
||||
|
||||
def _resolve_dsn() -> str:
|
||||
env_path = AI_ENGINE_DIR / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("DATABASE_URL="):
|
||||
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
||||
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
||||
|
||||
|
||||
def _fetch_matches(dsn: str, limit: int = 60) -> list[str]:
|
||||
query = """
|
||||
SELECT m.id
|
||||
FROM matches m
|
||||
WHERE m.status = 'FT'
|
||||
AND m.sport = 'football'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(query, (limit,))
|
||||
return [str(row["id"]) for row in cur.fetchall()]
|
||||
|
||||
|
||||
def _score_prediction(package: dict) -> dict[str, float]:
|
||||
rows = package.get("bet_summary", []) or []
|
||||
playable = [row for row in rows if row.get("playable")]
|
||||
return {
|
||||
"playable_count": float(len(playable)),
|
||||
"avg_edge": round(
|
||||
sum(float(row.get("ev_edge", 0.0)) for row in playable) / len(playable),
|
||||
4,
|
||||
)
|
||||
if playable
|
||||
else 0.0,
|
||||
"avg_confidence": round(
|
||||
sum(float(row.get("calibrated_confidence", 0.0)) for row in playable)
|
||||
/ len(playable),
|
||||
2,
|
||||
)
|
||||
if playable
|
||||
else 0.0,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dsn = _resolve_dsn()
|
||||
match_ids = _fetch_matches(dsn)
|
||||
orchestrator = SingleMatchOrchestrator()
|
||||
|
||||
results: list[dict[str, object]] = []
|
||||
for match_id in match_ids:
|
||||
orchestrator.engine_mode = "v25"
|
||||
v25 = orchestrator.analyze_match(match_id)
|
||||
orchestrator.engine_mode = "v26"
|
||||
v26 = orchestrator.analyze_match(match_id)
|
||||
if not v25 or not v26:
|
||||
continue
|
||||
results.append(
|
||||
{
|
||||
"match_id": match_id,
|
||||
"v25": _score_prediction(v25),
|
||||
"v26": _score_prediction(v26),
|
||||
"v25_main": (v25.get("main_pick") or {}).get("pick"),
|
||||
"v26_main": (v26.get("main_pick") or {}).get("pick"),
|
||||
}
|
||||
)
|
||||
|
||||
out_path = AI_ENGINE_DIR / "reports" / "backtest_v26_shadow.json"
|
||||
out_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
|
||||
print(f"[OK] Shadow backtest summary written to {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,505 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
|
||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_DIR))
|
||||
|
||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
||||
|
||||
|
||||
STRATEGIES = ("v25_aggressive", "v26_surprise", "v26_aggressive", "v26_main_htft")
|
||||
REVERSAL_LABELS = ("1/2", "2/1", "X/1", "X/2")
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchContext:
|
||||
match_id: str
|
||||
match_date_ms: int
|
||||
league: str
|
||||
home_team: str
|
||||
away_team: str
|
||||
final_home: int
|
||||
final_away: int
|
||||
ht_home: Optional[int]
|
||||
ht_away: Optional[int]
|
||||
|
||||
@property
|
||||
def match_name(self) -> str:
|
||||
return f"{self.home_team} vs {self.away_team}"
|
||||
|
||||
@property
|
||||
def final_score(self) -> str:
|
||||
return f"{self.final_home}-{self.final_away}"
|
||||
|
||||
@property
|
||||
def ht_score(self) -> str:
|
||||
if self.ht_home is None or self.ht_away is None:
|
||||
return "-"
|
||||
return f"{self.ht_home}-{self.ht_away}"
|
||||
|
||||
|
||||
def _resolve_dsn() -> str:
|
||||
env_path = AI_ENGINE_DIR / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("DATABASE_URL="):
|
||||
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
||||
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
||||
|
||||
|
||||
def _fetch_matches(dsn: str, limit: int) -> list[MatchContext]:
|
||||
query = """
|
||||
SELECT
|
||||
m.id,
|
||||
m.mst_utc,
|
||||
COALESCE(l.name, 'Unknown League') AS league,
|
||||
COALESCE(ht.name, 'Home') AS home_team,
|
||||
COALESCE(at.name, 'Away') AS away_team,
|
||||
COALESCE(m.score_home, 0) AS score_home,
|
||||
COALESCE(m.score_away, 0) AS score_away,
|
||||
m.ht_score_home,
|
||||
m.ht_score_away
|
||||
FROM matches m
|
||||
LEFT JOIN leagues l ON l.id = m.league_id
|
||||
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
||||
LEFT JOIN teams at ON at.id = m.away_team_id
|
||||
WHERE m.status = 'FT'
|
||||
AND m.sport = 'football'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
AND m.ht_score_home IS NOT NULL
|
||||
AND m.ht_score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(query, (limit,))
|
||||
rows = cur.fetchall()
|
||||
return [
|
||||
MatchContext(
|
||||
match_id=str(row["id"]),
|
||||
match_date_ms=int(row["mst_utc"] or 0),
|
||||
league=str(row["league"] or "Unknown League"),
|
||||
home_team=str(row["home_team"] or "Home"),
|
||||
away_team=str(row["away_team"] or "Away"),
|
||||
final_home=int(row["score_home"] or 0),
|
||||
final_away=int(row["score_away"] or 0),
|
||||
ht_home=int(row["ht_score_home"]) if row.get("ht_score_home") is not None else None,
|
||||
ht_away=int(row["ht_score_away"]) if row.get("ht_score_away") is not None else None,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def _safe_float(value: Any) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _outcome_symbol(home: int, away: int) -> str:
|
||||
if home > away:
|
||||
return "1"
|
||||
if home < away:
|
||||
return "2"
|
||||
return "X"
|
||||
|
||||
|
||||
def _resolve_htft(pick: str, context: MatchContext) -> Dict[str, Any]:
|
||||
if not pick or "/" not in str(pick):
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "htft_pick_invalid"}
|
||||
actual = f"{_outcome_symbol(context.ht_home or 0, context.ht_away or 0)}/{_outcome_symbol(context.final_home, context.final_away)}"
|
||||
won = str(pick).strip().upper() == actual
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
|
||||
def _market_odds(odds: Dict[str, Any], market: str, pick: str) -> float:
|
||||
mapping = {
|
||||
"HTFT": {
|
||||
"1/1": "htft_11",
|
||||
"1/X": "htft_1x",
|
||||
"1/2": "htft_12",
|
||||
"X/1": "htft_x1",
|
||||
"X/X": "htft_xx",
|
||||
"X/2": "htft_x2",
|
||||
"2/1": "htft_21",
|
||||
"2/X": "htft_2x",
|
||||
"2/2": "htft_22",
|
||||
},
|
||||
"MS": {"1": "ms_h", "X": "ms_d", "2": "ms_a"},
|
||||
}
|
||||
key = mapping.get(market, {}).get(str(pick))
|
||||
if not key:
|
||||
return 0.0
|
||||
value = _safe_float((odds or {}).get(key))
|
||||
return value if value > 1.0 else 0.0
|
||||
|
||||
|
||||
def _evaluate_pick(
|
||||
*,
|
||||
strategy: str,
|
||||
market: str,
|
||||
pick: str,
|
||||
odds: Any,
|
||||
playable: bool,
|
||||
confidence: Any,
|
||||
extra: Optional[Dict[str, Any]],
|
||||
context: MatchContext,
|
||||
) -> Dict[str, Any]:
|
||||
odds_value = _safe_float(odds)
|
||||
if market == "HT/FT":
|
||||
market = "HTFT"
|
||||
resolution = _resolve_htft(pick, context) if market == "HTFT" else {
|
||||
"result": "UNRESOLVED",
|
||||
"won": None,
|
||||
"note": "non_htft_market",
|
||||
}
|
||||
counted = bool(playable and market == "HTFT" and odds_value > 1.01 and resolution["result"] in {"WON", "LOST"})
|
||||
profit = 0.0
|
||||
if counted:
|
||||
profit = (odds_value - 1.0) if resolution["result"] == "WON" else -1.0
|
||||
row = {
|
||||
"strategy": strategy,
|
||||
"market": market,
|
||||
"pick": pick,
|
||||
"odds": round(odds_value, 2),
|
||||
"playable": playable,
|
||||
"confidence": round(_safe_float(confidence), 1),
|
||||
"result": resolution["result"],
|
||||
"counted_in_roi": counted,
|
||||
"profit_flat": round(profit, 4),
|
||||
"resolution_note": resolution["note"],
|
||||
}
|
||||
if extra:
|
||||
row.update(extra)
|
||||
return row
|
||||
|
||||
|
||||
def _extract_strategy_rows(
|
||||
*,
|
||||
context: MatchContext,
|
||||
odds_data: Dict[str, Any],
|
||||
v25: Dict[str, Any],
|
||||
v26: Dict[str, Any],
|
||||
) -> Dict[str, Optional[Dict[str, Any]]]:
|
||||
strategies: Dict[str, Optional[Dict[str, Any]]] = {name: None for name in STRATEGIES}
|
||||
|
||||
v25_aggressive = v25.get("aggressive_pick") or {}
|
||||
if v25_aggressive.get("pick"):
|
||||
pick = str(v25_aggressive.get("pick"))
|
||||
strategies["v25_aggressive"] = _evaluate_pick(
|
||||
strategy="v25_aggressive",
|
||||
market=str(v25_aggressive.get("market") or "HTFT"),
|
||||
pick=pick,
|
||||
odds=_market_odds(odds_data, "HTFT", pick),
|
||||
playable=True,
|
||||
confidence=v25_aggressive.get("confidence"),
|
||||
extra={
|
||||
"source": "v25.aggressive_pick",
|
||||
"reversal_pick": pick,
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
v26_surprise = v26.get("surprise_pick") or {}
|
||||
v26_hunter = v26.get("surprise_hunter") or {}
|
||||
if v26_surprise.get("pick"):
|
||||
pick = str(v26_surprise.get("raw_pick") or v26_surprise.get("pick"))
|
||||
strategies["v26_surprise"] = _evaluate_pick(
|
||||
strategy="v26_surprise",
|
||||
market=str(v26_surprise.get("market") or "HTFT"),
|
||||
pick=pick,
|
||||
odds=v26_surprise.get("odds") or _market_odds(odds_data, "HTFT", pick),
|
||||
playable=bool(v26_surprise.get("playable")),
|
||||
confidence=v26_surprise.get("calibrated_confidence", v26_surprise.get("confidence")),
|
||||
extra={
|
||||
"source": "v26.surprise_pick",
|
||||
"surprise_score": round(_safe_float(v26_surprise.get("surprise_score")), 1),
|
||||
"support_score": round(_safe_float(v26_surprise.get("support_score")), 1),
|
||||
"reversal_pick": v26_hunter.get("reversal_pick"),
|
||||
"reversal_prob": round(_safe_float(v26_hunter.get("reversal_prob")), 4),
|
||||
"favorite_gap": round(_safe_float(v26_hunter.get("favorite_gap")), 3),
|
||||
"favorite_odd": round(_safe_float(v26_hunter.get("favorite_odd")), 2),
|
||||
"odds_band_score": round(_safe_float(v26_hunter.get("odds_band_score")), 3),
|
||||
"odds_band_label": str(v26_hunter.get("odds_band_label") or ""),
|
||||
"league_reversal_rate": round(_safe_float(v26_hunter.get("league_reversal_rate")), 4),
|
||||
"league_strict_rev_rate": round(_safe_float(v26_hunter.get("league_strict_rev_rate")), 4),
|
||||
"referee_strict_rev_rate": round(_safe_float(v26_hunter.get("referee_strict_rev_rate")), 4),
|
||||
"reason_codes": ",".join(v26_hunter.get("reason_codes", [])),
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
v26_aggressive = v26.get("aggressive_pick") or {}
|
||||
if v26_aggressive.get("pick"):
|
||||
pick = str(v26_aggressive.get("pick"))
|
||||
strategies["v26_aggressive"] = _evaluate_pick(
|
||||
strategy="v26_aggressive",
|
||||
market=str(v26_aggressive.get("market") or "HTFT"),
|
||||
pick=pick,
|
||||
odds=v26_aggressive.get("odds") or _market_odds(odds_data, "HTFT", pick),
|
||||
playable=True,
|
||||
confidence=v26_aggressive.get("confidence"),
|
||||
extra={
|
||||
"source": "v26.aggressive_pick",
|
||||
"reversal_pick": pick,
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
v26_main = v26.get("main_pick") or {}
|
||||
if str(v26_main.get("market") or "") == "HTFT" and v26_main.get("pick"):
|
||||
pick = str(v26_main.get("raw_pick") or v26_main.get("pick"))
|
||||
strategies["v26_main_htft"] = _evaluate_pick(
|
||||
strategy="v26_main_htft",
|
||||
market="HTFT",
|
||||
pick=pick,
|
||||
odds=v26_main.get("odds") or _market_odds(odds_data, "HTFT", pick),
|
||||
playable=bool(v26_main.get("playable")),
|
||||
confidence=v26_main.get("calibrated_confidence", v26_main.get("confidence")),
|
||||
extra={
|
||||
"source": "v26.main_pick",
|
||||
"pick_reason": v26_main.get("pick_reason"),
|
||||
"surprise_score": round(_safe_float(v26_main.get("surprise_score")), 1),
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
return strategies
|
||||
|
||||
|
||||
def _summarize_bucket(bucket: Dict[str, float]) -> Dict[str, Any]:
|
||||
played = int(bucket["played"])
|
||||
won = int(bucket["won"])
|
||||
lost = int(bucket["lost"])
|
||||
candidate = int(bucket["candidate"])
|
||||
profit = round(bucket["profit"], 4)
|
||||
roi = round((profit / played) * 100.0, 2) if played else 0.0
|
||||
hit = round((won / played) * 100.0, 2) if played else 0.0
|
||||
return {
|
||||
"candidates": candidate,
|
||||
"played": played,
|
||||
"won": won,
|
||||
"lost": lost,
|
||||
"profit_flat": profit,
|
||||
"roi_flat_pct": roi,
|
||||
"hit_rate_pct": hit,
|
||||
}
|
||||
|
||||
|
||||
def _format_date(ms: int) -> str:
|
||||
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _build_markdown(report: Dict[str, Any]) -> str:
|
||||
lines: list[str] = []
|
||||
lines.append("# HT/FT + Upset Backtest")
|
||||
lines.append("")
|
||||
lines.append(f"- Sample: last {report['sample_size']} finished football matches")
|
||||
lines.append("- Scope: only HT/FT reversal and upset-oriented picks")
|
||||
lines.append("- ROI: flat `1 unit` per played pick")
|
||||
lines.append(f"- Generated at: {report['generated_at']}")
|
||||
lines.append("")
|
||||
lines.append("## Strategy Summary")
|
||||
lines.append("")
|
||||
lines.append("| Strategy | Candidates | Played | Won | Lost | Hit Rate | Profit | ROI |")
|
||||
lines.append("|---|---:|---:|---:|---:|---:|---:|---:|")
|
||||
for strategy in STRATEGIES:
|
||||
payload = report["summary"]["strategies"][strategy]
|
||||
lines.append(
|
||||
f"| {strategy} | {payload['candidates']} | {payload['played']} | {payload['won']} | "
|
||||
f"{payload['lost']} | {payload['hit_rate_pct']}% | {payload['profit_flat']:+.2f} | {payload['roi_flat_pct']:+.2f}% |"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## v26 Surprise By Reversal Type")
|
||||
lines.append("")
|
||||
lines.append("| Reversal | Candidates | Played | Won | Lost | Profit | ROI |")
|
||||
lines.append("|---|---:|---:|---:|---:|---:|---:|")
|
||||
for reversal, payload in report["summary"]["v26_surprise_by_pick"].items():
|
||||
lines.append(
|
||||
f"| {reversal} | {payload['candidates']} | {payload['played']} | {payload['won']} | "
|
||||
f"{payload['lost']} | {payload['profit_flat']:+.2f} | {payload['roi_flat_pct']:+.2f}% |"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## Match Detail")
|
||||
lines.append("")
|
||||
lines.append("| Date | Match | HT | FT | v25 aggressive | v26 surprise | v26 aggressive | v26 main HTFT |")
|
||||
lines.append("|---|---|---|---|---|---|---|---|")
|
||||
for match in report["matches"]:
|
||||
lines.append(
|
||||
f"| {_format_date(match['match_date_ms'])} | {match['match_name']} | {match['ht_score']} | {match['final_score']} | "
|
||||
f"{match['v25_aggressive']} | {match['v26_surprise']} | {match['v26_aggressive']} | {match['v26_main_htft']} |"
|
||||
)
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="HT/FT + upset focused backtest.")
|
||||
parser.add_argument("--limit", type=int, default=120, help="Number of finished matches to analyze.")
|
||||
args = parser.parse_args()
|
||||
|
||||
dsn = _resolve_dsn()
|
||||
orchestrator = SingleMatchOrchestrator()
|
||||
matches = _fetch_matches(dsn, max(1, args.limit))
|
||||
|
||||
strategy_buckets: Dict[str, Dict[str, float]] = {name: defaultdict(float) for name in STRATEGIES}
|
||||
v26_reversal_buckets: Dict[str, Dict[str, float]] = {label: defaultdict(float) for label in REVERSAL_LABELS}
|
||||
report_matches: list[Dict[str, Any]] = []
|
||||
csv_rows: list[Dict[str, Any]] = []
|
||||
|
||||
for context in matches:
|
||||
data = orchestrator._load_match_data(context.match_id) # noqa: SLF001
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
orchestrator.engine_mode = "v25"
|
||||
v25 = orchestrator.analyze_match(context.match_id) or {}
|
||||
orchestrator.engine_mode = "v26"
|
||||
v26 = orchestrator.analyze_match(context.match_id) or {}
|
||||
|
||||
extracted = _extract_strategy_rows(
|
||||
context=context,
|
||||
odds_data=data.odds_data or {},
|
||||
v25=v25,
|
||||
v26=v26,
|
||||
)
|
||||
|
||||
match_row: Dict[str, Any] = {
|
||||
"match_id": context.match_id,
|
||||
"match_name": context.match_name,
|
||||
"league": context.league,
|
||||
"match_date_ms": context.match_date_ms,
|
||||
"ht_score": context.ht_score,
|
||||
"final_score": context.final_score,
|
||||
}
|
||||
|
||||
for strategy, payload in extracted.items():
|
||||
if payload:
|
||||
strategy_buckets[strategy]["candidate"] += 1
|
||||
if payload["counted_in_roi"]:
|
||||
strategy_buckets[strategy]["played"] += 1
|
||||
if payload["result"] == "WON":
|
||||
strategy_buckets[strategy]["won"] += 1
|
||||
else:
|
||||
strategy_buckets[strategy]["lost"] += 1
|
||||
strategy_buckets[strategy]["profit"] += payload["profit_flat"]
|
||||
|
||||
if strategy == "v26_surprise":
|
||||
reversal_label = str(payload.get("reversal_pick") or "")
|
||||
if reversal_label in v26_reversal_buckets:
|
||||
v26_reversal_buckets[reversal_label]["candidate"] += 1
|
||||
if payload["counted_in_roi"]:
|
||||
v26_reversal_buckets[reversal_label]["played"] += 1
|
||||
if payload["result"] == "WON":
|
||||
v26_reversal_buckets[reversal_label]["won"] += 1
|
||||
else:
|
||||
v26_reversal_buckets[reversal_label]["lost"] += 1
|
||||
v26_reversal_buckets[reversal_label]["profit"] += payload["profit_flat"]
|
||||
|
||||
summary = (
|
||||
f"{payload['pick']} ({payload['result']}, {'played' if payload['counted_in_roi'] else 'not played'}, {payload['profit_flat']:+.2f})"
|
||||
)
|
||||
match_row[strategy] = summary
|
||||
|
||||
csv_rows.append(
|
||||
{
|
||||
"match_id": context.match_id,
|
||||
"date": _format_date(context.match_date_ms),
|
||||
"league": context.league,
|
||||
"match": context.match_name,
|
||||
"ht_score": context.ht_score,
|
||||
"final_score": context.final_score,
|
||||
**payload,
|
||||
}
|
||||
)
|
||||
else:
|
||||
match_row[strategy] = "-"
|
||||
|
||||
report_matches.append(match_row)
|
||||
|
||||
report = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"sample_size": len(report_matches),
|
||||
"summary": {
|
||||
"strategies": {
|
||||
strategy: _summarize_bucket(bucket)
|
||||
for strategy, bucket in strategy_buckets.items()
|
||||
},
|
||||
"v26_surprise_by_pick": {
|
||||
label: _summarize_bucket(bucket)
|
||||
for label, bucket in v26_reversal_buckets.items()
|
||||
},
|
||||
},
|
||||
"matches": report_matches,
|
||||
}
|
||||
|
||||
report_dir = AI_ENGINE_DIR / "reports"
|
||||
json_path = report_dir / "backtest_v26_shadow_htft_upset.json"
|
||||
csv_path = report_dir / "backtest_v26_shadow_htft_upset.csv"
|
||||
md_path = report_dir / "backtest_v26_shadow_htft_upset.md"
|
||||
|
||||
json_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
with csv_path.open("w", encoding="utf-8", newline="") as handle:
|
||||
writer = csv.DictWriter(
|
||||
handle,
|
||||
fieldnames=[
|
||||
"match_id",
|
||||
"date",
|
||||
"league",
|
||||
"match",
|
||||
"ht_score",
|
||||
"final_score",
|
||||
"strategy",
|
||||
"market",
|
||||
"pick",
|
||||
"odds",
|
||||
"playable",
|
||||
"confidence",
|
||||
"result",
|
||||
"counted_in_roi",
|
||||
"profit_flat",
|
||||
"resolution_note",
|
||||
"source",
|
||||
"reversal_pick",
|
||||
"reversal_prob",
|
||||
"favorite_gap",
|
||||
"favorite_odd",
|
||||
"support_score",
|
||||
"odds_band_score",
|
||||
"odds_band_label",
|
||||
"league_reversal_rate",
|
||||
"league_strict_rev_rate",
|
||||
"referee_strict_rev_rate",
|
||||
"surprise_score",
|
||||
"reason_codes",
|
||||
"pick_reason",
|
||||
],
|
||||
)
|
||||
writer.writeheader()
|
||||
writer.writerows(csv_rows)
|
||||
md_path.write_text(_build_markdown(report), encoding="utf-8")
|
||||
|
||||
print(f"[OK] JSON report written to {json_path}")
|
||||
print(f"[OK] CSV report written to {csv_path}")
|
||||
print(f"[OK] Markdown report written to {md_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,810 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
|
||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_DIR))
|
||||
|
||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
||||
from utils.top_leagues import load_top_league_ids
|
||||
|
||||
|
||||
MARKET_ORDER = [
|
||||
"MS",
|
||||
"DC",
|
||||
"OU15",
|
||||
"OU25",
|
||||
"OU35",
|
||||
"BTTS",
|
||||
"HT",
|
||||
"HT_OU05",
|
||||
"HT_OU15",
|
||||
"HTFT",
|
||||
"OE",
|
||||
"CARDS",
|
||||
"HCAP",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchContext:
|
||||
match_id: str
|
||||
match_date_ms: int
|
||||
league_id: Optional[str]
|
||||
league: str
|
||||
home_team: str
|
||||
away_team: str
|
||||
final_home: int
|
||||
final_away: int
|
||||
ht_home: Optional[int]
|
||||
ht_away: Optional[int]
|
||||
total_cards: Optional[float]
|
||||
|
||||
@property
|
||||
def match_name(self) -> str:
|
||||
return f"{self.home_team} vs {self.away_team}"
|
||||
|
||||
@property
|
||||
def final_score(self) -> str:
|
||||
return f"{self.final_home}-{self.final_away}"
|
||||
|
||||
@property
|
||||
def ht_score(self) -> Optional[str]:
|
||||
if self.ht_home is None or self.ht_away is None:
|
||||
return None
|
||||
return f"{self.ht_home}-{self.ht_away}"
|
||||
|
||||
@property
|
||||
def total_goals(self) -> int:
|
||||
return self.final_home + self.final_away
|
||||
|
||||
@property
|
||||
def total_ht_goals(self) -> Optional[int]:
|
||||
if self.ht_home is None or self.ht_away is None:
|
||||
return None
|
||||
return self.ht_home + self.ht_away
|
||||
|
||||
|
||||
def _resolve_dsn() -> str:
|
||||
env_path = AI_ENGINE_DIR / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("DATABASE_URL="):
|
||||
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
||||
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
||||
|
||||
|
||||
def _fetch_matches(
|
||||
dsn: str,
|
||||
limit: int,
|
||||
top_league_ids: Optional[list[str]] = None,
|
||||
) -> list[MatchContext]:
|
||||
query = """
|
||||
SELECT
|
||||
m.id,
|
||||
m.mst_utc,
|
||||
m.league_id,
|
||||
COALESCE(l.name, 'Unknown League') AS league,
|
||||
COALESCE(ht.name, 'Home') AS home_team,
|
||||
COALESCE(at.name, 'Away') AS away_team,
|
||||
COALESCE(m.score_home, 0) AS score_home,
|
||||
COALESCE(m.score_away, 0) AS score_away,
|
||||
m.ht_score_home,
|
||||
m.ht_score_away,
|
||||
cards.total_cards
|
||||
FROM matches m
|
||||
LEFT JOIN leagues l ON l.id = m.league_id
|
||||
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
||||
LEFT JOIN teams at ON at.id = m.away_team_id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
mpe.match_id,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN mpe.event_type::text LIKE '%%yellow_card%%' THEN 1
|
||||
WHEN mpe.event_type::text LIKE '%%red_card%%' THEN 2
|
||||
ELSE 1
|
||||
END
|
||||
)::float AS total_cards
|
||||
FROM match_player_events mpe
|
||||
WHERE mpe.event_type::text LIKE '%%card%%'
|
||||
GROUP BY mpe.match_id
|
||||
) cards ON cards.match_id = m.id
|
||||
WHERE m.status = 'FT'
|
||||
AND m.sport = 'football'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
"""
|
||||
params: list[Any] = []
|
||||
if top_league_ids:
|
||||
query += " AND m.league_id = ANY(%s)"
|
||||
params.append(top_league_ids)
|
||||
query += """
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
params.append(limit)
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(query, params)
|
||||
rows = cur.fetchall()
|
||||
|
||||
results: list[MatchContext] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
MatchContext(
|
||||
match_id=str(row["id"]),
|
||||
match_date_ms=int(row["mst_utc"] or 0),
|
||||
league_id=str(row["league_id"]) if row.get("league_id") else None,
|
||||
league=str(row["league"] or "Unknown League"),
|
||||
home_team=str(row["home_team"] or "Home"),
|
||||
away_team=str(row["away_team"] or "Away"),
|
||||
final_home=int(row["score_home"] or 0),
|
||||
final_away=int(row["score_away"] or 0),
|
||||
ht_home=(
|
||||
int(row["ht_score_home"])
|
||||
if row.get("ht_score_home") is not None
|
||||
else None
|
||||
),
|
||||
ht_away=(
|
||||
int(row["ht_score_away"])
|
||||
if row.get("ht_score_away") is not None
|
||||
else None
|
||||
),
|
||||
total_cards=(
|
||||
float(row["total_cards"])
|
||||
if row.get("total_cards") is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _odds_band(odds: float) -> str:
|
||||
if odds < 1.5:
|
||||
return "<1.50"
|
||||
if odds < 1.8:
|
||||
return "1.50-1.79"
|
||||
if odds < 2.1:
|
||||
return "1.80-2.09"
|
||||
if odds < 2.5:
|
||||
return "2.10-2.49"
|
||||
return "2.50+"
|
||||
|
||||
|
||||
def _confidence_band(confidence: float) -> str:
|
||||
if confidence < 55.0:
|
||||
return "<55"
|
||||
if confidence < 65.0:
|
||||
return "55-64.9"
|
||||
if confidence < 75.0:
|
||||
return "65-74.9"
|
||||
return "75+"
|
||||
|
||||
|
||||
def _edge_band(edge: float) -> str:
|
||||
if edge < 0.03:
|
||||
return "<0.03"
|
||||
if edge < 0.06:
|
||||
return "0.03-0.059"
|
||||
if edge < 0.10:
|
||||
return "0.06-0.099"
|
||||
return "0.10+"
|
||||
|
||||
|
||||
def _top_n_buckets(rows: Iterable[tuple[str, float]], limit: int = 10) -> list[dict[str, Any]]:
|
||||
ranked = sorted(rows, key=lambda item: (-item[1], item[0]))
|
||||
return [
|
||||
{"label": label, "count": int(count)}
|
||||
for label, count in ranked[:limit]
|
||||
]
|
||||
|
||||
|
||||
def _summarize_v26_losses(csv_rows: list[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
losses = [
|
||||
row for row in csv_rows
|
||||
if row.get("model") == "v26.shadow"
|
||||
and bool(row.get("counted_in_roi"))
|
||||
and row.get("result") == "LOST"
|
||||
]
|
||||
by_market: Dict[str, float] = defaultdict(float)
|
||||
by_league: Dict[str, float] = defaultdict(float)
|
||||
by_pick: Dict[str, float] = defaultdict(float)
|
||||
by_odds_band: Dict[str, float] = defaultdict(float)
|
||||
by_conf_band: Dict[str, float] = defaultdict(float)
|
||||
by_edge_band: Dict[str, float] = defaultdict(float)
|
||||
|
||||
for row in losses:
|
||||
market = str(row.get("market") or "UNKNOWN")
|
||||
league = str(row.get("league") or "Unknown League")
|
||||
pick = str(row.get("pick") or "")
|
||||
odds = _safe_float(row.get("odds"))
|
||||
confidence = _safe_float(row.get("confidence"))
|
||||
edge = _safe_float(row.get("edge"))
|
||||
|
||||
by_market[market] += 1
|
||||
by_league[league] += 1
|
||||
by_pick[f"{market} {pick}".strip()] += 1
|
||||
by_odds_band[_odds_band(odds)] += 1
|
||||
by_conf_band[_confidence_band(confidence)] += 1
|
||||
by_edge_band[_edge_band(edge)] += 1
|
||||
|
||||
return {
|
||||
"lost_bets": len(losses),
|
||||
"by_market": _top_n_buckets(by_market.items(), limit=20),
|
||||
"by_league": _top_n_buckets(by_league.items(), limit=15),
|
||||
"by_pick": _top_n_buckets(by_pick.items(), limit=15),
|
||||
"by_odds_band": _top_n_buckets(by_odds_band.items(), limit=10),
|
||||
"by_confidence_band": _top_n_buckets(by_conf_band.items(), limit=10),
|
||||
"by_edge_band": _top_n_buckets(by_edge_band.items(), limit=10),
|
||||
}
|
||||
|
||||
|
||||
def _safe_float(value: Any) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _normalize_text(value: Any) -> str:
|
||||
text = str(value or "").strip().upper()
|
||||
return (
|
||||
text.replace("İ", "I")
|
||||
.replace("İ", "I")
|
||||
.replace("Ş", "S")
|
||||
.replace("Ğ", "G")
|
||||
.replace("Ü", "U")
|
||||
.replace("Ö", "O")
|
||||
.replace("Ç", "C")
|
||||
)
|
||||
|
||||
|
||||
def _outcome_symbol(home: int, away: int) -> str:
|
||||
if home > away:
|
||||
return "1"
|
||||
if home < away:
|
||||
return "2"
|
||||
return "X"
|
||||
|
||||
|
||||
def _resolve_pick(
|
||||
market: str,
|
||||
pick: str,
|
||||
context: MatchContext,
|
||||
) -> Dict[str, Any]:
|
||||
market_code = _normalize_text(market).replace("/", "")
|
||||
pick_text = str(pick or "").strip()
|
||||
pick_norm = _normalize_text(pick_text)
|
||||
|
||||
if not market_code or not pick_norm:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "pick_missing"}
|
||||
|
||||
if market_code == "HTFT":
|
||||
market_code = "HTFT"
|
||||
if market_code == "HTFT" or market_code == "HTFT":
|
||||
if context.ht_home is None or context.ht_away is None:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "ht_score_missing"}
|
||||
if "/" not in pick_text:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "htft_pick_invalid"}
|
||||
ht_pick, ft_pick = pick_text.split("/", 1)
|
||||
actual = f"{_outcome_symbol(context.ht_home, context.ht_away)}/{_outcome_symbol(context.final_home, context.final_away)}"
|
||||
won = f"{_normalize_text(ht_pick)}/{_normalize_text(ft_pick)}" == actual
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code == "MS":
|
||||
actual = _outcome_symbol(context.final_home, context.final_away)
|
||||
won = pick_norm in {actual, f"MS {actual}"}
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code == "DC":
|
||||
actual = _outcome_symbol(context.final_home, context.final_away)
|
||||
winning = {
|
||||
"1X": {"1", "X"},
|
||||
"X2": {"X", "2"},
|
||||
"12": {"1", "2"},
|
||||
}
|
||||
won = actual in winning.get(pick_norm, set())
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code in {"OU15", "OU25", "OU35", "HTOU05", "HTOU15", "HT_OU05", "HT_OU15"}:
|
||||
if market_code in {"HTOU05", "HTOU15", "HT_OU05", "HT_OU15"}:
|
||||
if context.total_ht_goals is None:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "ht_score_missing"}
|
||||
total = context.total_ht_goals
|
||||
line = 0.5 if "05" in market_code else 1.5
|
||||
else:
|
||||
total = context.total_goals
|
||||
line = {"OU15": 1.5, "OU25": 2.5, "OU35": 3.5}[market_code]
|
||||
|
||||
if "UST" in pick_norm or "OVER" in pick_norm:
|
||||
won = total > line
|
||||
side = "OVER"
|
||||
elif "ALT" in pick_norm or "UNDER" in pick_norm:
|
||||
won = total < line
|
||||
side = "UNDER"
|
||||
else:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "ou_side_unknown"}
|
||||
return {
|
||||
"result": "WON" if won else "LOST",
|
||||
"won": won,
|
||||
"note": f"actual_total={total} side={side} line={line}",
|
||||
}
|
||||
|
||||
if market_code == "BTTS":
|
||||
both_scored = context.final_home > 0 and context.final_away > 0
|
||||
if "VAR" in pick_norm or "YES" in pick_norm:
|
||||
won = both_scored
|
||||
side = "YES"
|
||||
elif "YOK" in pick_norm or pick_norm.endswith("NO") or pick_norm == "NO":
|
||||
won = not both_scored
|
||||
side = "NO"
|
||||
else:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "btts_side_unknown"}
|
||||
return {
|
||||
"result": "WON" if won else "LOST",
|
||||
"won": won,
|
||||
"note": f"actual_btts={'YES' if both_scored else 'NO'} side={side}",
|
||||
}
|
||||
|
||||
if market_code == "HT":
|
||||
if context.ht_home is None or context.ht_away is None:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "ht_score_missing"}
|
||||
actual = _outcome_symbol(context.ht_home, context.ht_away)
|
||||
won = pick_norm == actual
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code == "OE":
|
||||
actual = "EVEN" if context.total_goals % 2 == 0 else "ODD"
|
||||
if pick_norm in {"CIFT", "EVEN"}:
|
||||
wanted = "EVEN"
|
||||
elif pick_norm in {"TEK", "ODD"}:
|
||||
wanted = "ODD"
|
||||
else:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "oe_pick_unknown"}
|
||||
won = actual == wanted
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code == "CARDS":
|
||||
if context.total_cards is None:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "cards_missing"}
|
||||
if "UST" in pick_norm or "OVER" in pick_norm:
|
||||
won = context.total_cards > 4.5
|
||||
side = "OVER"
|
||||
elif "ALT" in pick_norm or "UNDER" in pick_norm:
|
||||
won = context.total_cards < 4.5
|
||||
side = "UNDER"
|
||||
else:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "cards_side_unknown"}
|
||||
return {
|
||||
"result": "WON" if won else "LOST",
|
||||
"won": won,
|
||||
"note": f"actual_cards={context.total_cards:.1f} side={side} line=4.5",
|
||||
}
|
||||
|
||||
if market_code == "HCAP":
|
||||
adjusted_home = context.final_home - 1.0
|
||||
adjusted_away = float(context.final_away)
|
||||
if adjusted_home > adjusted_away:
|
||||
actual = "1"
|
||||
elif adjusted_home < adjusted_away:
|
||||
actual = "2"
|
||||
else:
|
||||
actual = "X"
|
||||
won = pick_norm == actual
|
||||
return {
|
||||
"result": "WON" if won else "LOST",
|
||||
"won": won,
|
||||
"note": f"actual={actual} line_home=-1.0",
|
||||
}
|
||||
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "market_not_supported"}
|
||||
|
||||
|
||||
def _evaluate_row(
|
||||
market: str,
|
||||
pick: str,
|
||||
odds: Any,
|
||||
playable: bool,
|
||||
stake_units: Any,
|
||||
context: MatchContext,
|
||||
) -> Dict[str, Any]:
|
||||
resolution = _resolve_pick(market, pick, context)
|
||||
odds_value = _safe_float(odds)
|
||||
stake_value = _safe_float(stake_units)
|
||||
counted = bool(playable and odds_value > 1.01 and resolution["result"] in {"WON", "LOST"})
|
||||
|
||||
flat_profit = 0.0
|
||||
stake_profit = 0.0
|
||||
if counted:
|
||||
flat_profit = (odds_value - 1.0) if resolution["result"] == "WON" else -1.0
|
||||
stake_profit = flat_profit * (stake_value if stake_value > 0 else 1.0)
|
||||
|
||||
return {
|
||||
"result": resolution["result"],
|
||||
"won": resolution["won"],
|
||||
"resolution_note": resolution["note"],
|
||||
"counted_in_roi": counted,
|
||||
"profit_flat": round(flat_profit, 4),
|
||||
"profit_stake": round(stake_profit, 4),
|
||||
}
|
||||
|
||||
|
||||
def _summarize_bucket(bucket: Dict[str, float]) -> Dict[str, Any]:
|
||||
played = int(bucket["played"])
|
||||
won = int(bucket["won"])
|
||||
lost = int(bucket["lost"])
|
||||
unresolved = int(bucket["unresolved"])
|
||||
profit = round(bucket["profit"], 4)
|
||||
roi = round((profit / played) * 100.0, 2) if played else 0.0
|
||||
win_rate = round((won / played) * 100.0, 2) if played else 0.0
|
||||
return {
|
||||
"played": played,
|
||||
"won": won,
|
||||
"lost": lost,
|
||||
"unresolved": unresolved,
|
||||
"profit_flat": profit,
|
||||
"roi_flat_pct": roi,
|
||||
"win_rate_pct": win_rate,
|
||||
}
|
||||
|
||||
|
||||
def _format_date(ms: int) -> str:
|
||||
if ms <= 0:
|
||||
return "-"
|
||||
dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _build_markdown_report(report: Dict[str, Any]) -> str:
|
||||
lines: list[str] = []
|
||||
lines.append("# v25 vs v26.shadow ROI Report")
|
||||
lines.append("")
|
||||
lines.append(f"- Sample: last {report['sample_size']} finished football matches")
|
||||
if report.get("top_leagues_only"):
|
||||
lines.append("- Filter: top leagues only")
|
||||
lines.append("- ROI calculation: flat `1 unit` per playable and resolvable bet")
|
||||
lines.append(f"- Generated at: {report['generated_at']}")
|
||||
lines.append("")
|
||||
lines.append("## Overall Summary")
|
||||
lines.append("")
|
||||
lines.append("| Model | Played | Won | Lost | Win Rate | Profit | ROI | Main Pick ROI | Main Pick W/L |")
|
||||
lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---|")
|
||||
for model_name, payload in report["summary"]["models"].items():
|
||||
main = payload["main_pick"]
|
||||
lines.append(
|
||||
f"| {model_name} | {payload['all_playable']['played']} | {payload['all_playable']['won']} | "
|
||||
f"{payload['all_playable']['lost']} | {payload['all_playable']['win_rate_pct']}% | "
|
||||
f"{payload['all_playable']['profit_flat']:+.2f} | {payload['all_playable']['roi_flat_pct']:+.2f}% | "
|
||||
f"{main['roi_flat_pct']:+.2f}% | {main['won']}/{main['played']} |"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## Market Summary")
|
||||
lines.append("")
|
||||
lines.append("| Model | Market | Played | Won | Lost | Profit | ROI |")
|
||||
lines.append("|---|---|---:|---:|---:|---:|---:|")
|
||||
for model_name, markets in report["summary"]["markets"].items():
|
||||
for market_name in MARKET_ORDER:
|
||||
payload = markets.get(market_name)
|
||||
if not payload or payload["played"] == 0:
|
||||
continue
|
||||
lines.append(
|
||||
f"| {model_name} | {market_name} | {payload['played']} | {payload['won']} | {payload['lost']} | "
|
||||
f"{payload['profit_flat']:+.2f} | {payload['roi_flat_pct']:+.2f}% |"
|
||||
)
|
||||
lines.append("")
|
||||
loss_summary = report["summary"].get("v26_loss_analysis", {})
|
||||
if loss_summary:
|
||||
lines.append("## v26 Loss Analysis")
|
||||
lines.append("")
|
||||
lines.append(f"- Lost bets: {loss_summary.get('lost_bets', 0)}")
|
||||
lines.append("")
|
||||
lines.append("| Bucket | Top Items |")
|
||||
lines.append("|---|---|")
|
||||
for label, key in (
|
||||
("By market", "by_market"),
|
||||
("By league", "by_league"),
|
||||
("By pick", "by_pick"),
|
||||
("By odds band", "by_odds_band"),
|
||||
("By confidence band", "by_confidence_band"),
|
||||
("By edge band", "by_edge_band"),
|
||||
):
|
||||
items = loss_summary.get(key) or []
|
||||
rendered = ", ".join(f"{item['label']} ({item['count']})" for item in items[:6]) or "-"
|
||||
lines.append(f"| {label} | {rendered} |")
|
||||
lines.append("")
|
||||
lines.append("## Match By Match")
|
||||
lines.append("")
|
||||
lines.append("| Date | Match | Score | v25 Main | v25 Played Picks | v25 Profit | v26 Main | v26 Played Picks | v26 Profit |")
|
||||
lines.append("|---|---|---|---|---|---:|---|---|---:|")
|
||||
for match in report["matches"]:
|
||||
v25 = match["models"]["v25"]
|
||||
v26 = match["models"]["v26.shadow"]
|
||||
lines.append(
|
||||
f"| {_format_date(match['match_date_ms'])} | {match['match_name']} | {match['final_score']} | "
|
||||
f"{v25['main_pick']['summary']} | {v25['played_picks_summary']} | {v25['profit_flat']:+.2f} | "
|
||||
f"{v26['main_pick']['summary']} | {v26['played_picks_summary']} | {v26['profit_flat']:+.2f} |"
|
||||
)
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Detailed ROI backtest for v25 vs v26.shadow.",
|
||||
)
|
||||
parser.add_argument("--limit", type=int, default=60, help="Number of finished matches to analyze.")
|
||||
parser.add_argument(
|
||||
"--top-leagues-only",
|
||||
action="store_true",
|
||||
help="Only analyze matches whose league_id exists in top_leagues.json.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
dsn = _resolve_dsn()
|
||||
top_league_ids = sorted(load_top_league_ids()) if args.top_leagues_only else None
|
||||
matches = _fetch_matches(dsn, max(1, args.limit), top_league_ids=top_league_ids)
|
||||
orchestrator = SingleMatchOrchestrator()
|
||||
|
||||
report_matches: list[Dict[str, Any]] = []
|
||||
model_aggregate: Dict[str, Dict[str, float]] = {
|
||||
"v25": defaultdict(float),
|
||||
"v26.shadow": defaultdict(float),
|
||||
}
|
||||
main_pick_aggregate: Dict[str, Dict[str, float]] = {
|
||||
"v25": defaultdict(float),
|
||||
"v26.shadow": defaultdict(float),
|
||||
}
|
||||
market_aggregate: Dict[str, Dict[str, Dict[str, float]]] = {
|
||||
"v25": defaultdict(lambda: defaultdict(float)),
|
||||
"v26.shadow": defaultdict(lambda: defaultdict(float)),
|
||||
}
|
||||
csv_rows: list[Dict[str, Any]] = []
|
||||
|
||||
for context in matches:
|
||||
match_payload = {
|
||||
"match_id": context.match_id,
|
||||
"match_name": context.match_name,
|
||||
"league": context.league,
|
||||
"match_date_ms": context.match_date_ms,
|
||||
"final_score": context.final_score,
|
||||
"ht_score": context.ht_score,
|
||||
"total_cards": context.total_cards,
|
||||
"models": {},
|
||||
}
|
||||
|
||||
for model_name, mode in (("v25", "v25"), ("v26.shadow", "v26")):
|
||||
orchestrator.engine_mode = mode
|
||||
package = orchestrator.analyze_match(context.match_id) or {}
|
||||
rows = package.get("bet_summary") or []
|
||||
evaluated_rows: list[Dict[str, Any]] = []
|
||||
match_profit = 0.0
|
||||
|
||||
for row in rows:
|
||||
market = str(row.get("market") or "")
|
||||
pick = str(row.get("pick") or "")
|
||||
evaluation = _evaluate_row(
|
||||
market=market,
|
||||
pick=pick,
|
||||
odds=row.get("odds"),
|
||||
playable=bool(row.get("playable")),
|
||||
stake_units=row.get("stake_units"),
|
||||
context=context,
|
||||
)
|
||||
combined = {
|
||||
"market": market,
|
||||
"pick": pick,
|
||||
"playable": bool(row.get("playable")),
|
||||
"bet_grade": row.get("bet_grade"),
|
||||
"odds": round(_safe_float(row.get("odds")), 2),
|
||||
"calibrated_confidence": round(_safe_float(row.get("calibrated_confidence")), 1),
|
||||
"edge": round(_safe_float(row.get("ev_edge", row.get("edge"))), 4),
|
||||
"stake_units": round(_safe_float(row.get("stake_units")), 2),
|
||||
**evaluation,
|
||||
}
|
||||
evaluated_rows.append(combined)
|
||||
|
||||
if combined["counted_in_roi"]:
|
||||
bucket = market_aggregate[model_name][market]
|
||||
bucket["played"] += 1
|
||||
if combined["result"] == "WON":
|
||||
bucket["won"] += 1
|
||||
else:
|
||||
bucket["lost"] += 1
|
||||
bucket["profit"] += combined["profit_flat"]
|
||||
|
||||
model_bucket = model_aggregate[model_name]
|
||||
model_bucket["played"] += 1
|
||||
if combined["result"] == "WON":
|
||||
model_bucket["won"] += 1
|
||||
else:
|
||||
model_bucket["lost"] += 1
|
||||
model_bucket["profit"] += combined["profit_flat"]
|
||||
match_profit += combined["profit_flat"]
|
||||
elif combined["playable"]:
|
||||
model_aggregate[model_name]["unresolved"] += 1
|
||||
market_aggregate[model_name][market]["unresolved"] += 1
|
||||
|
||||
csv_rows.append(
|
||||
{
|
||||
"match_id": context.match_id,
|
||||
"date": _format_date(context.match_date_ms),
|
||||
"league": context.league,
|
||||
"match": context.match_name,
|
||||
"final_score": context.final_score,
|
||||
"ht_score": context.ht_score or "",
|
||||
"model": model_name,
|
||||
"market": market,
|
||||
"pick": pick,
|
||||
"playable": combined["playable"],
|
||||
"bet_grade": combined["bet_grade"],
|
||||
"odds": combined["odds"],
|
||||
"confidence": combined["calibrated_confidence"],
|
||||
"edge": combined["edge"],
|
||||
"result": combined["result"],
|
||||
"counted_in_roi": combined["counted_in_roi"],
|
||||
"profit_flat": combined["profit_flat"],
|
||||
"resolution_note": combined["resolution_note"],
|
||||
}
|
||||
)
|
||||
|
||||
main_pick = package.get("main_pick") or {}
|
||||
main_eval = _evaluate_row(
|
||||
market=str(main_pick.get("market") or ""),
|
||||
pick=str(main_pick.get("pick") or ""),
|
||||
odds=main_pick.get("odds"),
|
||||
playable=bool(main_pick.get("playable")),
|
||||
stake_units=main_pick.get("stake_units"),
|
||||
context=context,
|
||||
)
|
||||
main_pick_summary = {
|
||||
"market": main_pick.get("market"),
|
||||
"pick": main_pick.get("pick"),
|
||||
"playable": bool(main_pick.get("playable")),
|
||||
"odds": round(_safe_float(main_pick.get("odds")), 2),
|
||||
"confidence": round(
|
||||
_safe_float(
|
||||
main_pick.get("calibrated_confidence", main_pick.get("confidence"))
|
||||
),
|
||||
1,
|
||||
),
|
||||
"edge": round(_safe_float(main_pick.get("ev_edge", main_pick.get("edge"))), 4),
|
||||
**main_eval,
|
||||
}
|
||||
|
||||
if main_pick_summary["counted_in_roi"]:
|
||||
summary_suffix = (
|
||||
f"{main_pick_summary['result']}, played, {main_pick_summary['profit_flat']:+.2f}"
|
||||
)
|
||||
elif main_pick_summary.get("market") and main_pick_summary.get("pick"):
|
||||
summary_suffix = f"{main_pick_summary['result']}, not played"
|
||||
else:
|
||||
summary_suffix = ""
|
||||
|
||||
if main_pick_summary["counted_in_roi"]:
|
||||
bucket = main_pick_aggregate[model_name]
|
||||
bucket["played"] += 1
|
||||
if main_pick_summary["result"] == "WON":
|
||||
bucket["won"] += 1
|
||||
else:
|
||||
bucket["lost"] += 1
|
||||
bucket["profit"] += main_pick_summary["profit_flat"]
|
||||
elif main_pick_summary["playable"]:
|
||||
main_pick_aggregate[model_name]["unresolved"] += 1
|
||||
|
||||
main_pick_summary["summary"] = (
|
||||
f"{main_pick_summary['market']} {main_pick_summary['pick']} "
|
||||
f"({summary_suffix})"
|
||||
if main_pick_summary.get("market") and main_pick_summary.get("pick")
|
||||
else "No main pick"
|
||||
)
|
||||
|
||||
played_rows = [row for row in evaluated_rows if row["counted_in_roi"]]
|
||||
played_picks_summary = (
|
||||
"; ".join(
|
||||
f"{row['market']} {row['pick']}={row['result']} ({row['profit_flat']:+.2f})"
|
||||
for row in played_rows
|
||||
)
|
||||
if played_rows
|
||||
else "-"
|
||||
)
|
||||
|
||||
match_payload["models"][model_name] = {
|
||||
"main_pick": main_pick_summary,
|
||||
"profit_flat": round(match_profit, 4),
|
||||
"played_picks_summary": played_picks_summary,
|
||||
"played_picks": played_rows,
|
||||
"all_picks": evaluated_rows,
|
||||
}
|
||||
|
||||
report_matches.append(match_payload)
|
||||
|
||||
summary = {
|
||||
"models": {
|
||||
model_name: {
|
||||
"all_playable": _summarize_bucket(model_aggregate[model_name]),
|
||||
"main_pick": _summarize_bucket(main_pick_aggregate[model_name]),
|
||||
}
|
||||
for model_name in ("v25", "v26.shadow")
|
||||
},
|
||||
"markets": {
|
||||
model_name: {
|
||||
market_name: _summarize_bucket(bucket)
|
||||
for market_name, bucket in sorted(
|
||||
market_aggregate[model_name].items(),
|
||||
key=lambda item: (
|
||||
MARKET_ORDER.index(item[0]) if item[0] in MARKET_ORDER else 999,
|
||||
item[0],
|
||||
),
|
||||
)
|
||||
}
|
||||
for model_name in ("v25", "v26.shadow")
|
||||
},
|
||||
"v26_loss_analysis": _summarize_v26_losses(csv_rows),
|
||||
}
|
||||
|
||||
report = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"sample_size": len(report_matches),
|
||||
"top_leagues_only": bool(args.top_leagues_only),
|
||||
"summary": summary,
|
||||
"matches": report_matches,
|
||||
}
|
||||
|
||||
report_dir = AI_ENGINE_DIR / "reports"
|
||||
json_path = report_dir / "backtest_v26_shadow_roi_detail.json"
|
||||
csv_path = report_dir / "backtest_v26_shadow_roi_picks.csv"
|
||||
md_path = report_dir / "backtest_v26_shadow_roi_report.md"
|
||||
|
||||
json_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
with csv_path.open("w", encoding="utf-8", newline="") as handle:
|
||||
writer = csv.DictWriter(
|
||||
handle,
|
||||
fieldnames=[
|
||||
"match_id",
|
||||
"date",
|
||||
"league",
|
||||
"match",
|
||||
"final_score",
|
||||
"ht_score",
|
||||
"model",
|
||||
"market",
|
||||
"pick",
|
||||
"playable",
|
||||
"bet_grade",
|
||||
"odds",
|
||||
"confidence",
|
||||
"edge",
|
||||
"result",
|
||||
"counted_in_roi",
|
||||
"profit_flat",
|
||||
"resolution_note",
|
||||
],
|
||||
)
|
||||
writer.writeheader()
|
||||
writer.writerows(csv_rows)
|
||||
|
||||
md_path.write_text(_build_markdown_report(report), encoding="utf-8")
|
||||
|
||||
print(f"[OK] JSON report written to {json_path}")
|
||||
print(f"[OK] CSV report written to {csv_path}")
|
||||
print(f"[OK] Markdown report written to {md_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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:
|
||||
if poss and poss > 0:
|
||||
poss_sum += poss
|
||||
poss_sum += float(poss)
|
||||
poss_count += 1
|
||||
sot_sum += sot or 0
|
||||
shots_sum += total_shots or 0
|
||||
corners_sum += corners or 0
|
||||
sot_sum += float(sot or 0)
|
||||
shots_sum += float(total_shots or 0)
|
||||
corners_sum += float(corners or 0)
|
||||
|
||||
goals_scored += team_goals or 0
|
||||
goals_scored += float(team_goals or 0)
|
||||
|
||||
return {
|
||||
"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,
|
||||
'btts_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 = {
|
||||
'clean_sheet_rate': 0.2,
|
||||
@@ -53,6 +58,25 @@ class FeatureEnrichmentService:
|
||||
_DEFAULT_LEAGUE = {
|
||||
'avg_goals': 2.7,
|
||||
'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 ──────────────────────────────────────────────
|
||||
@@ -186,6 +210,13 @@ class FeatureEnrichmentService:
|
||||
total_goals = 0
|
||||
btts_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:
|
||||
sh = int(row['score_home'])
|
||||
@@ -195,14 +226,22 @@ class FeatureEnrichmentService:
|
||||
|
||||
# Normalise: who is "home team" in THIS prediction context
|
||||
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:
|
||||
home_wins += 1
|
||||
home_team_venue_wins += 1
|
||||
elif sh == sa:
|
||||
draws += 1
|
||||
else:
|
||||
# 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:
|
||||
home_wins += 1
|
||||
away_team_venue_wins += 1
|
||||
elif sh == sa:
|
||||
draws += 1
|
||||
|
||||
@@ -211,6 +250,29 @@ class FeatureEnrichmentService:
|
||||
if match_goals > 2:
|
||||
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 {
|
||||
'total_matches': total,
|
||||
'home_win_rate': home_wins / total,
|
||||
@@ -218,6 +280,11 @@ class FeatureEnrichmentService:
|
||||
'avg_goals': total_goals / total,
|
||||
'btts_rate': btts_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 ──────────────────────────────────────────
|
||||
@@ -433,6 +500,10 @@ class FeatureEnrichmentService:
|
||||
total = len(rows)
|
||||
total_goals = 0
|
||||
zero_goal_matches = 0
|
||||
home_wins = 0
|
||||
draw_count = 0
|
||||
btts_count = 0
|
||||
over25_count = 0
|
||||
|
||||
for row in rows:
|
||||
sh = int(row['score_home'])
|
||||
@@ -441,10 +512,24 @@ class FeatureEnrichmentService:
|
||||
total_goals += match_goals
|
||||
if match_goals == 0:
|
||||
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 {
|
||||
'avg_goals': total_goals / 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 ───────────────────────────────────────────────
|
||||
@@ -514,6 +599,161 @@ class FeatureEnrichmentService:
|
||||
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 ────────────────────────────────────────────────────────
|
||||
|
||||
def _safe_avg(values: list, default: float) -> float:
|
||||
|
||||
@@ -15,6 +15,7 @@ import json
|
||||
import re
|
||||
import time
|
||||
import math
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
@@ -27,12 +28,15 @@ from psycopg2.extras import RealDictCursor
|
||||
from data.db import get_clean_dsn
|
||||
from models.v20_ensemble import FullMatchPrediction
|
||||
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 (
|
||||
BasketballMatchPrediction,
|
||||
get_basketball_v25_predictor,
|
||||
)
|
||||
from core.engines.player_predictor import PlayerPrediction, get_player_predictor
|
||||
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.league_reliability import load_league_reliability
|
||||
|
||||
@@ -137,81 +141,116 @@ class SingleMatchOrchestrator:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.v25_predictor: Optional[V25Predictor] = None
|
||||
self.v26_shadow_engine: Optional[V26ShadowEngine] = None
|
||||
self.basketball_predictor: Optional[Any] = None
|
||||
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.league_reliability = load_league_reliability()
|
||||
self.enrichment = FeatureEnrichmentService()
|
||||
# Market calibration multipliers — V31 rebalance
|
||||
# Previous values created mathematical impossibilities:
|
||||
# BTTS: max reachable = 100×0.45 = 45, but min_conf was 55 → NEVER playable
|
||||
# New approach: calibration = blend(backtest_accuracy, 0.80) to avoid crushing raw signal
|
||||
self.odds_band_analyzer = OddsBandAnalyzer()
|
||||
# ── V32 Calibration Rebalance ──────────────────────────────────
|
||||
# RULE: max_reachable = 100 × calibration MUST be > min_conf + 8
|
||||
# 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] = {
|
||||
"MS": 0.48,
|
||||
"DC": 0.82,
|
||||
"OU15": 0.84,
|
||||
"OU25": 0.54,
|
||||
"OU35": 0.44,
|
||||
"BTTS": 0.50,
|
||||
"HT": 0.42,
|
||||
"HT_OU05": 0.68,
|
||||
"HT_OU15": 0.46,
|
||||
"OE": 0.58,
|
||||
"CARDS": 0.45,
|
||||
"HCAP": 0.40,
|
||||
"HTFT": 0.28,
|
||||
"MS": 0.62, # max=62 vs min=42 ✓ (was 0.48→max=48 vs 44 ⚠️)
|
||||
"DC": 0.82, # max=82 vs min=52 ✓ (unchanged, already good)
|
||||
"OU15": 0.84, # max=84 vs min=55 ✓ (unchanged, already good)
|
||||
"OU25": 0.68, # max=68 vs min=48 ✓ (was 0.54→max=54 vs 52 ⚠️)
|
||||
"OU35": 0.60, # max=60 vs min=48 ✓ (was 0.44→max=44 vs 54 ❌)
|
||||
"BTTS": 0.65, # max=65 vs min=46 ✓ (was 0.50→max=50 vs 50 ⚠️)
|
||||
"HT": 0.58, # max=58 vs min=40 ✓ (was 0.42→max=42 vs 45 ❌)
|
||||
"HT_OU05": 0.68, # max=68 vs min=50 ✓ (unchanged)
|
||||
"HT_OU15": 0.60, # max=60 vs min=42 ✓ (was 0.46→max=46 vs 48 ❌)
|
||||
"OE": 0.62, # max=62 vs min=46 ✓ (was 0.58→max=58 vs 50 ok)
|
||||
"CARDS": 0.58, # max=58 vs min=42 ✓ (was 0.45→max=45 vs 48 ❌)
|
||||
"HCAP": 0.56, # max=56 vs min=40 ✓ (was 0.40→max=40 vs 46 ❌)
|
||||
"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] = {
|
||||
"MS": 44.0,
|
||||
"DC": 55.0,
|
||||
"OU15": 58.0,
|
||||
"OU25": 52.0,
|
||||
"OU35": 54.0,
|
||||
"BTTS": 50.0,
|
||||
"HT": 45.0,
|
||||
"HT_OU05": 54.0,
|
||||
"HT_OU15": 48.0,
|
||||
"OE": 50.0,
|
||||
"CARDS": 48.0,
|
||||
"HCAP": 46.0,
|
||||
"HTFT": 32.0,
|
||||
"MS": 42.0, # was 44 — 3-way market, hard to get high conf
|
||||
"DC": 52.0, # was 55 — double chance is easier
|
||||
"OU15": 55.0, # was 58 — binary + usually high conf
|
||||
"OU25": 48.0, # was 52 — core market, allow more through
|
||||
"OU35": 48.0, # was 54 — lowered to let signals pass
|
||||
"BTTS": 46.0, # was 50 — binary market
|
||||
"HT": 40.0, # was 45 — was ❌ impossible, now achievable
|
||||
"HT_OU05": 50.0, # was 54 — binary HT market
|
||||
"HT_OU15": 42.0, # was 48 — was ❌ impossible, now achievable
|
||||
"OE": 46.0, # was 50 — coin-flip market, lower bar
|
||||
"CARDS": 42.0, # was 48 — was ❌ impossible, now achievable
|
||||
"HCAP": 40.0, # was 46 — was ❌ impossible, now achievable
|
||||
"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] = {
|
||||
"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,
|
||||
"HT_OU15": 72.0,
|
||||
"OE": 66.0,
|
||||
"CARDS": 74.0,
|
||||
"HCAP": 76.0,
|
||||
"HTFT": 82.0,
|
||||
"MS": 65.0, # was 72 — let more MS through for tracking
|
||||
"DC": 58.0, # was 62 — DC is high accuracy
|
||||
"OU15": 60.0, # was 64 — strong market per backtest
|
||||
"OU25": 64.0, # was 70 — core market
|
||||
"OU35": 68.0, # was 76 — riskier market
|
||||
"BTTS": 64.0, # was 70 — allow more signals
|
||||
"HT": 66.0, # was 74 — was never reachable anyway
|
||||
"HT_OU05": 60.0, # was 64 — strong backtest market
|
||||
"HT_OU15": 64.0, # was 72 — moderate
|
||||
"OE": 60.0, # was 66 — low priority market
|
||||
"CARDS": 66.0, # was 74 — niche market
|
||||
"HCAP": 68.0, # was 76 — risky
|
||||
"HTFT": 72.0, # was 82 — 9-way, very risky
|
||||
}
|
||||
self.market_min_edge: Dict[str, float] = {
|
||||
"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,
|
||||
"HT_OU15": 0.03,
|
||||
"OE": 0.02,
|
||||
"CARDS": 0.03,
|
||||
"HCAP": 0.04,
|
||||
"HTFT": 0.06,
|
||||
"MS": 0.02, # was 0.03 — slight relaxation
|
||||
"DC": 0.01, # unchanged
|
||||
"OU15": 0.01, # unchanged
|
||||
"OU25": 0.02, # unchanged
|
||||
"OU35": 0.03, # was 0.04
|
||||
"BTTS": 0.02, # was 0.03
|
||||
"HT": 0.03, # was 0.04
|
||||
"HT_OU05": 0.01, # unchanged
|
||||
"HT_OU15": 0.02, # was 0.03
|
||||
"OE": 0.02, # unchanged
|
||||
"CARDS": 0.02, # was 0.03
|
||||
"HCAP": 0.03, # was 0.04
|
||||
"HTFT": 0.05, # was 0.06
|
||||
}
|
||||
|
||||
def _get_v25_predictor(self) -> V25Predictor:
|
||||
if self.v25_predictor is None:
|
||||
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
|
||||
|
||||
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]:
|
||||
"""
|
||||
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)
|
||||
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)
|
||||
# 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:
|
||||
# Full fallback — use all defaults
|
||||
home_stats = dict(enr._DEFAULT_TEAM_STATS)
|
||||
@@ -283,6 +339,14 @@ class SingleMatchOrchestrator:
|
||||
league = dict(enr._DEFAULT_LEAGUE)
|
||||
home_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_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,
|
||||
}
|
||||
|
||||
# ── 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 {
|
||||
# META (1)
|
||||
'mst_utc': float(data.match_date_ms),
|
||||
# ELO (8)
|
||||
'home_overall_elo': home_elo,
|
||||
'away_overall_elo': away_elo,
|
||||
'elo_diff': home_elo - away_elo,
|
||||
'elo_diff': elo_diff,
|
||||
'home_home_elo': home_venue_elo,
|
||||
'away_away_elo': away_venue_elo,
|
||||
'home_form_elo': home_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)
|
||||
'home_goals_avg': data.home_goals_avg,
|
||||
'home_conceded_avg': data.home_conceded_avg,
|
||||
@@ -330,13 +416,17 @@ class SingleMatchOrchestrator:
|
||||
'away_winning_streak': away_form['winning_streak'],
|
||||
'home_unbeaten_streak': home_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_home_win_rate': h2h['home_win_rate'],
|
||||
'h2h_draw_rate': h2h['draw_rate'],
|
||||
'h2h_avg_goals': h2h['avg_goals'],
|
||||
'h2h_btts_rate': h2h['btts_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)
|
||||
'home_avg_possession': home_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_n': float(odds.get('btts_n', 0)),
|
||||
**odds_presence,
|
||||
# League (4)
|
||||
'home_xga': data.home_conceded_avg,
|
||||
'away_xga': data.away_conceded_avg,
|
||||
# League (9 — original 2 + V27 expanded 5 + xga 2)
|
||||
'home_xga': xga_home,
|
||||
'away_xga': xga_away,
|
||||
'league_avg_goals': league['avg_goals'],
|
||||
'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_atmosphere': 0.0,
|
||||
'upset_motivation': 0.0,
|
||||
@@ -390,9 +485,45 @@ class SingleMatchOrchestrator:
|
||||
# Momentum (3)
|
||||
'home_momentum_score': home_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
|
||||
**self._get_squad_features(data),
|
||||
# V28 Odds-Band Historical Performance Features
|
||||
**odds_band_features,
|
||||
}
|
||||
|
||||
def _get_squad_features(self, data: MatchData) -> Dict[str, float]:
|
||||
@@ -657,6 +788,17 @@ class SingleMatchOrchestrator:
|
||||
if data is 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()
|
||||
if sport_key == "basketball":
|
||||
prediction = self._get_basketball_predictor().predict(
|
||||
@@ -676,7 +818,372 @@ class SingleMatchOrchestrator:
|
||||
features = self._build_v25_features(data)
|
||||
v25_signal = self._get_v25_signal(data, features)
|
||||
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]]:
|
||||
"""
|
||||
@@ -2426,17 +2933,17 @@ class SingleMatchOrchestrator:
|
||||
|
||||
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:
|
||||
# - Trust only markets that remain robust after pre-match replay.
|
||||
# - Current strongest football markets: DC, OU15, HT_OU05.
|
||||
#
|
||||
# Priority 1: High-accuracy market (DC/OU15/HT_OU05/OU25) + Odds >= 1.30 + Conf >= 40%
|
||||
# Priority 2: Any playable + 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 >= 44%
|
||||
# Priority 3: Playable + Odds >= 1.30
|
||||
# Priority 4: Best non-playable (fallback)
|
||||
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 = {"DC", "OU15", "HT_OU05"}
|
||||
@@ -2444,7 +2951,7 @@ class SingleMatchOrchestrator:
|
||||
# Priority 1: High-accuracy markets with good odds and confidence
|
||||
high_accuracy_picks = [
|
||||
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("calibrated_confidence", 0.0)) >= MIN_CONFIDENCE
|
||||
]
|
||||
@@ -2649,8 +3156,14 @@ class SingleMatchOrchestrator:
|
||||
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 {
|
||||
"model_version": "v25.main",
|
||||
"simulation_mode": "pre_match" if is_simulation else None,
|
||||
"match_info": {
|
||||
"match_id": data.match_id,
|
||||
"match_name": f"{data.home_team_name} vs {data.away_team_name}",
|
||||
@@ -3779,11 +4292,15 @@ class SingleMatchOrchestrator:
|
||||
playable = False
|
||||
reasons.append("high_risk_low_data_quality")
|
||||
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")
|
||||
if data.lineup_source == "probable_xi" and lineup_sensitive:
|
||||
playable = False
|
||||
reasons.append("lineup_not_confirmed")
|
||||
# V32: Penalty instead of hard block
|
||||
# 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
|
||||
# Reliable league: stricter (-0.03), unreliable: looser (-0.08)
|
||||
neg_edge_threshold = -0.03 - (1.0 - odds_rel) * 0.05
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -90,8 +90,10 @@ class _RouterCursor:
|
||||
def _build_orchestrator() -> SingleMatchOrchestrator:
|
||||
orchestrator = SingleMatchOrchestrator.__new__(SingleMatchOrchestrator)
|
||||
orchestrator.v25_predictor = MagicMock()
|
||||
orchestrator.v26_shadow_engine = None
|
||||
orchestrator.basketball_predictor = MagicMock()
|
||||
orchestrator.dsn = "postgresql://unit-test"
|
||||
orchestrator.engine_mode = "v25"
|
||||
orchestrator.league_reliability = {}
|
||||
orchestrator.market_calibration = {
|
||||
"MS": 0.82,
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
||||
|
||||
from services.v26_shadow_engine import V26ShadowEngine
|
||||
|
||||
|
||||
def _build_prediction():
|
||||
return SimpleNamespace(
|
||||
risk_level="MEDIUM",
|
||||
risk_score=42.0,
|
||||
is_surprise_risk=False,
|
||||
surprise_type="",
|
||||
surprise_score=0.0,
|
||||
surprise_comment="",
|
||||
surprise_reasons=[],
|
||||
risk_warnings=[],
|
||||
team_confidence=71.0,
|
||||
player_confidence=64.0,
|
||||
odds_confidence=75.0,
|
||||
referee_confidence=58.0,
|
||||
predicted_ft_score="2-1",
|
||||
predicted_ht_score="1-0",
|
||||
home_xg=1.72,
|
||||
away_xg=1.08,
|
||||
total_xg=2.8,
|
||||
ft_scores_top5=[
|
||||
{"score": "2-1", "prob": 0.093},
|
||||
{"score": "1-1", "prob": 0.086},
|
||||
],
|
||||
ms_home_prob=0.52,
|
||||
ms_draw_prob=0.24,
|
||||
ms_away_prob=0.24,
|
||||
)
|
||||
|
||||
|
||||
def _build_data(referee_name="Ref A", lineup_source="confirmed_live", league_id="league1"):
|
||||
return SimpleNamespace(
|
||||
match_id="m1",
|
||||
home_team_name="Home",
|
||||
away_team_name="Away",
|
||||
league_id=league_id,
|
||||
league_name="League",
|
||||
match_date_ms=1710000000000,
|
||||
sport="football",
|
||||
home_lineup=["h"] * 11,
|
||||
away_lineup=["a"] * 11,
|
||||
lineup_source=lineup_source,
|
||||
referee_name=referee_name,
|
||||
odds_data={
|
||||
"ms_h": 2.1,
|
||||
"ms_d": 3.4,
|
||||
"ms_a": 3.7,
|
||||
"dc_1x": 1.28,
|
||||
"dc_x2": 1.68,
|
||||
"dc_12": 1.34,
|
||||
"ou15_o": 1.24,
|
||||
"ou15_u": 4.1,
|
||||
"ou25_o": 1.77,
|
||||
"ou25_u": 2.05,
|
||||
"ou35_o": 2.95,
|
||||
"ou35_u": 1.4,
|
||||
"btts_y": 1.74,
|
||||
"btts_n": 2.04,
|
||||
"ht_h": 2.72,
|
||||
"ht_d": 2.05,
|
||||
"ht_a": 4.8,
|
||||
"ht_ou05_o": 1.38,
|
||||
"ht_ou05_u": 2.85,
|
||||
"ht_ou15_o": 2.48,
|
||||
"ht_ou15_u": 1.48,
|
||||
"oe_odd": 1.92,
|
||||
"oe_even": 1.9,
|
||||
"cards_o": 1.98,
|
||||
"cards_u": 1.84,
|
||||
"hcap_h": 3.3,
|
||||
"hcap_d": 3.7,
|
||||
"hcap_a": 1.93,
|
||||
"htft_11": 3.8,
|
||||
"htft_1x": 5.1,
|
||||
"htft_12": 16.5,
|
||||
"htft_x1": 5.6,
|
||||
"htft_xx": 4.8,
|
||||
"htft_x2": 7.4,
|
||||
"htft_21": 22.0,
|
||||
"htft_2x": 12.0,
|
||||
"htft_22": 6.2,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class V26ShadowEngineTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.engine = V26ShadowEngine()
|
||||
self.engine.top_league_ids = {"top1"}
|
||||
self.prediction = _build_prediction()
|
||||
self.quality = {
|
||||
"label": "HIGH",
|
||||
"score": 0.88,
|
||||
"home_lineup_count": 11,
|
||||
"away_lineup_count": 11,
|
||||
"lineup_source": "confirmed_live",
|
||||
"flags": [],
|
||||
}
|
||||
self.v25_signal = {
|
||||
"MS": {"probs": {"1": 0.46, "X": 0.27, "2": 0.27}},
|
||||
"HT": {"probs": {"1": 0.39, "X": 0.41, "2": 0.20}},
|
||||
"HTFT": {"probs": {"1/1": 0.22, "X/X": 0.18, "2/2": 0.14}},
|
||||
"HCAP": {"probs": {"1": 0.21, "X": 0.19, "2": 0.60}},
|
||||
"CARDS": {"probs": {"Under": 0.53, "Over": 0.47}},
|
||||
}
|
||||
|
||||
def test_build_package_exposes_shadow_metadata(self):
|
||||
package = self.engine.build_package(
|
||||
data=_build_data(),
|
||||
prediction=self.prediction,
|
||||
v25_signal=self.v25_signal,
|
||||
quality=self.quality,
|
||||
)
|
||||
|
||||
self.assertEqual(package["model_version"], "v26.shadow.2")
|
||||
self.assertIn("calibration_version", package)
|
||||
self.assertIn("decision_trace_id", package)
|
||||
self.assertIn("market_reliability", package)
|
||||
self.assertTrue(package["bet_summary"])
|
||||
|
||||
def test_cards_defaults_to_pass_when_referee_missing(self):
|
||||
package = self.engine.build_package(
|
||||
data=_build_data(referee_name=None),
|
||||
prediction=self.prediction,
|
||||
v25_signal=self.v25_signal,
|
||||
quality=self.quality,
|
||||
)
|
||||
|
||||
cards = next(item for item in package["bet_summary"] if item["market"] == "CARDS")
|
||||
self.assertFalse(cards["playable"])
|
||||
self.assertEqual(cards["bet_grade"], "PASS")
|
||||
|
||||
def test_select_main_pick_prioritizes_ms_when_playable(self):
|
||||
rows = [
|
||||
{
|
||||
"market": "OU25",
|
||||
"pick": "2.5 Üst",
|
||||
"playable": True,
|
||||
"selection_score": 86.0,
|
||||
"play_score": 83.0,
|
||||
"edge": 0.15,
|
||||
"calibrated_confidence": 72.0,
|
||||
},
|
||||
{
|
||||
"market": "MS",
|
||||
"pick": "1",
|
||||
"playable": True,
|
||||
"selection_score": 81.0,
|
||||
"play_score": 82.0,
|
||||
"edge": 0.08,
|
||||
"calibrated_confidence": 64.0,
|
||||
},
|
||||
]
|
||||
|
||||
main_pick = self.engine._select_main_pick(rows)
|
||||
|
||||
self.assertIsNotNone(main_pick)
|
||||
self.assertEqual(main_pick["market"], "MS")
|
||||
self.assertEqual(main_pick["pick_reason"], "ms_priority_market")
|
||||
|
||||
def test_build_package_exposes_surprise_pick_when_reversal_is_hot(self):
|
||||
prediction = _build_prediction()
|
||||
prediction.is_surprise_risk = True
|
||||
prediction.surprise_score = 82.0
|
||||
prediction.surprise_type = "favorite_reversal"
|
||||
v25_signal = dict(self.v25_signal)
|
||||
v25_signal["HTFT"] = {
|
||||
"probs": {
|
||||
"1/2": 0.24,
|
||||
"X/2": 0.14,
|
||||
"1/1": 0.12,
|
||||
"X/X": 0.10,
|
||||
}
|
||||
}
|
||||
package = self.engine.build_package(
|
||||
data=_build_data(),
|
||||
prediction=prediction,
|
||||
v25_signal=v25_signal,
|
||||
quality=self.quality,
|
||||
)
|
||||
|
||||
self.assertIn("surprise_hunter", package)
|
||||
self.assertIn("surprise_pick", package)
|
||||
self.assertTrue(package["surprise_hunter"]["playable"])
|
||||
self.assertEqual(package["surprise_pick"]["market"], "HTFT")
|
||||
self.assertEqual(package["surprise_pick"]["strategy_channel"], "surprise_sidecar")
|
||||
self.assertEqual(package["surprise_hunter"]["strategy_channel"], "surprise_sidecar")
|
||||
self.assertGreaterEqual(package["surprise_pick"]["surprise_score"], 66.0)
|
||||
self.assertEqual(package["main_pick"]["strategy_channel"], "standard")
|
||||
self.assertNotEqual(package["main_pick"].get("strategy_channel"), package["surprise_pick"].get("strategy_channel"))
|
||||
self.assertNotEqual(package["main_pick"].get("pick_reason"), "favorite_reversal_signal")
|
||||
|
||||
def test_top_league_policy_suppresses_early_and_extra_goal_markets(self):
|
||||
package = self.engine.build_package(
|
||||
data=_build_data(league_id="top1"),
|
||||
prediction=self.prediction,
|
||||
v25_signal=self.v25_signal,
|
||||
quality=self.quality,
|
||||
)
|
||||
|
||||
summary = {item["market"]: item for item in package["bet_summary"]}
|
||||
self.assertFalse(summary["HT_OU05"]["playable"])
|
||||
self.assertTrue(
|
||||
"top_league_early_market_suppressed" in summary["HT_OU05"]["reasons"]
|
||||
or "top_league_ht_ou05_over_disabled" in summary["HT_OU05"]["reasons"]
|
||||
)
|
||||
|
||||
playable_goal_cluster = [
|
||||
item for item in package["bet_summary"]
|
||||
if item["market"] in {"OU15", "OU25", "OU35", "BTTS"} and item["playable"]
|
||||
]
|
||||
self.assertLessEqual(len(playable_goal_cluster), 1)
|
||||
|
||||
def test_scoreline_consistency_blocks_conflicting_markets(self):
|
||||
rows = [
|
||||
{
|
||||
"market": "MS",
|
||||
"raw_pick": "1",
|
||||
"pick": "1",
|
||||
"playable": True,
|
||||
"bet_grade": "A",
|
||||
"stake_units": 1.0,
|
||||
"decision_reasons": [],
|
||||
},
|
||||
{
|
||||
"market": "BTTS",
|
||||
"raw_pick": "Yes",
|
||||
"pick": "KG Var",
|
||||
"playable": True,
|
||||
"bet_grade": "A",
|
||||
"stake_units": 1.0,
|
||||
"decision_reasons": [],
|
||||
},
|
||||
{
|
||||
"market": "OU25",
|
||||
"raw_pick": "Over",
|
||||
"pick": "2.5 Üst",
|
||||
"playable": True,
|
||||
"bet_grade": "A",
|
||||
"stake_units": 1.0,
|
||||
"decision_reasons": [],
|
||||
},
|
||||
{
|
||||
"market": "OU25",
|
||||
"raw_pick": "Under",
|
||||
"pick": "2.5 Alt",
|
||||
"playable": True,
|
||||
"bet_grade": "A",
|
||||
"stake_units": 1.0,
|
||||
"decision_reasons": [],
|
||||
},
|
||||
]
|
||||
prediction = _build_prediction()
|
||||
prediction.predicted_ft_score = "1-0"
|
||||
prediction.predicted_ht_score = "1-0"
|
||||
|
||||
controlled = self.engine._apply_scoreline_consistency_controls(rows, prediction)
|
||||
by_market_pick = {(row["market"], row["raw_pick"]): row for row in controlled}
|
||||
|
||||
self.assertTrue(by_market_pick[("MS", "1")]["playable"])
|
||||
self.assertIn(
|
||||
"scoreline_scenario_aligned",
|
||||
by_market_pick[("MS", "1")]["decision_reasons"],
|
||||
)
|
||||
self.assertFalse(by_market_pick[("BTTS", "Yes")]["playable"])
|
||||
self.assertFalse(by_market_pick[("OU25", "Over")]["playable"])
|
||||
self.assertTrue(by_market_pick[("OU25", "Under")]["playable"])
|
||||
self.assertIn(
|
||||
"scoreline_scenario_conflict",
|
||||
by_market_pick[("BTTS", "Yes")]["decision_reasons"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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",
|
||||
"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"
|
||||
,
|
||||
"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": {
|
||||
"@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")
|
||||
}
|
||||
|
||||
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 {
|
||||
id Int @id @default(autoincrement())
|
||||
matchId String @map("match_id")
|
||||
|
||||
+330
-695
File diff suppressed because it is too large
Load Diff
@@ -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(),
|
||||
// AI Engine
|
||||
AI_ENGINE_URL: z.string().url().default("http://localhost:8000"),
|
||||
AI_ENGINE_MODE: z.enum(["v25", "dual", "v26"]).default("v25"),
|
||||
|
||||
// JWT
|
||||
JWT_SECRET: z.string().min(32),
|
||||
|
||||
@@ -259,15 +259,21 @@ export class AdminController {
|
||||
premiumUsers,
|
||||
totalMatches,
|
||||
totalPredictions,
|
||||
totalCoupons,
|
||||
] = await Promise.all([
|
||||
this.prisma.user.count(),
|
||||
this.prisma.user.count({ where: { isActive: true } }),
|
||||
this.prisma.user.count({ where: { subscriptionStatus: "active" } }),
|
||||
this.prisma.match.count(),
|
||||
this.prisma.prediction.count(),
|
||||
this.prisma.userCoupon.count(),
|
||||
]);
|
||||
|
||||
return createSuccessResponse({
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
totalPredictions,
|
||||
totalCoupons,
|
||||
users: {
|
||||
total: totalUsers,
|
||||
active: activeUsers,
|
||||
|
||||
@@ -13,11 +13,13 @@ import {
|
||||
ROLES_KEY,
|
||||
PERMISSIONS_KEY,
|
||||
} from "../../../common/decorators";
|
||||
import { normalizeRole } from "../../../common/constants/roles";
|
||||
|
||||
interface AuthenticatedUser {
|
||||
id: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
role?: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
@@ -88,11 +90,28 @@ export class RolesGuard implements CanActivate {
|
||||
|
||||
const user = req.user as AuthenticatedUser | undefined;
|
||||
|
||||
if (!user || !user.roles) {
|
||||
if (!user) {
|
||||
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) {
|
||||
throw new ForbiddenException("PERMISSION_DENIED");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PassportStrategy } from "@nestjs/passport";
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { AuthService, JwtPayload } from "../auth.service";
|
||||
import { normalizeRole } from "../../../common/constants/roles";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
@@ -29,9 +30,13 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedRole = normalizeRole(payload.role);
|
||||
|
||||
return {
|
||||
...user,
|
||||
role: payload.role,
|
||||
role: normalizedRole,
|
||||
roles: normalizedRole ? [normalizedRole] : [],
|
||||
permissions: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
AnalyzeMatchDto,
|
||||
DailyBankoDto,
|
||||
SuggestCouponDto,
|
||||
FrequencyCouponDto,
|
||||
} from "./dto/coupons-request.dto";
|
||||
import { Public } from "../../common/decorators";
|
||||
import { JwtAuthGuard } from "../auth/guards/auth.guards"; // Assuming standard guard
|
||||
@@ -188,8 +189,43 @@ export class CouponsController {
|
||||
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 { SmartCouponService } from "./services/smart-coupon.service";
|
||||
import { UserCouponService } from "./services/user-coupon.service";
|
||||
import { FrequencyEngineService } from "./services/frequency-engine.service";
|
||||
import { CouponsService } from "./coupons.service";
|
||||
import { DatabaseModule } from "../../database/database.module";
|
||||
import { ServicesModule } from "../../services/services.module";
|
||||
@@ -10,7 +11,18 @@ import { MatchesModule } from "../matches/matches.module";
|
||||
@Module({
|
||||
imports: [DatabaseModule, ServicesModule, MatchesModule],
|
||||
controllers: [CouponsController],
|
||||
providers: [CouponsService, SmartCouponService, UserCouponService],
|
||||
exports: [CouponsService, SmartCouponService, UserCouponService],
|
||||
providers: [
|
||||
CouponsService,
|
||||
SmartCouponService,
|
||||
UserCouponService,
|
||||
FrequencyEngineService,
|
||||
],
|
||||
exports: [
|
||||
CouponsService,
|
||||
SmartCouponService,
|
||||
UserCouponService,
|
||||
FrequencyEngineService,
|
||||
],
|
||||
})
|
||||
export class CouponsModule {}
|
||||
|
||||
|
||||
@@ -74,3 +74,47 @@ export class SuggestCouponDto {
|
||||
@Max(100)
|
||||
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 axios from "axios";
|
||||
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 PredictionDataQuality = "HIGH" | "MEDIUM" | "LOW";
|
||||
@@ -126,24 +134,37 @@ export interface SmartCouponResult {
|
||||
export class SmartCouponService {
|
||||
private readonly logger = new Logger(SmartCouponService.name);
|
||||
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.aiEngineClient = new AiEngineClient({
|
||||
baseUrl: this.aiEngineUrl,
|
||||
logger: this.logger,
|
||||
serviceName: SmartCouponService.name,
|
||||
timeoutMs: 60000,
|
||||
maxRetries: 2,
|
||||
retryDelayMs: 750,
|
||||
});
|
||||
}
|
||||
|
||||
async analyzeMatch(matchId: string): Promise<SingleMatchPredictionPackage> {
|
||||
let prediction: SingleMatchPredictionPackage;
|
||||
try {
|
||||
const response = await axios.post<SingleMatchPredictionPackage>(
|
||||
`${this.aiEngineUrl}/v20plus/analyze/${matchId}`,
|
||||
const response = await this.aiEngineClient.post<SingleMatchPredictionPackage>(
|
||||
`/v20plus/analyze/${matchId}`,
|
||||
);
|
||||
prediction = response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const detail = error.response?.data?.detail || error.message;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AiEngineRequestError) {
|
||||
const detail =
|
||||
typeof error.detail === "string" ? error.detail : error.message;
|
||||
throw new HttpException(
|
||||
`AI analyze failed: ${detail}`,
|
||||
error.response?.status || HttpStatus.SERVICE_UNAVAILABLE,
|
||||
error.status || HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
throw new HttpException(
|
||||
@@ -205,8 +226,8 @@ export class SmartCouponService {
|
||||
options: { maxMatches?: number; minConfidence?: number } = {},
|
||||
): Promise<SmartCouponResult> {
|
||||
try {
|
||||
const response = await axios.post<SmartCouponResult>(
|
||||
`${this.aiEngineUrl}/v20plus/coupon`,
|
||||
const response = await this.aiEngineClient.post<SmartCouponResult>(
|
||||
"/v20plus/coupon",
|
||||
{
|
||||
match_ids: matchIds,
|
||||
strategy,
|
||||
@@ -215,13 +236,14 @@ export class SmartCouponService {
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
this.logger.error("Failed to generate smart coupon", error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
const detail = error.response?.data?.detail || error.message;
|
||||
if (error instanceof AiEngineRequestError) {
|
||||
const detail =
|
||||
typeof error.detail === "string" ? error.detail : error.message;
|
||||
throw new HttpException(
|
||||
`Coupon generation failed: ${detail}`,
|
||||
error.response?.status || HttpStatus.SERVICE_UNAVAILABLE,
|
||||
error.status || HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
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.
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
BasketballTeamStats,
|
||||
} from "./feeder.types";
|
||||
import { ImageUtils } from "../../common/utils/image.util";
|
||||
import { deriveStoredMatchStatus } from "../../common/utils/match-status.util";
|
||||
|
||||
@Injectable()
|
||||
export class FeederPersistenceService {
|
||||
@@ -311,33 +312,15 @@ export class FeederPersistenceService {
|
||||
headerData?.htScoreAway ??
|
||||
this.safeInt(matchSummary.score?.ht?.away);
|
||||
|
||||
let status = "NS";
|
||||
if (headerData?.matchStatus) {
|
||||
if (
|
||||
headerData.matchStatus === "postGame" ||
|
||||
headerData.matchStatus === "post"
|
||||
) {
|
||||
status = "FT";
|
||||
} else if (
|
||||
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";
|
||||
}
|
||||
const status = deriveStoredMatchStatus({
|
||||
state: headerData?.matchStatus ?? matchSummary.state,
|
||||
status: matchSummary.status,
|
||||
substate: matchSummary.substate,
|
||||
statusBoxContent: matchSummary.statusBoxContent,
|
||||
scoreHome: finalScoreHome,
|
||||
scoreAway: finalScoreAway,
|
||||
score: matchSummary.score,
|
||||
});
|
||||
|
||||
await tx.match.upsert({
|
||||
where: { id: matchId },
|
||||
@@ -870,15 +853,11 @@ export class FeederPersistenceService {
|
||||
}
|
||||
|
||||
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({
|
||||
where: {
|
||||
id: { in: matchIds },
|
||||
AND: [
|
||||
{ oddCategories: { some: {} } },
|
||||
{ playerEvents: { some: {} } },
|
||||
{ officials: { some: {} } },
|
||||
{
|
||||
OR: [
|
||||
{ footballTeamStats: { some: {} } },
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
DbEventPayload,
|
||||
DbMarketPayload,
|
||||
} from "./feeder.types";
|
||||
import { isMatchCompleted } from "../../common/utils/match-status.util";
|
||||
|
||||
interface ProcessDateOptions {
|
||||
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 {
|
||||
if (match.statusBoxContent === "ERT") return false;
|
||||
|
||||
const normalizedState = String(match.state || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const normalizedStatus = String(match.status || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
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;
|
||||
return isMatchCompleted({
|
||||
state: match.state,
|
||||
status: match.status,
|
||||
substate: match.substate,
|
||||
statusBoxContent: match.statusBoxContent,
|
||||
score: match.score,
|
||||
scoreHome: match.homeScore,
|
||||
scoreAway: match.awayScore,
|
||||
});
|
||||
}
|
||||
|
||||
async runPreviousDayCompletedMatchesScan(
|
||||
@@ -957,15 +923,30 @@ export class FeederService {
|
||||
*/
|
||||
// ==========================================
|
||||
|
||||
if (saved && hasCriticalError) {
|
||||
// Collect missing components
|
||||
const completedMatch = isMatchCompleted({
|
||||
state: headerData?.matchStatus ?? matchSummary.state,
|
||||
status: matchSummary.status,
|
||||
substate: matchSummary.substate,
|
||||
statusBoxContent: matchSummary.statusBoxContent,
|
||||
scoreHome: headerData?.scoreHome ?? matchSummary.score?.home,
|
||||
scoreAway: headerData?.scoreAway ?? matchSummary.score?.away,
|
||||
});
|
||||
|
||||
const missingParts: string[] = [];
|
||||
if (!stats) missingParts.push("Stats");
|
||||
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 (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(
|
||||
`[${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 };
|
||||
}
|
||||
|
||||
@@ -1,44 +1,90 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { Controller, Get, Res } from "@nestjs/common";
|
||||
import { ApiTags, ApiOperation } from "@nestjs/swagger";
|
||||
import {
|
||||
HealthCheck,
|
||||
HealthCheckService,
|
||||
PrismaHealthIndicator,
|
||||
} from "@nestjs/terminus";
|
||||
import type { Response } from "express";
|
||||
import { Public } from "../../common/decorators";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import { PredictionsService } from "../predictions/predictions.service";
|
||||
|
||||
@ApiTags("Health")
|
||||
@Controller("health")
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private health: HealthCheckService,
|
||||
private prismaHealth: PrismaHealthIndicator,
|
||||
private prisma: PrismaService,
|
||||
private readonly predictionsService: PredictionsService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: "Basic health check" })
|
||||
check() {
|
||||
return this.health.check([]);
|
||||
async check(@Res() response: Response) {
|
||||
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")
|
||||
@Public()
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: "Readiness check (includes database)" })
|
||||
readiness() {
|
||||
return this.health.check([
|
||||
() => this.prismaHealth.pingCheck("database", this.prisma),
|
||||
]);
|
||||
async readiness(@Res() response: Response) {
|
||||
const database = await this.getDatabaseHealth();
|
||||
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")
|
||||
@Public()
|
||||
@ApiOperation({ summary: "Liveness check" })
|
||||
liveness() {
|
||||
return { status: "ok", timestamp: new Date().toISOString() };
|
||||
liveness(@Res() response: Response) {
|
||||
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 { TerminusModule } from "@nestjs/terminus";
|
||||
import { PrismaHealthIndicator } from "@nestjs/terminus";
|
||||
import { HealthController } from "./health.controller";
|
||||
import { PredictionsModule } from "../predictions/predictions.module";
|
||||
|
||||
@Module({
|
||||
imports: [TerminusModule],
|
||||
imports: [PredictionsModule],
|
||||
controllers: [HealthController],
|
||||
providers: [PrismaHealthIndicator],
|
||||
})
|
||||
export class HealthModule {}
|
||||
|
||||
@@ -119,20 +119,23 @@ export class LeaguesController {
|
||||
|
||||
/**
|
||||
* GET /leagues/teams/:id/matches
|
||||
* Get team's recent matches
|
||||
* Get team's recent matches (paginated)
|
||||
*/
|
||||
@Get("teams/:id/matches")
|
||||
@Public()
|
||||
@ApiOperation({ summary: "Get team's recent matches" })
|
||||
@ApiOperation({ summary: "Get team's recent matches (paginated)" })
|
||||
@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)" })
|
||||
async getTeamMatches(
|
||||
@Param("id") id: string,
|
||||
@Query("page") page?: string,
|
||||
@Query("limit") limit?: string,
|
||||
) {
|
||||
return this.leaguesService.getTeamRecentMatches(
|
||||
id,
|
||||
parseInt(limit || "10", 10),
|
||||
parseInt(page || "1", 10),
|
||||
parseInt(limit || "20", 10),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -99,21 +99,40 @@ export class LeaguesService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team's matches (past + upcoming)
|
||||
* Get team's matches (past + upcoming) with pagination
|
||||
*/
|
||||
async getTeamRecentMatches(teamId: string, limit: number = 50) {
|
||||
return this.prisma.match.findMany({
|
||||
where: {
|
||||
async getTeamRecentMatches(
|
||||
teamId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
) {
|
||||
const skip = (page - 1) * limit;
|
||||
const where = {
|
||||
OR: [{ homeTeamId: teamId }, { awayTeamId: teamId }],
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,13 @@ import {
|
||||
ActiveLeagueDto,
|
||||
} from "./dto";
|
||||
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()
|
||||
export class MatchesService {
|
||||
@@ -38,23 +45,12 @@ export class MatchesService {
|
||||
OR: [
|
||||
{
|
||||
status: {
|
||||
in: [
|
||||
"LIVE",
|
||||
"1H",
|
||||
"2H",
|
||||
"HT",
|
||||
"1Q",
|
||||
"2Q",
|
||||
"3Q",
|
||||
"4Q",
|
||||
"Playing",
|
||||
"Half Time",
|
||||
],
|
||||
in: LIVE_STATUS_VALUES_FOR_DB,
|
||||
},
|
||||
},
|
||||
{
|
||||
state: {
|
||||
in: ["live", "firsthalf", "secondhalf"],
|
||||
in: LIVE_STATE_VALUES_FOR_DB,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -66,14 +62,23 @@ export class MatchesService {
|
||||
OR: [
|
||||
{
|
||||
status: {
|
||||
in: ["Finished", "Played", "FT", "AET", "PEN", "Ended"],
|
||||
in: FINISHED_STATUS_VALUES_FOR_DB,
|
||||
},
|
||||
},
|
||||
{
|
||||
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
|
||||
let displayStatus = match.status || "NS";
|
||||
if (match.state === "live") {
|
||||
displayStatus = "LIVE";
|
||||
} else if (
|
||||
match.state === "post" ||
|
||||
match.state === "FT" ||
|
||||
match.status === "Finished"
|
||||
) {
|
||||
displayStatus = "Finished";
|
||||
}
|
||||
const displayStatus = getDisplayMatchStatus({
|
||||
state: match.state,
|
||||
status: match.status,
|
||||
substate: match.substate,
|
||||
scoreHome: match.scoreHome,
|
||||
scoreAway: match.scoreAway,
|
||||
});
|
||||
|
||||
league.matches.push({
|
||||
id: match.id,
|
||||
@@ -562,16 +564,13 @@ export class MatchesService {
|
||||
|
||||
if (liveMatch) {
|
||||
// Map liveMatch status
|
||||
let displayStatus = liveMatch.status || "NS";
|
||||
if (liveMatch.state === "live") {
|
||||
displayStatus = "LIVE";
|
||||
} else if (
|
||||
liveMatch.state === "post" ||
|
||||
liveMatch.state === "FT" ||
|
||||
liveMatch.status === "Finished"
|
||||
) {
|
||||
displayStatus = "Finished";
|
||||
}
|
||||
const displayStatus = getDisplayMatchStatus({
|
||||
state: liveMatch.state,
|
||||
status: liveMatch.status,
|
||||
substate: liveMatch.substate,
|
||||
scoreHome: liveMatch.scoreHome,
|
||||
scoreAway: liveMatch.scoreAway,
|
||||
});
|
||||
|
||||
match = {
|
||||
...liveMatch,
|
||||
|
||||
@@ -115,6 +115,9 @@ export class MatchPickDto {
|
||||
@ApiProperty()
|
||||
market: string;
|
||||
|
||||
@ApiProperty({ required: false, default: "standard" })
|
||||
strategy_channel?: string;
|
||||
|
||||
@ApiProperty()
|
||||
pick: string;
|
||||
|
||||
@@ -350,6 +353,15 @@ export class MatchPredictionDto {
|
||||
@ApiProperty()
|
||||
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 })
|
||||
match_info: MatchInfoDto;
|
||||
|
||||
@@ -368,6 +380,9 @@ export class MatchPredictionDto {
|
||||
@ApiProperty({ type: MatchPickDto, nullable: true })
|
||||
value_pick: MatchPickDto | null;
|
||||
|
||||
@ApiProperty({ type: MatchPickDto, nullable: true, required: false })
|
||||
surprise_pick?: MatchPickDto | null;
|
||||
|
||||
@ApiProperty({ type: MatchBetAdviceDto })
|
||||
bet_advice: MatchBetAdviceDto;
|
||||
|
||||
@@ -394,6 +409,23 @@ export class MatchPredictionDto {
|
||||
|
||||
@ApiProperty({ type: [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 {
|
||||
@@ -461,6 +493,24 @@ export class AIHealthDto {
|
||||
|
||||
@ApiProperty()
|
||||
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";
|
||||
|
||||
@@ -19,11 +19,14 @@ import {
|
||||
ValueBetDto,
|
||||
AIHealthDto,
|
||||
} from "./dto";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { FeederService } from "../feeder/feeder.service";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
AiEngineClient,
|
||||
AiEngineRequestError,
|
||||
} from "../../common/utils/ai-engine-client";
|
||||
|
||||
type ConfidenceBand = "HIGH" | "MEDIUM" | "LOW";
|
||||
|
||||
@@ -45,6 +48,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(PredictionsService.name);
|
||||
private queueEvents: QueueEvents | null = null;
|
||||
private readonly aiEngineUrl: string;
|
||||
private readonly aiEngineClient: AiEngineClient;
|
||||
private readonly topLeagueIds = new Set<string>();
|
||||
private readonly reasonTranslations: Record<string, string> = {
|
||||
confidence_below_threshold: "Güven eşiğin altında",
|
||||
@@ -125,6 +129,14 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
"AI_ENGINE_URL",
|
||||
"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();
|
||||
}
|
||||
|
||||
@@ -149,12 +161,55 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
checkHealth(): Promise<AIHealthDto> {
|
||||
return Promise.resolve({
|
||||
status: "healthy",
|
||||
modelLoaded: true,
|
||||
predictionServiceReady: true,
|
||||
async checkHealth(): Promise<AIHealthDto> {
|
||||
const circuit = this.aiEngineClient.getSnapshot();
|
||||
|
||||
try {
|
||||
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", "v25"),
|
||||
};
|
||||
} 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", "v25"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getPredictionById(matchId: string): Promise<MatchPredictionDto | null> {
|
||||
@@ -169,6 +224,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
if (!data || data.error) {
|
||||
return null;
|
||||
}
|
||||
await this.recordPredictionRun(matchId, data as MatchPredictionDto);
|
||||
return this.enrichPredictionResponse(
|
||||
data as MatchPredictionDto,
|
||||
matchContext,
|
||||
@@ -182,22 +238,22 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
// Direct HTTP mode (no Redis)
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.aiEngineUrl}/v20plus/analyze/${matchId}`,
|
||||
const response = await this.aiEngineClient.post<MatchPredictionDto>(
|
||||
`/v20plus/analyze/${matchId}`,
|
||||
{},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await this.recordPredictionRun(matchId, response.data);
|
||||
return this.enrichPredictionResponse(
|
||||
response.data as MatchPredictionDto,
|
||||
matchContext,
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
const error = e as AxiosError<Record<string, unknown>>;
|
||||
const status = error?.response?.status;
|
||||
const detail =
|
||||
error?.response?.data?.detail ||
|
||||
error?.response?.data ||
|
||||
error?.message;
|
||||
const requestError =
|
||||
e instanceof AiEngineRequestError
|
||||
? e
|
||||
: new AiEngineRequestError("AI Engine request failed");
|
||||
const status = requestError.status;
|
||||
const detail = requestError.detail || requestError.message;
|
||||
this.logger.error(
|
||||
`Direct AI Engine call failed for ${matchId}: status=${status}, detail=${JSON.stringify(detail)}`,
|
||||
);
|
||||
@@ -988,14 +1044,18 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
// Direct HTTP mode
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.aiEngineUrl}/smart-coupon`,
|
||||
const response = await this.aiEngineClient.post(
|
||||
"/smart-coupon",
|
||||
{ match_ids: matchIds, strategy, ...options },
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof AiEngineRequestError
|
||||
? error.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
this.logger.error(`Direct smart coupon call failed: ${message}`);
|
||||
this.throwAiError(message);
|
||||
}
|
||||
@@ -1018,6 +1078,12 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
);
|
||||
}
|
||||
if (message.includes("circuit breaker is open")) {
|
||||
throw new HttpException(
|
||||
"AI Engine is temporarily unavailable",
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
throw new HttpException(
|
||||
"Failed to get prediction from AI Engine",
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
@@ -1169,4 +1235,124 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { RolesGuard } from "../auth/guards/auth.guards";
|
||||
@ApiTags("Social Poster")
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles("admin")
|
||||
@Roles("superadmin")
|
||||
@Controller("social-poster")
|
||||
export class SocialPosterController {
|
||||
constructor(private readonly socialPosterService: SocialPosterService) {}
|
||||
|
||||
@@ -43,7 +43,7 @@ export class SporTotoController {
|
||||
|
||||
@Post("sync")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Roles("admin")
|
||||
@Roles("superadmin")
|
||||
@ApiBearerAuth()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
@@ -114,7 +114,7 @@ export class SporTotoController {
|
||||
|
||||
@Post("bulletins")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Roles("admin")
|
||||
@Roles("superadmin")
|
||||
@ApiBearerAuth()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({
|
||||
@@ -135,7 +135,7 @@ export class SporTotoController {
|
||||
|
||||
@Patch("bulletins/:id/results")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Roles("admin")
|
||||
@Roles("superadmin")
|
||||
@ApiBearerAuth()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
|
||||
@@ -84,7 +84,7 @@ export class UsersController extends BaseController<
|
||||
}
|
||||
|
||||
// Override create to require admin role
|
||||
@Roles("admin")
|
||||
@Roles("superadmin")
|
||||
async create(
|
||||
...args: Parameters<
|
||||
BaseController<User, CreateUserDto, UpdateUserDto>["create"]
|
||||
@@ -94,7 +94,7 @@ export class UsersController extends BaseController<
|
||||
}
|
||||
|
||||
// Override delete to require admin role
|
||||
@Roles("admin")
|
||||
@Roles("superadmin")
|
||||
async delete(
|
||||
...args: Parameters<
|
||||
BaseController<User, CreateUserDto, UpdateUserDto>["delete"]
|
||||
|
||||
+28
-15
@@ -1,7 +1,9 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { HttpService } from "@nestjs/axios";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import {
|
||||
AiEngineClient,
|
||||
AiEngineRequestError,
|
||||
} from "../common/utils/ai-engine-client";
|
||||
|
||||
export interface AIPredictionResult {
|
||||
matchId: string;
|
||||
@@ -40,13 +42,21 @@ export interface AIPredictionResult {
|
||||
export class AiService {
|
||||
private readonly logger = new Logger(AiService.name);
|
||||
private readonly pythonEngineUrl: string;
|
||||
private readonly aiEngineClient: AiEngineClient;
|
||||
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.pythonEngineUrl =
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,14 +81,9 @@ export class AiService {
|
||||
`Calling Python V25 Engine for ${matchDetails.homeTeam} vs ${matchDetails.awayTeam}`,
|
||||
);
|
||||
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.post(
|
||||
`${this.pythonEngineUrl}/v20plus/analyze/${matchId}`,
|
||||
const response = await this.aiEngineClient.post(
|
||||
`/v20plus/analyze/${matchId}`,
|
||||
{},
|
||||
{
|
||||
timeout: 30000,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.data) {
|
||||
@@ -86,8 +91,14 @@ export class AiService {
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`Python Engine error: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -286,10 +297,12 @@ export class AiService {
|
||||
*/
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get(`${this.pythonEngineUrl}/health`, {
|
||||
const response = await this.aiEngineClient.get<{ status?: string }>(
|
||||
"/health",
|
||||
{
|
||||
timeout: 5000,
|
||||
}),
|
||||
retryCount: 0,
|
||||
},
|
||||
);
|
||||
return response.data?.status === "healthy";
|
||||
} catch {
|
||||
|
||||
+170
-91
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Cron } from "@nestjs/schedule";
|
||||
import { HttpService } from "@nestjs/axios";
|
||||
import { PrismaService } from "../database/prisma.service";
|
||||
@@ -8,10 +8,22 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { Prisma } from "@prisma/client";
|
||||
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
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface LiveScoreTeamPayload {
|
||||
id: string;
|
||||
@@ -64,75 +76,119 @@ interface LiveLineupsJson {
|
||||
|
||||
type SportType = "football" | "basketball";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Service
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
@Injectable()
|
||||
export class DataFetcherTask {
|
||||
private readonly logger = new Logger(DataFetcherTask.name);
|
||||
private readonly timeZone = "Europe/Istanbul";
|
||||
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly scraper: FeederScraperService,
|
||||
private readonly taskLock: TaskLockService,
|
||||
) {}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// CRON 1: Main sync — every 15 minutes
|
||||
// Phases: match list → live scores → odds → lineups
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// CRON 1: Main sync — every 15 minutes
|
||||
// Phases: match list → live scores → odds → lineups
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
@Cron("*/15 * * * *")
|
||||
async syncLiveMatches(): Promise<void> {
|
||||
if (this.shouldSkipInHistoricalMode("syncLiveMatches")) return;
|
||||
this.logger.log("━━━ syncLiveMatches START ━━━");
|
||||
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
|
||||
// Phase 1: Match list (football + basketball)
|
||||
await this.syncMatchList(today);
|
||||
|
||||
// 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 ━━━");
|
||||
await this.taskLock.runWithLease(
|
||||
"syncLiveMatches",
|
||||
30 * 60 * 1000,
|
||||
async () => {
|
||||
await this.runLiveSync();
|
||||
},
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// CRON 2: Daily cleanup + full sync — 07:00 Istanbul
|
||||
// Truncates live_matches, then runs full sync
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// CRON 2: Daily cleanup + full sync — 07:00 Istanbul
|
||||
// Preserve yesterday as a fallback until the 08:00 archive job completes.
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
@Cron("0 7 * * *", { timeZone: "Europe/Istanbul" })
|
||||
async cleanAndFullSync(): Promise<void> {
|
||||
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 {
|
||||
const deleted = await this.prisma.liveMatch.deleteMany({});
|
||||
const yesterdayDate = getShiftedDateStringInTimeZone(
|
||||
-1,
|
||||
this.timeZone,
|
||||
);
|
||||
const { startMs: yesterdayStartMs } = getDayBoundsForTimeZone(
|
||||
yesterdayDate,
|
||||
this.timeZone,
|
||||
);
|
||||
const cutoffDate = new Date(yesterdayStartMs);
|
||||
|
||||
const deleted = await this.prisma.liveMatch.deleteMany({
|
||||
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(
|
||||
`🧹 Deleted ${deleted.count} live matches. Starting full sync...`,
|
||||
`Pruned ${deleted.count} stale live matches. Starting full sync...`,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Truncate failed: ${message}`);
|
||||
this.logger.error(`Stale live_match cleanup failed: ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Run full sync immediately after cleanup
|
||||
await this.syncLiveMatches();
|
||||
await this.runLiveSync();
|
||||
},
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// 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> {
|
||||
// Football
|
||||
@@ -141,7 +197,7 @@ export class DataFetcherTask {
|
||||
await this.fetchMatchesForSport("football", date, footballLeagues);
|
||||
} else {
|
||||
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());
|
||||
}
|
||||
@@ -170,17 +226,18 @@ export class DataFetcherTask {
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Phase 2: Live score updates (merged from live-updater.task)
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
private async updateLiveScores(): Promise<void> {
|
||||
try {
|
||||
const liveMatches = await this.prisma.liveMatch.findMany({
|
||||
where: {
|
||||
state: {
|
||||
in: ["live", "firsthalf", "secondhalf", "1H", "2H", "HT", "LIVE"],
|
||||
},
|
||||
OR: [
|
||||
{ state: { in: LIVE_STATE_VALUES_FOR_DB } },
|
||||
{ status: { in: LIVE_STATUS_VALUES_FOR_DB } },
|
||||
],
|
||||
},
|
||||
select: { id: true, matchSlug: true },
|
||||
});
|
||||
@@ -191,7 +248,7 @@ export class DataFetcherTask {
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`📡 Updating scores for ${liveMatches.length} live matches`,
|
||||
`📡 Updating scores for ${liveMatches.length} live matches`,
|
||||
);
|
||||
|
||||
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) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Live score update failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Phase 3: Odds + referee + lineups + sidelined
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
private async fetchOddsForMatches(): Promise<void> {
|
||||
this.logger.log("💰 Fetching odds for live matches...");
|
||||
this.logger.log("💰 Fetching odds for live matches...");
|
||||
|
||||
try {
|
||||
// Load both league filters
|
||||
@@ -266,11 +323,11 @@ export class DataFetcherTask {
|
||||
});
|
||||
|
||||
if (matchesToFetch.length === 0) {
|
||||
this.logger.log("💰 No matches to fetch odds for");
|
||||
this.logger.log("💰 No matches to fetch odds for");
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`💰 Fetching odds for ${matchesToFetch.length} matches`);
|
||||
this.logger.log(`💰 Fetching odds for ${matchesToFetch.length} matches`);
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
@@ -299,7 +356,7 @@ export class DataFetcherTask {
|
||||
// Retry failed matches (502/Timeout)
|
||||
if (failedMatches.length > 0) {
|
||||
this.logger.warn(
|
||||
`⚠️ Retrying ${failedMatches.length} failed matches (502/Timeout)...`,
|
||||
`âš ï¸ Retrying ${failedMatches.length} failed matches (502/Timeout)...`,
|
||||
);
|
||||
|
||||
for (const match of failedMatches) {
|
||||
@@ -307,19 +364,19 @@ export class DataFetcherTask {
|
||||
try {
|
||||
await this.processMatchOdds(match);
|
||||
successCount++;
|
||||
this.logger.log(`✅ Retry successful for match ${match.id}`);
|
||||
this.logger.log(`✅ Retry successful for match ${match.id}`);
|
||||
} catch (retryErr: unknown) {
|
||||
const message =
|
||||
retryErr instanceof Error ? retryErr.message : String(retryErr);
|
||||
this.logger.error(
|
||||
`❌ Retry failed for match ${match.id}: ${message}`,
|
||||
`⌠Retry failed for match ${match.id}: ${message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`💰 Odds complete: ${successCount} success, ${errorCount} errors (initially)`,
|
||||
`💰 Odds complete: ${successCount} success, ${errorCount} errors (initially)`,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -327,14 +384,36 @@ export class DataFetcherTask {
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Phase 4: Fill missing lineups (backup)
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
private async fillMissingLineups(): Promise<void> {
|
||||
try {
|
||||
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 },
|
||||
take: 30,
|
||||
});
|
||||
@@ -345,11 +424,11 @@ export class DataFetcherTask {
|
||||
);
|
||||
|
||||
if (toUpdate.length === 0) {
|
||||
this.logger.debug("👕 All lineups already filled");
|
||||
this.logger.debug("👕 All lineups already filled");
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`👕 Filling lineups for ${toUpdate.length} matches...`);
|
||||
this.logger.log(`👕 Filling lineups for ${toUpdate.length} matches...`);
|
||||
|
||||
for (const match of toUpdate) {
|
||||
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);
|
||||
} catch (err: unknown) {
|
||||
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(
|
||||
sport: SportType,
|
||||
@@ -650,7 +729,7 @@ export class DataFetcherTask {
|
||||
upsertCount + skippedCount === targetMatches.length
|
||||
) {
|
||||
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) {
|
||||
@@ -668,10 +747,10 @@ export class DataFetcherTask {
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// processMatchOdds — odds + referee + lineups + sidelined
|
||||
// (Preserved from original — no logic changes)
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// processMatchOdds — odds + referee + lineups + sidelined
|
||||
// (Preserved from original — no logic changes)
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
private async processMatchOdds(match: LiveMatchOddsTarget): Promise<void> {
|
||||
const matchSlug = match.matchSlug || "match";
|
||||
@@ -687,7 +766,7 @@ export class DataFetcherTask {
|
||||
let lineups: LiveLineupsJson | 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}`;
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
@@ -722,7 +801,7 @@ export class DataFetcherTask {
|
||||
typeof mainResp.data === "string" ? mainResp.data : "",
|
||||
);
|
||||
} catch {
|
||||
// Non-critical — referee is optional
|
||||
// Non-critical — referee is optional
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,7 +830,7 @@ export class DataFetcherTask {
|
||||
subs: substitutions?.stats?.away || [],
|
||||
},
|
||||
};
|
||||
this.logger.log(`👥 Lineups found for ${match.matchName}`);
|
||||
this.logger.log(`👥 Lineups found for ${match.matchName}`);
|
||||
} else {
|
||||
this.logger.debug(`No lineups (yet) for ${match.matchName}`);
|
||||
}
|
||||
@@ -779,7 +858,7 @@ export class DataFetcherTask {
|
||||
sidelined.awayTeam?.totalSidelined > 0
|
||||
) {
|
||||
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))
|
||||
) {
|
||||
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 {
|
||||
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
|
||||
* 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(
|
||||
html: string,
|
||||
@@ -914,17 +993,17 @@ export class DataFetcherTask {
|
||||
const lower = name.toLowerCase();
|
||||
|
||||
// Specific & Compound names FIRST
|
||||
if (lower.includes("ilk yarı/maç sonucu")) return "HTFT";
|
||||
if (lower.includes("1. yarı sonucu")) return "HT";
|
||||
if (lower.includes("çifte şans")) return "CS";
|
||||
if (lower.includes("ilk yarı/maç sonucu")) return "HTFT";
|
||||
if (lower.includes("1. yarı sonucu")) return "HT";
|
||||
if (lower.includes("çifte şans")) return "CS";
|
||||
|
||||
// General names LATER
|
||||
if (lower.includes("maç sonucu") && !lower.includes("handikap"))
|
||||
if (lower.includes("maç sonucu") && !lower.includes("handikap"))
|
||||
return "MS";
|
||||
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("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("karşılıklı gol")) return "KG";
|
||||
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("3,5 alt/üst") || lower.includes("3.5")) return "AU35";
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -934,7 +1013,7 @@ export class DataFetcherTask {
|
||||
*/
|
||||
private extractRefereeFromHtml(html: string): string | null {
|
||||
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 =
|
||||
/official-list-item--main[^>]*>\s*(?:<[^>]*>\s*)*?<span[^>]*official-name[^>]*>\s*([^<]+)/i;
|
||||
const mainMatch = mainOfficialPattern.exec(html);
|
||||
@@ -970,9 +1049,9 @@ export class DataFetcherTask {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Low-level Helpers (preserved — no logic changes)
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Low-level Helpers (preserved — no logic changes)
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
private shouldSkipInHistoricalMode(jobName: string): boolean {
|
||||
if (process.env.FEEDER_MODE === "historical") {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Cron } from "@nestjs/schedule";
|
||||
import { FeederService } from "../modules/feeder/feeder.service";
|
||||
import { TaskLockService } from "./task-lock.service";
|
||||
|
||||
@Injectable()
|
||||
export class HistoricalResultsSyncTask {
|
||||
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 {
|
||||
if (process.env.FEEDER_MODE === "historical") {
|
||||
@@ -25,6 +29,10 @@ export class HistoricalResultsSyncTask {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.taskLock.runWithLease(
|
||||
"syncPreviousDayCompletedMatches",
|
||||
6 * 60 * 60 * 1000,
|
||||
async () => {
|
||||
this.logger.log(
|
||||
"Starting previous-day completed match sync for football and basketball...",
|
||||
);
|
||||
@@ -37,5 +45,8 @@ export class HistoricalResultsSyncTask {
|
||||
`Previous-day completed match sync failed: ${error.message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Cron } from "@nestjs/schedule";
|
||||
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()
|
||||
export class LimitResetterTask {
|
||||
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 {
|
||||
if (process.env.FEEDER_MODE === "historical") {
|
||||
@@ -22,13 +38,15 @@ export class LimitResetterTask {
|
||||
@Cron("0 3 * * *", { timeZone: "Europe/Istanbul" })
|
||||
async resetUsageLimits() {
|
||||
if (this.shouldSkipInHistoricalMode("resetUsageLimits")) return;
|
||||
await this.taskLock.runWithLease(
|
||||
"resetUsageLimits",
|
||||
30 * 60 * 1000,
|
||||
async () => {
|
||||
this.logger.log("Starting daily usage limit reset job...");
|
||||
|
||||
try {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const today = getDateOnlyValueForTimeZone(this.timeZone);
|
||||
|
||||
// Reset all limits that were last reset before today
|
||||
const result = await this.prisma.usageLimit.updateMany({
|
||||
where: {
|
||||
lastResetDate: { lt: today },
|
||||
@@ -50,6 +68,9 @@ export class LimitResetterTask {
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Limit reset job failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,28 +79,53 @@ export class LimitResetterTask {
|
||||
@Cron("0 4 * * *", { timeZone: "Europe/Istanbul" })
|
||||
async cleanupOldData() {
|
||||
if (this.shouldSkipInHistoricalMode("cleanupOldData")) return;
|
||||
await this.taskLock.runWithLease(
|
||||
"cleanupOldData",
|
||||
60 * 60 * 1000,
|
||||
async () => {
|
||||
this.logger.log("Starting data cleanup job...");
|
||||
|
||||
try {
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
// Delete old AI prediction logs
|
||||
const deletedLogs = await this.prisma.aiPredictionsLog.deleteMany({
|
||||
where: {
|
||||
createdAt: { lt: thirtyDaysAgo },
|
||||
},
|
||||
});
|
||||
|
||||
// Delete old live matches (finished more than 1 day ago)
|
||||
// Historical data is already persisted in the 'matches' table
|
||||
const oneDayAgo = new Date();
|
||||
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
|
||||
const yesterdayDate = getShiftedDateStringInTimeZone(
|
||||
-1,
|
||||
this.timeZone,
|
||||
);
|
||||
const { startMs: yesterdayStartMs } = getDayBoundsForTimeZone(
|
||||
yesterdayDate,
|
||||
this.timeZone,
|
||||
);
|
||||
const liveMatchCutoff = new Date(yesterdayStartMs);
|
||||
|
||||
const deletedLiveMatches = await this.prisma.liveMatch.deleteMany({
|
||||
where: {
|
||||
state: "Finished",
|
||||
updatedAt: { lt: oneDayAgo },
|
||||
updatedAt: { lt: liveMatchCutoff },
|
||||
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 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,6 +135,9 @@ export class LimitResetterTask {
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Cleanup job failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +146,10 @@ export class LimitResetterTask {
|
||||
@Cron("0 0 * * *", { timeZone: "Europe/Istanbul" })
|
||||
async checkSubscriptions() {
|
||||
if (this.shouldSkipInHistoricalMode("checkSubscriptions")) return;
|
||||
await this.taskLock.runWithLease(
|
||||
"checkSubscriptions",
|
||||
30 * 60 * 1000,
|
||||
async () => {
|
||||
this.logger.log("Checking expired subscriptions...");
|
||||
|
||||
try {
|
||||
@@ -118,5 +171,8 @@ export class LimitResetterTask {
|
||||
} catch (error: any) {
|
||||
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 { ScheduleModule } from "@nestjs/schedule";
|
||||
import { HttpModule } from "@nestjs/axios";
|
||||
import { DataFetcherTask } from "./data-fetcher.task";
|
||||
import { HistoricalResultsSyncTask } from "./historical-results-sync.task";
|
||||
import { LimitResetterTask } from "./limit-resetter.task";
|
||||
import { TaskLockService } from "./task-lock.service";
|
||||
import { DatabaseModule } from "../database/database.module";
|
||||
import { FeederModule } from "../modules/feeder/feeder.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ScheduleModule.forRoot(),
|
||||
HttpModule.register({
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
@@ -20,7 +19,12 @@ import { FeederModule } from "../modules/feeder/feeder.module";
|
||||
DatabaseModule,
|
||||
FeederModule,
|
||||
],
|
||||
providers: [DataFetcherTask, HistoricalResultsSyncTask, LimitResetterTask],
|
||||
providers: [
|
||||
TaskLockService,
|
||||
DataFetcherTask,
|
||||
HistoricalResultsSyncTask,
|
||||
LimitResetterTask,
|
||||
],
|
||||
exports: [DataFetcherTask, HistoricalResultsSyncTask, LimitResetterTask],
|
||||
})
|
||||
export class TasksModule {}
|
||||
|
||||
Reference in New Issue
Block a user