5 Commits

Author SHA1 Message Date
fahricansecer fb53fdf1df Merge pull request 'v26-shadow' (#2) from v26-shadow into main
Deploy Iddaai Backend / build-and-deploy (push) Successful in 2m51s
Reviewed-on: #2
2026-04-23 22:29:23 +03:00
fahricansecer 634204acf0 v28 2026-04-23 22:22:59 +03:00
fahricansecer df428ed1e8 gg 2026-04-22 02:17:02 +03:00
fahricansecer 2ccd6831eb gg 2026-04-21 16:53:56 +03:00
fahricansecer 1346924387 gg 2026-04-19 13:23:00 +03:00
70 changed files with 440541 additions and 1177 deletions
@@ -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.
+871
View File
@@ -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
1 iter Logloss
2 0 0.692389481
3 1 0.6916338586
4 2 0.6910159214
5 3 0.6903417151
6 4 0.6896961461
7 5 0.6890979366
8 6 0.6884946167
9 7 0.6879503686
10 8 0.6874528094
11 9 0.6869036785
12 10 0.6863761921
13 11 0.6859038678
14 12 0.685410175
15 13 0.6849483392
16 14 0.6845417792
17 15 0.6841038875
18 16 0.6836957422
19 17 0.6832947461
20 18 0.6829014105
21 19 0.6825264546
22 20 0.6822106577
23 21 0.6818649349
24 22 0.6815467855
25 23 0.6812293319
26 24 0.6808837443
27 25 0.6805816494
28 26 0.6803209634
29 27 0.6800350862
30 28 0.6797703947
31 29 0.6794926675
32 30 0.6792251865
33 31 0.6789670166
34 32 0.678722402
35 33 0.678476935
36 34 0.6782297335
37 35 0.6780226701
38 36 0.6778291026
39 37 0.6776045324
40 38 0.6773969079
41 39 0.6771819602
42 40 0.6769816736
43 41 0.6767984027
44 42 0.6766201184
45 43 0.6764394377
46 44 0.6762698797
47 45 0.6760974263
48 46 0.6759245179
49 47 0.6757673909
50 48 0.6756172628
51 49 0.675474531
52 50 0.6753286933
53 51 0.6751900513
54 52 0.6750574835
55 53 0.6749329567
56 54 0.6748033265
57 55 0.6746797823
58 56 0.674535525
59 57 0.6744256514
60 58 0.674310819
61 59 0.6741967947
62 60 0.6740879654
63 61 0.6739772476
64 62 0.67388281
65 63 0.6737789726
66 64 0.6736812332
67 65 0.6735930009
68 66 0.6734947116
69 67 0.6733961481
70 68 0.6732990195
71 69 0.6732133575
72 70 0.673111539
73 71 0.6730080451
74 72 0.6729157861
75 73 0.6728347949
76 74 0.6727640693
77 75 0.6726808811
78 76 0.6726029645
79 77 0.6725356026
80 78 0.6724606887
81 79 0.6723849561
82 80 0.6723050519
83 81 0.6722508802
84 82 0.6721773904
85 83 0.6721007598
86 84 0.6720353564
87 85 0.6719790902
88 86 0.6719140024
89 87 0.6718573633
90 88 0.671795602
91 89 0.6717369134
92 90 0.6716711079
93 91 0.6716070843
94 92 0.6715517232
95 93 0.6714957378
96 94 0.6714364567
97 95 0.6713881758
98 96 0.6713336502
99 97 0.6712700267
100 98 0.6712154424
101 99 0.6711600413
102 100 0.6711060533
103 101 0.6710494943
104 102 0.6709936897
105 103 0.6709472183
106 104 0.6708914508
107 105 0.6708388195
108 106 0.6707885854
109 107 0.6707454167
110 108 0.6706973013
111 109 0.6706577031
112 110 0.67061108
113 111 0.6705625485
114 112 0.6705146484
115 113 0.6704704423
116 114 0.6704155922
117 115 0.6703687117
118 116 0.6703324232
119 117 0.6702884624
120 118 0.670253478
121 119 0.6702140804
122 120 0.6701682529
123 121 0.6701320588
124 122 0.6700939824
125 123 0.6700655902
126 124 0.6700190743
127 125 0.6699792296
128 126 0.6699379404
129 127 0.669895454
130 128 0.6698563938
131 129 0.6698215571
132 130 0.6697857067
133 131 0.6697449303
134 132 0.6697052425
135 133 0.6696695553
136 134 0.6696269265
137 135 0.6695969271
138 136 0.6695489786
139 137 0.6695173859
140 138 0.6694811164
141 139 0.6694477439
142 140 0.6694082161
143 141 0.6693679185
144 142 0.6693341916
145 143 0.6692933159
146 144 0.6692619696
147 145 0.6692229289
148 146 0.6691840164
149 147 0.6691581406
150 148 0.6691177196
151 149 0.6690851126
152 150 0.6690518144
153 151 0.6690149711
154 152 0.668993877
155 153 0.6689596579
156 154 0.6689372651
157 155 0.6689003045
158 156 0.6688680182
159 157 0.6688348164
160 158 0.6687947046
161 159 0.6687605251
162 160 0.668726253
163 161 0.6686862718
164 162 0.668663478
165 163 0.6686399521
166 164 0.6686058279
167 165 0.6685761282
168 166 0.6685469327
169 167 0.6685157003
170 168 0.6684805143
171 169 0.6684485765
172 170 0.6684144429
173 171 0.6683849752
174 172 0.6683568537
175 173 0.6683266628
176 174 0.6682937842
177 175 0.6682657097
178 176 0.6682301443
179 177 0.6681995916
180 178 0.6681658267
181 179 0.6681422687
182 180 0.6681216601
183 181 0.6680899019
184 182 0.6680676394
185 183 0.6680413672
186 184 0.6680088406
187 185 0.6679873982
188 186 0.6679663544
189 187 0.6679417375
190 188 0.6679100197
191 189 0.667881208
192 190 0.6678475427
193 191 0.6678310341
194 192 0.6678060257
195 193 0.6677789336
196 194 0.6677478773
197 195 0.6677212408
198 196 0.667704316
199 197 0.6676819639
200 198 0.6676554448
201 199 0.6676318346
202 200 0.6676074705
203 201 0.6675849784
204 202 0.6675631744
205 203 0.6675397619
206 204 0.6675169086
207 205 0.6674864762
208 206 0.6674670714
209 207 0.6674375599
210 208 0.6674148457
211 209 0.6673974446
212 210 0.6673812139
213 211 0.6673515687
214 212 0.6673197956
215 213 0.6672900754
216 214 0.6672550009
217 215 0.6672271563
218 216 0.667204521
219 217 0.667181968
220 218 0.6671640023
221 219 0.66714351
222 220 0.6671167156
223 221 0.6670915937
224 222 0.6670595279
225 223 0.667033994
226 224 0.6670008246
227 225 0.6669858319
228 226 0.6669553964
229 227 0.6669274683
230 228 0.666896348
231 229 0.6668698686
232 230 0.6668513411
233 231 0.6668309985
234 232 0.6668058585
235 233 0.6667845908
236 234 0.6667582863
237 235 0.6667332943
238 236 0.6667070085
239 237 0.6666907315
240 238 0.6666633028
241 239 0.6666406707
242 240 0.6666134624
243 241 0.6665850522
244 242 0.6665631193
245 243 0.6665412643
246 244 0.6665168385
247 245 0.6664904845
248 246 0.6664678274
249 247 0.6664539777
250 248 0.6664334121
251 249 0.6664121724
252 250 0.666392034
253 251 0.666366899
254 252 0.6663414098
255 253 0.6663157816
256 254 0.6662989799
257 255 0.6662696102
258 256 0.6662479711
259 257 0.6662231874
260 258 0.6661947927
261 259 0.6661669951
262 260 0.6661426137
263 261 0.6661216749
264 262 0.6660983123
265 263 0.6660803402
266 264 0.6660617842
267 265 0.6660443878
268 266 0.6660176079
269 267 0.6659967546
270 268 0.6659751467
271 269 0.6659539329
272 270 0.6659263951
273 271 0.6659038921
274 272 0.6658767418
275 273 0.6658510507
276 274 0.6658210119
277 275 0.6657963011
278 276 0.6657748552
279 277 0.6657490013
280 278 0.665732402
281 279 0.6657118786
282 280 0.665684467
283 281 0.6656584634
284 282 0.6656309991
285 283 0.6656073482
286 284 0.6655890957
287 285 0.6655665563
288 286 0.6655452454
289 287 0.6655255286
290 288 0.6655053548
291 289 0.6654893396
292 290 0.6654648912
293 291 0.6654442759
294 292 0.6654173127
295 293 0.6653914518
296 294 0.6653648946
297 295 0.665344141
298 296 0.6653140817
299 297 0.665295365
300 298 0.6652787488
301 299 0.6652502991
302 300 0.665231168
303 301 0.6652136682
304 302 0.6651903001
305 303 0.6651697153
306 304 0.6651525958
307 305 0.6651322685
308 306 0.6651113828
309 307 0.6650886807
310 308 0.6650622251
311 309 0.6650429987
312 310 0.665015513
313 311 0.6650019022
314 312 0.664979951
315 313 0.6649549638
316 314 0.6649340455
317 315 0.6649162445
318 316 0.6649048119
319 317 0.6648796463
320 318 0.6648605481
321 319 0.6648429084
322 320 0.6648238121
323 321 0.6647969527
324 322 0.6647854723
325 323 0.6647589304
326 324 0.6647429024
327 325 0.6647237508
328 326 0.6647059396
329 327 0.664686288
330 328 0.6646532527
331 329 0.6646306438
332 330 0.6646098516
333 331 0.6645858284
334 332 0.6645707188
335 333 0.6645485788
336 334 0.6645305696
337 335 0.6645108881
338 336 0.6644923286
339 337 0.6644805222
340 338 0.6644572776
341 339 0.6644320741
342 340 0.6644115048
343 341 0.6643949013
344 342 0.6643619789
345 343 0.6643389502
346 344 0.6643088915
347 345 0.664286972
348 346 0.664274149
349 347 0.6642536926
350 348 0.6642357634
351 349 0.664207914
352 350 0.6641853097
353 351 0.6641654917
354 352 0.664143804
355 353 0.6641290647
356 354 0.6641117244
357 355 0.6640880219
358 356 0.6640669415
359 357 0.6640462999
360 358 0.664030296
361 359 0.6640028542
362 360 0.6639813347
363 361 0.6639597941
364 362 0.6639429832
365 363 0.6639222708
366 364 0.6639065546
367 365 0.6638823236
368 366 0.6638648195
369 367 0.6638436235
370 368 0.6638208732
371 369 0.6637956357
372 370 0.6637718453
373 371 0.663756918
374 372 0.6637353525
375 373 0.6637143112
376 374 0.6636956547
377 375 0.663680995
378 376 0.66366728
379 377 0.6636487567
380 378 0.6636266904
381 379 0.6636116064
382 380 0.6635902746
383 381 0.6635654896
384 382 0.6635393029
385 383 0.6635171734
386 384 0.663500789
387 385 0.663477743
388 386 0.6634584806
389 387 0.6634337499
390 388 0.6634135584
391 389 0.6633868455
392 390 0.6633755323
393 391 0.663356103
394 392 0.6633337631
395 393 0.663319422
396 394 0.6632911566
397 395 0.6632687875
398 396 0.6632431997
399 397 0.6632189331
400 398 0.663201035
401 399 0.6631898553
402 400 0.6631712482
403 401 0.663143025
404 402 0.663121538
405 403 0.6631087792
406 404 0.6630859067
407 405 0.663066483
408 406 0.6630443652
409 407 0.6630250376
410 408 0.6630007822
411 409 0.6629768728
412 410 0.6629528093
413 411 0.6629260936
414 412 0.6629102182
415 413 0.6628863488
416 414 0.6628648972
417 415 0.6628454339
418 416 0.6628200274
419 417 0.6627942591
420 418 0.6627744647
421 419 0.662765485
422 420 0.6627503257
423 421 0.6627323029
424 422 0.6627111509
425 423 0.6626785863
426 424 0.6626576561
427 425 0.6626363113
428 426 0.6626181065
429 427 0.66259794
430 428 0.6625765658
431 429 0.6625526572
432 430 0.66253135
433 431 0.6625035695
434 432 0.662480212
435 433 0.6624611632
436 434 0.6624332625
437 435 0.6624120584
438 436 0.6623941719
439 437 0.6623766304
440 438 0.6623623329
441 439 0.6623442925
442 440 0.6623212715
443 441 0.6623025941
444 442 0.6622749791
445 443 0.6622534499
446 444 0.6622305473
447 445 0.6622059333
448 446 0.6621871707
449 447 0.6621638454
450 448 0.6621511296
451 449 0.6621349978
452 450 0.6621120424
453 451 0.6620958271
454 452 0.6620793528
455 453 0.6620572713
456 454 0.6620395025
457 455 0.6620188044
458 456 0.6620017347
459 457 0.6619811454
460 458 0.6619695569
461 459 0.661952377
462 460 0.6619237442
463 461 0.6619089407
464 462 0.6618886168
465 463 0.6618831383
466 464 0.6618690774
467 465 0.661845878
468 466 0.6618290213
469 467 0.6618050064
470 468 0.6617832833
471 469 0.6617652311
472 470 0.6617443144
473 471 0.6617202619
474 472 0.6617005831
475 473 0.6616824419
476 474 0.6616538226
477 475 0.6616314155
478 476 0.6616127861
479 477 0.6616029072
480 478 0.6615843751
481 479 0.661563216
482 480 0.6615432257
483 481 0.6615263324
484 482 0.6615033259
485 483 0.661484293
486 484 0.6614678231
487 485 0.6614463024
488 486 0.6614155436
489 487 0.6613958945
490 488 0.661380611
491 489 0.6613677802
492 490 0.6613530086
493 491 0.6613248211
494 492 0.6613059359
495 493 0.6612729965
496 494 0.6612624948
497 495 0.6612401679
498 496 0.6612191637
499 497 0.6611912219
500 498 0.6611773017
501 499 0.6611638216
502 500 0.6611450533
503 501 0.6611179111
504 502 0.6610959069
505 503 0.6610728788
506 504 0.6610436668
507 505 0.6610188976
508 506 0.6610030555
509 507 0.6609831174
510 508 0.6609586562
511 509 0.660935882
512 510 0.6609202024
513 511 0.6609011137
514 512 0.6608726737
515 513 0.6608608849
516 514 0.6608387256
517 515 0.6608136063
518 516 0.6607946343
519 517 0.6607703935
520 518 0.6607509625
521 519 0.6607238109
522 520 0.6606999858
523 521 0.6606813873
524 522 0.6606610372
525 523 0.660638456
526 524 0.6606156483
527 525 0.6605968623
528 526 0.6605735776
529 527 0.6605517294
530 528 0.6605309239
531 529 0.6605086434
532 530 0.6604803349
533 531 0.6604566326
534 532 0.6604430839
535 533 0.6604273738
536 534 0.6604048016
537 535 0.6603845173
538 536 0.6603669212
539 537 0.6603488983
540 538 0.6603176881
541 539 0.6602953862
542 540 0.6602672025
543 541 0.6602568636
544 542 0.660235705
545 543 0.6602152295
546 544 0.6601897709
547 545 0.6601683731
548 546 0.6601472267
549 547 0.6601262337
550 548 0.6601119991
551 549 0.6600869973
552 550 0.6600667497
553 551 0.6600397508
554 552 0.660016863
555 553 0.6599933158
556 554 0.6599632649
557 555 0.6599446007
558 556 0.6599138126
559 557 0.6598965504
560 558 0.6598785723
561 559 0.659860838
562 560 0.6598408724
563 561 0.6598244857
564 562 0.6598082469
565 563 0.6597851673
566 564 0.6597683521
567 565 0.6597479006
568 566 0.6597310938
569 567 0.6597096581
570 568 0.6596862311
571 569 0.6596574779
572 570 0.6596385418
573 571 0.6596189903
574 572 0.65959275
575 573 0.6595730662
576 574 0.6595566809
577 575 0.6595365076
578 576 0.6595163446
579 577 0.6594816637
580 578 0.6594570142
581 579 0.6594353055
582 580 0.6594162362
583 581 0.659395036
584 582 0.6593798831
585 583 0.6593556719
586 584 0.6593292627
587 585 0.6592976737
588 586 0.6592754841
589 587 0.6592510441
590 588 0.6592290326
591 589 0.6592097404
592 590 0.6591876204
593 591 0.6591705995
594 592 0.6591456195
595 593 0.6591107122
596 594 0.6590819533
597 595 0.6590551327
598 596 0.6590373916
599 597 0.6590177149
600 598 0.6589946095
601 599 0.6589697628
602 600 0.6589442269
603 601 0.6589182437
604 602 0.6588837179
605 603 0.6588674101
606 604 0.6588406916
607 605 0.6588149945
608 606 0.6587866031
609 607 0.6587636648
610 608 0.6587502469
611 609 0.6587292784
612 610 0.6587104112
613 611 0.6586953782
614 612 0.6586641191
615 613 0.6586450136
616 614 0.6586136263
617 615 0.6585862768
618 616 0.6585585235
619 617 0.6585371631
620 618 0.6585092632
621 619 0.6584914317
622 620 0.6584662432
623 621 0.6584454668
624 622 0.6584249408
625 623 0.6583931228
626 624 0.6583660767
627 625 0.658354264
628 626 0.6583253625
629 627 0.6582968632
630 628 0.6582687399
631 629 0.658242535
632 630 0.6582199874
633 631 0.6581918101
634 632 0.6581735218
635 633 0.6581445869
636 634 0.6581202427
637 635 0.6580977862
638 636 0.6580724179
639 637 0.6580426322
640 638 0.6580111256
641 639 0.6579834747
642 640 0.6579541367
643 641 0.6579254503
644 642 0.657898555
645 643 0.6578676875
646 644 0.6578324163
647 645 0.6578062223
648 646 0.6577760631
649 647 0.6577483474
650 648 0.6577249642
651 649 0.6576974966
652 650 0.657675114
653 651 0.6576447891
654 652 0.6576102356
655 653 0.6575793887
656 654 0.6575543309
657 655 0.6575340787
658 656 0.6575061464
659 657 0.657476113
660 658 0.6574447014
661 659 0.6574247361
662 660 0.6574034983
663 661 0.6573783832
664 662 0.657357694
665 663 0.6573411592
666 664 0.6573118559
667 665 0.6572819076
668 666 0.6572430097
669 667 0.6572160391
670 668 0.6571931413
671 669 0.6571737099
672 670 0.6571532872
673 671 0.6571208939
674 672 0.6570887673
675 673 0.6570633692
676 674 0.6570454361
677 675 0.6570231031
678 676 0.6570052089
679 677 0.6569855794
680 678 0.6569579709
681 679 0.6569333354
682 680 0.6569069617
683 681 0.6568931857
684 682 0.6568734532
685 683 0.6568435196
686 684 0.6568108038
687 685 0.6567811374
688 686 0.6567467284
689 687 0.6567172734
690 688 0.6566967606
691 689 0.6566720128
692 690 0.6566441608
693 691 0.6566172287
694 692 0.6565952549
695 693 0.6565702687
696 694 0.6565392213
697 695 0.6565157938
698 696 0.6564902789
699 697 0.6564644734
700 698 0.6564349549
701 699 0.6564046572
702 700 0.6563744107
703 701 0.6563525063
704 702 0.6563189867
705 703 0.6562939062
706 704 0.6562739297
707 705 0.656256438
708 706 0.6562366475
709 707 0.6562073096
710 708 0.6561864222
711 709 0.6561578826
712 710 0.6561208567
713 711 0.6560924703
714 712 0.6560656907
715 713 0.6560362588
716 714 0.6560124527
717 715 0.6559875055
718 716 0.6559547281
719 717 0.6559230866
720 718 0.6558924823
721 719 0.6558676469
722 720 0.6558459277
723 721 0.6558149638
724 722 0.6557812248
725 723 0.6557546502
726 724 0.6557274948
727 725 0.6557044723
728 726 0.6556751811
729 727 0.6556539158
730 728 0.6556182915
731 729 0.6555977079
732 730 0.6555667903
733 731 0.6555394075
734 732 0.6555122742
735 733 0.6554814941
736 734 0.6554517373
737 735 0.655429552
738 736 0.655396579
739 737 0.6553735864
740 738 0.6553472597
741 739 0.6553252832
742 740 0.6552971659
743 741 0.6552763852
744 742 0.6552488203
745 743 0.65521229
746 744 0.6551949744
747 745 0.6551673797
748 746 0.6551421856
749 747 0.6551255516
750 748 0.6551019608
751 749 0.6550758728
752 750 0.655051966
753 751 0.6550351058
754 752 0.6549998756
755 753 0.6549721212
756 754 0.6549401744
757 755 0.6549207325
758 756 0.6548900891
759 757 0.6548682731
760 758 0.6548418938
761 759 0.6548234717
762 760 0.6547996833
763 761 0.6547726174
764 762 0.6547509314
765 763 0.6547168175
766 764 0.6546907846
767 765 0.6546671611
768 766 0.6546475893
769 767 0.6546206223
770 768 0.6545874193
771 769 0.6545620629
772 770 0.6545346297
773 771 0.6545172316
774 772 0.6544943049
775 773 0.6544632323
776 774 0.6544384097
777 775 0.6544084745
778 776 0.6543765257
779 777 0.6543536123
780 778 0.6543303593
781 779 0.6543005831
782 780 0.6542678123
783 781 0.6542439303
784 782 0.6542100401
785 783 0.6541836178
786 784 0.654158129
787 785 0.6541343464
788 786 0.6541092921
789 787 0.6540812254
790 788 0.654060259
791 789 0.6540467253
792 790 0.6540306837
793 791 0.6540103667
794 792 0.6539821302
795 793 0.6539577914
796 794 0.653923724
797 795 0.6539086888
798 796 0.6538798424
799 797 0.6538566996
800 798 0.6538290752
801 799 0.6538051255
802 800 0.6537917354
803 801 0.6537684302
804 802 0.6537402991
805 803 0.6537165427
806 804 0.6536853601
807 805 0.6536681479
808 806 0.6536409101
809 807 0.6536120189
810 808 0.6535912493
811 809 0.6535617421
812 810 0.6535315174
813 811 0.6534972927
814 812 0.6534818476
815 813 0.6534498323
816 814 0.6534305025
817 815 0.6534081059
818 816 0.6533765804
819 817 0.6533441549
820 818 0.6533053405
821 819 0.6532838469
822 820 0.6532604302
823 821 0.6532364412
824 822 0.6532100089
825 823 0.6531782515
826 824 0.6531449701
827 825 0.653115452
828 826 0.6530787602
829 827 0.653052397
830 828 0.6530313579
831 829 0.6530010363
832 830 0.6529752146
833 831 0.652954801
834 832 0.6529330351
835 833 0.6528993709
836 834 0.6528665883
837 835 0.6528413041
838 836 0.6528217161
839 837 0.6527978782
840 838 0.6527789461
841 839 0.6527432001
842 840 0.6527139767
843 841 0.6526857244
844 842 0.652657086
845 843 0.6526355016
846 844 0.6526054936
847 845 0.6525793707
848 846 0.6525584692
849 847 0.6525279747
850 848 0.6525038765
851 849 0.6524849104
852 850 0.6524610603
853 851 0.6524357337
854 852 0.6524082286
855 853 0.65238051
856 854 0.6523557826
857 855 0.6523391233
858 856 0.652325347
859 857 0.6522924958
860 858 0.6522623584
861 859 0.6522343891
862 860 0.6522094424
863 861 0.6521841478
864 862 0.6521657946
865 863 0.6521304278
866 864 0.6521045712
867 865 0.6520753696
868 866 0.6520519528
869 867 0.6520216555
870 868 0.6519926935
871 869 0.6519734186
Binary file not shown.
+871
View File
@@ -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
1 iter Logloss
2 0 0.6924099937
3 1 0.6916660956
4 2 0.691108145
5 3 0.6904585078
6 4 0.689812816
7 5 0.689192261
8 6 0.6886032715
9 7 0.6880706742
10 8 0.6876192378
11 9 0.6870868859
12 10 0.6865493528
13 11 0.686105086
14 12 0.6856345086
15 13 0.6852027185
16 14 0.6848238481
17 15 0.6844045699
18 16 0.6840077621
19 17 0.6836197496
20 18 0.6832475033
21 19 0.6829012069
22 20 0.6825880966
23 21 0.6822424968
24 22 0.6819180513
25 23 0.6816384467
26 24 0.6813262593
27 25 0.6810353411
28 26 0.6808138172
29 27 0.6805550049
30 28 0.680347991
31 29 0.680089679
32 30 0.6798451919
33 31 0.6796090443
34 32 0.6793890865
35 33 0.6791683772
36 34 0.6789766369
37 35 0.6787930242
38 36 0.6786087714
39 37 0.6784161299
40 38 0.6782227897
41 39 0.6780242369
42 40 0.6778499631
43 41 0.6776975784
44 42 0.6775231674
45 43 0.6773582124
46 44 0.6772234666
47 45 0.6770659843
48 46 0.6769049529
49 47 0.6767664194
50 48 0.6766584917
51 49 0.6765507257
52 50 0.6764489911
53 51 0.6763947956
54 52 0.6762778712
55 53 0.6761865366
56 54 0.6760679685
57 55 0.6759774874
58 56 0.6758500622
59 57 0.6757625065
60 58 0.6756876412
61 59 0.6756151069
62 60 0.6755303655
63 61 0.6754565036
64 62 0.6753738983
65 63 0.6752897299
66 64 0.6752115539
67 65 0.6751595431
68 66 0.6750764658
69 67 0.6750179194
70 68 0.6749408803
71 69 0.6748795802
72 70 0.674790372
73 71 0.6747239773
74 72 0.6746701254
75 73 0.6746120937
76 74 0.6745550085
77 75 0.6744855074
78 76 0.6744264172
79 77 0.674381715
80 78 0.6743331681
81 79 0.67428564
82 80 0.6742202413
83 81 0.6741620971
84 82 0.6741109453
85 83 0.6740556003
86 84 0.6740146772
87 85 0.673983295
88 86 0.6739595301
89 87 0.6739336659
90 88 0.673890361
91 89 0.673863586
92 90 0.6738190616
93 91 0.6737799295
94 92 0.6737364374
95 93 0.6737093719
96 94 0.6736630475
97 95 0.67364367
98 96 0.6735998081
99 97 0.6735526984
100 98 0.6735012924
101 99 0.6734818024
102 100 0.6734379341
103 101 0.6734059869
104 102 0.6733740852
105 103 0.6733330971
106 104 0.6733060254
107 105 0.6732755898
108 106 0.6732294722
109 107 0.6732035176
110 108 0.673196437
111 109 0.6731652709
112 110 0.673138808
113 111 0.6731062725
114 112 0.6730726625
115 113 0.6730285927
116 114 0.6729872702
117 115 0.6729721425
118 116 0.6729564624
119 117 0.6729312424
120 118 0.6729354345
121 119 0.6729085401
122 120 0.6728898322
123 121 0.6728773638
124 122 0.6728618874
125 123 0.6728540413
126 124 0.6728441291
127 125 0.672815631
128 126 0.6728082021
129 127 0.6727900064
130 128 0.6727649552
131 129 0.6727467657
132 130 0.6727396032
133 131 0.6727245271
134 132 0.6726955143
135 133 0.67269209
136 134 0.672677932
137 135 0.6726540285
138 136 0.6726288583
139 137 0.6725863431
140 138 0.6725837967
141 139 0.6725772977
142 140 0.6725685594
143 141 0.6725553829
144 142 0.6725484347
145 143 0.6725306172
146 144 0.672543149
147 145 0.6725196247
148 146 0.6725226452
149 147 0.6725056913
150 148 0.6724771476
151 149 0.6724439435
152 150 0.672442532
153 151 0.6724303064
154 152 0.6724235788
155 153 0.6724294499
156 154 0.6724285935
157 155 0.6724172017
158 156 0.6724130745
159 157 0.6723860878
160 158 0.6723707604
161 159 0.6723566111
162 160 0.6723469906
163 161 0.6723287161
164 162 0.6723155898
165 163 0.6722970834
166 164 0.6722872244
167 165 0.6722800481
168 166 0.6722550973
169 167 0.6722394313
170 168 0.6722204135
171 169 0.6721982148
172 170 0.6721971176
173 171 0.6721880705
174 172 0.672179176
175 173 0.6721769709
176 174 0.6721693215
177 175 0.6721581386
178 176 0.6721638661
179 177 0.6721598475
180 178 0.6721433342
181 179 0.6721335599
182 180 0.6721300594
183 181 0.6721153533
184 182 0.6721076397
185 183 0.6721009911
186 184 0.6720999252
187 185 0.6720953028
188 186 0.6720942505
189 187 0.6720856237
190 188 0.6720876136
191 189 0.6720880182
192 190 0.6720743856
193 191 0.6720598415
194 192 0.6720563492
195 193 0.6720389527
196 194 0.6720317324
197 195 0.672000736
198 196 0.6719895017
199 197 0.6719725302
200 198 0.6719770493
201 199 0.6719667172
202 200 0.6719511616
203 201 0.6719427289
204 202 0.6719299116
205 203 0.6719106583
206 204 0.6718967065
207 205 0.671890967
208 206 0.6718896293
209 207 0.6718883534
210 208 0.6718827289
211 209 0.6718763224
212 210 0.67187262
213 211 0.6718590402
214 212 0.6718455115
215 213 0.6718253747
216 214 0.671794877
217 215 0.6717873786
218 216 0.6717765089
219 217 0.6717616726
220 218 0.6717499215
221 219 0.6717326052
222 220 0.6717161937
223 221 0.6717056951
224 222 0.6717021438
225 223 0.6716868488
226 224 0.6716751909
227 225 0.671670116
228 226 0.6716558757
229 227 0.6716559962
230 228 0.6716487875
231 229 0.6716427451
232 230 0.6716323255
233 231 0.6716303547
234 232 0.6716309509
235 233 0.6716215401
236 234 0.6716162103
237 235 0.6716135097
238 236 0.6716156696
239 237 0.6716020054
240 238 0.6715921704
241 239 0.6715804466
242 240 0.6715882966
243 241 0.6715753942
244 242 0.6715752261
245 243 0.6715625509
246 244 0.6715628214
247 245 0.6715601629
248 246 0.6715576255
249 247 0.6715550274
250 248 0.6715448645
251 249 0.6715308166
252 250 0.671519334
253 251 0.6715184071
254 252 0.6715163019
255 253 0.6715096094
256 254 0.6714992963
257 255 0.6714917256
258 256 0.671477406
259 257 0.6714741542
260 258 0.6714576155
261 259 0.6714473645
262 260 0.6714427232
263 261 0.6714364275
264 262 0.6714339587
265 263 0.6714336287
266 264 0.6714283568
267 265 0.6714271895
268 266 0.671413471
269 267 0.6714072396
270 268 0.6714002677
271 269 0.6714001163
272 270 0.6713933952
273 271 0.6713926761
274 272 0.6713836619
275 273 0.6713772112
276 274 0.6713603715
277 275 0.6713560246
278 276 0.6713837913
279 277 0.6713684274
280 278 0.6713619356
281 279 0.6713584836
282 280 0.6713673572
283 281 0.6713625568
284 282 0.6713542652
285 283 0.6713512017
286 284 0.671342038
287 285 0.6713279798
288 286 0.6713123285
289 287 0.6713035326
290 288 0.6713022203
291 289 0.671296041
292 290 0.6712829551
293 291 0.6712769751
294 292 0.6712702915
295 293 0.6712379343
296 294 0.6712192006
297 295 0.6712074061
298 296 0.6711953324
299 297 0.6711891001
300 298 0.6711870526
301 299 0.6711812809
302 300 0.6711768946
303 301 0.6711845012
304 302 0.6711869636
305 303 0.671186884
306 304 0.6711890401
307 305 0.6711868603
308 306 0.6711900892
309 307 0.6711884242
310 308 0.6711837119
311 309 0.6711766645
312 310 0.671172959
313 311 0.6711740433
314 312 0.6711715069
315 313 0.6711589843
316 314 0.6711446402
317 315 0.6711415366
318 316 0.6711359351
319 317 0.671143361
320 318 0.6711353638
321 319 0.6711444387
322 320 0.6711487352
323 321 0.67114436
324 322 0.6711444722
325 323 0.6711325635
326 324 0.6711269403
327 325 0.6711154078
328 326 0.6711203043
329 327 0.6711241333
330 328 0.6711213497
331 329 0.6711231641
332 330 0.6711049215
333 331 0.6711031963
334 332 0.6710996314
335 333 0.6710867309
336 334 0.6710914578
337 335 0.6710929585
338 336 0.6710984779
339 337 0.6710923199
340 338 0.6710893917
341 339 0.6710923306
342 340 0.6710927901
343 341 0.6711092802
344 342 0.6711012995
345 343 0.6711015305
346 344 0.6710975574
347 345 0.6710899474
348 346 0.671085152
349 347 0.6710814533
350 348 0.6710701892
351 349 0.67105503
352 350 0.6710527861
353 351 0.6710508715
354 352 0.6710560803
355 353 0.6710465693
356 354 0.6710440741
357 355 0.6710496913
358 356 0.6710404659
359 357 0.6710293986
360 358 0.6710353817
361 359 0.6710271815
362 360 0.6710288077
363 361 0.6710169894
364 362 0.6710119848
365 363 0.6710114775
366 364 0.6710013614
367 365 0.6709985657
368 366 0.6709948954
369 367 0.6709970591
370 368 0.6709739289
371 369 0.6709754911
372 370 0.6709717066
373 371 0.67096845
374 372 0.6709739445
375 373 0.6709728881
376 374 0.6709694284
377 375 0.6709604166
378 376 0.6709605025
379 377 0.6709603727
380 378 0.670944339
381 379 0.6709447187
382 380 0.6709538679
383 381 0.6709640912
384 382 0.6709534847
385 383 0.6709471555
386 384 0.6709506783
387 385 0.6709546729
388 386 0.670930774
389 387 0.6709287322
390 388 0.6709198643
391 389 0.6709220389
392 390 0.6709230923
393 391 0.670930414
394 392 0.6709354296
395 393 0.6709351544
396 394 0.6709414935
397 395 0.6709445943
398 396 0.6709475685
399 397 0.6709533591
400 398 0.6709592222
401 399 0.6709508704
402 400 0.6709479912
403 401 0.6709417519
404 402 0.6709476082
405 403 0.6709480979
406 404 0.6709448724
407 405 0.6709421934
408 406 0.6709386261
409 407 0.6709461564
410 408 0.670934384
411 409 0.6709312987
412 410 0.670931806
413 411 0.6709286111
414 412 0.6709224729
415 413 0.6709236504
416 414 0.6709245901
417 415 0.6709463437
418 416 0.6709567049
419 417 0.670945606
420 418 0.6709479298
421 419 0.6709464351
422 420 0.6709414048
423 421 0.6709414427
424 422 0.6709296343
425 423 0.670924721
426 424 0.670906284
427 425 0.6708996826
428 426 0.6708987677
429 427 0.670909526
430 428 0.6709033226
431 429 0.6708750209
432 430 0.6708752079
433 431 0.6708776566
434 432 0.6708736133
435 433 0.6708754298
436 434 0.6708751084
437 435 0.6708642042
438 436 0.6708610465
439 437 0.6708574768
440 438 0.6708557953
441 439 0.670871378
442 440 0.6708640187
443 441 0.6708700565
444 442 0.6708667534
445 443 0.6708675383
446 444 0.6708740175
447 445 0.6708774523
448 446 0.6708697231
449 447 0.6708614971
450 448 0.6708607946
451 449 0.6708740865
452 450 0.6708729562
453 451 0.6708674017
454 452 0.6708693088
455 453 0.6708712037
456 454 0.6708703905
457 455 0.6708577595
458 456 0.6708493546
459 457 0.6708523777
460 458 0.6708454134
461 459 0.6708404483
462 460 0.6708274771
463 461 0.6708244992
464 462 0.6708344314
465 463 0.6708279081
466 464 0.6708258106
467 465 0.6708049714
468 466 0.670810989
469 467 0.6708212237
470 468 0.6708221741
471 469 0.6708259658
472 470 0.6708159692
473 471 0.6708136212
474 472 0.6708224942
475 473 0.6708363084
476 474 0.670850875
477 475 0.6708527236
478 476 0.6708453401
479 477 0.6708413844
480 478 0.6708364569
481 479 0.6708251774
482 480 0.6708154393
483 481 0.6708111613
484 482 0.6708102339
485 483 0.6707929623
486 484 0.6707900226
487 485 0.6707832384
488 486 0.6707739118
489 487 0.6707737538
490 488 0.6707730234
491 489 0.6707796291
492 490 0.670791408
493 491 0.6707944906
494 492 0.6707835635
495 493 0.6707908928
496 494 0.670796262
497 495 0.6707877825
498 496 0.6707854132
499 497 0.6707756206
500 498 0.6707707899
501 499 0.6707704386
502 500 0.6707621465
503 501 0.6707661931
504 502 0.6707651988
505 503 0.6707607827
506 504 0.670760242
507 505 0.6707506008
508 506 0.6707452886
509 507 0.6707355189
510 508 0.6707312551
511 509 0.6707199485
512 510 0.6707131947
513 511 0.6707154112
514 512 0.6706982346
515 513 0.6706988941
516 514 0.6706989098
517 515 0.670693306
518 516 0.6706944515
519 517 0.6706899688
520 518 0.6706909374
521 519 0.6706855074
522 520 0.6706787779
523 521 0.6706737082
524 522 0.6706761225
525 523 0.670685455
526 524 0.6706693855
527 525 0.6706647216
528 526 0.6706569188
529 527 0.6706549134
530 528 0.6706547978
531 529 0.6706564214
532 530 0.6706559196
533 531 0.6706515072
534 532 0.6706474616
535 533 0.6706424204
536 534 0.6706520008
537 535 0.6706448306
538 536 0.6706415789
539 537 0.6706305359
540 538 0.6706152774
541 539 0.670616585
542 540 0.6705963243
543 541 0.6706027368
544 542 0.6706003522
545 543 0.6706044301
546 544 0.6706047241
547 545 0.6706038235
548 546 0.6706026913
549 547 0.6705845786
550 548 0.6705873967
551 549 0.6705755426
552 550 0.6705715731
553 551 0.6705757153
554 552 0.6705516814
555 553 0.6705530864
556 554 0.6705552479
557 555 0.6705563336
558 556 0.6705718544
559 557 0.6705688384
560 558 0.6705641528
561 559 0.6705628467
562 560 0.670558488
563 561 0.6705544404
564 562 0.6705617451
565 563 0.6705631717
566 564 0.6705636201
567 565 0.6705537522
568 566 0.670555083
569 567 0.6705524541
570 568 0.6705503132
571 569 0.6705354602
572 570 0.6705387012
573 571 0.6705411923
574 572 0.6705390018
575 573 0.6705354939
576 574 0.670531296
577 575 0.6705377163
578 576 0.6705248875
579 577 0.6705252902
580 578 0.6705181562
581 579 0.6705123446
582 580 0.6705128345
583 581 0.6705173712
584 582 0.670541941
585 583 0.6705463243
586 584 0.6705513215
587 585 0.6705455889
588 586 0.6705408087
589 587 0.6705510193
590 588 0.6705456751
591 589 0.6705402427
592 590 0.6705443402
593 591 0.67054441
594 592 0.6705441955
595 593 0.6705319356
596 594 0.6705358843
597 595 0.6705334396
598 596 0.6705320462
599 597 0.6705332043
600 598 0.6705328363
601 599 0.6705315638
602 600 0.6705274435
603 601 0.670509808
604 602 0.6705077789
605 603 0.6705212132
606 604 0.6705098442
607 605 0.6705061509
608 606 0.6705003071
609 607 0.6705045031
610 608 0.6705083194
611 609 0.6705329997
612 610 0.6705269987
613 611 0.6705315607
614 612 0.6705142835
615 613 0.6705165015
616 614 0.6705001061
617 615 0.6705013916
618 616 0.6705037253
619 617 0.67049647
620 618 0.6705005632
621 619 0.6704957943
622 620 0.6704955333
623 621 0.6704961207
624 622 0.6704921459
625 623 0.6704751713
626 624 0.6704753101
627 625 0.6704620888
628 626 0.6704604282
629 627 0.6704663192
630 628 0.6704680085
631 629 0.670453228
632 630 0.6704577785
633 631 0.67046675
634 632 0.6704731863
635 633 0.6704811116
636 634 0.6704839644
637 635 0.6704854798
638 636 0.6704835837
639 637 0.6704736198
640 638 0.6704640242
641 639 0.670465663
642 640 0.6704646829
643 641 0.6704600961
644 642 0.6704643207
645 643 0.6704600533
646 644 0.6704614691
647 645 0.6704728212
648 646 0.6704758731
649 647 0.6704833026
650 648 0.6704767664
651 649 0.6704702727
652 650 0.6704671372
653 651 0.6704699936
654 652 0.6704587989
655 653 0.6704637668
656 654 0.6704653717
657 655 0.6704598273
658 656 0.6704522865
659 657 0.6704558586
660 658 0.6704466331
661 659 0.6704405886
662 660 0.6704463767
663 661 0.6704475216
664 662 0.6704572386
665 663 0.6704658153
666 664 0.6704600945
667 665 0.6704561998
668 666 0.6704535154
669 667 0.6704413781
670 668 0.6704450013
671 669 0.6704422199
672 670 0.67044342
673 671 0.6704415341
674 672 0.6704439539
675 673 0.6704498197
676 674 0.6704452194
677 675 0.6704366524
678 676 0.6704427124
679 677 0.6704395579
680 678 0.6704401246
681 679 0.6704415621
682 680 0.6704341343
683 681 0.6704369615
684 682 0.6704357425
685 683 0.6704294622
686 684 0.6704289794
687 685 0.6704272409
688 686 0.6704101162
689 687 0.6704069439
690 688 0.6704100747
691 689 0.6704122261
692 690 0.6704137826
693 691 0.6704207952
694 692 0.6704154834
695 693 0.6704253514
696 694 0.6704155636
697 695 0.6704141298
698 696 0.6704207635
699 697 0.6704268341
700 698 0.6704243126
701 699 0.6704235165
702 700 0.6704257736
703 701 0.6704247758
704 702 0.6704331799
705 703 0.6704252722
706 704 0.6704146644
707 705 0.6704164122
708 706 0.6704118954
709 707 0.6704043129
710 708 0.6703978198
711 709 0.6703935976
712 710 0.6703839683
713 711 0.6703843723
714 712 0.6703879502
715 713 0.6703895978
716 714 0.6703894359
717 715 0.6703928777
718 716 0.6703933128
719 717 0.6703844355
720 718 0.6703825151
721 719 0.6703983542
722 720 0.670399556
723 721 0.6703931808
724 722 0.6703886918
725 723 0.6703847574
726 724 0.6703885941
727 725 0.6703788615
728 726 0.6703799906
729 727 0.6703774518
730 728 0.6703783496
731 729 0.6703648854
732 730 0.6703716654
733 731 0.6703550938
734 732 0.6703467057
735 733 0.6703484503
736 734 0.6703549183
737 735 0.6703501504
738 736 0.6703672622
739 737 0.6703560249
740 738 0.6703547155
741 739 0.6703593236
742 740 0.6703606827
743 741 0.6703511404
744 742 0.6703431646
745 743 0.6703475116
746 744 0.6703483634
747 745 0.6703475713
748 746 0.670360457
749 747 0.6703664352
750 748 0.6703617612
751 749 0.6703669926
752 750 0.6703670837
753 751 0.6703706628
754 752 0.670369618
755 753 0.6703692351
756 754 0.6703624433
757 755 0.6703686285
758 756 0.6703598432
759 757 0.6703618766
760 758 0.6703694148
761 759 0.6703683652
762 760 0.6703604855
763 761 0.6703758987
764 762 0.6703773302
765 763 0.6703641028
766 764 0.6703649602
767 765 0.6703567811
768 766 0.6703544688
769 767 0.6703611821
770 768 0.6703527821
771 769 0.6703523616
772 770 0.6703616298
773 771 0.6703603551
774 772 0.6703675655
775 773 0.6703582411
776 774 0.6703581437
777 775 0.6703551885
778 776 0.6703608491
779 777 0.6703674554
780 778 0.6703679619
781 779 0.6703701757
782 780 0.6703603462
783 781 0.670359801
784 782 0.6703523669
785 783 0.6703365674
786 784 0.6703486118
787 785 0.6703450011
788 786 0.6703473135
789 787 0.670350998
790 788 0.6703417767
791 789 0.6703349821
792 790 0.6703457717
793 791 0.6703506266
794 792 0.6703596395
795 793 0.6703799895
796 794 0.6703687687
797 795 0.6703780675
798 796 0.670374835
799 797 0.6703831387
800 798 0.670377656
801 799 0.6703689741
802 800 0.6703709756
803 801 0.6703737517
804 802 0.6703818964
805 803 0.6703812173
806 804 0.6703960068
807 805 0.6703976729
808 806 0.6704024604
809 807 0.6704085008
810 808 0.6704076633
811 809 0.6704111719
812 810 0.6704220803
813 811 0.6704265011
814 812 0.6704251162
815 813 0.6704375472
816 814 0.6704319336
817 815 0.670437614
818 816 0.6704554331
819 817 0.6704603317
820 818 0.6704548042
821 819 0.6704502654
822 820 0.6704512072
823 821 0.6704433481
824 822 0.6704095112
825 823 0.6704086019
826 824 0.6703987131
827 825 0.6704019708
828 826 0.6704046556
829 827 0.6704091961
830 828 0.6704103204
831 829 0.6704074257
832 830 0.6704115335
833 831 0.6704041275
834 832 0.6704004556
835 833 0.6704037097
836 834 0.6704035477
837 835 0.6704025281
838 836 0.6704024549
839 837 0.670405721
840 838 0.6703983189
841 839 0.6704035256
842 840 0.6704047613
843 841 0.6704139617
844 842 0.6704066193
845 843 0.670402892
846 844 0.6704081961
847 845 0.6704029862
848 846 0.6704014281
849 847 0.6704036115
850 848 0.6704016777
851 849 0.6704085392
852 850 0.6704042952
853 851 0.670394789
854 852 0.6703885644
855 853 0.6703946813
856 854 0.6704042137
857 855 0.6704077517
858 856 0.6704118698
859 857 0.6704114259
860 858 0.6704157567
861 859 0.6703837005
862 860 0.6703829482
863 861 0.6703818491
864 862 0.6703826129
865 863 0.6703834487
866 864 0.6703868275
867 865 0.6703853357
868 866 0.670450644
869 867 0.6704556991
870 868 0.6704535742
871 869 0.6704495915
+871
View File
@@ -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
1 iter Passed Remaining
2 0 46 93548
3 1 83 83419
4 2 132 88415
5 3 162 81250
6 4 196 78573
7 5 230 76747
8 6 269 76701
9 7 319 79674
10 8 364 80653
11 9 411 81918
12 10 456 82497
13 11 491 81432
14 12 522 79809
15 13 555 78774
16 14 595 78777
17 15 630 78123
18 16 662 77290
19 17 700 77124
20 18 730 76120
21 19 764 75651
22 20 804 75774
23 21 835 75128
24 22 886 76169
25 23 920 75764
26 24 960 75853
27 25 989 75130
28 26 1025 74941
29 27 1060 74714
30 28 1104 75079
31 29 1141 74976
32 30 1180 74975
33 31 1213 74640
34 32 1245 74260
35 33 1287 74434
36 34 1327 74528
37 35 1376 75071
38 36 1427 75741
39 37 1468 75804
40 38 1508 75857
41 39 1549 75922
42 40 1586 75781
43 41 1621 75590
44 42 1663 75705
45 43 1701 75621
46 44 1739 75591
47 45 1776 75460
48 46 1819 75616
49 47 1869 76025
50 48 1916 76288
51 49 1953 76191
52 50 1993 76197
53 51 2038 76381
54 52 2080 76420
55 53 2158 77788
56 54 2220 78529
57 55 2286 79390
58 56 2328 79372
59 57 2367 79254
60 58 2409 79257
61 59 2444 79049
62 60 2484 78985
63 61 2521 78820
64 62 2554 78528
65 63 2593 78466
66 64 2623 78111
67 65 2660 77969
68 66 2695 77776
69 67 2725 77446
70 68 2761 77291
71 69 2791 76975
72 70 2824 76739
73 71 2861 76611
74 72 2897 76476
75 73 2935 76408
76 74 3040 78027
77 75 3097 78411
78 76 3152 78741
79 77 3216 79248
80 78 3256 79195
81 79 3305 79336
82 80 3348 79320
83 81 3381 79089
84 82 3416 78911
85 83 3480 79399
86 84 3535 79649
87 85 3581 79716
88 86 3612 79428
89 87 3644 79185
90 88 3678 78975
91 89 3712 78785
92 90 3743 78531
93 91 3775 78297
94 92 3806 78047
95 93 3837 77821
96 94 3871 77629
97 95 3913 77618
98 96 3945 77403
99 97 3989 77433
100 98 4020 77204
101 99 4053 77020
102 100 4084 76789
103 101 4116 76597
104 102 4148 76401
105 103 4176 76141
106 104 4202 75845
107 105 4232 75634
108 106 4261 75390
109 107 4290 75168
110 108 4324 75018
111 109 4351 74766
112 110 4386 74648
113 111 4424 74577
114 112 4458 74455
115 113 4497 74400
116 114 4533 74307
117 115 4564 74136
118 116 4596 73981
119 117 4628 73818
120 118 4668 73786
121 119 4692 73509
122 120 4723 73354
123 121 4756 73220
124 122 4788 73065
125 123 4815 72854
126 124 4843 72647
127 125 4875 72514
128 126 4916 72515
129 127 4952 72436
130 128 4991 72397
131 129 5028 72327
132 130 5059 72180
133 131 5096 72116
134 132 5125 71946
135 133 5156 71804
136 134 5190 71704
137 135 5221 71564
138 136 5251 71407
139 137 5274 71165
140 138 5309 71084
141 139 5344 71008
142 140 5377 70902
143 141 5416 70866
144 142 5452 70803
145 143 5490 70760
146 144 5521 70641
147 145 5553 70522
148 146 5582 70365
149 147 5611 70217
150 148 5636 70026
151 149 5673 69975
152 150 5706 69874
153 151 5738 69764
154 152 5765 69605
155 153 5795 69471
156 154 5817 69246
157 155 5853 69191
158 156 5888 69122
159 157 5924 69070
160 158 5964 69061
161 159 5996 68963
162 160 6022 68789
163 161 6050 68650
164 162 6079 68510
165 163 6108 68385
166 164 6140 68292
167 165 6169 68162
168 166 6202 68074
169 167 6231 67953
170 168 6263 67858
171 169 6295 67764
172 170 6325 67656
173 171 6356 67561
174 172 6395 67545
175 173 6437 67554
176 174 6472 67495
177 175 6503 67395
178 176 6533 67291
179 177 6562 67174
180 178 6590 67049
181 179 6624 66982
182 180 6655 66882
183 181 6687 66804
184 182 6718 66703
185 183 6751 66632
186 184 6784 66559
187 185 6810 66424
188 186 6832 66246
189 187 6867 66187
190 188 6918 66294
191 189 6969 66393
192 190 7018 66470
193 191 7074 66614
194 192 7117 66635
195 193 7191 66943
196 194 7242 67036
197 195 7282 67027
198 196 7317 66967
199 197 7351 66903
200 198 7389 66879
201 199 7432 66896
202 200 7471 66869
203 201 7506 66814
204 202 7540 66752
205 203 7568 66628
206 204 7605 66596
207 205 7638 66519
208 206 7665 66397
209 207 7700 66340
210 208 7734 66276
211 209 7766 66197
212 210 7796 66106
213 211 7831 66053
214 212 7871 66037
215 213 7910 66016
216 214 7951 66014
217 215 7989 65983
218 216 8025 65946
219 217 8058 65872
220 218 8087 65768
221 219 8112 65638
222 220 8148 65594
223 221 8197 65655
224 222 8239 65655
225 223 8268 65556
226 224 8298 65466
227 225 8327 65366
228 226 8357 65278
229 227 8384 65167
230 228 8418 65103
231 229 8453 65058
232 230 8490 65020
233 231 8523 64958
234 232 8550 64848
235 233 8575 64718
236 234 8607 64648
237 235 8635 64545
238 236 8660 64426
239 237 8691 64345
240 238 8719 64250
241 239 8746 64137
242 240 8773 64038
243 241 8803 63951
244 242 8833 63873
245 243 8862 63779
246 244 8892 63698
247 245 8932 63688
248 246 8962 63611
249 247 8991 63521
250 248 9021 63442
251 249 9051 63358
252 250 9085 63306
253 251 9110 63193
254 252 9137 63093
255 253 9174 63066
256 254 9196 62935
257 255 9238 62934
258 256 9267 62855
259 257 9297 62776
260 258 9324 62681
261 259 9357 62625
262 260 9388 62552
263 261 9427 62536
264 262 9461 62491
265 263 9496 62443
266 264 9524 62356
267 265 9553 62278
268 266 9590 62247
269 267 9620 62172
270 268 9645 62071
271 269 9682 62040
272 270 9711 61962
273 271 9739 61872
274 272 9768 61797
275 273 9804 61761
276 274 9848 61777
277 275 9886 61755
278 276 9925 61740
279 277 9965 61728
280 278 9995 61656
281 279 10022 61564
282 280 10055 61516
283 281 10080 61410
284 282 10111 61344
285 283 10147 61311
286 284 10175 61230
287 285 10202 61141
288 286 10234 61084
289 287 10264 61018
290 288 10299 60977
291 289 10323 60874
292 290 10353 60804
293 291 10394 60803
294 292 10431 60773
295 293 10471 60763
296 294 10503 60707
297 295 10534 60645
298 296 10576 60646
299 297 10612 60612
300 298 10639 60525
301 299 10668 60453
302 300 10702 60411
303 301 10729 60326
304 302 10764 60290
305 303 10801 60263
306 304 10829 60182
307 305 10857 60108
308 306 10892 60067
309 307 10930 60047
310 308 10972 60045
311 309 11002 59983
312 310 11030 59902
313 311 11058 59828
314 312 11092 59788
315 313 11117 59696
316 314 11149 59641
317 315 11187 59617
318 316 11211 59525
319 317 11243 59468
320 318 11274 59413
321 319 11304 59346
322 320 11334 59287
323 321 11362 59209
324 322 11394 59158
325 323 11436 59158
326 324 11477 59153
327 325 11513 59122
328 326 11547 59081
329 327 11572 58991
330 328 11607 58956
331 329 11637 58894
332 330 11668 58833
333 331 11700 58785
334 332 11724 58694
335 333 11757 58648
336 334 11780 58550
337 335 11815 58515
338 336 11844 58451
339 337 11869 58364
340 338 11905 58335
341 339 11941 58302
342 340 11986 58315
343 341 12020 58274
344 342 12066 58292
345 343 12122 58358
346 344 12177 58415
347 345 12221 58422
348 346 12264 58423
349 347 12300 58394
350 348 12324 58304
351 349 12354 58243
352 350 12401 58262
353 351 12438 58232
354 352 12479 58228
355 353 12512 58179
356 354 12541 58116
357 355 12569 58044
358 356 12597 57977
359 357 12628 57920
360 358 12653 57839
361 359 12682 57775
362 360 12720 57752
363 361 12744 57666
364 362 12770 57592
365 363 12811 57583
366 364 12841 57522
367 365 12870 57460
368 366 12897 57386
369 367 12938 57378
370 368 12974 57347
371 369 13009 57313
372 370 13038 57249
373 371 13078 57235
374 372 13117 57216
375 373 13147 57159
376 374 13181 57118
377 375 13205 57036
378 376 13235 56979
379 377 13274 56960
380 378 13306 56911
381 379 13333 56841
382 380 13366 56798
383 381 13396 56741
384 382 13421 56666
385 383 13467 56674
386 384 13508 56664
387 385 13540 56616
388 386 13569 56559
389 387 13598 56496
390 388 13627 56438
391 389 13656 56376
392 390 13685 56317
393 391 13717 56271
394 392 13750 56227
395 393 13771 56135
396 394 13804 56090
397 395 13825 55999
398 396 13858 55957
399 397 13888 55904
400 398 13917 55843
401 399 13953 55812
402 400 13994 55802
403 401 14025 55752
404 402 14048 55670
405 403 14076 55607
406 404 14105 55551
407 405 14142 55526
408 406 14182 55511
409 407 14214 55464
410 408 14240 55394
411 409 14267 55328
412 410 14299 55284
413 411 14324 55213
414 412 14351 55146
415 413 14379 55086
416 414 14410 55036
417 415 14451 55025
418 416 14484 54984
419 417 14513 54929
420 418 14536 54851
421 419 14565 54793
422 420 14587 54710
423 421 14615 54650
424 422 14642 54588
425 423 14666 54515
426 424 14690 54441
427 425 14719 54384
428 426 14739 54297
429 427 14772 54257
430 428 14790 54164
431 429 14824 54125
432 430 14844 54039
433 431 14876 53995
434 432 14906 53946
435 433 14938 53902
436 434 14980 53894
437 435 15006 53829
438 436 15033 53770
439 437 15059 53706
440 438 15085 53639
441 439 15110 53574
442 440 15134 53503
443 441 15160 53438
444 442 15184 53369
445 443 15211 53308
446 444 15234 53236
447 445 15266 53193
448 446 15287 53114
449 447 15316 53059
450 448 15336 52978
451 449 15366 52929
452 450 15393 52870
453 451 15429 52843
454 452 15469 52828
455 453 15490 52748
456 454 15523 52712
457 455 15550 52653
458 456 15577 52594
459 457 15604 52536
460 458 15630 52476
461 459 15656 52414
462 460 15682 52353
463 461 15711 52304
464 462 15736 52238
465 463 15765 52188
466 464 15786 52112
467 465 15817 52068
468 466 15839 51996
469 467 15873 51961
470 468 15903 51916
471 469 15935 51873
472 470 15969 51840
473 471 15994 51779
474 472 16022 51726
475 473 16047 51663
476 474 16073 51605
477 475 16099 51546
478 476 16128 51495
479 477 16152 51431
480 478 16176 51367
481 479 16205 51317
482 480 16228 51250
483 481 16255 51194
484 482 16277 51123
485 483 16305 51071
486 484 16328 51005
487 485 16362 50973
488 486 16392 50928
489 487 16426 50894
490 488 16459 50860
491 489 16480 50787
492 490 16510 50743
493 491 16530 50668
494 492 16561 50625
495 493 16585 50562
496 494 16613 50510
497 495 16638 50453
498 496 16663 50393
499 497 16690 50339
500 498 16716 50282
501 499 16740 50222
502 500 16773 50186
503 501 16802 50139
504 502 16836 50107
505 503 16873 50085
506 504 16921 50094
507 505 16989 50163
508 506 17038 50173
509 507 17069 50132
510 508 17110 50121
511 509 17145 50091
512 510 17190 50091
513 511 17219 50044
514 512 17247 49994
515 513 17271 49932
516 514 17298 49878
517 515 17343 49878
518 516 17373 49836
519 517 17417 49831
520 518 17460 49823
521 519 17490 49781
522 520 17518 49731
523 521 17546 49680
524 522 17571 49622
525 523 17600 49577
526 524 17625 49520
527 525 17655 49474
528 526 17679 49414
529 527 17707 49366
530 528 17729 49300
531 529 17758 49254
532 530 17781 49191
533 531 17808 49141
534 532 17829 49071
535 533 17862 49038
536 534 17905 49031
537 535 18028 49241
538 536 18072 49236
539 537 18106 49203
540 538 18135 49157
541 539 18165 49114
542 540 18200 49083
543 541 18223 49022
544 542 18254 48980
545 543 18280 48927
546 544 18307 48876
547 545 18338 48834
548 546 18367 48790
549 547 18411 48783
550 548 18444 48747
551 549 18470 48693
552 550 18503 48660
553 551 18531 48611
554 552 18557 48558
555 553 18584 48508
556 554 18625 48493
557 555 18650 48436
558 556 18677 48388
559 557 18703 48333
560 558 18729 48282
561 559 18756 48231
562 560 18781 48176
563 561 18808 48126
564 562 18834 48074
565 563 18869 48043
566 564 18902 48008
567 565 18930 47960
568 566 18958 47914
569 567 18983 47859
570 568 19016 47824
571 569 19037 47761
572 570 19068 47720
573 571 19090 47660
574 572 19111 47595
575 573 19141 47553
576 574 19164 47494
577 575 19196 47458
578 576 19217 47393
579 577 19249 47358
580 578 19274 47303
581 579 19298 47247
582 580 19324 47195
583 581 19357 47162
584 582 19391 47130
585 583 19427 47103
586 584 19460 47070
587 585 19483 47012
588 586 19511 46967
589 587 19542 46929
590 588 19564 46867
591 589 19597 46833
592 590 19621 46779
593 591 19647 46729
594 592 19670 46672
595 593 19699 46627
596 594 19726 46582
597 595 19753 46532
598 596 19778 46480
599 597 19803 46429
600 598 19830 46381
601 599 19857 46335
602 600 19896 46313
603 601 19925 46271
604 602 19957 46236
605 603 19991 46204
606 604 20019 46159
607 605 20047 46115
608 606 20072 46063
609 607 20098 46015
610 608 20123 45963
611 609 20149 45913
612 610 20176 45867
613 611 20202 45817
614 612 20230 45774
615 613 20253 45719
616 614 20285 45682
617 615 20307 45626
618 616 20338 45589
619 617 20361 45532
620 618 20394 45500
621 619 20423 45459
622 620 20454 45420
623 621 20488 45390
624 622 20510 45333
625 623 20543 45301
626 624 20569 45252
627 625 20594 45201
628 626 20619 45151
629 627 20646 45107
630 628 20675 45066
631 629 20701 45016
632 630 20727 44970
633 631 20752 44919
634 632 20782 44881
635 633 20804 44825
636 634 20837 44791
637 635 20862 44742
638 636 20892 44704
639 637 20931 44683
640 638 20960 44643
641 639 20994 44612
642 640 21022 44570
643 641 21052 44531
644 642 21082 44493
645 643 21107 44443
646 644 21135 44401
647 645 21160 44351
648 646 21185 44302
649 647 21210 44253
650 648 21236 44208
651 649 21262 44161
652 650 21288 44113
653 651 21315 44068
654 652 21343 44027
655 653 21377 43997
656 654 21403 43949
657 655 21440 43926
658 656 21477 43903
659 657 21502 43854
660 658 21533 43819
661 659 21559 43772
662 660 21586 43727
663 661 21611 43680
664 662 21637 43633
665 663 21662 43586
666 664 21688 43539
667 665 21714 43493
668 666 21742 43451
669 667 21771 43413
670 668 21818 43409
671 669 21846 43366
672 670 21888 43352
673 671 21934 43345
674 672 21971 43322
675 673 22019 43320
676 674 22053 43289
677 675 22090 43266
678 676 22141 43269
679 677 22176 43240
680 678 22213 43215
681 679 22239 43171
682 680 22270 43134
683 681 22296 43088
684 682 22321 43041
685 683 22350 43002
686 684 22379 42962
687 685 22419 42944
688 686 22452 42912
689 687 22484 42878
690 688 22511 42834
691 689 22537 42789
692 690 22571 42757
693 691 22598 42714
694 692 22624 42669
695 693 22653 42630
696 694 22680 42586
697 695 22708 42545
698 696 22739 42510
699 697 22761 42457
700 698 22792 42421
701 699 22816 42373
702 700 22845 42333
703 701 22870 42288
704 702 22902 42253
705 703 22942 42234
706 704 22974 42201
707 705 23002 42160
708 706 23033 42124
709 707 23054 42071
710 708 23086 42038
711 709 23115 41999
712 710 23143 41957
713 711 23169 41914
714 712 23195 41868
715 713 23230 41840
716 714 23259 41801
717 715 23287 41760
718 716 23311 41713
719 717 23341 41676
720 718 23372 41641
721 719 23405 41610
722 720 23438 41578
723 721 23483 41566
724 722 23507 41519
725 723 23540 41488
726 724 23566 41444
727 725 23595 41406
728 726 23623 41365
729 727 23648 41320
730 728 23677 41281
731 729 23700 41231
732 730 23728 41192
733 731 23752 41144
734 732 23784 41111
735 733 23807 41063
736 734 23840 41031
737 735 23870 40994
738 736 23908 40972
739 737 23941 40940
740 738 23974 40909
741 739 24006 40875
742 740 24036 40838
743 741 24064 40798
744 742 24092 40759
745 743 24127 40730
746 744 24153 40688
747 745 24179 40644
748 746 24207 40604
749 747 24233 40561
750 748 24261 40522
751 749 24295 40491
752 750 24318 40444
753 751 24349 40410
754 752 24376 40368
755 753 24408 40335
756 754 24442 40306
757 755 24474 40273
758 756 24508 40242
759 757 24548 40222
760 758 24575 40182
761 759 24605 40145
762 760 24632 40104
763 761 24660 40064
764 762 24689 40027
765 763 24714 39982
766 764 24745 39949
767 765 24766 39897
768 766 24797 39863
769 767 24825 39823
770 768 24854 39786
771 769 24880 39744
772 770 24909 39706
773 771 24940 39672
774 772 24970 39635
775 773 25004 39606
776 774 25030 39564
777 775 25056 39522
778 776 25086 39486
779 777 25107 39436
780 778 25139 39403
781 779 25159 39351
782 780 25188 39314
783 781 25214 39272
784 782 25240 39230
785 783 25266 39188
786 784 25288 39141
787 785 25315 39101
788 786 25341 39058
789 787 25367 39016
790 788 25391 38972
791 789 25417 38930
792 790 25448 38895
793 791 25482 38867
794 792 25514 38834
795 793 25542 38795
796 794 25569 38756
797 795 25595 38714
798 796 25618 38669
799 797 25643 38626
800 798 25667 38581
801 799 25695 38543
802 800 25716 38494
803 801 25743 38454
804 802 25770 38415
805 803 25790 38364
806 804 25822 38332
807 805 25843 38284
808 806 25873 38249
809 807 25896 38203
810 808 25925 38167
811 809 25955 38131
812 810 25988 38101
813 811 26028 38080
814 812 26055 38042
815 813 26081 38000
816 814 26108 37961
817 815 26131 37916
818 816 26159 37878
819 817 26188 37841
820 818 26214 37800
821 819 26242 37764
822 820 26272 37728
823 821 26298 37688
824 822 26327 37652
825 823 26359 37619
826 824 26385 37580
827 825 26408 37534
828 826 26444 37507
829 827 26477 37478
830 828 26517 37456
831 829 26539 37411
832 830 26573 37382
833 831 26597 37339
834 832 26623 37298
835 833 26650 37259
836 834 26677 37221
837 835 26704 37182
838 836 26728 37138
839 837 26763 37111
840 838 26791 37073
841 839 26822 37041
842 840 26872 37033
843 841 26924 37029
844 842 26982 37033
845 843 27054 37055
846 844 27097 37038
847 845 27120 36994
848 846 27146 36954
849 847 27180 36925
850 848 27206 36884
851 849 27234 36846
852 850 27260 36807
853 851 27289 36770
854 852 27318 36734
855 853 27347 36698
856 854 27386 36675
857 855 27413 36637
858 856 27439 36596
859 857 27471 36564
860 858 27501 36529
861 859 27535 36500
862 860 27572 36474
863 861 27595 36431
864 862 27627 36398
865 863 27654 36360
866 864 27683 36324
867 865 27711 36287
868 866 27738 36249
869 867 27765 36210
870 868 27794 36175
871 869 27820 36135
File diff suppressed because it is too large Load Diff
+243
View File
@@ -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
View File
@@ -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)}
+902
View File
@@ -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,,,,,,,,,,,,
1 match_id date league match ht_score final_score strategy market pick odds playable confidence result counted_in_roi profit_flat resolution_note source reversal_pick reversal_prob favorite_gap favorite_odd support_score odds_band_score odds_band_label league_reversal_rate league_strict_rev_rate referee_strict_rev_rate surprise_score reason_codes pick_reason
2 b6uzz042mizu0dqpci538z4lw 2025-09-06 Süper Kupa V. Sarsfield vs C. Cordoba 0-0 2-0 v25_aggressive HTFT 2/1 34.5 True 21.0 LOST True -1.0 actual=X/1 v25.aggressive_pick 2/1
3 b6uzz042mizu0dqpci538z4lw 2025-09-06 Süper Kupa V. Sarsfield vs C. Cordoba 0-0 2-0 v26_aggressive HTFT 1/1 3.26 True 16.0 LOST True -1.0 actual=X/1 v26.aggressive_pick 1/1
4 ytsc38rm4j22govgwo3as6j8 2025-09-06 DK Elemeler Avusturya vs G. Kıbrıs Rum Kesimi 0-0 1-0 v25_aggressive HTFT X/1 4.01 True 11.4 WON True 3.01 actual=X/1 v25.aggressive_pick X/1
5 cfar57gsu6hy770n7e58u8duc 2025-09-06 Eerste Divisie Cambuur vs Willem II 0-1 2-2 v25_aggressive HTFT 2/1 20.7 True 15.6 LOST True -1.0 actual=2/X v25.aggressive_pick 2/1
6 cfar57gsu6hy770n7e58u8duc 2025-09-06 Eerste Divisie Cambuur vs Willem II 0-1 2-2 v26_aggressive HTFT 1/1 2.13 True 14.4 LOST True -1.0 actual=2/X v26.aggressive_pick 1/1
7 99r0d7ggi1169dhklo4eroopg 2025-09-06 2. Lig Bromley vs Gillingham 2-0 2-2 v25_aggressive HTFT 2/1 30.5 True 25.6 LOST True -1.0 actual=1/X v25.aggressive_pick 2/1
8 99r0d7ggi1169dhklo4eroopg 2025-09-06 2. Lig Bromley vs Gillingham 2-0 2-2 v26_surprise HTFT 1/2 34.5 False 4.9 LOST False 0.0 actual=1/X v26.surprise_pick 1/2 0.0679 1.02 1.92 73.5 0.642 0.04 0.0 0.0 62.7 favorite_gap_large,favorite_price_supported,reversal_prob_warm,quality_supports_reversal,favorite_odds_band_reversal_window,favorite_streak_break_window
9 99r0d7ggi1169dhklo4eroopg 2025-09-06 2. Lig Bromley vs Gillingham 2-0 2-2 v26_aggressive HTFT 1/1 3.16 True 16.1 LOST True -1.0 actual=1/X v26.aggressive_pick 1/1
10 790qnaweqoyffb5ndxnb4hlas 2025-09-06 Ulusal Lig Aldershot vs Brackley 1-0 2-2 v25_aggressive HTFT X/2 6.07 True 9.0 LOST True -1.0 actual=1/X v25.aggressive_pick X/2
11 790qnaweqoyffb5ndxnb4hlas 2025-09-06 Ulusal Lig Aldershot vs Brackley 1-0 2-2 v26_aggressive HTFT 2/2 3.8 True 37.1 LOST True -1.0 actual=1/X v26.aggressive_pick 2/2
12 8808y1x2hmz52k3mr598orqc4 2025-09-06 DK Elemeler Ermenistan vs Portekiz 0-3 0-5 v25_aggressive HTFT X/2 4.03 True 11.6 LOST True -1.0 actual=2/2 v25.aggressive_pick X/2
13 7gfnwoxqz5o2clin40a85uqdw 2025-09-06 1. Lig Port Vale vs Leyton Orient 1-2 2-3 v25_aggressive HTFT X/1 4.84 True 12.3 LOST True -1.0 actual=2/2 v25.aggressive_pick X/1
14 7gfnwoxqz5o2clin40a85uqdw 2025-09-06 1. Lig Port Vale vs Leyton Orient 1-2 2-3 v26_surprise HTFT 1/2 33.0 False 5.4 LOST False 0.0 actual=2/2 v26.surprise_pick 1/2 0.0749 0.62 1.97 52.1 0.642 0.044 0.0 0.0 57.0 favorite_gap_large,favorite_price_supported,reversal_prob_warm,draw_swing_support,quality_supports_reversal,favorite_odds_band_reversal_window
15 7gfnwoxqz5o2clin40a85uqdw 2025-09-06 1. Lig Port Vale vs Leyton Orient 1-2 2-3 v26_aggressive HTFT 2/2 4.51 True 17.3 WON True 3.51 actual=2/2 v26.aggressive_pick 2/2
16 7fld91ykj1kfuoc8wn4r2frbo 2025-09-06 1. Lig Lincoln City vs Wigan Ath 2-1 2-2 v25_aggressive HTFT X/1 4.7 True 19.2 LOST True -1.0 actual=1/X v25.aggressive_pick X/1
17 7fld91ykj1kfuoc8wn4r2frbo 2025-09-06 1. Lig Lincoln City vs Wigan Ath 2-1 2-2 v26_surprise HTFT 1/2 34.5 False 7.1 LOST False 0.0 actual=1/X v26.surprise_pick 1/2 0.0984 0.71 2.0 61.5 0.642 0.044 0.0 0.0 61.7 favorite_gap_large,favorite_price_supported,reversal_prob_hot,upset_risk_detected,quality_supports_reversal,favorite_odds_band_reversal_window
18 7fld91ykj1kfuoc8wn4r2frbo 2025-09-06 1. Lig Lincoln City vs Wigan Ath 2-1 2-2 v26_aggressive HTFT 1/1 3.36 True 19.5 LOST True -1.0 actual=1/X v26.aggressive_pick 1/1
19 7i4nhkex1qssyp3x6rsgj03ro 2025-09-06 1. Lig Wycombe vs Mansfield 1-0 2-0 v25_aggressive HTFT X/2 6.92 True 11.1 LOST True -1.0 actual=1/1 v25.aggressive_pick X/2
20 7i4nhkex1qssyp3x6rsgj03ro 2025-09-06 1. Lig Wycombe vs Mansfield 1-0 2-0 v26_main_htft HTFT 2/2 5.22 False 39.9 LOST False 0.0 actual=1/1 v26.main_pick 0.0
21 7hagai8xazmsj5exw7idwhhck 2025-09-06 1. Lig Rotherham vs Exeter City 1-0 1-0 v25_aggressive HTFT X/2 6.28 True 6.7 LOST True -1.0 actual=1/1 v25.aggressive_pick X/2
22 7hagai8xazmsj5exw7idwhhck 2025-09-06 1. Lig Rotherham vs Exeter City 1-0 1-0 v26_aggressive HTFT 2/2 4.47 True 29.6 LOST True -1.0 actual=1/1 v26.aggressive_pick 2/2
23 6e37x17qvk0snpokd1698lkb8 2025-09-06 Premiership Crusaders vs Coleraine 0-3 0-4 v25_aggressive HTFT X/2 4.24 True 11.5 LOST True -1.0 actual=2/2 v25.aggressive_pick X/2
24 6e37x17qvk0snpokd1698lkb8 2025-09-06 Premiership Crusaders vs Coleraine 0-3 0-4 v26_aggressive HTFT 2/2 2.32 True 37.7 WON True 1.32 actual=2/2 v26.aggressive_pick 2/2
25 6f0fqlafaei9oj8yd9hdi6rdg 2025-09-06 Premiership Linfield vs Portadown 0-0 3-0 v25_aggressive HTFT 2/1 23.2 True 18.6 LOST True -1.0 actual=X/1 v25.aggressive_pick 2/1
26 6f0fqlafaei9oj8yd9hdi6rdg 2025-09-06 Premiership Linfield vs Portadown 0-0 3-0 v26_aggressive HTFT 1/1 1.74 True 20.4 LOST True -1.0 actual=X/1 v26.aggressive_pick 1/1
27 6dny1bmxcj6shc5382dno2al0 2025-09-06 Premiership Carrick vs Cliftonville 0-1 1-2 v25_aggressive HTFT 2/1 30.5 True 14.4 LOST True -1.0 actual=2/2 v25.aggressive_pick 2/1
28 6dny1bmxcj6shc5382dno2al0 2025-09-06 Premiership Carrick vs Cliftonville 0-1 1-2 v26_surprise HTFT 2/1 30.5 False 10.1 LOST False 0.0 actual=2/2 v26.surprise_pick 2/1 0.1401 0.59 1.97 62.8 0.642 0.0667 0.0 0.0 66.2 favorite_price_supported,reversal_prob_hot,quality_supports_reversal,favorite_odds_band_reversal_window,league_strict_reversal_prior,draw_pressure_supports_swing
29 6dny1bmxcj6shc5382dno2al0 2025-09-06 Premiership Carrick vs Cliftonville 0-1 1-2 v26_aggressive HTFT 1/1 4.36 True 14.6 LOST True -1.0 actual=2/2 v26.aggressive_pick 1/1
30 9d9nwo82prx06riy5odgnr190 2025-09-06 2. Lig Walsall vs Chesterfield 1-0 1-0 v25_aggressive HTFT X/2 5.38 True 10.3 LOST True -1.0 actual=1/1 v25.aggressive_pick X/2
31 9d9nwo82prx06riy5odgnr190 2025-09-06 2. Lig Walsall vs Chesterfield 1-0 1-0 v26_aggressive HTFT 2/2 3.78 True 35.1 LOST True -1.0 actual=1/1 v26.aggressive_pick 2/2
32 7dwtx5tr7g66bsqkoc1wos9as 2025-09-06 1. Lig Bolton vs Wimbledon 1-0 3-0 v25_aggressive HTFT X/1 3.76 True 12.1 LOST True -1.0 actual=1/1 v25.aggressive_pick X/1
33 7dwtx5tr7g66bsqkoc1wos9as 2025-09-06 1. Lig Bolton vs Wimbledon 1-0 3-0 v26_aggressive HTFT 1/1 1.81 True 33.3 WON True 0.81 actual=1/1 v26.aggressive_pick 1/1
34 7c85gguekhbhadtm7qdbgir6c 2025-09-06 Ulusal Lig Tamworth vs Eastleigh 0-0 1-0 v25_aggressive HTFT 1/2 34.5 True 14.1 LOST True -1.0 actual=X/1 v25.aggressive_pick 1/2
35 7c85gguekhbhadtm7qdbgir6c 2025-09-06 Ulusal Lig Tamworth vs Eastleigh 0-0 1-0 v26_aggressive HTFT 2/2 5.53 True 14.8 LOST True -1.0 actual=X/1 v26.aggressive_pick 2/2
File diff suppressed because it is too large Load Diff
@@ -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"
]
}
+1
View File
@@ -17,3 +17,4 @@ pyyaml>=6.0
# V2 async database
asyncpg>=0.29.0
pydantic>=2.5.0
pytest>=8.0.0
+215
View File
@@ -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")
+94
View File
@@ -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!")
+5 -5
View File
@@ -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()
+317
View File
@@ -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ı!")
+58
View File
@@ -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()
+480
View File
@@ -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()
+240
View File
@@ -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:
+590 -73
View File
@@ -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:
self.v25_predictor = get_v25_predictor()
try:
self.v25_predictor = get_v25_predictor()
print(f"[V25] ✅ Predictor loaded: {len(self.v25_predictor.models)} market models")
except Exception as e:
print(f"[V25] ❌ PREDICTOR LOAD FAILED: {e}")
raise
return self.v25_predictor
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,
+286
View File
@@ -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()
+155
View File
@@ -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.
+7
View File
@@ -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);
+16
View File
@@ -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")
+331 -696
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -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() ?? "";
}
+267
View File
@@ -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));
}
}
+203
View File
@@ -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");
}
+82
View File
@@ -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`);
}
+1
View File
@@ -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),
+6
View File
@@ -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,
+21 -2
View File
@@ -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");
}
+6 -1
View File
@@ -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: [],
};
}
}
+37 -1
View File
@@ -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
// ============================================
/**
+14 -2
View File
@@ -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: {} } },
+31 -50
View File
@@ -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 missingParts: string[] = [];
if (!stats) missingParts.push("Stats");
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 (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 };
}
+64 -18
View File
@@ -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",
};
}
}
}
+2 -4
View File
@@ -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 {}
+7 -4
View File
@@ -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),
);
}
+33 -14
View File
@@ -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: {
OR: [{ homeTeamId: teamId }, { awayTeamId: teamId }],
},
include: {
homeTeam: true,
awayTeam: true,
league: { include: { country: true } },
},
orderBy: { mstUtc: "desc" },
take: limit,
});
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),
};
}
/**
+34 -35
View File
@@ -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,
+50
View File
@@ -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";
+207 -21
View File
@@ -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({
+2 -2
View File
@@ -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"]
+29 -16
View File
@@ -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}`,
{},
{
timeout: 30000,
},
),
const response = await this.aiEngineClient.post(
`/v20plus/analyze/${matchId}`,
{},
);
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 {
+177 -98
View File
@@ -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({});
this.logger.log(
`🧹 Deleted ${deleted.count} live matches. Starting full sync...`,
);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error(`Truncate failed: ${message}`);
return;
}
try {
const yesterdayDate = getShiftedDateStringInTimeZone(
-1,
this.timeZone,
);
const { startMs: yesterdayStartMs } = getDayBoundsForTimeZone(
yesterdayDate,
this.timeZone,
);
const cutoffDate = new Date(yesterdayStartMs);
// Run full sync immediately after cleanup
await this.syncLiveMatches();
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(
`Pruned ${deleted.count} stale live matches. Starting full sync...`,
);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error(`Stale live_match cleanup failed: ${message}`);
return;
}
await this.runLiveSync();
},
this.logger,
);
}
// ────────────────────────────────────────────────────────────
// ────────────────────────────────────────────────────────────
// Phase 1: Fetch match list for all sports
// ────────────────────────────────────────────────────────────
// ────────────────────────────────────────────────────────────
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") {
+23 -12
View File
@@ -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,17 +29,24 @@ export class HistoricalResultsSyncTask {
return;
}
this.logger.log(
"Starting previous-day completed match sync for football and basketball...",
);
await this.taskLock.runWithLease(
"syncPreviousDayCompletedMatches",
6 * 60 * 60 * 1000,
async () => {
this.logger.log(
"Starting previous-day completed match sync for football and basketball...",
);
try {
await this.feederService.runPreviousDayCompletedMatchesScan();
this.logger.log("Previous-day completed match sync finished");
} catch (error: any) {
this.logger.error(
`Previous-day completed match sync failed: ${error.message}`,
);
}
try {
await this.feederService.runPreviousDayCompletedMatchesScan();
this.logger.log("Previous-day completed match sync finished");
} catch (error: any) {
this.logger.error(
`Previous-day completed match sync failed: ${error.message}`,
);
}
},
this.logger,
);
}
}
+126 -70
View File
@@ -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,34 +38,39 @@ export class LimitResetterTask {
@Cron("0 3 * * *", { timeZone: "Europe/Istanbul" })
async resetUsageLimits() {
if (this.shouldSkipInHistoricalMode("resetUsageLimits")) return;
this.logger.log("Starting daily usage limit reset job...");
await this.taskLock.runWithLease(
"resetUsageLimits",
30 * 60 * 1000,
async () => {
this.logger.log("Starting daily usage limit reset job...");
try {
const today = new Date();
today.setHours(0, 0, 0, 0);
try {
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 },
},
data: {
analysisCount: 0,
couponCount: 0,
lastResetDate: today,
},
});
const result = await this.prisma.usageLimit.updateMany({
where: {
lastResetDate: { lt: today },
},
data: {
analysisCount: 0,
couponCount: 0,
lastResetDate: today,
},
});
if (result.count > 0) {
this.logger.log(
`Usage limits for ${result.count} users have been reset`,
);
} else {
this.logger.log("No user limits needed resetting");
}
} catch (error: any) {
this.logger.error(`Limit reset job failed: ${error.message}`);
}
if (result.count > 0) {
this.logger.log(
`Usage limits for ${result.count} users have been reset`,
);
} else {
this.logger.log("No user limits needed resetting");
}
} catch (error: any) {
this.logger.error(`Limit reset job failed: ${error.message}`);
}
},
this.logger,
);
}
/**
@@ -58,37 +79,65 @@ export class LimitResetterTask {
@Cron("0 4 * * *", { timeZone: "Europe/Istanbul" })
async cleanupOldData() {
if (this.shouldSkipInHistoricalMode("cleanupOldData")) return;
this.logger.log("Starting data cleanup job...");
await this.taskLock.runWithLease(
"cleanupOldData",
60 * 60 * 1000,
async () => {
this.logger.log("Starting data cleanup job...");
try {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
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 },
},
});
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 },
},
});
const deletedLiveMatches = await this.prisma.liveMatch.deleteMany({
where: {
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 } },
],
},
},
],
},
],
},
});
this.logger.log(
`Cleanup complete: ${deletedLogs.count} old logs, ${deletedLiveMatches.count} old live matches`,
);
} catch (error: any) {
this.logger.error(`Cleanup job failed: ${error.message}`);
}
this.logger.log(
`Cleanup complete: ${deletedLogs.count} old logs, ${deletedLiveMatches.count} old live matches`,
);
} catch (error: any) {
this.logger.error(`Cleanup job failed: ${error.message}`);
}
},
this.logger,
);
}
/**
@@ -97,26 +146,33 @@ export class LimitResetterTask {
@Cron("0 0 * * *", { timeZone: "Europe/Istanbul" })
async checkSubscriptions() {
if (this.shouldSkipInHistoricalMode("checkSubscriptions")) return;
this.logger.log("Checking expired subscriptions...");
await this.taskLock.runWithLease(
"checkSubscriptions",
30 * 60 * 1000,
async () => {
this.logger.log("Checking expired subscriptions...");
try {
const now = new Date();
try {
const now = new Date();
const result = await this.prisma.user.updateMany({
where: {
subscriptionStatus: "active",
subscriptionExpiresAt: { lt: now },
},
data: {
subscriptionStatus: "expired",
},
});
const result = await this.prisma.user.updateMany({
where: {
subscriptionStatus: "active",
subscriptionExpiresAt: { lt: now },
},
data: {
subscriptionStatus: "expired",
},
});
if (result.count > 0) {
this.logger.log(`${result.count} subscriptions marked as expired`);
}
} catch (error: any) {
this.logger.error(`Subscription check failed: ${error.message}`);
}
if (result.count > 0) {
this.logger.log(`${result.count} subscriptions marked as expired`);
}
} catch (error: any) {
this.logger.error(`Subscription check failed: ${error.message}`);
}
},
this.logger,
);
}
}
+80
View File
@@ -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}`;
}
}
+7 -3
View File
@@ -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 {}