Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5c2edf346 | |||
| 1f26a5bf2f | |||
| 634204acf0 | |||
| df428ed1e8 | |||
| 2ccd6831eb | |||
| 1346924387 | |||
| e4c74025e5 | |||
| c8e7e4e927 |
@@ -1,26 +0,0 @@
|
||||
name: Check Docker Pi
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [check-docker]
|
||||
|
||||
jobs:
|
||||
check-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get Docker Info
|
||||
run: |
|
||||
date > docker_info.txt
|
||||
echo "==== DOCKER PS ====" >> docker_info.txt
|
||||
docker ps -a >> docker_info.txt
|
||||
echo "==== DOCKER STATS ====" >> docker_info.txt
|
||||
docker stats --no-stream >> docker_info.txt
|
||||
|
||||
git config --global user.name "Gitea Actions"
|
||||
git config --global user.email "actions@gitea.local"
|
||||
git add docker_info.txt
|
||||
git commit -m "chore: add docker info" || true
|
||||
git push origin check-docker
|
||||
@@ -0,0 +1,874 @@
|
||||
{
|
||||
"meta":{"test_sets":["test"],"test_metrics":[{"best_value":"Min","name":"Logloss"}],"learn_metrics":[{"best_value":"Min","name":"Logloss"}],"launch_mode":"Train","parameters":"","iteration_count":2000,"learn_sets":["learn"],"name":"experiment"},
|
||||
"iterations":[
|
||||
{"learn":[0.692389481],"iteration":0,"passed_time":0.04679785798,"remaining_time":93.54891809,"test":[0.6924099937]},
|
||||
{"learn":[0.6916338586],"iteration":1,"passed_time":0.08350330552,"remaining_time":83.41980222,"test":[0.6916660956]},
|
||||
{"learn":[0.6910159214],"iteration":2,"passed_time":0.132821758,"remaining_time":88.41501689,"test":[0.691108145]},
|
||||
{"learn":[0.6903417151],"iteration":3,"passed_time":0.162826233,"remaining_time":81.25029026,"test":[0.6904585078]},
|
||||
{"learn":[0.6896961461],"iteration":4,"passed_time":0.1969265393,"remaining_time":78.57368918,"test":[0.689812816]},
|
||||
{"learn":[0.6890979366],"iteration":5,"passed_time":0.2309352918,"remaining_time":76.74749531,"test":[0.689192261]},
|
||||
{"learn":[0.6884946167],"iteration":6,"passed_time":0.2693987513,"remaining_time":76.70167304,"test":[0.6886032715]},
|
||||
{"learn":[0.6879503686],"iteration":7,"passed_time":0.3199759681,"remaining_time":79.67401607,"test":[0.6880706742]},
|
||||
{"learn":[0.6874528094],"iteration":8,"passed_time":0.3645802206,"remaining_time":80.65324659,"test":[0.6876192378]},
|
||||
{"learn":[0.6869036785],"iteration":9,"passed_time":0.4116507506,"remaining_time":81.91849936,"test":[0.6870868859]},
|
||||
{"learn":[0.6863761921],"iteration":10,"passed_time":0.4562469316,"remaining_time":82.49774064,"test":[0.6865493528]},
|
||||
{"learn":[0.6859038678],"iteration":11,"passed_time":0.491541699,"remaining_time":81.43207481,"test":[0.686105086]},
|
||||
{"learn":[0.685410175],"iteration":12,"passed_time":0.5221556769,"remaining_time":79.80948692,"test":[0.6856345086]},
|
||||
{"learn":[0.6849483392],"iteration":13,"passed_time":0.5553110353,"remaining_time":78.77483686,"test":[0.6852027185]},
|
||||
{"learn":[0.6845417792],"iteration":14,"passed_time":0.5952927147,"remaining_time":78.77706925,"test":[0.6848238481]},
|
||||
{"learn":[0.6841038875],"iteration":15,"passed_time":0.6300274185,"remaining_time":78.12339989,"test":[0.6844045699]},
|
||||
{"learn":[0.6836957422],"iteration":16,"passed_time":0.662600544,"remaining_time":77.29040464,"test":[0.6840077621]},
|
||||
{"learn":[0.6832947461],"iteration":17,"passed_time":0.7004221698,"remaining_time":77.12426337,"test":[0.6836197496]},
|
||||
{"learn":[0.6829014105],"iteration":18,"passed_time":0.7300844347,"remaining_time":76.12090869,"test":[0.6832475033]},
|
||||
{"learn":[0.6825264546],"iteration":19,"passed_time":0.7641559459,"remaining_time":75.65143865,"test":[0.6829012069]},
|
||||
{"learn":[0.6822106577],"iteration":20,"passed_time":0.8040792063,"remaining_time":75.77489282,"test":[0.6825880966]},
|
||||
{"learn":[0.6818649349],"iteration":21,"passed_time":0.8356039756,"remaining_time":75.12839381,"test":[0.6822424968]},
|
||||
{"learn":[0.6815467855],"iteration":22,"passed_time":0.8861440327,"remaining_time":76.16985881,"test":[0.6819180513]},
|
||||
{"learn":[0.6812293319],"iteration":23,"passed_time":0.920219319,"remaining_time":75.76472393,"test":[0.6816384467]},
|
||||
{"learn":[0.6808837443],"iteration":24,"passed_time":0.960164738,"remaining_time":75.8530143,"test":[0.6813262593]},
|
||||
{"learn":[0.6805816494],"iteration":25,"passed_time":0.9895547925,"remaining_time":75.13004463,"test":[0.6810353411]},
|
||||
{"learn":[0.6803209634],"iteration":26,"passed_time":1.025550161,"remaining_time":74.94112844,"test":[0.6808138172]},
|
||||
{"learn":[0.6800350862],"iteration":27,"passed_time":1.060852064,"remaining_time":74.71429535,"test":[0.6805550049]},
|
||||
{"learn":[0.6797703947],"iteration":28,"passed_time":1.10467538,"remaining_time":75.07983357,"test":[0.680347991]},
|
||||
{"learn":[0.6794926675],"iteration":29,"passed_time":1.141766834,"remaining_time":74.97602208,"test":[0.680089679]},
|
||||
{"learn":[0.6792251865],"iteration":30,"passed_time":1.180421588,"remaining_time":74.9758099,"test":[0.6798451919]},
|
||||
{"learn":[0.6789670166],"iteration":31,"passed_time":1.213674604,"remaining_time":74.64098814,"test":[0.6796090443]},
|
||||
{"learn":[0.678722402],"iteration":32,"passed_time":1.245848393,"remaining_time":74.26011482,"test":[0.6793890865]},
|
||||
{"learn":[0.678476935],"iteration":33,"passed_time":1.287262512,"remaining_time":74.43406171,"test":[0.6791683772]},
|
||||
{"learn":[0.6782297335],"iteration":34,"passed_time":1.327473991,"remaining_time":74.52818262,"test":[0.6789766369]},
|
||||
{"learn":[0.6780226701],"iteration":35,"passed_time":1.3760549,"remaining_time":75.07143955,"test":[0.6787930242]},
|
||||
{"learn":[0.6778291026],"iteration":36,"passed_time":1.427620019,"remaining_time":75.74102965,"test":[0.6786087714]},
|
||||
{"learn":[0.6776045324],"iteration":37,"passed_time":1.468182407,"remaining_time":75.80457587,"test":[0.6784161299]},
|
||||
{"learn":[0.6773969079],"iteration":38,"passed_time":1.508647379,"remaining_time":75.85788487,"test":[0.6782227897]},
|
||||
{"learn":[0.6771819602],"iteration":39,"passed_time":1.549435187,"remaining_time":75.92232419,"test":[0.6780242369]},
|
||||
{"learn":[0.6769816736],"iteration":40,"passed_time":1.586036608,"remaining_time":75.78160282,"test":[0.6778499631]},
|
||||
{"learn":[0.6767984027],"iteration":41,"passed_time":1.621458864,"remaining_time":75.59086802,"test":[0.6776975784]},
|
||||
{"learn":[0.6766201184],"iteration":42,"passed_time":1.663424818,"remaining_time":75.70517136,"test":[0.6775231674]},
|
||||
{"learn":[0.6764394377],"iteration":43,"passed_time":1.70110089,"remaining_time":75.62166686,"test":[0.6773582124]},
|
||||
{"learn":[0.6762698797],"iteration":44,"passed_time":1.739954496,"remaining_time":75.59135644,"test":[0.6772234666]},
|
||||
{"learn":[0.6760974263],"iteration":45,"passed_time":1.776461223,"remaining_time":75.46098325,"test":[0.6770659843]},
|
||||
{"learn":[0.6759245179],"iteration":46,"passed_time":1.819761638,"remaining_time":75.61690381,"test":[0.6769049529]},
|
||||
{"learn":[0.6757673909],"iteration":47,"passed_time":1.869479807,"remaining_time":76.02551217,"test":[0.6767664194]},
|
||||
{"learn":[0.6756172628],"iteration":48,"passed_time":1.916010121,"remaining_time":76.28848462,"test":[0.6766584917]},
|
||||
{"learn":[0.675474531],"iteration":49,"passed_time":1.953635244,"remaining_time":76.19177452,"test":[0.6765507257]},
|
||||
{"learn":[0.6753286933],"iteration":50,"passed_time":1.993876686,"remaining_time":76.19736591,"test":[0.6764489911]},
|
||||
{"learn":[0.6751900513],"iteration":51,"passed_time":2.038943041,"remaining_time":76.38194316,"test":[0.6763947956]},
|
||||
{"learn":[0.6750574835],"iteration":52,"passed_time":2.080276765,"remaining_time":76.42073325,"test":[0.6762778712]},
|
||||
{"learn":[0.6749329567],"iteration":53,"passed_time":2.158576742,"remaining_time":77.78871001,"test":[0.6761865366]},
|
||||
{"learn":[0.6748033265],"iteration":54,"passed_time":2.220619687,"remaining_time":78.52918711,"test":[0.6760679685]},
|
||||
{"learn":[0.6746797823],"iteration":55,"passed_time":2.286959228,"remaining_time":79.39015604,"test":[0.6759774874]},
|
||||
{"learn":[0.674535525],"iteration":56,"passed_time":2.328472096,"remaining_time":79.3723032,"test":[0.6758500622]},
|
||||
{"learn":[0.6744256514],"iteration":57,"passed_time":2.367031568,"remaining_time":79.25474665,"test":[0.6757625065]},
|
||||
{"learn":[0.674310819],"iteration":58,"passed_time":2.409161286,"remaining_time":79.25732298,"test":[0.6756876412]},
|
||||
{"learn":[0.6741967947],"iteration":59,"passed_time":2.444825903,"remaining_time":79.04937087,"test":[0.6756151069]},
|
||||
{"learn":[0.6740879654],"iteration":60,"passed_time":2.48484996,"remaining_time":78.98564055,"test":[0.6755303655]},
|
||||
{"learn":[0.6739772476],"iteration":61,"passed_time":2.521603395,"remaining_time":78.8204416,"test":[0.6754565036]},
|
||||
{"learn":[0.67388281],"iteration":62,"passed_time":2.554102332,"remaining_time":78.5285114,"test":[0.6753738983]},
|
||||
{"learn":[0.6737789726],"iteration":63,"passed_time":2.593937938,"remaining_time":78.46662263,"test":[0.6752897299]},
|
||||
{"learn":[0.6736812332],"iteration":64,"passed_time":2.623889155,"remaining_time":78.11116175,"test":[0.6752115539]},
|
||||
{"learn":[0.6735930009],"iteration":65,"passed_time":2.660795108,"remaining_time":77.96935967,"test":[0.6751595431]},
|
||||
{"learn":[0.6734947116],"iteration":66,"passed_time":2.695822592,"remaining_time":77.77649358,"test":[0.6750764658]},
|
||||
{"learn":[0.6733961481],"iteration":67,"passed_time":2.725876686,"remaining_time":77.44696703,"test":[0.6750179194]},
|
||||
{"learn":[0.6732990195],"iteration":68,"passed_time":2.761848366,"remaining_time":77.29172746,"test":[0.6749408803]},
|
||||
{"learn":[0.6732133575],"iteration":69,"passed_time":2.791847449,"remaining_time":76.97522253,"test":[0.6748795802]},
|
||||
{"learn":[0.673111539],"iteration":70,"passed_time":2.824541003,"remaining_time":76.73999429,"test":[0.674790372]},
|
||||
{"learn":[0.6730080451],"iteration":71,"passed_time":2.861023716,"remaining_time":76.61185729,"test":[0.6747239773]},
|
||||
{"learn":[0.6729157861],"iteration":72,"passed_time":2.897136588,"remaining_time":76.47646857,"test":[0.6746701254]},
|
||||
{"learn":[0.6728347949],"iteration":73,"passed_time":2.935718661,"remaining_time":76.40802894,"test":[0.6746120937]},
|
||||
{"learn":[0.6727640693],"iteration":74,"passed_time":3.040023476,"remaining_time":78.02726921,"test":[0.6745550085]},
|
||||
{"learn":[0.6726808811],"iteration":75,"passed_time":3.097341794,"remaining_time":78.41165279,"test":[0.6744855074]},
|
||||
{"learn":[0.6726029645],"iteration":76,"passed_time":3.152948955,"remaining_time":78.74182909,"test":[0.6744264172]},
|
||||
{"learn":[0.6725356026],"iteration":77,"passed_time":3.216126808,"remaining_time":79.24866314,"test":[0.674381715]},
|
||||
{"learn":[0.6724606887],"iteration":78,"passed_time":3.256861302,"remaining_time":79.19532355,"test":[0.6743331681]},
|
||||
{"learn":[0.6723849561],"iteration":79,"passed_time":3.305679851,"remaining_time":79.33631641,"test":[0.67428564]},
|
||||
{"learn":[0.6723050519],"iteration":80,"passed_time":3.348083566,"remaining_time":79.32064647,"test":[0.6742202413]},
|
||||
{"learn":[0.6722508802],"iteration":81,"passed_time":3.38129387,"remaining_time":79.08928832,"test":[0.6741620971]},
|
||||
{"learn":[0.6721773904],"iteration":82,"passed_time":3.41660066,"remaining_time":78.91112609,"test":[0.6741109453]},
|
||||
{"learn":[0.6721007598],"iteration":83,"passed_time":3.48099347,"remaining_time":79.39980344,"test":[0.6740556003]},
|
||||
{"learn":[0.6720353564],"iteration":84,"passed_time":3.535359896,"remaining_time":79.64957884,"test":[0.6740146772]},
|
||||
{"learn":[0.6719790902],"iteration":85,"passed_time":3.581806996,"remaining_time":79.71603012,"test":[0.673983295]},
|
||||
{"learn":[0.6719140024],"iteration":86,"passed_time":3.612293661,"remaining_time":79.42893993,"test":[0.6739595301]},
|
||||
{"learn":[0.6718573633],"iteration":87,"passed_time":3.644530261,"remaining_time":79.18570293,"test":[0.6739336659]},
|
||||
{"learn":[0.671795602],"iteration":88,"passed_time":3.67809653,"remaining_time":78.97575809,"test":[0.673890361]},
|
||||
{"learn":[0.6717369134],"iteration":89,"passed_time":3.712417516,"remaining_time":78.78574951,"test":[0.673863586]},
|
||||
{"learn":[0.6716711079],"iteration":90,"passed_time":3.743502971,"remaining_time":78.53128759,"test":[0.6738190616]},
|
||||
{"learn":[0.6716070843],"iteration":91,"passed_time":3.775351679,"remaining_time":78.2975109,"test":[0.6737799295]},
|
||||
{"learn":[0.6715517232],"iteration":92,"passed_time":3.806186247,"remaining_time":78.04728142,"test":[0.6737364374]},
|
||||
{"learn":[0.6714957378],"iteration":93,"passed_time":3.83798807,"remaining_time":77.82133257,"test":[0.6737093719]},
|
||||
{"learn":[0.6714364567],"iteration":94,"passed_time":3.871278973,"remaining_time":77.62933099,"test":[0.6736630475]},
|
||||
{"learn":[0.6713881758],"iteration":95,"passed_time":3.913531039,"remaining_time":77.6183656,"test":[0.67364367]},
|
||||
{"learn":[0.6713336502],"iteration":96,"passed_time":3.945433866,"remaining_time":77.40371802,"test":[0.6735998081]},
|
||||
{"learn":[0.6712700267],"iteration":97,"passed_time":3.989716281,"remaining_time":77.43306496,"test":[0.6735526984]},
|
||||
{"learn":[0.6712154424],"iteration":98,"passed_time":4.020621946,"remaining_time":77.20406384,"test":[0.6735012924]},
|
||||
{"learn":[0.6711600413],"iteration":99,"passed_time":4.053732144,"remaining_time":77.02091074,"test":[0.6734818024]},
|
||||
{"learn":[0.6711060533],"iteration":100,"passed_time":4.084124711,"remaining_time":76.78963194,"test":[0.6734379341]},
|
||||
{"learn":[0.6710494943],"iteration":101,"passed_time":4.116434744,"remaining_time":76.59797199,"test":[0.6734059869]},
|
||||
{"learn":[0.6709936897],"iteration":102,"passed_time":4.148330356,"remaining_time":76.40177365,"test":[0.6733740852]},
|
||||
{"learn":[0.6709472183],"iteration":103,"passed_time":4.176511193,"remaining_time":76.14101176,"test":[0.6733330971]},
|
||||
{"learn":[0.6708914508],"iteration":104,"passed_time":4.2025065,"remaining_time":75.84523636,"test":[0.6733060254]},
|
||||
{"learn":[0.6708388195],"iteration":105,"passed_time":4.232975206,"remaining_time":75.63448151,"test":[0.6732755898]},
|
||||
{"learn":[0.6707885854],"iteration":106,"passed_time":4.261364958,"remaining_time":75.39031649,"test":[0.6732294722]},
|
||||
{"learn":[0.6707454167],"iteration":107,"passed_time":4.290824713,"remaining_time":75.1688922,"test":[0.6732035176]},
|
||||
{"learn":[0.6706973013],"iteration":108,"passed_time":4.324192493,"remaining_time":75.01878903,"test":[0.673196437]},
|
||||
{"learn":[0.6706577031],"iteration":109,"passed_time":4.351512102,"remaining_time":74.76688976,"test":[0.6731652709]},
|
||||
{"learn":[0.67061108],"iteration":110,"passed_time":4.38641502,"remaining_time":74.64808984,"test":[0.673138808]},
|
||||
{"learn":[0.6705625485],"iteration":111,"passed_time":4.424063991,"remaining_time":74.57707871,"test":[0.6731062725]},
|
||||
{"learn":[0.6705146484],"iteration":112,"passed_time":4.45863849,"remaining_time":74.45531709,"test":[0.6730726625]},
|
||||
{"learn":[0.6704704423],"iteration":113,"passed_time":4.497153675,"remaining_time":74.40027922,"test":[0.6730285927]},
|
||||
{"learn":[0.6704155922],"iteration":114,"passed_time":4.533368584,"remaining_time":74.30782417,"test":[0.6729872702]},
|
||||
{"learn":[0.6703687117],"iteration":115,"passed_time":4.564651269,"remaining_time":74.13623268,"test":[0.6729721425]},
|
||||
{"learn":[0.6703324232],"iteration":116,"passed_time":4.596824343,"remaining_time":73.98136956,"test":[0.6729564624]},
|
||||
{"learn":[0.6702884624],"iteration":117,"passed_time":4.628377967,"remaining_time":73.81870623,"test":[0.6729312424]},
|
||||
{"learn":[0.670253478],"iteration":118,"passed_time":4.668052254,"remaining_time":73.78660748,"test":[0.6729354345]},
|
||||
{"learn":[0.6702140804],"iteration":119,"passed_time":4.692108266,"remaining_time":73.50969617,"test":[0.6729085401]},
|
||||
{"learn":[0.6701682529],"iteration":120,"passed_time":4.723741667,"remaining_time":73.354633,"test":[0.6728898322]},
|
||||
{"learn":[0.6701320588],"iteration":121,"passed_time":4.756626425,"remaining_time":73.22085595,"test":[0.6728773638]},
|
||||
{"learn":[0.6700939824],"iteration":122,"passed_time":4.788008428,"remaining_time":73.06578714,"test":[0.6728618874]},
|
||||
{"learn":[0.6700655902],"iteration":123,"passed_time":4.815546648,"remaining_time":72.85456058,"test":[0.6728540413]},
|
||||
{"learn":[0.6700190743],"iteration":124,"passed_time":4.843186806,"remaining_time":72.64780209,"test":[0.6728441291]},
|
||||
{"learn":[0.6699792296],"iteration":125,"passed_time":4.875548614,"remaining_time":72.51411192,"test":[0.672815631]},
|
||||
{"learn":[0.6699379404],"iteration":126,"passed_time":4.916953662,"remaining_time":72.51538748,"test":[0.6728082021]},
|
||||
{"learn":[0.669895454],"iteration":127,"passed_time":4.952918369,"remaining_time":72.43643115,"test":[0.6727900064]},
|
||||
{"learn":[0.6698563938],"iteration":128,"passed_time":4.991585558,"remaining_time":72.39733782,"test":[0.6727649552]},
|
||||
{"learn":[0.6698215571],"iteration":129,"passed_time":5.028084166,"remaining_time":72.32705685,"test":[0.6727467657]},
|
||||
{"learn":[0.6697857067],"iteration":130,"passed_time":5.059198996,"remaining_time":72.18048033,"test":[0.6727396032]},
|
||||
{"learn":[0.6697449303],"iteration":131,"passed_time":5.096035515,"remaining_time":72.1166238,"test":[0.6727245271]},
|
||||
{"learn":[0.6697052425],"iteration":132,"passed_time":5.125282589,"remaining_time":71.94663604,"test":[0.6726955143]},
|
||||
{"learn":[0.6696695553],"iteration":133,"passed_time":5.156392608,"remaining_time":71.80469109,"test":[0.67269209]},
|
||||
{"learn":[0.6696269265],"iteration":134,"passed_time":5.190402292,"remaining_time":71.70444647,"test":[0.672677932]},
|
||||
{"learn":[0.6695969271],"iteration":135,"passed_time":5.221466142,"remaining_time":71.56480065,"test":[0.6726540285]},
|
||||
{"learn":[0.6695489786],"iteration":136,"passed_time":5.251144663,"remaining_time":71.40790151,"test":[0.6726288583]},
|
||||
{"learn":[0.6695173859],"iteration":137,"passed_time":5.274361693,"remaining_time":71.16566285,"test":[0.6725863431]},
|
||||
{"learn":[0.6694811164],"iteration":138,"passed_time":5.309398952,"remaining_time":71.08483058,"test":[0.6725837967]},
|
||||
{"learn":[0.6694477439],"iteration":139,"passed_time":5.344693175,"remaining_time":71.00806646,"test":[0.6725772977]},
|
||||
{"learn":[0.6694082161],"iteration":140,"passed_time":5.377737126,"remaining_time":70.90222211,"test":[0.6725685594]},
|
||||
{"learn":[0.6693679185],"iteration":141,"passed_time":5.416087925,"remaining_time":70.8668406,"test":[0.6725553829]},
|
||||
{"learn":[0.6693341916],"iteration":142,"passed_time":5.452286939,"remaining_time":70.80347444,"test":[0.6725484347]},
|
||||
{"learn":[0.6692933159],"iteration":143,"passed_time":5.490006789,"remaining_time":70.7600875,"test":[0.6725306172]},
|
||||
{"learn":[0.6692619696],"iteration":144,"passed_time":5.521869859,"remaining_time":70.64185233,"test":[0.672543149]},
|
||||
{"learn":[0.6692229289],"iteration":145,"passed_time":5.553520721,"remaining_time":70.5221056,"test":[0.6725196247]},
|
||||
{"learn":[0.6691840164],"iteration":146,"passed_time":5.582178524,"remaining_time":70.3658286,"test":[0.6725226452]},
|
||||
{"learn":[0.6691581406],"iteration":147,"passed_time":5.611368671,"remaining_time":70.21793769,"test":[0.6725056913]},
|
||||
{"learn":[0.6691177196],"iteration":148,"passed_time":5.636941079,"remaining_time":70.02669757,"test":[0.6724771476]},
|
||||
{"learn":[0.6690851126],"iteration":149,"passed_time":5.673704689,"remaining_time":69.97569117,"test":[0.6724439435]},
|
||||
{"learn":[0.6690518144],"iteration":150,"passed_time":5.706346207,"remaining_time":69.87439826,"test":[0.672442532]},
|
||||
{"learn":[0.6690149711],"iteration":151,"passed_time":5.738210991,"remaining_time":69.76456521,"test":[0.6724303064]},
|
||||
{"learn":[0.668993877],"iteration":152,"passed_time":5.765951318,"remaining_time":69.60596133,"test":[0.6724235788]},
|
||||
{"learn":[0.6689596579],"iteration":153,"passed_time":5.795573467,"remaining_time":69.47161442,"test":[0.6724294499]},
|
||||
{"learn":[0.6689372651],"iteration":154,"passed_time":5.81744896,"remaining_time":69.24640858,"test":[0.6724285935]},
|
||||
{"learn":[0.6689003045],"iteration":155,"passed_time":5.853529431,"remaining_time":69.19171968,"test":[0.6724172017]},
|
||||
{"learn":[0.6688680182],"iteration":156,"passed_time":5.888380392,"remaining_time":69.12283479,"test":[0.6724130745]},
|
||||
{"learn":[0.6688348164],"iteration":157,"passed_time":5.924601775,"remaining_time":69.07035741,"test":[0.6723860878]},
|
||||
{"learn":[0.6687947046],"iteration":158,"passed_time":5.964531924,"remaining_time":69.06102687,"test":[0.6723707604]},
|
||||
{"learn":[0.6687605251],"iteration":159,"passed_time":5.996805452,"remaining_time":68.9632627,"test":[0.6723566111]},
|
||||
{"learn":[0.668726253],"iteration":160,"passed_time":6.022341459,"remaining_time":68.78935368,"test":[0.6723469906]},
|
||||
{"learn":[0.6686862718],"iteration":161,"passed_time":6.05082584,"remaining_time":68.65072774,"test":[0.6723287161]},
|
||||
{"learn":[0.668663478],"iteration":162,"passed_time":6.079027554,"remaining_time":68.51026759,"test":[0.6723155898]},
|
||||
{"learn":[0.6686399521],"iteration":163,"passed_time":6.108511297,"remaining_time":68.38552891,"test":[0.6722970834]},
|
||||
{"learn":[0.6686058279],"iteration":164,"passed_time":6.140719309,"remaining_time":68.29224202,"test":[0.6722872244]},
|
||||
{"learn":[0.6685761282],"iteration":165,"passed_time":6.169540017,"remaining_time":68.16226742,"test":[0.6722800481]},
|
||||
{"learn":[0.6685469327],"iteration":166,"passed_time":6.2020892,"remaining_time":68.07442817,"test":[0.6722550973]},
|
||||
{"learn":[0.6685157003],"iteration":167,"passed_time":6.231576547,"remaining_time":67.95385854,"test":[0.6722394313]},
|
||||
{"learn":[0.6684805143],"iteration":168,"passed_time":6.263261652,"remaining_time":67.85817802,"test":[0.6722204135]},
|
||||
{"learn":[0.6684485765],"iteration":169,"passed_time":6.295102833,"remaining_time":67.7649305,"test":[0.6721982148]},
|
||||
{"learn":[0.6684144429],"iteration":170,"passed_time":6.325415964,"remaining_time":67.65605729,"test":[0.6721971176]},
|
||||
{"learn":[0.6683849752],"iteration":171,"passed_time":6.35697084,"remaining_time":67.56129474,"test":[0.6721880705]},
|
||||
{"learn":[0.6683568537],"iteration":172,"passed_time":6.395913563,"remaining_time":67.5452837,"test":[0.672179176]},
|
||||
{"learn":[0.6683266628],"iteration":173,"passed_time":6.437330522,"remaining_time":67.55497433,"test":[0.6721769709]},
|
||||
{"learn":[0.6682937842],"iteration":174,"passed_time":6.472195712,"remaining_time":67.49575528,"test":[0.6721693215]},
|
||||
{"learn":[0.6682657097],"iteration":175,"passed_time":6.503044842,"remaining_time":67.395192,"test":[0.6721581386]},
|
||||
{"learn":[0.6682301443],"iteration":176,"passed_time":6.533528251,"remaining_time":67.29164972,"test":[0.6721638661]},
|
||||
{"learn":[0.6681995916],"iteration":177,"passed_time":6.562589882,"remaining_time":67.17437509,"test":[0.6721598475]},
|
||||
{"learn":[0.6681658267],"iteration":178,"passed_time":6.590816982,"remaining_time":67.04959623,"test":[0.6721433342]},
|
||||
{"learn":[0.6681422687],"iteration":179,"passed_time":6.624646227,"remaining_time":66.98253407,"test":[0.6721335599]},
|
||||
{"learn":[0.6681216601],"iteration":180,"passed_time":6.655147334,"remaining_time":66.88239227,"test":[0.6721300594]},
|
||||
{"learn":[0.6680899019],"iteration":181,"passed_time":6.687788902,"remaining_time":66.80439684,"test":[0.6721153533]},
|
||||
{"learn":[0.6680676394],"iteration":182,"passed_time":6.718057043,"remaining_time":66.7033314,"test":[0.6721076397]},
|
||||
{"learn":[0.6680413672],"iteration":183,"passed_time":6.751300957,"remaining_time":66.6324051,"test":[0.6721009911]},
|
||||
{"learn":[0.6680088406],"iteration":184,"passed_time":6.784288393,"remaining_time":66.55936991,"test":[0.6720999252]},
|
||||
{"learn":[0.6679873982],"iteration":185,"passed_time":6.810905309,"remaining_time":66.42463565,"test":[0.6720953028]},
|
||||
{"learn":[0.6679663544],"iteration":186,"passed_time":6.832974292,"remaining_time":66.24696466,"test":[0.6720942505]},
|
||||
{"learn":[0.6679417375],"iteration":187,"passed_time":6.867184511,"remaining_time":66.18796986,"test":[0.6720856237]},
|
||||
{"learn":[0.6679100197],"iteration":188,"passed_time":6.918652024,"remaining_time":66.29459691,"test":[0.6720876136]},
|
||||
{"learn":[0.667881208],"iteration":189,"passed_time":6.96948149,"remaining_time":66.39348156,"test":[0.6720880182]},
|
||||
{"learn":[0.6678475427],"iteration":190,"passed_time":7.018176318,"remaining_time":66.47058094,"test":[0.6720743856]},
|
||||
{"learn":[0.6678310341],"iteration":191,"passed_time":7.074099623,"remaining_time":66.61443812,"test":[0.6720598415]},
|
||||
{"learn":[0.6678060257],"iteration":192,"passed_time":7.117099742,"remaining_time":66.63522919,"test":[0.6720563492]},
|
||||
{"learn":[0.6677789336],"iteration":193,"passed_time":7.191058554,"remaining_time":66.94356571,"test":[0.6720389527]},
|
||||
{"learn":[0.6677478773],"iteration":194,"passed_time":7.2421897,"remaining_time":67.03667902,"test":[0.6720317324]},
|
||||
{"learn":[0.6677212408],"iteration":195,"passed_time":7.282401129,"remaining_time":67.02781447,"test":[0.672000736]},
|
||||
{"learn":[0.667704316],"iteration":196,"passed_time":7.317019235,"remaining_time":66.96744,"test":[0.6719895017]},
|
||||
{"learn":[0.6676819639],"iteration":197,"passed_time":7.351194179,"remaining_time":66.90329248,"test":[0.6719725302]},
|
||||
{"learn":[0.6676554448],"iteration":198,"passed_time":7.389840926,"remaining_time":66.87991712,"test":[0.6719770493]},
|
||||
{"learn":[0.6676318346],"iteration":199,"passed_time":7.432994652,"remaining_time":66.89695187,"test":[0.6719667172]},
|
||||
{"learn":[0.6676074705],"iteration":200,"passed_time":7.471295231,"remaining_time":66.86995085,"test":[0.6719511616]},
|
||||
{"learn":[0.6675849784],"iteration":201,"passed_time":7.506377837,"remaining_time":66.8141948,"test":[0.6719427289]},
|
||||
{"learn":[0.6675631744],"iteration":202,"passed_time":7.540821494,"remaining_time":66.75298633,"test":[0.6719299116]},
|
||||
{"learn":[0.6675397619],"iteration":203,"passed_time":7.56808212,"remaining_time":66.62880141,"test":[0.6719106583]},
|
||||
{"learn":[0.6675169086],"iteration":204,"passed_time":7.605676901,"remaining_time":66.59604896,"test":[0.6718967065]},
|
||||
{"learn":[0.6674864762],"iteration":205,"passed_time":7.638300222,"remaining_time":66.51995436,"test":[0.671890967]},
|
||||
{"learn":[0.6674670714],"iteration":206,"passed_time":7.665554951,"remaining_time":66.39777791,"test":[0.6718896293]},
|
||||
{"learn":[0.6674375599],"iteration":207,"passed_time":7.700277678,"remaining_time":66.34085384,"test":[0.6718883534]},
|
||||
{"learn":[0.6674148457],"iteration":208,"passed_time":7.734145802,"remaining_time":66.27681881,"test":[0.6718827289]},
|
||||
{"learn":[0.6673974446],"iteration":209,"passed_time":7.766232144,"remaining_time":66.19788351,"test":[0.6718763224]},
|
||||
{"learn":[0.6673812139],"iteration":210,"passed_time":7.796801222,"remaining_time":66.1065279,"test":[0.67187262]},
|
||||
{"learn":[0.6673515687],"iteration":211,"passed_time":7.831891449,"remaining_time":66.05387693,"test":[0.6718590402]},
|
||||
{"learn":[0.6673197956],"iteration":212,"passed_time":7.871259964,"remaining_time":66.0372843,"test":[0.6718455115]},
|
||||
{"learn":[0.6672900754],"iteration":213,"passed_time":7.910110502,"remaining_time":66.01615587,"test":[0.6718253747]},
|
||||
{"learn":[0.6672550009],"iteration":214,"passed_time":7.951342226,"remaining_time":66.01463197,"test":[0.671794877]},
|
||||
{"learn":[0.6672271563],"iteration":215,"passed_time":7.989001461,"remaining_time":65.98323429,"test":[0.6717873786]},
|
||||
{"learn":[0.667204521],"iteration":216,"passed_time":8.025973631,"remaining_time":65.94613357,"test":[0.6717765089]},
|
||||
{"learn":[0.667181968],"iteration":217,"passed_time":8.058434478,"remaining_time":65.87215707,"test":[0.6717616726]},
|
||||
{"learn":[0.6671640023],"iteration":218,"passed_time":8.087145957,"remaining_time":65.76806826,"test":[0.6717499215]},
|
||||
{"learn":[0.66714351],"iteration":219,"passed_time":8.112590578,"remaining_time":65.63823286,"test":[0.6717326052]},
|
||||
{"learn":[0.6671167156],"iteration":220,"passed_time":8.148644349,"remaining_time":65.59474342,"test":[0.6717161937]},
|
||||
{"learn":[0.6670915937],"iteration":221,"passed_time":8.197662625,"remaining_time":65.65515382,"test":[0.6717056951]},
|
||||
{"learn":[0.6670595279],"iteration":222,"passed_time":8.239228431,"remaining_time":65.65519696,"test":[0.6717021438]},
|
||||
{"learn":[0.667033994],"iteration":223,"passed_time":8.268371203,"remaining_time":65.55637168,"test":[0.6716868488]},
|
||||
{"learn":[0.6670008246],"iteration":224,"passed_time":8.298555216,"remaining_time":65.46638004,"test":[0.6716751909]},
|
||||
{"learn":[0.6669858319],"iteration":225,"passed_time":8.327401394,"remaining_time":65.36641625,"test":[0.671670116]},
|
||||
{"learn":[0.6669553964],"iteration":226,"passed_time":8.357648377,"remaining_time":65.27802014,"test":[0.6716558757]},
|
||||
{"learn":[0.6669274683],"iteration":227,"passed_time":8.384989701,"remaining_time":65.16755154,"test":[0.6716559962]},
|
||||
{"learn":[0.666896348],"iteration":228,"passed_time":8.418297538,"remaining_time":65.1039517,"test":[0.6716487875]},
|
||||
{"learn":[0.6668698686],"iteration":229,"passed_time":8.453919972,"remaining_time":65.05842761,"test":[0.6716427451]},
|
||||
{"learn":[0.6668513411],"iteration":230,"passed_time":8.49049033,"remaining_time":65.02024846,"test":[0.6716323255]},
|
||||
{"learn":[0.6668309985],"iteration":231,"passed_time":8.523986676,"remaining_time":64.95865708,"test":[0.6716303547]},
|
||||
{"learn":[0.6668058585],"iteration":232,"passed_time":8.550998228,"remaining_time":64.84812819,"test":[0.6716309509]},
|
||||
{"learn":[0.6667845908],"iteration":233,"passed_time":8.575382398,"remaining_time":64.71848425,"test":[0.6716215401]},
|
||||
{"learn":[0.6667582863],"iteration":234,"passed_time":8.607602961,"remaining_time":64.64859245,"test":[0.6716162103]},
|
||||
{"learn":[0.6667332943],"iteration":235,"passed_time":8.6353786,"remaining_time":64.54579597,"test":[0.6716135097]},
|
||||
{"learn":[0.6667070085],"iteration":236,"passed_time":8.66085309,"remaining_time":64.42651476,"test":[0.6716156696]},
|
||||
{"learn":[0.6666907315],"iteration":237,"passed_time":8.691362456,"remaining_time":64.34529684,"test":[0.6716020054]},
|
||||
{"learn":[0.6666633028],"iteration":238,"passed_time":8.719983169,"remaining_time":64.25058728,"test":[0.6715921704]},
|
||||
{"learn":[0.6666406707],"iteration":239,"passed_time":8.746012652,"remaining_time":64.13742611,"test":[0.6715804466]},
|
||||
{"learn":[0.6666134624],"iteration":240,"passed_time":8.773898765,"remaining_time":64.03853912,"test":[0.6715882966]},
|
||||
{"learn":[0.6665850522],"iteration":241,"passed_time":8.803292064,"remaining_time":63.9511878,"test":[0.6715753942]},
|
||||
{"learn":[0.6665631193],"iteration":242,"passed_time":8.833976809,"remaining_time":63.87365125,"test":[0.6715752261]},
|
||||
{"learn":[0.6665412643],"iteration":243,"passed_time":8.862338006,"remaining_time":63.7797768,"test":[0.6715625509]},
|
||||
{"learn":[0.6665168385],"iteration":244,"passed_time":8.892424073,"remaining_time":63.69879285,"test":[0.6715628214]},
|
||||
{"learn":[0.6664904845],"iteration":245,"passed_time":8.932383667,"remaining_time":63.68862175,"test":[0.6715601629]},
|
||||
{"learn":[0.6664678274],"iteration":246,"passed_time":8.962911123,"remaining_time":63.61126801,"test":[0.6715576255]},
|
||||
{"learn":[0.6664539777],"iteration":247,"passed_time":8.991624872,"remaining_time":63.52147894,"test":[0.6715550274]},
|
||||
{"learn":[0.6664334121],"iteration":248,"passed_time":9.021847081,"remaining_time":63.44278811,"test":[0.6715448645]},
|
||||
{"learn":[0.6664121724],"iteration":249,"passed_time":9.05121341,"remaining_time":63.35849387,"test":[0.6715308166]},
|
||||
{"learn":[0.666392034],"iteration":250,"passed_time":9.085113431,"remaining_time":63.30622865,"test":[0.671519334]},
|
||||
{"learn":[0.666366899],"iteration":251,"passed_time":9.110250512,"remaining_time":63.19332498,"test":[0.6715184071]},
|
||||
{"learn":[0.6663414098],"iteration":252,"passed_time":9.137253573,"remaining_time":63.09399997,"test":[0.6715163019]},
|
||||
{"learn":[0.6663157816],"iteration":253,"passed_time":9.174559864,"remaining_time":63.06606899,"test":[0.6715096094]},
|
||||
{"learn":[0.6662989799],"iteration":254,"passed_time":9.196898204,"remaining_time":62.93563673,"test":[0.6714992963]},
|
||||
{"learn":[0.6662696102],"iteration":255,"passed_time":9.238149902,"remaining_time":62.9348962,"test":[0.6714917256]},
|
||||
{"learn":[0.6662479711],"iteration":256,"passed_time":9.267818291,"remaining_time":62.85528125,"test":[0.671477406]},
|
||||
{"learn":[0.6662231874],"iteration":257,"passed_time":9.297538986,"remaining_time":62.77640665,"test":[0.6714741542]},
|
||||
{"learn":[0.6661947927],"iteration":258,"passed_time":9.324772701,"remaining_time":62.68119411,"test":[0.6714576155]},
|
||||
{"learn":[0.6661669951],"iteration":259,"passed_time":9.357824574,"remaining_time":62.62544138,"test":[0.6714473645]},
|
||||
{"learn":[0.6661426137],"iteration":260,"passed_time":9.388345461,"remaining_time":62.55299907,"test":[0.6714427232]},
|
||||
{"learn":[0.6661216749],"iteration":261,"passed_time":9.427290804,"remaining_time":62.53676114,"test":[0.6714364275]},
|
||||
{"learn":[0.6660983123],"iteration":262,"passed_time":9.461913185,"remaining_time":62.49179925,"test":[0.6714339587]},
|
||||
{"learn":[0.6660803402],"iteration":263,"passed_time":9.496090562,"remaining_time":62.44398945,"test":[0.6714336287]},
|
||||
{"learn":[0.6660617842],"iteration":264,"passed_time":9.524189317,"remaining_time":62.35648477,"test":[0.6714283568]},
|
||||
{"learn":[0.6660443878],"iteration":265,"passed_time":9.55372419,"remaining_time":62.27878852,"test":[0.6714271895]},
|
||||
{"learn":[0.6660176079],"iteration":266,"passed_time":9.590356068,"remaining_time":62.2475171,"test":[0.671413471]},
|
||||
{"learn":[0.6659967546],"iteration":267,"passed_time":9.620235131,"remaining_time":62.17256436,"test":[0.6714072396]},
|
||||
{"learn":[0.6659751467],"iteration":268,"passed_time":9.645948482,"remaining_time":62.0711406,"test":[0.6714002677]},
|
||||
{"learn":[0.6659539329],"iteration":269,"passed_time":9.682675077,"remaining_time":62.04084401,"test":[0.6714001163]},
|
||||
{"learn":[0.6659263951],"iteration":270,"passed_time":9.711914203,"remaining_time":61.96272936,"test":[0.6713933952]},
|
||||
{"learn":[0.6659038921],"iteration":271,"passed_time":9.739142426,"remaining_time":61.87219894,"test":[0.6713926761]},
|
||||
{"learn":[0.6658767418],"iteration":272,"passed_time":9.768751964,"remaining_time":61.79719649,"test":[0.6713836619]},
|
||||
{"learn":[0.6658510507],"iteration":273,"passed_time":9.804576737,"remaining_time":61.76167682,"test":[0.6713772112]},
|
||||
{"learn":[0.6658210119],"iteration":274,"passed_time":9.848653906,"remaining_time":61.77791996,"test":[0.6713603715]},
|
||||
{"learn":[0.6657963011],"iteration":275,"passed_time":9.88663261,"remaining_time":61.75563268,"test":[0.6713560246]},
|
||||
{"learn":[0.6657748552],"iteration":276,"passed_time":9.925808942,"remaining_time":61.74068161,"test":[0.6713837913]},
|
||||
{"learn":[0.6657490013],"iteration":277,"passed_time":9.965409489,"remaining_time":61.72818396,"test":[0.6713684274]},
|
||||
{"learn":[0.665732402],"iteration":278,"passed_time":9.99537326,"remaining_time":61.65604796,"test":[0.6713619356]},
|
||||
{"learn":[0.6657118786],"iteration":279,"passed_time":10.02216777,"remaining_time":61.5647449,"test":[0.6713584836]},
|
||||
{"learn":[0.665684467],"iteration":280,"passed_time":10.05593393,"remaining_time":61.51654955,"test":[0.6713673572]},
|
||||
{"learn":[0.6656584634],"iteration":281,"passed_time":10.08025153,"remaining_time":61.41089406,"test":[0.6713625568]},
|
||||
{"learn":[0.6656309991],"iteration":282,"passed_time":10.11102202,"remaining_time":61.34496401,"test":[0.6713542652]},
|
||||
{"learn":[0.6656073482],"iteration":283,"passed_time":10.14714598,"remaining_time":61.31162855,"test":[0.6713512017]},
|
||||
{"learn":[0.6655890957],"iteration":284,"passed_time":10.17528061,"remaining_time":61.23019734,"test":[0.671342038]},
|
||||
{"learn":[0.6655665563],"iteration":285,"passed_time":10.2021403,"remaining_time":61.14149818,"test":[0.6713279798]},
|
||||
{"learn":[0.6655452454],"iteration":286,"passed_time":10.23423432,"remaining_time":61.08447174,"test":[0.6713123285]},
|
||||
{"learn":[0.6655255286],"iteration":287,"passed_time":10.26481698,"remaining_time":61.0186343,"test":[0.6713035326]},
|
||||
{"learn":[0.6655053548],"iteration":288,"passed_time":10.29945844,"remaining_time":60.97707056,"test":[0.6713022203]},
|
||||
{"learn":[0.6654893396],"iteration":289,"passed_time":10.32366496,"remaining_time":60.87402441,"test":[0.671296041]},
|
||||
{"learn":[0.6654648912],"iteration":290,"passed_time":10.35344703,"remaining_time":60.80426453,"test":[0.6712829551]},
|
||||
{"learn":[0.6654442759],"iteration":291,"passed_time":10.3949915,"remaining_time":60.8035804,"test":[0.6712769751]},
|
||||
{"learn":[0.6654173127],"iteration":292,"passed_time":10.43148765,"remaining_time":60.77320621,"test":[0.6712702915]},
|
||||
{"learn":[0.6653914518],"iteration":293,"passed_time":10.47162738,"remaining_time":60.76393303,"test":[0.6712379343]},
|
||||
{"learn":[0.6653648946],"iteration":294,"passed_time":10.50360107,"remaining_time":60.70725362,"test":[0.6712192006]},
|
||||
{"learn":[0.665344141],"iteration":295,"passed_time":10.53460819,"remaining_time":60.64517686,"test":[0.6712074061]},
|
||||
{"learn":[0.6653140817],"iteration":296,"passed_time":10.57659448,"remaining_time":60.64626395,"test":[0.6711953324]},
|
||||
{"learn":[0.665295365],"iteration":297,"passed_time":10.61260262,"remaining_time":60.61291829,"test":[0.6711891001]},
|
||||
{"learn":[0.6652787488],"iteration":298,"passed_time":10.63910358,"remaining_time":60.52546889,"test":[0.6711870526]},
|
||||
{"learn":[0.6652502991],"iteration":299,"passed_time":10.6681867,"remaining_time":60.45305797,"test":[0.6711812809]},
|
||||
{"learn":[0.665231168],"iteration":300,"passed_time":10.70260503,"remaining_time":60.41104967,"test":[0.6711768946]},
|
||||
{"learn":[0.6652136682],"iteration":301,"passed_time":10.72952096,"remaining_time":60.32690925,"test":[0.6711845012]},
|
||||
{"learn":[0.6651903001],"iteration":302,"passed_time":10.76489952,"remaining_time":60.29054288,"test":[0.6711869636]},
|
||||
{"learn":[0.6651697153],"iteration":303,"passed_time":10.80197155,"remaining_time":60.26363073,"test":[0.671186884]},
|
||||
{"learn":[0.6651525958],"iteration":304,"passed_time":10.82922271,"remaining_time":60.18207375,"test":[0.6711890401]},
|
||||
{"learn":[0.6651322685],"iteration":305,"passed_time":10.8578399,"remaining_time":60.10843394,"test":[0.6711868603]},
|
||||
{"learn":[0.6651113828],"iteration":306,"passed_time":10.89228879,"remaining_time":60.06724727,"test":[0.6711900892]},
|
||||
{"learn":[0.6650886807],"iteration":307,"passed_time":10.93056436,"remaining_time":60.04712628,"test":[0.6711884242]},
|
||||
{"learn":[0.6650622251],"iteration":308,"passed_time":10.97231236,"remaining_time":60.04589061,"test":[0.6711837119]},
|
||||
{"learn":[0.6650429987],"iteration":309,"passed_time":11.00296848,"remaining_time":59.98392494,"test":[0.6711766645]},
|
||||
{"learn":[0.665015513],"iteration":310,"passed_time":11.03002276,"remaining_time":59.90259947,"test":[0.671172959]},
|
||||
{"learn":[0.6650019022],"iteration":311,"passed_time":11.05828865,"remaining_time":59.82817707,"test":[0.6711740433]},
|
||||
{"learn":[0.664979951],"iteration":312,"passed_time":11.09287745,"remaining_time":59.78812863,"test":[0.6711715069]},
|
||||
{"learn":[0.6649549638],"iteration":313,"passed_time":11.1177757,"remaining_time":59.69608229,"test":[0.6711589843]},
|
||||
{"learn":[0.6649340455],"iteration":314,"passed_time":11.14959087,"remaining_time":59.64146228,"test":[0.6711446402]},
|
||||
{"learn":[0.6649162445],"iteration":315,"passed_time":11.18718772,"remaining_time":59.61779784,"test":[0.6711415366]},
|
||||
{"learn":[0.6649048119],"iteration":316,"passed_time":11.21179073,"remaining_time":59.52505932,"test":[0.6711359351]},
|
||||
{"learn":[0.6648796463],"iteration":317,"passed_time":11.24311165,"remaining_time":59.46828238,"test":[0.671143361]},
|
||||
{"learn":[0.6648605481],"iteration":318,"passed_time":11.27486028,"remaining_time":59.41391889,"test":[0.6711353638]},
|
||||
{"learn":[0.6648429084],"iteration":319,"passed_time":11.30400807,"remaining_time":59.34604237,"test":[0.6711444387]},
|
||||
{"learn":[0.6648238121],"iteration":320,"passed_time":11.33488419,"remaining_time":59.28744721,"test":[0.6711487352]},
|
||||
{"learn":[0.6647969527],"iteration":321,"passed_time":11.36208838,"remaining_time":59.20988915,"test":[0.67114436]},
|
||||
{"learn":[0.6647854723],"iteration":322,"passed_time":11.39429642,"remaining_time":59.15862259,"test":[0.6711444722]},
|
||||
{"learn":[0.6647589304],"iteration":323,"passed_time":11.4363998,"remaining_time":59.15866068,"test":[0.6711325635]},
|
||||
{"learn":[0.6647429024],"iteration":324,"passed_time":11.47751019,"remaining_time":59.15332173,"test":[0.6711269403]},
|
||||
{"learn":[0.6647237508],"iteration":325,"passed_time":11.5136833,"remaining_time":59.12241054,"test":[0.6711154078]},
|
||||
{"learn":[0.6647059396],"iteration":326,"passed_time":11.54795566,"remaining_time":59.08174257,"test":[0.6711203043]},
|
||||
{"learn":[0.664686288],"iteration":327,"passed_time":11.57245915,"remaining_time":58.99131613,"test":[0.6711241333]},
|
||||
{"learn":[0.6646532527],"iteration":328,"passed_time":11.60790333,"remaining_time":58.95685857,"test":[0.6711213497]},
|
||||
{"learn":[0.6646306438],"iteration":329,"passed_time":11.63787346,"remaining_time":58.89469298,"test":[0.6711231641]},
|
||||
{"learn":[0.6646098516],"iteration":330,"passed_time":11.66805718,"remaining_time":58.83379887,"test":[0.6711049215]},
|
||||
{"learn":[0.6645858284],"iteration":331,"passed_time":11.70070223,"remaining_time":58.78545579,"test":[0.6711031963]},
|
||||
{"learn":[0.6645707188],"iteration":332,"passed_time":11.724753,"remaining_time":58.69418391,"test":[0.6710996314]},
|
||||
{"learn":[0.6645485788],"iteration":333,"passed_time":11.75795297,"remaining_time":58.64895104,"test":[0.6710867309]},
|
||||
{"learn":[0.6645305696],"iteration":334,"passed_time":11.78053066,"remaining_time":58.55099567,"test":[0.6710914578]},
|
||||
{"learn":[0.6645108881],"iteration":335,"passed_time":11.81570271,"remaining_time":58.51586106,"test":[0.6710929585]},
|
||||
{"learn":[0.6644923286],"iteration":336,"passed_time":11.8448851,"remaining_time":58.45116888,"test":[0.6710984779]},
|
||||
{"learn":[0.6644805222],"iteration":337,"passed_time":11.86964023,"remaining_time":58.36491734,"test":[0.6710923199]},
|
||||
{"learn":[0.6644572776],"iteration":338,"passed_time":11.90591446,"remaining_time":58.33546879,"test":[0.6710893917]},
|
||||
{"learn":[0.6644320741],"iteration":339,"passed_time":11.94145444,"remaining_time":58.30239521,"test":[0.6710923306]},
|
||||
{"learn":[0.6644115048],"iteration":340,"passed_time":11.98658051,"remaining_time":58.31594449,"test":[0.6710927901]},
|
||||
{"learn":[0.6643949013],"iteration":341,"passed_time":12.02038848,"remaining_time":58.27428098,"test":[0.6711092802]},
|
||||
{"learn":[0.6643619789],"iteration":342,"passed_time":12.06653941,"remaining_time":58.29229096,"test":[0.6711012995]},
|
||||
{"learn":[0.6643389502],"iteration":343,"passed_time":12.12283646,"remaining_time":58.35877087,"test":[0.6711015305]},
|
||||
{"learn":[0.6643088915],"iteration":344,"passed_time":12.17733618,"remaining_time":58.41591705,"test":[0.6710975574]},
|
||||
{"learn":[0.664286972],"iteration":345,"passed_time":12.22133732,"remaining_time":58.42223099,"test":[0.6710899474]},
|
||||
{"learn":[0.664274149],"iteration":346,"passed_time":12.2642467,"remaining_time":58.42305415,"test":[0.671085152]},
|
||||
{"learn":[0.6642536926],"iteration":347,"passed_time":12.30091895,"remaining_time":58.39401755,"test":[0.6710814533]},
|
||||
{"learn":[0.6642357634],"iteration":348,"passed_time":12.32484094,"remaining_time":58.30462002,"test":[0.6710701892]},
|
||||
{"learn":[0.664207914],"iteration":349,"passed_time":12.35469303,"remaining_time":58.24355287,"test":[0.67105503]},
|
||||
{"learn":[0.6641853097],"iteration":350,"passed_time":12.40148755,"remaining_time":58.26225919,"test":[0.6710527861]},
|
||||
{"learn":[0.6641654917],"iteration":351,"passed_time":12.43803877,"remaining_time":58.23263605,"test":[0.6710508715]},
|
||||
{"learn":[0.664143804],"iteration":352,"passed_time":12.47995438,"remaining_time":58.22800245,"test":[0.6710560803]},
|
||||
{"learn":[0.6641290647],"iteration":353,"passed_time":12.51241326,"remaining_time":58.17918707,"test":[0.6710465693]},
|
||||
{"learn":[0.6641117244],"iteration":354,"passed_time":12.5417829,"remaining_time":58.11614893,"test":[0.6710440741]},
|
||||
{"learn":[0.6640880219],"iteration":355,"passed_time":12.5692936,"remaining_time":58.0447154,"test":[0.6710496913]},
|
||||
{"learn":[0.6640669415],"iteration":356,"passed_time":12.5976392,"remaining_time":57.97737034,"test":[0.6710404659]},
|
||||
{"learn":[0.6640462999],"iteration":357,"passed_time":12.62815847,"remaining_time":57.92021287,"test":[0.6710293986]},
|
||||
{"learn":[0.664030296],"iteration":358,"passed_time":12.65342509,"remaining_time":57.8391938,"test":[0.6710353817]},
|
||||
{"learn":[0.6640028542],"iteration":359,"passed_time":12.68233453,"remaining_time":57.77507954,"test":[0.6710271815]},
|
||||
{"learn":[0.6639813347],"iteration":360,"passed_time":12.72037964,"remaining_time":57.75263774,"test":[0.6710288077]},
|
||||
{"learn":[0.6639597941],"iteration":361,"passed_time":12.744473,"remaining_time":57.66698004,"test":[0.6710169894]},
|
||||
{"learn":[0.6639429832],"iteration":362,"passed_time":12.77086568,"remaining_time":57.59203063,"test":[0.6710119848]},
|
||||
{"learn":[0.6639222708],"iteration":363,"passed_time":12.81194554,"remaining_time":57.58335961,"test":[0.6710114775]},
|
||||
{"learn":[0.6639065546],"iteration":364,"passed_time":12.84133287,"remaining_time":57.52213492,"test":[0.6710013614]},
|
||||
{"learn":[0.6638823236],"iteration":365,"passed_time":12.87057337,"remaining_time":57.46042866,"test":[0.6709985657]},
|
||||
{"learn":[0.6638648195],"iteration":366,"passed_time":12.8971183,"remaining_time":57.38690512,"test":[0.6709948954]},
|
||||
{"learn":[0.6638436235],"iteration":367,"passed_time":12.93825161,"remaining_time":57.37833324,"test":[0.6709970591]},
|
||||
{"learn":[0.6638208732],"iteration":368,"passed_time":12.97444296,"remaining_time":57.3477411,"test":[0.6709739289]},
|
||||
{"learn":[0.6637956357],"iteration":369,"passed_time":13.00974924,"remaining_time":57.31321963,"test":[0.6709754911]},
|
||||
{"learn":[0.6637718453],"iteration":370,"passed_time":13.03832239,"remaining_time":57.24912984,"test":[0.6709717066]},
|
||||
{"learn":[0.663756918],"iteration":371,"passed_time":13.07843077,"remaining_time":57.23571316,"test":[0.67096845]},
|
||||
{"learn":[0.6637353525],"iteration":372,"passed_time":13.11729124,"remaining_time":57.21671005,"test":[0.6709739445]},
|
||||
{"learn":[0.6637143112],"iteration":373,"passed_time":13.14745329,"remaining_time":57.15978354,"test":[0.6709728881]},
|
||||
{"learn":[0.6636956547],"iteration":374,"passed_time":13.18118022,"remaining_time":57.11844761,"test":[0.6709694284]},
|
||||
{"learn":[0.663680995],"iteration":375,"passed_time":13.20539229,"remaining_time":57.03605604,"test":[0.6709604166]},
|
||||
{"learn":[0.66366728],"iteration":376,"passed_time":13.23563977,"remaining_time":56.97995583,"test":[0.6709605025]},
|
||||
{"learn":[0.6636487567],"iteration":377,"passed_time":13.27428255,"remaining_time":56.96001665,"test":[0.6709603727]},
|
||||
{"learn":[0.6636266904],"iteration":378,"passed_time":13.30625754,"remaining_time":56.91146033,"test":[0.670944339]},
|
||||
{"learn":[0.6636116064],"iteration":379,"passed_time":13.33327871,"remaining_time":56.84187241,"test":[0.6709447187]},
|
||||
{"learn":[0.6635902746],"iteration":380,"passed_time":13.36632239,"remaining_time":56.79809961,"test":[0.6709538679]},
|
||||
{"learn":[0.6635654896],"iteration":381,"passed_time":13.39639051,"remaining_time":56.74177969,"test":[0.6709640912]},
|
||||
{"learn":[0.6635393029],"iteration":382,"passed_time":13.42189438,"remaining_time":56.66632694,"test":[0.6709534847]},
|
||||
{"learn":[0.6635171734],"iteration":383,"passed_time":13.46730432,"remaining_time":56.6749057,"test":[0.6709471555]},
|
||||
{"learn":[0.663500789],"iteration":384,"passed_time":13.50832777,"remaining_time":56.66480351,"test":[0.6709506783]},
|
||||
{"learn":[0.663477743],"iteration":385,"passed_time":13.54029627,"remaining_time":56.61667921,"test":[0.6709546729]},
|
||||
{"learn":[0.6634584806],"iteration":386,"passed_time":13.56996301,"remaining_time":56.5590448,"test":[0.670930774]},
|
||||
{"learn":[0.6634337499],"iteration":387,"passed_time":13.59835745,"remaining_time":56.4962686,"test":[0.6709287322]},
|
||||
{"learn":[0.6634135584],"iteration":388,"passed_time":13.6279617,"remaining_time":56.43867943,"test":[0.6709198643]},
|
||||
{"learn":[0.6633868455],"iteration":389,"passed_time":13.65633448,"remaining_time":56.37615005,"test":[0.6709220389]},
|
||||
{"learn":[0.6633755323],"iteration":390,"passed_time":13.68565529,"remaining_time":56.31769658,"test":[0.6709230923]},
|
||||
{"learn":[0.663356103],"iteration":391,"passed_time":13.71789303,"remaining_time":56.27135714,"test":[0.670930414]},
|
||||
{"learn":[0.6633337631],"iteration":392,"passed_time":13.75060752,"remaining_time":56.2270389,"test":[0.6709354296]},
|
||||
{"learn":[0.663319422],"iteration":393,"passed_time":13.77167974,"remaining_time":56.13532403,"test":[0.6709351544]},
|
||||
{"learn":[0.6632911566],"iteration":394,"passed_time":13.80416242,"remaining_time":56.09033084,"test":[0.6709414935]},
|
||||
{"learn":[0.6632687875],"iteration":395,"passed_time":13.82525369,"remaining_time":55.9992599,"test":[0.6709445943]},
|
||||
{"learn":[0.6632431997],"iteration":396,"passed_time":13.85836516,"remaining_time":55.95707646,"test":[0.6709475685]},
|
||||
{"learn":[0.6632189331],"iteration":397,"passed_time":13.88898168,"remaining_time":55.90489613,"test":[0.6709533591]},
|
||||
{"learn":[0.663201035],"iteration":398,"passed_time":13.91726355,"remaining_time":55.84345598,"test":[0.6709592222]},
|
||||
{"learn":[0.6631898553],"iteration":399,"passed_time":13.95316828,"remaining_time":55.81267311,"test":[0.6709508704]},
|
||||
{"learn":[0.6631712482],"iteration":400,"passed_time":13.99418497,"remaining_time":55.80224881,"test":[0.6709479912]},
|
||||
{"learn":[0.663143025],"iteration":401,"passed_time":14.0253575,"remaining_time":55.75254052,"test":[0.6709417519]},
|
||||
{"learn":[0.663121538],"iteration":402,"passed_time":14.04844239,"remaining_time":55.67087467,"test":[0.6709476082]},
|
||||
{"learn":[0.6631087792],"iteration":403,"passed_time":14.0761289,"remaining_time":55.60767753,"test":[0.6709480979]},
|
||||
{"learn":[0.6630859067],"iteration":404,"passed_time":14.10555105,"remaining_time":55.55149118,"test":[0.6709448724]},
|
||||
{"learn":[0.663066483],"iteration":405,"passed_time":14.1427661,"remaining_time":55.52603242,"test":[0.6709421934]},
|
||||
{"learn":[0.6630443652],"iteration":406,"passed_time":14.18285552,"remaining_time":55.51176619,"test":[0.6709386261]},
|
||||
{"learn":[0.6630250376],"iteration":407,"passed_time":14.21458769,"remaining_time":55.46476372,"test":[0.6709461564]},
|
||||
{"learn":[0.6630007822],"iteration":408,"passed_time":14.24035708,"remaining_time":55.39464088,"test":[0.670934384]},
|
||||
{"learn":[0.6629768728],"iteration":409,"passed_time":14.26711915,"remaining_time":55.32858403,"test":[0.6709312987]},
|
||||
{"learn":[0.6629528093],"iteration":410,"passed_time":14.29943785,"remaining_time":55.28420133,"test":[0.670931806]},
|
||||
{"learn":[0.6629260936],"iteration":411,"passed_time":14.32489173,"remaining_time":55.21341763,"test":[0.6709286111]},
|
||||
{"learn":[0.6629102182],"iteration":412,"passed_time":14.35119075,"remaining_time":55.14610101,"test":[0.6709224729]},
|
||||
{"learn":[0.6628863488],"iteration":413,"passed_time":14.37946054,"remaining_time":55.08653242,"test":[0.6709236504]},
|
||||
{"learn":[0.6628648972],"iteration":414,"passed_time":14.41005914,"remaining_time":55.03600899,"test":[0.6709245901]},
|
||||
{"learn":[0.6628454339],"iteration":415,"passed_time":14.45103793,"remaining_time":55.02510598,"test":[0.6709463437]},
|
||||
{"learn":[0.6628200274],"iteration":416,"passed_time":14.48428995,"remaining_time":54.98472661,"test":[0.6709567049]},
|
||||
{"learn":[0.6627942591],"iteration":417,"passed_time":14.5135184,"remaining_time":54.92915339,"test":[0.670945606]},
|
||||
{"learn":[0.6627744647],"iteration":418,"passed_time":14.53698524,"remaining_time":54.85196578,"test":[0.6709479298]},
|
||||
{"learn":[0.662765485],"iteration":419,"passed_time":14.56542473,"remaining_time":54.79374067,"test":[0.6709464351]},
|
||||
{"learn":[0.6627503257],"iteration":420,"passed_time":14.58728594,"remaining_time":54.71098455,"test":[0.6709414048]},
|
||||
{"learn":[0.6627323029],"iteration":421,"passed_time":14.61501375,"remaining_time":54.65045425,"test":[0.6709414427]},
|
||||
{"learn":[0.6627111509],"iteration":422,"passed_time":14.64231614,"remaining_time":54.58849302,"test":[0.6709296343]},
|
||||
{"learn":[0.6626785863],"iteration":423,"passed_time":14.66665432,"remaining_time":54.51567739,"test":[0.670924721]},
|
||||
{"learn":[0.6626576561],"iteration":424,"passed_time":14.69050441,"remaining_time":54.44128104,"test":[0.670906284]},
|
||||
{"learn":[0.6626363113],"iteration":425,"passed_time":14.71910475,"remaining_time":54.38467341,"test":[0.6708996826]},
|
||||
{"learn":[0.6626181065],"iteration":426,"passed_time":14.73941058,"remaining_time":54.2976413,"test":[0.6708987677]},
|
||||
{"learn":[0.66259794],"iteration":427,"passed_time":14.77242451,"remaining_time":54.25759657,"test":[0.670909526]},
|
||||
{"learn":[0.6625765658],"iteration":428,"passed_time":14.79088688,"remaining_time":54.1642967,"test":[0.6709033226]},
|
||||
{"learn":[0.6625526572],"iteration":429,"passed_time":14.82430966,"remaining_time":54.12596783,"test":[0.6708750209]},
|
||||
{"learn":[0.66253135],"iteration":430,"passed_time":14.84439175,"remaining_time":54.03909666,"test":[0.6708752079]},
|
||||
{"learn":[0.6625035695],"iteration":431,"passed_time":14.8764415,"remaining_time":53.99597284,"test":[0.6708776566]},
|
||||
{"learn":[0.662480212],"iteration":432,"passed_time":14.90666075,"remaining_time":53.94627573,"test":[0.6708736133]},
|
||||
{"learn":[0.6624611632],"iteration":433,"passed_time":14.93845927,"remaining_time":53.90236684,"test":[0.6708754298]},
|
||||
{"learn":[0.6624332625],"iteration":434,"passed_time":14.98024104,"remaining_time":53.89443041,"test":[0.6708751084]},
|
||||
{"learn":[0.6624120584],"iteration":435,"passed_time":15.00605075,"remaining_time":53.82904442,"test":[0.6708642042]},
|
||||
{"learn":[0.6623941719],"iteration":436,"passed_time":15.03384083,"remaining_time":53.77092268,"test":[0.6708610465]},
|
||||
{"learn":[0.6623766304],"iteration":437,"passed_time":15.05972545,"remaining_time":53.70614417,"test":[0.6708574768]},
|
||||
{"learn":[0.6623623329],"iteration":438,"passed_time":15.08505889,"remaining_time":53.63958297,"test":[0.6708557953]},
|
||||
{"learn":[0.6623442925],"iteration":439,"passed_time":15.11080547,"remaining_time":53.57467393,"test":[0.670871378]},
|
||||
{"learn":[0.6623212715],"iteration":440,"passed_time":15.13466304,"remaining_time":53.50326458,"test":[0.6708640187]},
|
||||
{"learn":[0.6623025941],"iteration":441,"passed_time":15.16037021,"remaining_time":53.43859001,"test":[0.6708700565]},
|
||||
{"learn":[0.6622749791],"iteration":442,"passed_time":15.18471062,"remaining_time":53.36928767,"test":[0.6708667534]},
|
||||
{"learn":[0.6622534499],"iteration":443,"passed_time":15.21140556,"remaining_time":53.30843931,"test":[0.6708675383]},
|
||||
{"learn":[0.6622305473],"iteration":444,"passed_time":15.23498219,"remaining_time":53.23684787,"test":[0.6708740175]},
|
||||
{"learn":[0.6622059333],"iteration":445,"passed_time":15.26647355,"remaining_time":53.19304911,"test":[0.6708774523]},
|
||||
{"learn":[0.6621871707],"iteration":446,"passed_time":15.28793136,"remaining_time":53.11444609,"test":[0.6708697231]},
|
||||
{"learn":[0.6621638454],"iteration":447,"passed_time":15.31613827,"remaining_time":53.05947899,"test":[0.6708614971]},
|
||||
{"learn":[0.6621511296],"iteration":448,"passed_time":15.33689091,"remaining_time":52.9788815,"test":[0.6708607946]},
|
||||
{"learn":[0.6621349978],"iteration":449,"passed_time":15.36674634,"remaining_time":52.92990406,"test":[0.6708740865]},
|
||||
{"learn":[0.6621120424],"iteration":450,"passed_time":15.393642,"remaining_time":52.87084582,"test":[0.6708729562]},
|
||||
{"learn":[0.6620958271],"iteration":451,"passed_time":15.42984657,"remaining_time":52.84381082,"test":[0.6708674017]},
|
||||
{"learn":[0.6620793528],"iteration":452,"passed_time":15.46956188,"remaining_time":52.82872456,"test":[0.6708693088]},
|
||||
{"learn":[0.6620572713],"iteration":453,"passed_time":15.49032259,"remaining_time":52.74898396,"test":[0.6708712037]},
|
||||
{"learn":[0.6620395025],"iteration":454,"passed_time":15.52379393,"remaining_time":52.71266289,"test":[0.6708703905]},
|
||||
{"learn":[0.6620188044],"iteration":455,"passed_time":15.55053135,"remaining_time":52.65355352,"test":[0.6708577595]},
|
||||
{"learn":[0.6620017347],"iteration":456,"passed_time":15.57735398,"remaining_time":52.59487352,"test":[0.6708493546]},
|
||||
{"learn":[0.6619811454],"iteration":457,"passed_time":15.60434803,"remaining_time":52.53690973,"test":[0.6708523777]},
|
||||
{"learn":[0.6619695569],"iteration":458,"passed_time":15.63056555,"remaining_time":52.47647387,"test":[0.6708454134]},
|
||||
{"learn":[0.661952377],"iteration":459,"passed_time":15.656355,"remaining_time":52.41475368,"test":[0.6708404483]},
|
||||
{"learn":[0.6619237442],"iteration":460,"passed_time":15.68232112,"remaining_time":52.35377918,"test":[0.6708274771]},
|
||||
{"learn":[0.6619089407],"iteration":461,"passed_time":15.71164945,"remaining_time":52.30414904,"test":[0.6708244992]},
|
||||
{"learn":[0.6618886168],"iteration":462,"passed_time":15.7361944,"remaining_time":52.23872743,"test":[0.6708344314]},
|
||||
{"learn":[0.6618831383],"iteration":463,"passed_time":15.76527735,"remaining_time":52.18850433,"test":[0.6708279081]},
|
||||
{"learn":[0.6618690774],"iteration":464,"passed_time":15.78652262,"remaining_time":52.11249942,"test":[0.6708258106]},
|
||||
{"learn":[0.661845878],"iteration":465,"passed_time":15.81756836,"remaining_time":52.06899113,"test":[0.6708049714]},
|
||||
{"learn":[0.6618290213],"iteration":466,"passed_time":15.83979966,"remaining_time":51.99660146,"test":[0.670810989]},
|
||||
{"learn":[0.6618050064],"iteration":467,"passed_time":15.87342473,"remaining_time":51.9617237,"test":[0.6708212237]},
|
||||
{"learn":[0.6617832833],"iteration":468,"passed_time":15.90381555,"remaining_time":51.9162934,"test":[0.6708221741]},
|
||||
{"learn":[0.6617652311],"iteration":469,"passed_time":15.93502938,"remaining_time":51.87360627,"test":[0.6708259658]},
|
||||
{"learn":[0.6617443144],"iteration":470,"passed_time":15.96919221,"remaining_time":51.84054117,"test":[0.6708159692]},
|
||||
{"learn":[0.6617202619],"iteration":471,"passed_time":15.99477329,"remaining_time":51.77968981,"test":[0.6708136212]},
|
||||
{"learn":[0.6617005831],"iteration":472,"passed_time":16.02279091,"remaining_time":51.72685354,"test":[0.6708224942]},
|
||||
{"learn":[0.6616824419],"iteration":473,"passed_time":16.04763422,"remaining_time":51.66390258,"test":[0.6708363084]},
|
||||
{"learn":[0.6616538226],"iteration":474,"passed_time":16.07374645,"remaining_time":51.60518598,"test":[0.670850875]},
|
||||
{"learn":[0.6616314155],"iteration":475,"passed_time":16.09993591,"remaining_time":51.54685363,"test":[0.6708527236]},
|
||||
{"learn":[0.6616127861],"iteration":476,"passed_time":16.12811357,"remaining_time":51.49500411,"test":[0.6708453401]},
|
||||
{"learn":[0.6616029072],"iteration":477,"passed_time":16.15264086,"remaining_time":51.43163051,"test":[0.6708413844]},
|
||||
{"learn":[0.6615843751],"iteration":478,"passed_time":16.17696751,"remaining_time":51.36778201,"test":[0.6708364569]},
|
||||
{"learn":[0.661563216],"iteration":479,"passed_time":16.20551145,"remaining_time":51.31745293,"test":[0.6708251774]},
|
||||
{"learn":[0.6615432257],"iteration":480,"passed_time":16.22860577,"remaining_time":51.2500045,"test":[0.6708154393]},
|
||||
{"learn":[0.6615263324],"iteration":481,"passed_time":16.25544093,"remaining_time":51.19452144,"test":[0.6708111613]},
|
||||
{"learn":[0.6615033259],"iteration":482,"passed_time":16.27729221,"remaining_time":51.12350369,"test":[0.6708102339]},
|
||||
{"learn":[0.661484293],"iteration":483,"passed_time":16.30502335,"remaining_time":51.07110619,"test":[0.6707929623]},
|
||||
{"learn":[0.6614678231],"iteration":484,"passed_time":16.32842702,"remaining_time":51.00529266,"test":[0.6707900226]},
|
||||
{"learn":[0.6614463024],"iteration":485,"passed_time":16.36272839,"remaining_time":50.97360242,"test":[0.6707832384]},
|
||||
{"learn":[0.6614155436],"iteration":486,"passed_time":16.39272506,"remaining_time":50.92852776,"test":[0.6707739118]},
|
||||
{"learn":[0.6613958945],"iteration":487,"passed_time":16.42636604,"remaining_time":50.89480625,"test":[0.6707737538]},
|
||||
{"learn":[0.661380611],"iteration":488,"passed_time":16.4597142,"remaining_time":50.86018027,"test":[0.6707730234]},
|
||||
{"learn":[0.6613677802],"iteration":489,"passed_time":16.48056007,"remaining_time":50.78703206,"test":[0.6707796291]},
|
||||
{"learn":[0.6613530086],"iteration":490,"passed_time":16.51091177,"remaining_time":50.74331132,"test":[0.670791408]},
|
||||
{"learn":[0.6613248211],"iteration":491,"passed_time":16.53097438,"remaining_time":50.66810846,"test":[0.6707944906]},
|
||||
{"learn":[0.6613059359],"iteration":492,"passed_time":16.56161402,"remaining_time":50.62546112,"test":[0.6707835635]},
|
||||
{"learn":[0.6612729965],"iteration":493,"passed_time":16.5854633,"remaining_time":50.56216139,"test":[0.6707908928]},
|
||||
{"learn":[0.6612624948],"iteration":494,"passed_time":16.61302735,"remaining_time":50.51031547,"test":[0.670796262]},
|
||||
{"learn":[0.6612401679],"iteration":495,"passed_time":16.63896978,"remaining_time":50.45365029,"test":[0.6707877825]},
|
||||
{"learn":[0.6612191637],"iteration":496,"passed_time":16.663707,"remaining_time":50.39346403,"test":[0.6707854132]},
|
||||
{"learn":[0.6611912219],"iteration":497,"passed_time":16.69040179,"remaining_time":50.33932428,"test":[0.6707756206]},
|
||||
{"learn":[0.6611773017],"iteration":498,"passed_time":16.71612789,"remaining_time":50.28238068,"test":[0.6707707899]},
|
||||
{"learn":[0.6611638216],"iteration":499,"passed_time":16.74072553,"remaining_time":50.2221766,"test":[0.6707704386]},
|
||||
{"learn":[0.6611450533],"iteration":500,"passed_time":16.77346538,"remaining_time":50.18647626,"test":[0.6707621465]},
|
||||
{"learn":[0.6611179111],"iteration":501,"passed_time":16.80230735,"remaining_time":50.13915621,"test":[0.6707661931]},
|
||||
{"learn":[0.6610959069],"iteration":502,"passed_time":16.83637769,"remaining_time":50.10747,"test":[0.6707651988]},
|
||||
{"learn":[0.6610728788],"iteration":503,"passed_time":16.87382128,"remaining_time":50.08578697,"test":[0.6707607827]},
|
||||
{"learn":[0.6610436668],"iteration":504,"passed_time":16.92151611,"remaining_time":50.09438927,"test":[0.670760242]},
|
||||
{"learn":[0.6610188976],"iteration":505,"passed_time":16.9898618,"remaining_time":50.16374216,"test":[0.6707506008]},
|
||||
{"learn":[0.6610030555],"iteration":506,"passed_time":17.03818668,"remaining_time":50.17359509,"test":[0.6707452886]},
|
||||
{"learn":[0.6609831174],"iteration":507,"passed_time":17.06933058,"remaining_time":50.13275833,"test":[0.6707355189]},
|
||||
{"learn":[0.6609586562],"iteration":508,"passed_time":17.1106164,"remaining_time":50.12166807,"test":[0.6707312551]},
|
||||
{"learn":[0.660935882],"iteration":509,"passed_time":17.14537899,"remaining_time":50.09140137,"test":[0.6707199485]},
|
||||
{"learn":[0.6609202024],"iteration":510,"passed_time":17.19066307,"remaining_time":50.09177556,"test":[0.6707131947]},
|
||||
{"learn":[0.6609011137],"iteration":511,"passed_time":17.21958034,"remaining_time":50.04440537,"test":[0.6707154112]},
|
||||
{"learn":[0.6608726737],"iteration":512,"passed_time":17.24756917,"remaining_time":49.99441591,"test":[0.6706982346]},
|
||||
{"learn":[0.6608608849],"iteration":513,"passed_time":17.27150822,"remaining_time":49.93280391,"test":[0.6706988941]},
|
||||
{"learn":[0.6608387256],"iteration":514,"passed_time":17.29800365,"remaining_time":49.87870957,"test":[0.6706989098]},
|
||||
{"learn":[0.6608136063],"iteration":515,"passed_time":17.34332283,"remaining_time":49.87885868,"test":[0.670693306]},
|
||||
{"learn":[0.6607946343],"iteration":516,"passed_time":17.37393636,"remaining_time":49.83664916,"test":[0.6706944515]},
|
||||
{"learn":[0.6607703935],"iteration":517,"passed_time":17.4173655,"remaining_time":49.83114994,"test":[0.6706899688]},
|
||||
{"learn":[0.6607509625],"iteration":518,"passed_time":17.46008645,"remaining_time":49.82348368,"test":[0.6706909374]},
|
||||
{"learn":[0.6607238109],"iteration":519,"passed_time":17.4906988,"remaining_time":49.78121967,"test":[0.6706855074]},
|
||||
{"learn":[0.6606999858],"iteration":520,"passed_time":17.5186435,"remaining_time":49.7314275,"test":[0.6706787779]},
|
||||
{"learn":[0.6606813873],"iteration":521,"passed_time":17.54613056,"remaining_time":49.6804233,"test":[0.6706737082]},
|
||||
{"learn":[0.6606610372],"iteration":522,"passed_time":17.57100039,"remaining_time":49.62211774,"test":[0.6706761225]},
|
||||
{"learn":[0.660638456],"iteration":523,"passed_time":17.60084283,"remaining_time":49.5779466,"test":[0.670685455]},
|
||||
{"learn":[0.6606156483],"iteration":524,"passed_time":17.62599925,"remaining_time":49.52066456,"test":[0.6706693855]},
|
||||
{"learn":[0.6605968623],"iteration":525,"passed_time":17.65519625,"remaining_time":49.47482751,"test":[0.6706647216]},
|
||||
{"learn":[0.6605735776],"iteration":526,"passed_time":17.67910836,"remaining_time":49.41428199,"test":[0.6706569188]},
|
||||
{"learn":[0.6605517294],"iteration":527,"passed_time":17.70744827,"remaining_time":49.36621942,"test":[0.6706549134]},
|
||||
{"learn":[0.6605309239],"iteration":528,"passed_time":17.72943083,"remaining_time":49.3005534,"test":[0.6706547978]},
|
||||
{"learn":[0.6605086434],"iteration":529,"passed_time":17.75830336,"remaining_time":49.25416215,"test":[0.6706564214]},
|
||||
{"learn":[0.6604803349],"iteration":530,"passed_time":17.78141858,"remaining_time":49.19190939,"test":[0.6706559196]},
|
||||
{"learn":[0.6604566326],"iteration":531,"passed_time":17.80870208,"remaining_time":49.14130574,"test":[0.6706515072]},
|
||||
{"learn":[0.6604430839],"iteration":532,"passed_time":17.82904188,"remaining_time":49.07167811,"test":[0.6706474616]},
|
||||
{"learn":[0.6604273738],"iteration":533,"passed_time":17.86246645,"remaining_time":49.03815696,"test":[0.6706424204]},
|
||||
{"learn":[0.6604048016],"iteration":534,"passed_time":17.90552779,"remaining_time":49.03102469,"test":[0.6706520008]},
|
||||
{"learn":[0.6603845173],"iteration":535,"passed_time":18.02843143,"remaining_time":49.24183511,"test":[0.6706448306]},
|
||||
{"learn":[0.6603669212],"iteration":536,"passed_time":18.07245966,"remaining_time":49.23651485,"test":[0.6706415789]},
|
||||
{"learn":[0.6603488983],"iteration":537,"passed_time":18.10631942,"remaining_time":49.20341819,"test":[0.6706305359]},
|
||||
{"learn":[0.6603176881],"iteration":538,"passed_time":18.13531438,"remaining_time":49.1571323,"test":[0.6706152774]},
|
||||
{"learn":[0.6602953862],"iteration":539,"passed_time":18.16575265,"remaining_time":49.11481272,"test":[0.670616585]},
|
||||
{"learn":[0.6602672025],"iteration":540,"passed_time":18.20025584,"remaining_time":49.08349958,"test":[0.6705963243]},
|
||||
{"learn":[0.6602568636],"iteration":541,"passed_time":18.22381751,"remaining_time":49.02274158,"test":[0.6706027368]},
|
||||
{"learn":[0.660235705],"iteration":542,"passed_time":18.25438575,"remaining_time":48.98092088,"test":[0.6706003522]},
|
||||
{"learn":[0.6602152295],"iteration":543,"passed_time":18.28070524,"remaining_time":48.9277699,"test":[0.6706044301]},
|
||||
{"learn":[0.6601897709],"iteration":544,"passed_time":18.30768805,"remaining_time":48.87648827,"test":[0.6706047241]},
|
||||
{"learn":[0.6601683731],"iteration":545,"passed_time":18.33807201,"remaining_time":48.83435294,"test":[0.6706038235]},
|
||||
{"learn":[0.6601472267],"iteration":546,"passed_time":18.36776304,"remaining_time":48.79041993,"test":[0.6706026913]},
|
||||
{"learn":[0.6601262337],"iteration":547,"passed_time":18.41134623,"remaining_time":48.78334803,"test":[0.6705845786]},
|
||||
{"learn":[0.6601119991],"iteration":548,"passed_time":18.44405381,"remaining_time":48.74739905,"test":[0.6705873967]},
|
||||
{"learn":[0.6600869973],"iteration":549,"passed_time":18.47010718,"remaining_time":48.69391893,"test":[0.6705755426]},
|
||||
{"learn":[0.6600667497],"iteration":550,"passed_time":18.5036553,"remaining_time":48.66024779,"test":[0.6705715731]},
|
||||
{"learn":[0.6600397508],"iteration":551,"passed_time":18.53164471,"remaining_time":48.61199556,"test":[0.6705757153]},
|
||||
{"learn":[0.660016863],"iteration":552,"passed_time":18.5577607,"remaining_time":48.55891452,"test":[0.6705516814]},
|
||||
{"learn":[0.6599933158],"iteration":553,"passed_time":18.58492994,"remaining_time":48.50867995,"test":[0.6705530864]},
|
||||
{"learn":[0.6599632649],"iteration":554,"passed_time":18.62562092,"remaining_time":48.49373376,"test":[0.6705552479]},
|
||||
{"learn":[0.6599446007],"iteration":555,"passed_time":18.65010209,"remaining_time":48.43659608,"test":[0.6705563336]},
|
||||
{"learn":[0.6599138126],"iteration":556,"passed_time":18.67796421,"remaining_time":48.38833458,"test":[0.6705718544]},
|
||||
{"learn":[0.6598965504],"iteration":557,"passed_time":18.70319381,"remaining_time":48.33334314,"test":[0.6705688384]},
|
||||
{"learn":[0.6598785723],"iteration":558,"passed_time":18.72995694,"remaining_time":48.28241136,"test":[0.6705641528]},
|
||||
{"learn":[0.659860838],"iteration":559,"passed_time":18.75657945,"remaining_time":48.23120429,"test":[0.6705628467]},
|
||||
{"learn":[0.6598408724],"iteration":560,"passed_time":18.78181322,"remaining_time":48.17652269,"test":[0.670558488]},
|
||||
{"learn":[0.6598244857],"iteration":561,"passed_time":18.80867415,"remaining_time":48.12610931,"test":[0.6705544404]},
|
||||
{"learn":[0.6598082469],"iteration":562,"passed_time":18.83488797,"remaining_time":48.0741279,"test":[0.6705617451]},
|
||||
{"learn":[0.6597851673],"iteration":563,"passed_time":18.86939449,"remaining_time":48.04335193,"test":[0.6705631717]},
|
||||
{"learn":[0.6597683521],"iteration":564,"passed_time":18.90235988,"remaining_time":48.00864854,"test":[0.6705636201]},
|
||||
{"learn":[0.6597479006],"iteration":565,"passed_time":18.93001053,"remaining_time":47.96048604,"test":[0.6705537522]},
|
||||
{"learn":[0.6597310938],"iteration":566,"passed_time":18.95858079,"remaining_time":47.91472006,"test":[0.670555083]},
|
||||
{"learn":[0.6597096581],"iteration":567,"passed_time":18.9833487,"remaining_time":47.85942842,"test":[0.6705524541]},
|
||||
{"learn":[0.6596862311],"iteration":568,"passed_time":19.0162481,"remaining_time":47.82469425,"test":[0.6705503132]},
|
||||
{"learn":[0.6596574779],"iteration":569,"passed_time":19.03781666,"remaining_time":47.76154004,"test":[0.6705354602]},
|
||||
{"learn":[0.6596385418],"iteration":570,"passed_time":19.0681355,"remaining_time":47.72043018,"test":[0.6705387012]},
|
||||
{"learn":[0.6596189903],"iteration":571,"passed_time":19.09073714,"remaining_time":47.66009201,"test":[0.6705411923]},
|
||||
{"learn":[0.65959275],"iteration":572,"passed_time":19.11146842,"remaining_time":47.59522765,"test":[0.6705390018]},
|
||||
{"learn":[0.6595730662],"iteration":573,"passed_time":19.141368,"remaining_time":47.55329403,"test":[0.6705354939]},
|
||||
{"learn":[0.6595566809],"iteration":574,"passed_time":19.16428373,"remaining_time":47.49409447,"test":[0.670531296]},
|
||||
{"learn":[0.6595365076],"iteration":575,"passed_time":19.19652276,"remaining_time":47.45807015,"test":[0.6705377163]},
|
||||
{"learn":[0.6595163446],"iteration":576,"passed_time":19.21727405,"remaining_time":47.39372785,"test":[0.6705248875]},
|
||||
{"learn":[0.6594816637],"iteration":577,"passed_time":19.24969594,"remaining_time":47.35824848,"test":[0.6705252902]},
|
||||
{"learn":[0.6594570142],"iteration":578,"passed_time":19.27445137,"remaining_time":47.30396442,"test":[0.6705181562]},
|
||||
{"learn":[0.6594353055],"iteration":579,"passed_time":19.29822455,"remaining_time":47.24737734,"test":[0.6705123446]},
|
||||
{"learn":[0.6594162362],"iteration":580,"passed_time":19.32403522,"remaining_time":47.19587948,"test":[0.6705128345]},
|
||||
{"learn":[0.659395036],"iteration":581,"passed_time":19.35739555,"remaining_time":47.16286408,"test":[0.6705173712]},
|
||||
{"learn":[0.6593798831],"iteration":582,"passed_time":19.39112791,"remaining_time":47.13075172,"test":[0.670541941]},
|
||||
{"learn":[0.6593556719],"iteration":583,"passed_time":19.42704318,"remaining_time":47.1039266,"test":[0.6705463243]},
|
||||
{"learn":[0.6593292627],"iteration":584,"passed_time":19.46022169,"remaining_time":47.07045077,"test":[0.6705513215]},
|
||||
{"learn":[0.6592976737],"iteration":585,"passed_time":19.48332075,"remaining_time":47.01265452,"test":[0.6705455889]},
|
||||
{"learn":[0.6592754841],"iteration":586,"passed_time":19.5115578,"remaining_time":46.9673444,"test":[0.6705408087]},
|
||||
{"learn":[0.6592510441],"iteration":587,"passed_time":19.54275193,"remaining_time":46.92919341,"test":[0.6705510193]},
|
||||
{"learn":[0.6592290326],"iteration":588,"passed_time":19.56411389,"remaining_time":46.86751222,"test":[0.6705456751]},
|
||||
{"learn":[0.6592097404],"iteration":589,"passed_time":19.59700884,"remaining_time":46.8335296,"test":[0.6705402427]},
|
||||
{"learn":[0.6591876204],"iteration":590,"passed_time":19.62169623,"remaining_time":46.77998306,"test":[0.6705443402]},
|
||||
{"learn":[0.6591705995],"iteration":591,"passed_time":19.64747626,"remaining_time":46.72913272,"test":[0.67054441]},
|
||||
{"learn":[0.6591456195],"iteration":592,"passed_time":19.67090184,"remaining_time":46.67278059,"test":[0.6705441955]},
|
||||
{"learn":[0.6591107122],"iteration":593,"passed_time":19.69910949,"remaining_time":46.62785848,"test":[0.6705319356]},
|
||||
{"learn":[0.6590819533],"iteration":594,"passed_time":19.72694709,"remaining_time":46.58211876,"test":[0.6705358843]},
|
||||
{"learn":[0.6590551327],"iteration":595,"passed_time":19.7530808,"remaining_time":46.53242523,"test":[0.6705334396]},
|
||||
{"learn":[0.6590373916],"iteration":596,"passed_time":19.77835609,"remaining_time":46.48079328,"test":[0.6705320462]},
|
||||
{"learn":[0.6590177149],"iteration":597,"passed_time":19.80378809,"remaining_time":46.4296169,"test":[0.6705332043]},
|
||||
{"learn":[0.6589946095],"iteration":598,"passed_time":19.83052585,"remaining_time":46.38158048,"test":[0.6705328363]},
|
||||
{"learn":[0.6589697628],"iteration":599,"passed_time":19.8579153,"remaining_time":46.33513569,"test":[0.6705315638]},
|
||||
{"learn":[0.6589442269],"iteration":600,"passed_time":19.89600309,"remaining_time":46.31365777,"test":[0.6705274435]},
|
||||
{"learn":[0.6589182437],"iteration":601,"passed_time":19.92518872,"remaining_time":46.27145155,"test":[0.670509808]},
|
||||
{"learn":[0.6588837179],"iteration":602,"passed_time":19.95754179,"remaining_time":46.23662666,"test":[0.6705077789]},
|
||||
{"learn":[0.6588674101],"iteration":603,"passed_time":19.99116426,"remaining_time":46.20474388,"test":[0.6705212132]},
|
||||
{"learn":[0.6588406916],"iteration":604,"passed_time":20.01900069,"remaining_time":46.15951398,"test":[0.6705098442]},
|
||||
{"learn":[0.6588149945],"iteration":605,"passed_time":20.04735837,"remaining_time":46.11554053,"test":[0.6705061509]},
|
||||
{"learn":[0.6587866031],"iteration":606,"passed_time":20.07232044,"remaining_time":46.06382599,"test":[0.6705003071]},
|
||||
{"learn":[0.6587636648],"iteration":607,"passed_time":20.09871086,"remaining_time":46.01546959,"test":[0.6705045031]},
|
||||
{"learn":[0.6587502469],"iteration":608,"passed_time":20.12348304,"remaining_time":45.96348917,"test":[0.6705083194]},
|
||||
{"learn":[0.6587292784],"iteration":609,"passed_time":20.14920752,"remaining_time":45.91376797,"test":[0.6705329997]},
|
||||
{"learn":[0.6587104112],"iteration":610,"passed_time":20.17662353,"remaining_time":45.86797068,"test":[0.6705269987]},
|
||||
{"learn":[0.6586953782],"iteration":611,"passed_time":20.20202219,"remaining_time":45.81765818,"test":[0.6705315607]},
|
||||
{"learn":[0.6586641191],"iteration":612,"passed_time":20.23050051,"remaining_time":45.77439512,"test":[0.6705142835]},
|
||||
{"learn":[0.6586450136],"iteration":613,"passed_time":20.25381994,"remaining_time":45.71953492,"test":[0.6705165015]},
|
||||
{"learn":[0.6586136263],"iteration":614,"passed_time":20.28518384,"remaining_time":45.68289369,"test":[0.6705001061]},
|
||||
{"learn":[0.6585862768],"iteration":615,"passed_time":20.3078175,"remaining_time":45.62665489,"test":[0.6705013916]},
|
||||
{"learn":[0.6585585235],"iteration":616,"passed_time":20.33878033,"remaining_time":45.5891948,"test":[0.6705037253]},
|
||||
{"learn":[0.6585371631],"iteration":617,"passed_time":20.36122842,"remaining_time":45.53271469,"test":[0.67049647]},
|
||||
{"learn":[0.6585092632],"iteration":618,"passed_time":20.3943397,"remaining_time":45.50013429,"test":[0.6705005632]},
|
||||
{"learn":[0.6584914317],"iteration":619,"passed_time":20.42384285,"remaining_time":45.45952119,"test":[0.6704957943]},
|
||||
{"learn":[0.6584662432],"iteration":620,"passed_time":20.45411533,"remaining_time":45.42065225,"test":[0.6704955333]},
|
||||
{"learn":[0.6584454668],"iteration":621,"passed_time":20.488223,"remaining_time":45.39030754,"test":[0.6704961207]},
|
||||
{"learn":[0.6584249408],"iteration":622,"passed_time":20.51043528,"remaining_time":45.33365872,"test":[0.6704921459]},
|
||||
{"learn":[0.6583931228],"iteration":623,"passed_time":20.54384208,"remaining_time":45.30180561,"test":[0.6704751713]},
|
||||
{"learn":[0.6583660767],"iteration":624,"passed_time":20.56912557,"remaining_time":45.25207624,"test":[0.6704753101]},
|
||||
{"learn":[0.658354264],"iteration":625,"passed_time":20.59414123,"remaining_time":45.20183714,"test":[0.6704620888]},
|
||||
{"learn":[0.6583253625],"iteration":626,"passed_time":20.61901142,"remaining_time":45.15135993,"test":[0.6704604282]},
|
||||
{"learn":[0.6582968632],"iteration":627,"passed_time":20.6468542,"remaining_time":45.10745855,"test":[0.6704663192]},
|
||||
{"learn":[0.6582687399],"iteration":628,"passed_time":20.67583093,"remaining_time":45.06607981,"test":[0.6704680085]},
|
||||
{"learn":[0.658242535],"iteration":629,"passed_time":20.7010198,"remaining_time":45.01650336,"test":[0.670453228]},
|
||||
{"learn":[0.6582199874],"iteration":630,"passed_time":20.72783977,"remaining_time":44.97054302,"test":[0.6704577785]},
|
||||
{"learn":[0.6581918101],"iteration":631,"passed_time":20.75222724,"remaining_time":44.91937795,"test":[0.67046675]},
|
||||
{"learn":[0.6581735218],"iteration":632,"passed_time":20.78264004,"remaining_time":44.88130954,"test":[0.6704731863]},
|
||||
{"learn":[0.6581445869],"iteration":633,"passed_time":20.80459182,"remaining_time":44.82503538,"test":[0.6704811116]},
|
||||
{"learn":[0.6581202427],"iteration":634,"passed_time":20.83717209,"remaining_time":44.79171637,"test":[0.6704839644]},
|
||||
{"learn":[0.6580977862],"iteration":635,"passed_time":20.86231353,"remaining_time":44.74244599,"test":[0.6704854798]},
|
||||
{"learn":[0.6580724179],"iteration":636,"passed_time":20.89269601,"remaining_time":44.70446572,"test":[0.6704835837]},
|
||||
{"learn":[0.6580426322],"iteration":637,"passed_time":20.93117347,"remaining_time":44.68379039,"test":[0.6704736198]},
|
||||
{"learn":[0.6580111256],"iteration":638,"passed_time":20.96066949,"remaining_time":44.64392985,"test":[0.6704640242]},
|
||||
{"learn":[0.6579834747],"iteration":639,"passed_time":20.9941179,"remaining_time":44.61250055,"test":[0.670465663]},
|
||||
{"learn":[0.6579541367],"iteration":640,"passed_time":21.0224519,"remaining_time":44.57022174,"test":[0.6704646829]},
|
||||
{"learn":[0.6579254503],"iteration":641,"passed_time":21.0522529,"remaining_time":44.53108946,"test":[0.6704600961]},
|
||||
{"learn":[0.657898555],"iteration":642,"passed_time":21.08260618,"remaining_time":44.49315178,"test":[0.6704643207]},
|
||||
{"learn":[0.6578676875],"iteration":643,"passed_time":21.10716702,"remaining_time":44.44304112,"test":[0.6704600533]},
|
||||
{"learn":[0.6578324163],"iteration":644,"passed_time":21.13594828,"remaining_time":44.40187584,"test":[0.6704614691]},
|
||||
{"learn":[0.6578062223],"iteration":645,"passed_time":21.1601277,"remaining_time":44.35110357,"test":[0.6704728212]},
|
||||
{"learn":[0.6577760631],"iteration":646,"passed_time":21.18552999,"remaining_time":44.30297075,"test":[0.6704758731]},
|
||||
{"learn":[0.6577483474],"iteration":647,"passed_time":21.21048648,"remaining_time":44.25397797,"test":[0.6704833026]},
|
||||
{"learn":[0.6577249642],"iteration":648,"passed_time":21.23686209,"remaining_time":44.20801337,"test":[0.6704767664]},
|
||||
{"learn":[0.6576974966],"iteration":649,"passed_time":21.26287585,"remaining_time":44.16135753,"test":[0.6704702727]},
|
||||
{"learn":[0.657675114],"iteration":650,"passed_time":21.28806218,"remaining_time":44.11305051,"test":[0.6704671372]},
|
||||
{"learn":[0.6576447891],"iteration":651,"passed_time":21.31506267,"remaining_time":44.06856515,"test":[0.6704699936]},
|
||||
{"learn":[0.6576102356],"iteration":652,"passed_time":21.3435081,"remaining_time":44.02711394,"test":[0.6704587989]},
|
||||
{"learn":[0.6575793887],"iteration":653,"passed_time":21.37776713,"remaining_time":43.99766753,"test":[0.6704637668]},
|
||||
{"learn":[0.6575543309],"iteration":654,"passed_time":21.40301154,"remaining_time":43.94969545,"test":[0.6704653717]},
|
||||
{"learn":[0.6575340787],"iteration":655,"passed_time":21.44023109,"remaining_time":43.92632711,"test":[0.6704598273]},
|
||||
{"learn":[0.6575061464],"iteration":656,"passed_time":21.4778965,"remaining_time":43.903828,"test":[0.6704522865]},
|
||||
{"learn":[0.657476113],"iteration":657,"passed_time":21.50245582,"remaining_time":43.85455275,"test":[0.6704558586]},
|
||||
{"learn":[0.6574447014],"iteration":658,"passed_time":21.53379663,"remaining_time":43.81915217,"test":[0.6704466331]},
|
||||
{"learn":[0.6574247361],"iteration":659,"passed_time":21.55955041,"remaining_time":43.77242053,"test":[0.6704405886]},
|
||||
{"learn":[0.6574034983],"iteration":660,"passed_time":21.58626671,"remaining_time":43.72770215,"test":[0.6704463767]},
|
||||
{"learn":[0.6573783832],"iteration":661,"passed_time":21.61183918,"remaining_time":43.68072633,"test":[0.6704475216]},
|
||||
{"learn":[0.657357694],"iteration":662,"passed_time":21.6373217,"remaining_time":43.63363366,"test":[0.6704572386]},
|
||||
{"learn":[0.6573411592],"iteration":663,"passed_time":21.66283476,"remaining_time":43.58666753,"test":[0.6704658153]},
|
||||
{"learn":[0.6573118559],"iteration":664,"passed_time":21.68841321,"remaining_time":43.5398972,"test":[0.6704600945]},
|
||||
{"learn":[0.6572819076],"iteration":665,"passed_time":21.71420973,"remaining_time":43.4936273,"test":[0.6704561998]},
|
||||
{"learn":[0.6572430097],"iteration":666,"passed_time":21.74213421,"remaining_time":43.45167151,"test":[0.6704535154]},
|
||||
{"learn":[0.6572160391],"iteration":667,"passed_time":21.77174463,"remaining_time":43.41311953,"test":[0.6704413781]},
|
||||
{"learn":[0.6571931413],"iteration":668,"passed_time":21.81895309,"remaining_time":43.40960622,"test":[0.6704450013]},
|
||||
{"learn":[0.6571737099],"iteration":669,"passed_time":21.84627583,"remaining_time":43.36648784,"test":[0.6704422199]},
|
||||
{"learn":[0.6571532872],"iteration":670,"passed_time":21.88834724,"remaining_time":43.35262814,"test":[0.67044342]},
|
||||
{"learn":[0.6571208939],"iteration":671,"passed_time":21.93403139,"remaining_time":43.34582395,"test":[0.6704415341]},
|
||||
{"learn":[0.6570887673],"iteration":672,"passed_time":21.9714274,"remaining_time":43.32256191,"test":[0.6704439539]},
|
||||
{"learn":[0.6570633692],"iteration":673,"passed_time":22.01942449,"remaining_time":43.32011406,"test":[0.6704498197]},
|
||||
{"learn":[0.6570454361],"iteration":674,"passed_time":22.05319867,"remaining_time":43.2896122,"test":[0.6704452194]},
|
||||
{"learn":[0.6570231031],"iteration":675,"passed_time":22.09079747,"remaining_time":43.26659149,"test":[0.6704366524]},
|
||||
{"learn":[0.6570052089],"iteration":676,"passed_time":22.14192346,"remaining_time":43.26996269,"test":[0.6704427124]},
|
||||
{"learn":[0.6569855794],"iteration":677,"passed_time":22.17624471,"remaining_time":43.24040635,"test":[0.6704395579]},
|
||||
{"learn":[0.6569579709],"iteration":678,"passed_time":22.213192,"remaining_time":43.21594497,"test":[0.6704401246]},
|
||||
{"learn":[0.6569333354],"iteration":679,"passed_time":22.23966403,"remaining_time":43.17111253,"test":[0.6704415621]},
|
||||
{"learn":[0.6569069617],"iteration":680,"passed_time":22.27051241,"remaining_time":43.13481039,"test":[0.6704341343]},
|
||||
{"learn":[0.6568931857],"iteration":681,"passed_time":22.29625075,"remaining_time":43.08864881,"test":[0.6704369615]},
|
||||
{"learn":[0.6568734532],"iteration":682,"passed_time":22.32160622,"remaining_time":43.04180877,"test":[0.6704357425]},
|
||||
{"learn":[0.6568435196],"iteration":683,"passed_time":22.35059872,"remaining_time":43.00202911,"test":[0.6704294622]},
|
||||
{"learn":[0.6568108038],"iteration":684,"passed_time":22.37956576,"remaining_time":42.96223208,"test":[0.6704289794]},
|
||||
{"learn":[0.6567811374],"iteration":685,"passed_time":22.41993338,"remaining_time":42.94430389,"test":[0.6704272409]},
|
||||
{"learn":[0.6567467284],"iteration":686,"passed_time":22.45285267,"remaining_time":42.91207504,"test":[0.6704101162]},
|
||||
{"learn":[0.6567172734],"iteration":687,"passed_time":22.4848431,"remaining_time":42.8780729,"test":[0.6704069439]},
|
||||
{"learn":[0.6566967606],"iteration":688,"passed_time":22.51193834,"remaining_time":42.83476221,"test":[0.6704100747]},
|
||||
{"learn":[0.6566720128],"iteration":689,"passed_time":22.53798671,"remaining_time":42.78951101,"test":[0.6704122261]},
|
||||
{"learn":[0.6566441608],"iteration":690,"passed_time":22.57108439,"remaining_time":42.75766928,"test":[0.6704137826]},
|
||||
{"learn":[0.6566172287],"iteration":691,"passed_time":22.59836588,"remaining_time":42.7148303,"test":[0.6704207952]},
|
||||
{"learn":[0.6565952549],"iteration":692,"passed_time":22.62447507,"remaining_time":42.66982528,"test":[0.6704154834]},
|
||||
{"learn":[0.6565702687],"iteration":693,"passed_time":22.65349415,"remaining_time":42.63035067,"test":[0.6704253514]},
|
||||
{"learn":[0.6565392213],"iteration":694,"passed_time":22.68028991,"remaining_time":42.58673141,"test":[0.6704155636]},
|
||||
{"learn":[0.6565157938],"iteration":695,"passed_time":22.70844406,"remaining_time":42.54570555,"test":[0.6704141298]},
|
||||
{"learn":[0.6564902789],"iteration":696,"passed_time":22.73944116,"remaining_time":42.51003133,"test":[0.6704207635]},
|
||||
{"learn":[0.6564644734],"iteration":697,"passed_time":22.7613976,"remaining_time":42.45750671,"test":[0.6704268341]},
|
||||
{"learn":[0.6564349549],"iteration":698,"passed_time":22.79216825,"remaining_time":42.42147482,"test":[0.6704243126]},
|
||||
{"learn":[0.6564046572],"iteration":699,"passed_time":22.8167121,"remaining_time":42.37389389,"test":[0.6704235165]},
|
||||
{"learn":[0.6563744107],"iteration":700,"passed_time":22.84507296,"remaining_time":42.33345189,"test":[0.6704257736]},
|
||||
{"learn":[0.6563525063],"iteration":701,"passed_time":22.87088832,"remaining_time":42.28833766,"test":[0.6704247758]},
|
||||
{"learn":[0.6563189867],"iteration":702,"passed_time":22.90238907,"remaining_time":42.25376759,"test":[0.6704331799]},
|
||||
{"learn":[0.6562939062],"iteration":703,"passed_time":22.94246813,"remaining_time":42.23499815,"test":[0.6704252722]},
|
||||
{"learn":[0.6562739297],"iteration":704,"passed_time":22.97441688,"remaining_time":42.20123385,"test":[0.6704146644]},
|
||||
{"learn":[0.656256438],"iteration":705,"passed_time":23.00262167,"remaining_time":42.16061253,"test":[0.6704164122]},
|
||||
{"learn":[0.6562366475],"iteration":706,"passed_time":23.033437,"remaining_time":42.12480062,"test":[0.6704118954]},
|
||||
{"learn":[0.6562073096],"iteration":707,"passed_time":23.0545813,"remaining_time":42.07135458,"test":[0.6704043129]},
|
||||
{"learn":[0.6561864222],"iteration":708,"passed_time":23.08699831,"remaining_time":42.03852584,"test":[0.6703978198]},
|
||||
{"learn":[0.6561578826],"iteration":709,"passed_time":23.11590694,"remaining_time":41.99932387,"test":[0.6703935976]},
|
||||
{"learn":[0.6561208567],"iteration":710,"passed_time":23.14362702,"remaining_time":41.9579961,"test":[0.6703839683]},
|
||||
{"learn":[0.6560924703],"iteration":711,"passed_time":23.16985155,"remaining_time":41.91400112,"test":[0.6703843723]},
|
||||
{"learn":[0.6560656907],"iteration":712,"passed_time":23.19510285,"remaining_time":41.86829925,"test":[0.6703879502]},
|
||||
{"learn":[0.6560362588],"iteration":713,"passed_time":23.23034771,"remaining_time":41.84065429,"test":[0.6703895978]},
|
||||
{"learn":[0.6560124527],"iteration":714,"passed_time":23.25923754,"remaining_time":41.80156678,"test":[0.6703894359]},
|
||||
{"learn":[0.6559875055],"iteration":715,"passed_time":23.28703452,"remaining_time":41.76054794,"test":[0.6703928777]},
|
||||
{"learn":[0.6559547281],"iteration":716,"passed_time":23.31161175,"remaining_time":41.71380457,"test":[0.6703933128]},
|
||||
{"learn":[0.6559230866],"iteration":717,"passed_time":23.34170355,"remaining_time":41.67696929,"test":[0.6703844355]},
|
||||
{"learn":[0.6558924823],"iteration":718,"passed_time":23.37263658,"remaining_time":41.64165155,"test":[0.6703825151]},
|
||||
{"learn":[0.6558676469],"iteration":719,"passed_time":23.40571088,"remaining_time":41.61015268,"test":[0.6703983542]},
|
||||
{"learn":[0.6558459277],"iteration":720,"passed_time":23.4389719,"remaining_time":41.57898067,"test":[0.670399556]},
|
||||
{"learn":[0.6558149638],"iteration":721,"passed_time":23.48304084,"remaining_time":41.56693379,"test":[0.6703931808]},
|
||||
{"learn":[0.6557812248],"iteration":722,"passed_time":23.50734531,"remaining_time":41.5198893,"test":[0.6703886918]},
|
||||
{"learn":[0.6557546502],"iteration":723,"passed_time":23.54055835,"remaining_time":41.48860836,"test":[0.6703847574]},
|
||||
{"learn":[0.6557274948],"iteration":724,"passed_time":23.56652491,"remaining_time":41.44457829,"test":[0.6703885941]},
|
||||
{"learn":[0.6557044723],"iteration":725,"passed_time":23.59580183,"remaining_time":41.40640708,"test":[0.6703788615]},
|
||||
{"learn":[0.6556751811],"iteration":726,"passed_time":23.62334313,"remaining_time":41.36522119,"test":[0.6703799906]},
|
||||
{"learn":[0.6556539158],"iteration":727,"passed_time":23.64879831,"remaining_time":41.32042782,"test":[0.6703774518]},
|
||||
{"learn":[0.6556182915],"iteration":728,"passed_time":23.67755213,"remaining_time":41.28143862,"test":[0.6703783496]},
|
||||
{"learn":[0.6555977079],"iteration":729,"passed_time":23.70012944,"remaining_time":41.23173204,"test":[0.6703648854]},
|
||||
{"learn":[0.6555667903],"iteration":730,"passed_time":23.72866102,"remaining_time":41.19243615,"test":[0.6703716654]},
|
||||
{"learn":[0.6555394075],"iteration":731,"passed_time":23.75226732,"remaining_time":41.14463793,"test":[0.6703550938]},
|
||||
{"learn":[0.6555122742],"iteration":732,"passed_time":23.7844108,"remaining_time":41.11166233,"test":[0.6703467057]},
|
||||
{"learn":[0.6554814941],"iteration":733,"passed_time":23.80747563,"remaining_time":41.06303017,"test":[0.6703484503]},
|
||||
{"learn":[0.6554517373],"iteration":734,"passed_time":23.84023587,"remaining_time":41.03115425,"test":[0.6703549183]},
|
||||
{"learn":[0.655429552],"iteration":735,"passed_time":23.87042124,"remaining_time":40.99485387,"test":[0.6703501504]},
|
||||
{"learn":[0.655396579],"iteration":736,"passed_time":23.9087808,"remaining_time":40.97257823,"test":[0.6703672622]},
|
||||
{"learn":[0.6553735864],"iteration":737,"passed_time":23.94161529,"remaining_time":40.94081097,"test":[0.6703560249]},
|
||||
{"learn":[0.6553472597],"iteration":738,"passed_time":23.97478791,"remaining_time":40.90961779,"test":[0.6703547155]},
|
||||
{"learn":[0.6553252832],"iteration":739,"passed_time":24.00628859,"remaining_time":40.87557247,"test":[0.6703593236]},
|
||||
{"learn":[0.6552971659],"iteration":740,"passed_time":24.03623034,"remaining_time":40.83888528,"test":[0.6703606827]},
|
||||
{"learn":[0.6552763852],"iteration":741,"passed_time":24.06404686,"remaining_time":40.79861313,"test":[0.6703511404]},
|
||||
{"learn":[0.6552488203],"iteration":742,"passed_time":24.09270947,"remaining_time":40.75980593,"test":[0.6703431646]},
|
||||
{"learn":[0.65521229],"iteration":743,"passed_time":24.12724624,"remaining_time":40.73094258,"test":[0.6703475116]},
|
||||
{"learn":[0.6551949744],"iteration":744,"passed_time":24.15397955,"remaining_time":40.68891857,"test":[0.6703483634]},
|
||||
{"learn":[0.6551673797],"iteration":745,"passed_time":24.17955779,"remaining_time":40.64499392,"test":[0.6703475713]},
|
||||
{"learn":[0.6551421856],"iteration":746,"passed_time":24.20715317,"remaining_time":40.60450191,"test":[0.670360457]},
|
||||
{"learn":[0.6551255516],"iteration":747,"passed_time":24.23336836,"remaining_time":40.5617342,"test":[0.6703664352]},
|
||||
{"learn":[0.6551019608],"iteration":748,"passed_time":24.2614437,"remaining_time":40.52211759,"test":[0.6703617612]},
|
||||
{"learn":[0.6550758728],"iteration":749,"passed_time":24.29512083,"remaining_time":40.49186805,"test":[0.6703669926]},
|
||||
{"learn":[0.655051966],"iteration":750,"passed_time":24.31839238,"remaining_time":40.44430371,"test":[0.6703670837]},
|
||||
{"learn":[0.6550351058],"iteration":751,"passed_time":24.34977118,"remaining_time":40.41025856,"test":[0.6703706628]},
|
||||
{"learn":[0.6549998756],"iteration":752,"passed_time":24.3762114,"remaining_time":40.36804198,"test":[0.670369618]},
|
||||
{"learn":[0.6549721212],"iteration":753,"passed_time":24.40831154,"remaining_time":40.3352204,"test":[0.6703692351]},
|
||||
{"learn":[0.6549401744],"iteration":754,"passed_time":24.44267281,"remaining_time":40.30612934,"test":[0.6703624433]},
|
||||
{"learn":[0.6549207325],"iteration":755,"passed_time":24.47460721,"remaining_time":40.27303091,"test":[0.6703686285]},
|
||||
{"learn":[0.6548900891],"iteration":756,"passed_time":24.50826603,"remaining_time":40.24276708,"test":[0.6703598432]},
|
||||
{"learn":[0.6548682731],"iteration":757,"passed_time":24.54826542,"remaining_time":40.22288345,"test":[0.6703618766]},
|
||||
{"learn":[0.6548418938],"iteration":758,"passed_time":24.57546587,"remaining_time":40.18201996,"test":[0.6703694148]},
|
||||
{"learn":[0.6548234717],"iteration":759,"passed_time":24.60502723,"remaining_time":40.14504442,"test":[0.6703683652]},
|
||||
{"learn":[0.6547996833],"iteration":760,"passed_time":24.63261096,"remaining_time":40.10486856,"test":[0.6703604855]},
|
||||
{"learn":[0.6547726174],"iteration":761,"passed_time":24.66001655,"remaining_time":40.06443634,"test":[0.6703758987]},
|
||||
{"learn":[0.6547509314],"iteration":762,"passed_time":24.68929907,"remaining_time":40.02708119,"test":[0.6703773302]},
|
||||
{"learn":[0.6547168175],"iteration":763,"passed_time":24.71425118,"remaining_time":39.98274144,"test":[0.6703641028]},
|
||||
{"learn":[0.6546907846],"iteration":764,"passed_time":24.74589169,"remaining_time":39.94924999,"test":[0.6703649602]},
|
||||
{"learn":[0.6546671611],"iteration":765,"passed_time":24.76625006,"remaining_time":39.89758822,"test":[0.6703567811]},
|
||||
{"learn":[0.6546475893],"iteration":766,"passed_time":24.79734832,"remaining_time":39.86327312,"test":[0.6703544688]},
|
||||
{"learn":[0.6546206223],"iteration":767,"passed_time":24.82531049,"remaining_time":39.82393558,"test":[0.6703611821]},
|
||||
{"learn":[0.6545874193],"iteration":768,"passed_time":24.85435247,"remaining_time":39.78635616,"test":[0.6703527821]},
|
||||
{"learn":[0.6545620629],"iteration":769,"passed_time":24.88095966,"remaining_time":39.74490958,"test":[0.6703523616]},
|
||||
{"learn":[0.6545346297],"iteration":770,"passed_time":24.90935211,"remaining_time":39.70634726,"test":[0.6703616298]},
|
||||
{"learn":[0.6545172316],"iteration":771,"passed_time":24.94098876,"remaining_time":39.67297175,"test":[0.6703603551]},
|
||||
{"learn":[0.6544943049],"iteration":772,"passed_time":24.97035098,"remaining_time":39.6359905,"test":[0.6703675655]},
|
||||
{"learn":[0.6544632323],"iteration":773,"passed_time":25.00434422,"remaining_time":39.60636436,"test":[0.6703582411]},
|
||||
{"learn":[0.6544384097],"iteration":774,"passed_time":25.03067441,"remaining_time":39.56461439,"test":[0.6703581437]},
|
||||
{"learn":[0.6544084745],"iteration":775,"passed_time":25.05692652,"remaining_time":39.522781,"test":[0.6703551885]},
|
||||
{"learn":[0.6543765257],"iteration":776,"passed_time":25.08660163,"remaining_time":39.48637554,"test":[0.6703608491]},
|
||||
{"learn":[0.6543536123],"iteration":777,"passed_time":25.10764591,"remaining_time":39.43643098,"test":[0.6703674554]},
|
||||
{"learn":[0.6543303593],"iteration":778,"passed_time":25.13940138,"remaining_time":39.40334928,"test":[0.6703679619]},
|
||||
{"learn":[0.6543005831],"iteration":779,"passed_time":25.15916899,"remaining_time":39.35152074,"test":[0.6703701757]},
|
||||
{"learn":[0.6542678123],"iteration":780,"passed_time":25.18841105,"remaining_time":39.31456219,"test":[0.6703603462]},
|
||||
{"learn":[0.6542439303],"iteration":781,"passed_time":25.21444083,"remaining_time":39.27262012,"test":[0.670359801]},
|
||||
{"learn":[0.6542100401],"iteration":782,"passed_time":25.24017824,"remaining_time":39.23026426,"test":[0.6703523669]},
|
||||
{"learn":[0.6541836178],"iteration":783,"passed_time":25.2660091,"remaining_time":39.18809574,"test":[0.6703365674]},
|
||||
{"learn":[0.654158129],"iteration":784,"passed_time":25.28891553,"remaining_time":39.1414425,"test":[0.6703486118]},
|
||||
{"learn":[0.6541343464],"iteration":785,"passed_time":25.31589904,"remaining_time":39.10114686,"test":[0.6703450011]},
|
||||
{"learn":[0.6541092921],"iteration":786,"passed_time":25.34123581,"remaining_time":39.05834694,"test":[0.6703473135]},
|
||||
{"learn":[0.6540812254],"iteration":787,"passed_time":25.36728606,"remaining_time":39.01668871,"test":[0.670350998]},
|
||||
{"learn":[0.654060259],"iteration":788,"passed_time":25.39177931,"remaining_time":38.97268028,"test":[0.6703417767]},
|
||||
{"learn":[0.6540467253],"iteration":789,"passed_time":25.41712461,"remaining_time":38.9300263,"test":[0.6703349821]},
|
||||
{"learn":[0.6540306837],"iteration":790,"passed_time":25.44804125,"remaining_time":38.89593157,"test":[0.6703457717]},
|
||||
{"learn":[0.6540103667],"iteration":791,"passed_time":25.48249341,"remaining_time":38.86723743,"test":[0.6703506266]},
|
||||
{"learn":[0.6539821302],"iteration":792,"passed_time":25.51450657,"remaining_time":38.83481643,"test":[0.6703596395]},
|
||||
{"learn":[0.6539577914],"iteration":793,"passed_time":25.54216564,"remaining_time":38.79578307,"test":[0.6703799895]},
|
||||
{"learn":[0.653923724],"iteration":794,"passed_time":25.56982738,"remaining_time":38.75678238,"test":[0.6703687687]},
|
||||
{"learn":[0.6539086888],"iteration":795,"passed_time":25.59539769,"remaining_time":38.71464675,"test":[0.6703780675]},
|
||||
{"learn":[0.6538798424],"iteration":796,"passed_time":25.61874122,"remaining_time":38.66919157,"test":[0.670374835]},
|
||||
{"learn":[0.6538566996],"iteration":797,"passed_time":25.64394874,"remaining_time":38.62659947,"test":[0.6703831387]},
|
||||
{"learn":[0.6538290752],"iteration":798,"passed_time":25.66776244,"remaining_time":38.58195581,"test":[0.670377656]},
|
||||
{"learn":[0.6538051255],"iteration":799,"passed_time":25.69593415,"remaining_time":38.54390122,"test":[0.6703689741]},
|
||||
{"learn":[0.6537917354],"iteration":800,"passed_time":25.71651353,"remaining_time":38.49450652,"test":[0.6703709756]},
|
||||
{"learn":[0.6537684302],"iteration":801,"passed_time":25.74304126,"remaining_time":38.45406912,"test":[0.6703737517]},
|
||||
{"learn":[0.6537402991],"iteration":802,"passed_time":25.77084871,"remaining_time":38.41557398,"test":[0.6703818964]},
|
||||
{"learn":[0.6537165427],"iteration":803,"passed_time":25.79028824,"remaining_time":38.36465763,"test":[0.6703812173]},
|
||||
{"learn":[0.6536853601],"iteration":804,"passed_time":25.82203653,"remaining_time":38.3320915,"test":[0.6703960068]},
|
||||
{"learn":[0.6536681479],"iteration":805,"passed_time":25.84395064,"remaining_time":38.28495914,"test":[0.6703976729]},
|
||||
{"learn":[0.6536409101],"iteration":806,"passed_time":25.87390688,"remaining_time":38.24977808,"test":[0.6704024604]},
|
||||
{"learn":[0.6536120189],"iteration":807,"passed_time":25.89606204,"remaining_time":38.20310143,"test":[0.6704085008]},
|
||||
{"learn":[0.6535912493],"iteration":808,"passed_time":25.92585483,"remaining_time":38.16772942,"test":[0.6704076633]},
|
||||
{"learn":[0.6535617421],"iteration":809,"passed_time":25.95539059,"remaining_time":38.13199358,"test":[0.6704111719]},
|
||||
{"learn":[0.6535315174],"iteration":810,"passed_time":25.98822968,"remaining_time":38.10111601,"test":[0.6704220803]},
|
||||
{"learn":[0.6534972927],"iteration":811,"passed_time":26.02835773,"remaining_time":38.08089777,"test":[0.6704265011]},
|
||||
{"learn":[0.6534818476],"iteration":812,"passed_time":26.0558565,"remaining_time":38.04219146,"test":[0.6704251162]},
|
||||
{"learn":[0.6534498323],"iteration":813,"passed_time":26.08151817,"remaining_time":38.00083606,"test":[0.6704375472]},
|
||||
{"learn":[0.6534305025],"iteration":814,"passed_time":26.10848988,"remaining_time":37.96142393,"test":[0.6704319336]},
|
||||
{"learn":[0.6534081059],"iteration":815,"passed_time":26.13143346,"remaining_time":37.91619757,"test":[0.670437614]},
|
||||
{"learn":[0.6533765804],"iteration":816,"passed_time":26.15923661,"remaining_time":37.87806231,"test":[0.6704554331]},
|
||||
{"learn":[0.6533441549],"iteration":817,"passed_time":26.18805523,"remaining_time":37.84141966,"test":[0.6704603317]},
|
||||
{"learn":[0.6533053405],"iteration":818,"passed_time":26.2140726,"remaining_time":37.8007567,"test":[0.6704548042]},
|
||||
{"learn":[0.6532838469],"iteration":819,"passed_time":26.24289367,"remaining_time":37.76416405,"test":[0.6704502654]},
|
||||
{"learn":[0.6532604302],"iteration":820,"passed_time":26.27260776,"remaining_time":37.72887277,"test":[0.6704512072]},
|
||||
{"learn":[0.6532364412],"iteration":821,"passed_time":26.29880394,"remaining_time":37.68855358,"test":[0.6704433481]},
|
||||
{"learn":[0.6532100089],"iteration":822,"passed_time":26.32785215,"remaining_time":37.65234749,"test":[0.6704095112]},
|
||||
{"learn":[0.6531782515],"iteration":823,"passed_time":26.35925682,"remaining_time":37.61952188,"test":[0.6704086019]},
|
||||
{"learn":[0.6531449701],"iteration":824,"passed_time":26.38596096,"remaining_time":37.580005,"test":[0.6703987131]},
|
||||
{"learn":[0.653115452],"iteration":825,"passed_time":26.40854839,"remaining_time":37.53466805,"test":[0.6704019708]},
|
||||
{"learn":[0.6530787602],"iteration":826,"passed_time":26.44419918,"remaining_time":37.50791492,"test":[0.6704046556]},
|
||||
{"learn":[0.653052397],"iteration":827,"passed_time":26.47784276,"remaining_time":37.47829917,"test":[0.6704091961]},
|
||||
{"learn":[0.6530313579],"iteration":828,"passed_time":26.51701028,"remaining_time":37.45647652,"test":[0.6704103204]},
|
||||
{"learn":[0.6530010363],"iteration":829,"passed_time":26.53963123,"remaining_time":37.41128739,"test":[0.6704074257]},
|
||||
{"learn":[0.6529752146],"iteration":830,"passed_time":26.57362226,"remaining_time":37.38214732,"test":[0.6704115335]},
|
||||
{"learn":[0.652954801],"iteration":831,"passed_time":26.59767057,"remaining_time":37.33903754,"test":[0.6704041275]},
|
||||
{"learn":[0.6529330351],"iteration":832,"passed_time":26.62378941,"remaining_time":37.29887425,"test":[0.6704004556]},
|
||||
{"learn":[0.6528993709],"iteration":833,"passed_time":26.65024746,"remaining_time":37.25921887,"test":[0.6704037097]},
|
||||
{"learn":[0.6528665883],"iteration":834,"passed_time":26.67774911,"remaining_time":37.22105115,"test":[0.6704035477]},
|
||||
{"learn":[0.6528413041],"iteration":835,"passed_time":26.70473813,"remaining_time":37.1821952,"test":[0.6704025281]},
|
||||
{"learn":[0.6528217161],"iteration":836,"passed_time":26.72833235,"remaining_time":37.13865056,"test":[0.6704024549]},
|
||||
{"learn":[0.6527978782],"iteration":837,"passed_time":26.76384162,"remaining_time":37.11167537,"test":[0.670405721]},
|
||||
{"learn":[0.6527789461],"iteration":838,"passed_time":26.79137369,"remaining_time":37.07364106,"test":[0.6703983189]},
|
||||
{"learn":[0.6527432001],"iteration":839,"passed_time":26.82295602,"remaining_time":37.04122498,"test":[0.6704035256]},
|
||||
{"learn":[0.6527139767],"iteration":840,"passed_time":26.87217031,"remaining_time":37.03310985,"test":[0.6704047613]},
|
||||
{"learn":[0.6526857244],"iteration":841,"passed_time":26.92488006,"remaining_time":37.0297044,"test":[0.6704139617]},
|
||||
{"learn":[0.652657086],"iteration":842,"passed_time":26.98258041,"remaining_time":37.03303147,"test":[0.6704066193]},
|
||||
{"learn":[0.6526355016],"iteration":843,"passed_time":27.05424841,"remaining_time":37.05534497,"test":[0.670402892]},
|
||||
{"learn":[0.6526054936],"iteration":844,"passed_time":27.09765154,"remaining_time":37.03880181,"test":[0.6704081961]},
|
||||
{"learn":[0.6525793707],"iteration":845,"passed_time":27.12038959,"remaining_time":36.99400661,"test":[0.6704029862]},
|
||||
{"learn":[0.6525584692],"iteration":846,"passed_time":27.14691224,"remaining_time":36.95441537,"test":[0.6704014281]},
|
||||
{"learn":[0.6525279747],"iteration":847,"passed_time":27.18096334,"remaining_time":36.92508227,"test":[0.6704036115]},
|
||||
{"learn":[0.6525038765],"iteration":848,"passed_time":27.20686017,"remaining_time":36.88468322,"test":[0.6704016777]},
|
||||
{"learn":[0.6524849104],"iteration":849,"passed_time":27.23465701,"remaining_time":36.8468889,"test":[0.6704085392]},
|
||||
{"learn":[0.6524610603],"iteration":850,"passed_time":27.26094834,"remaining_time":36.80708536,"test":[0.6704042952]},
|
||||
{"learn":[0.6524357337],"iteration":851,"passed_time":27.28945577,"remaining_time":36.77029957,"test":[0.670394789]},
|
||||
{"learn":[0.6524082286],"iteration":852,"passed_time":27.31865398,"remaining_time":36.73446203,"test":[0.6703885644]},
|
||||
{"learn":[0.65238051],"iteration":853,"passed_time":27.34791322,"remaining_time":36.69872195,"test":[0.6703946813]},
|
||||
{"learn":[0.6523557826],"iteration":854,"passed_time":27.3865535,"remaining_time":36.67555995,"test":[0.6704042137]},
|
||||
{"learn":[0.6523391233],"iteration":855,"passed_time":27.41370907,"remaining_time":36.63701306,"test":[0.6704077517]},
|
||||
{"learn":[0.652325347],"iteration":856,"passed_time":27.43905921,"remaining_time":36.5960848,"test":[0.6704118698]},
|
||||
{"learn":[0.6522924958],"iteration":857,"passed_time":27.47159295,"remaining_time":36.56475425,"test":[0.6704114259]},
|
||||
{"learn":[0.6522623584],"iteration":858,"passed_time":27.50124299,"remaining_time":36.52959052,"test":[0.6704157567]},
|
||||
{"learn":[0.6522343891],"iteration":859,"passed_time":27.53509105,"remaining_time":36.50000442,"test":[0.6703837005]},
|
||||
{"learn":[0.6522094424],"iteration":860,"passed_time":27.57211091,"remaining_time":36.47460432,"test":[0.6703829482]},
|
||||
{"learn":[0.6521841478],"iteration":861,"passed_time":27.59555719,"remaining_time":36.43125764,"test":[0.6703818491]},
|
||||
{"learn":[0.6521657946],"iteration":862,"passed_time":27.6272049,"remaining_time":36.39876242,"test":[0.6703826129]},
|
||||
{"learn":[0.6521304278],"iteration":863,"passed_time":27.65462267,"remaining_time":36.36070759,"test":[0.6703834487]},
|
||||
{"learn":[0.6521045712],"iteration":864,"passed_time":27.68321566,"remaining_time":36.3242194,"test":[0.6703868275]},
|
||||
{"learn":[0.6520753696],"iteration":865,"passed_time":27.71151671,"remaining_time":36.28736714,"test":[0.6703853357]},
|
||||
{"learn":[0.6520519528],"iteration":866,"passed_time":27.73884016,"remaining_time":36.2492571,"test":[0.670450644]},
|
||||
{"learn":[0.6520216555],"iteration":867,"passed_time":27.76583897,"remaining_time":36.21074851,"test":[0.6704556991]},
|
||||
{"learn":[0.6519926935],"iteration":868,"passed_time":27.79498714,"remaining_time":36.17506382,"test":[0.6704535742]},
|
||||
{"learn":[0.6519734186],"iteration":869,"passed_time":27.82082723,"remaining_time":36.13509744,"test":[0.6704495915]}
|
||||
]}
|
||||
Binary file not shown.
@@ -0,0 +1,871 @@
|
||||
iter Logloss
|
||||
0 0.692389481
|
||||
1 0.6916338586
|
||||
2 0.6910159214
|
||||
3 0.6903417151
|
||||
4 0.6896961461
|
||||
5 0.6890979366
|
||||
6 0.6884946167
|
||||
7 0.6879503686
|
||||
8 0.6874528094
|
||||
9 0.6869036785
|
||||
10 0.6863761921
|
||||
11 0.6859038678
|
||||
12 0.685410175
|
||||
13 0.6849483392
|
||||
14 0.6845417792
|
||||
15 0.6841038875
|
||||
16 0.6836957422
|
||||
17 0.6832947461
|
||||
18 0.6829014105
|
||||
19 0.6825264546
|
||||
20 0.6822106577
|
||||
21 0.6818649349
|
||||
22 0.6815467855
|
||||
23 0.6812293319
|
||||
24 0.6808837443
|
||||
25 0.6805816494
|
||||
26 0.6803209634
|
||||
27 0.6800350862
|
||||
28 0.6797703947
|
||||
29 0.6794926675
|
||||
30 0.6792251865
|
||||
31 0.6789670166
|
||||
32 0.678722402
|
||||
33 0.678476935
|
||||
34 0.6782297335
|
||||
35 0.6780226701
|
||||
36 0.6778291026
|
||||
37 0.6776045324
|
||||
38 0.6773969079
|
||||
39 0.6771819602
|
||||
40 0.6769816736
|
||||
41 0.6767984027
|
||||
42 0.6766201184
|
||||
43 0.6764394377
|
||||
44 0.6762698797
|
||||
45 0.6760974263
|
||||
46 0.6759245179
|
||||
47 0.6757673909
|
||||
48 0.6756172628
|
||||
49 0.675474531
|
||||
50 0.6753286933
|
||||
51 0.6751900513
|
||||
52 0.6750574835
|
||||
53 0.6749329567
|
||||
54 0.6748033265
|
||||
55 0.6746797823
|
||||
56 0.674535525
|
||||
57 0.6744256514
|
||||
58 0.674310819
|
||||
59 0.6741967947
|
||||
60 0.6740879654
|
||||
61 0.6739772476
|
||||
62 0.67388281
|
||||
63 0.6737789726
|
||||
64 0.6736812332
|
||||
65 0.6735930009
|
||||
66 0.6734947116
|
||||
67 0.6733961481
|
||||
68 0.6732990195
|
||||
69 0.6732133575
|
||||
70 0.673111539
|
||||
71 0.6730080451
|
||||
72 0.6729157861
|
||||
73 0.6728347949
|
||||
74 0.6727640693
|
||||
75 0.6726808811
|
||||
76 0.6726029645
|
||||
77 0.6725356026
|
||||
78 0.6724606887
|
||||
79 0.6723849561
|
||||
80 0.6723050519
|
||||
81 0.6722508802
|
||||
82 0.6721773904
|
||||
83 0.6721007598
|
||||
84 0.6720353564
|
||||
85 0.6719790902
|
||||
86 0.6719140024
|
||||
87 0.6718573633
|
||||
88 0.671795602
|
||||
89 0.6717369134
|
||||
90 0.6716711079
|
||||
91 0.6716070843
|
||||
92 0.6715517232
|
||||
93 0.6714957378
|
||||
94 0.6714364567
|
||||
95 0.6713881758
|
||||
96 0.6713336502
|
||||
97 0.6712700267
|
||||
98 0.6712154424
|
||||
99 0.6711600413
|
||||
100 0.6711060533
|
||||
101 0.6710494943
|
||||
102 0.6709936897
|
||||
103 0.6709472183
|
||||
104 0.6708914508
|
||||
105 0.6708388195
|
||||
106 0.6707885854
|
||||
107 0.6707454167
|
||||
108 0.6706973013
|
||||
109 0.6706577031
|
||||
110 0.67061108
|
||||
111 0.6705625485
|
||||
112 0.6705146484
|
||||
113 0.6704704423
|
||||
114 0.6704155922
|
||||
115 0.6703687117
|
||||
116 0.6703324232
|
||||
117 0.6702884624
|
||||
118 0.670253478
|
||||
119 0.6702140804
|
||||
120 0.6701682529
|
||||
121 0.6701320588
|
||||
122 0.6700939824
|
||||
123 0.6700655902
|
||||
124 0.6700190743
|
||||
125 0.6699792296
|
||||
126 0.6699379404
|
||||
127 0.669895454
|
||||
128 0.6698563938
|
||||
129 0.6698215571
|
||||
130 0.6697857067
|
||||
131 0.6697449303
|
||||
132 0.6697052425
|
||||
133 0.6696695553
|
||||
134 0.6696269265
|
||||
135 0.6695969271
|
||||
136 0.6695489786
|
||||
137 0.6695173859
|
||||
138 0.6694811164
|
||||
139 0.6694477439
|
||||
140 0.6694082161
|
||||
141 0.6693679185
|
||||
142 0.6693341916
|
||||
143 0.6692933159
|
||||
144 0.6692619696
|
||||
145 0.6692229289
|
||||
146 0.6691840164
|
||||
147 0.6691581406
|
||||
148 0.6691177196
|
||||
149 0.6690851126
|
||||
150 0.6690518144
|
||||
151 0.6690149711
|
||||
152 0.668993877
|
||||
153 0.6689596579
|
||||
154 0.6689372651
|
||||
155 0.6689003045
|
||||
156 0.6688680182
|
||||
157 0.6688348164
|
||||
158 0.6687947046
|
||||
159 0.6687605251
|
||||
160 0.668726253
|
||||
161 0.6686862718
|
||||
162 0.668663478
|
||||
163 0.6686399521
|
||||
164 0.6686058279
|
||||
165 0.6685761282
|
||||
166 0.6685469327
|
||||
167 0.6685157003
|
||||
168 0.6684805143
|
||||
169 0.6684485765
|
||||
170 0.6684144429
|
||||
171 0.6683849752
|
||||
172 0.6683568537
|
||||
173 0.6683266628
|
||||
174 0.6682937842
|
||||
175 0.6682657097
|
||||
176 0.6682301443
|
||||
177 0.6681995916
|
||||
178 0.6681658267
|
||||
179 0.6681422687
|
||||
180 0.6681216601
|
||||
181 0.6680899019
|
||||
182 0.6680676394
|
||||
183 0.6680413672
|
||||
184 0.6680088406
|
||||
185 0.6679873982
|
||||
186 0.6679663544
|
||||
187 0.6679417375
|
||||
188 0.6679100197
|
||||
189 0.667881208
|
||||
190 0.6678475427
|
||||
191 0.6678310341
|
||||
192 0.6678060257
|
||||
193 0.6677789336
|
||||
194 0.6677478773
|
||||
195 0.6677212408
|
||||
196 0.667704316
|
||||
197 0.6676819639
|
||||
198 0.6676554448
|
||||
199 0.6676318346
|
||||
200 0.6676074705
|
||||
201 0.6675849784
|
||||
202 0.6675631744
|
||||
203 0.6675397619
|
||||
204 0.6675169086
|
||||
205 0.6674864762
|
||||
206 0.6674670714
|
||||
207 0.6674375599
|
||||
208 0.6674148457
|
||||
209 0.6673974446
|
||||
210 0.6673812139
|
||||
211 0.6673515687
|
||||
212 0.6673197956
|
||||
213 0.6672900754
|
||||
214 0.6672550009
|
||||
215 0.6672271563
|
||||
216 0.667204521
|
||||
217 0.667181968
|
||||
218 0.6671640023
|
||||
219 0.66714351
|
||||
220 0.6671167156
|
||||
221 0.6670915937
|
||||
222 0.6670595279
|
||||
223 0.667033994
|
||||
224 0.6670008246
|
||||
225 0.6669858319
|
||||
226 0.6669553964
|
||||
227 0.6669274683
|
||||
228 0.666896348
|
||||
229 0.6668698686
|
||||
230 0.6668513411
|
||||
231 0.6668309985
|
||||
232 0.6668058585
|
||||
233 0.6667845908
|
||||
234 0.6667582863
|
||||
235 0.6667332943
|
||||
236 0.6667070085
|
||||
237 0.6666907315
|
||||
238 0.6666633028
|
||||
239 0.6666406707
|
||||
240 0.6666134624
|
||||
241 0.6665850522
|
||||
242 0.6665631193
|
||||
243 0.6665412643
|
||||
244 0.6665168385
|
||||
245 0.6664904845
|
||||
246 0.6664678274
|
||||
247 0.6664539777
|
||||
248 0.6664334121
|
||||
249 0.6664121724
|
||||
250 0.666392034
|
||||
251 0.666366899
|
||||
252 0.6663414098
|
||||
253 0.6663157816
|
||||
254 0.6662989799
|
||||
255 0.6662696102
|
||||
256 0.6662479711
|
||||
257 0.6662231874
|
||||
258 0.6661947927
|
||||
259 0.6661669951
|
||||
260 0.6661426137
|
||||
261 0.6661216749
|
||||
262 0.6660983123
|
||||
263 0.6660803402
|
||||
264 0.6660617842
|
||||
265 0.6660443878
|
||||
266 0.6660176079
|
||||
267 0.6659967546
|
||||
268 0.6659751467
|
||||
269 0.6659539329
|
||||
270 0.6659263951
|
||||
271 0.6659038921
|
||||
272 0.6658767418
|
||||
273 0.6658510507
|
||||
274 0.6658210119
|
||||
275 0.6657963011
|
||||
276 0.6657748552
|
||||
277 0.6657490013
|
||||
278 0.665732402
|
||||
279 0.6657118786
|
||||
280 0.665684467
|
||||
281 0.6656584634
|
||||
282 0.6656309991
|
||||
283 0.6656073482
|
||||
284 0.6655890957
|
||||
285 0.6655665563
|
||||
286 0.6655452454
|
||||
287 0.6655255286
|
||||
288 0.6655053548
|
||||
289 0.6654893396
|
||||
290 0.6654648912
|
||||
291 0.6654442759
|
||||
292 0.6654173127
|
||||
293 0.6653914518
|
||||
294 0.6653648946
|
||||
295 0.665344141
|
||||
296 0.6653140817
|
||||
297 0.665295365
|
||||
298 0.6652787488
|
||||
299 0.6652502991
|
||||
300 0.665231168
|
||||
301 0.6652136682
|
||||
302 0.6651903001
|
||||
303 0.6651697153
|
||||
304 0.6651525958
|
||||
305 0.6651322685
|
||||
306 0.6651113828
|
||||
307 0.6650886807
|
||||
308 0.6650622251
|
||||
309 0.6650429987
|
||||
310 0.665015513
|
||||
311 0.6650019022
|
||||
312 0.664979951
|
||||
313 0.6649549638
|
||||
314 0.6649340455
|
||||
315 0.6649162445
|
||||
316 0.6649048119
|
||||
317 0.6648796463
|
||||
318 0.6648605481
|
||||
319 0.6648429084
|
||||
320 0.6648238121
|
||||
321 0.6647969527
|
||||
322 0.6647854723
|
||||
323 0.6647589304
|
||||
324 0.6647429024
|
||||
325 0.6647237508
|
||||
326 0.6647059396
|
||||
327 0.664686288
|
||||
328 0.6646532527
|
||||
329 0.6646306438
|
||||
330 0.6646098516
|
||||
331 0.6645858284
|
||||
332 0.6645707188
|
||||
333 0.6645485788
|
||||
334 0.6645305696
|
||||
335 0.6645108881
|
||||
336 0.6644923286
|
||||
337 0.6644805222
|
||||
338 0.6644572776
|
||||
339 0.6644320741
|
||||
340 0.6644115048
|
||||
341 0.6643949013
|
||||
342 0.6643619789
|
||||
343 0.6643389502
|
||||
344 0.6643088915
|
||||
345 0.664286972
|
||||
346 0.664274149
|
||||
347 0.6642536926
|
||||
348 0.6642357634
|
||||
349 0.664207914
|
||||
350 0.6641853097
|
||||
351 0.6641654917
|
||||
352 0.664143804
|
||||
353 0.6641290647
|
||||
354 0.6641117244
|
||||
355 0.6640880219
|
||||
356 0.6640669415
|
||||
357 0.6640462999
|
||||
358 0.664030296
|
||||
359 0.6640028542
|
||||
360 0.6639813347
|
||||
361 0.6639597941
|
||||
362 0.6639429832
|
||||
363 0.6639222708
|
||||
364 0.6639065546
|
||||
365 0.6638823236
|
||||
366 0.6638648195
|
||||
367 0.6638436235
|
||||
368 0.6638208732
|
||||
369 0.6637956357
|
||||
370 0.6637718453
|
||||
371 0.663756918
|
||||
372 0.6637353525
|
||||
373 0.6637143112
|
||||
374 0.6636956547
|
||||
375 0.663680995
|
||||
376 0.66366728
|
||||
377 0.6636487567
|
||||
378 0.6636266904
|
||||
379 0.6636116064
|
||||
380 0.6635902746
|
||||
381 0.6635654896
|
||||
382 0.6635393029
|
||||
383 0.6635171734
|
||||
384 0.663500789
|
||||
385 0.663477743
|
||||
386 0.6634584806
|
||||
387 0.6634337499
|
||||
388 0.6634135584
|
||||
389 0.6633868455
|
||||
390 0.6633755323
|
||||
391 0.663356103
|
||||
392 0.6633337631
|
||||
393 0.663319422
|
||||
394 0.6632911566
|
||||
395 0.6632687875
|
||||
396 0.6632431997
|
||||
397 0.6632189331
|
||||
398 0.663201035
|
||||
399 0.6631898553
|
||||
400 0.6631712482
|
||||
401 0.663143025
|
||||
402 0.663121538
|
||||
403 0.6631087792
|
||||
404 0.6630859067
|
||||
405 0.663066483
|
||||
406 0.6630443652
|
||||
407 0.6630250376
|
||||
408 0.6630007822
|
||||
409 0.6629768728
|
||||
410 0.6629528093
|
||||
411 0.6629260936
|
||||
412 0.6629102182
|
||||
413 0.6628863488
|
||||
414 0.6628648972
|
||||
415 0.6628454339
|
||||
416 0.6628200274
|
||||
417 0.6627942591
|
||||
418 0.6627744647
|
||||
419 0.662765485
|
||||
420 0.6627503257
|
||||
421 0.6627323029
|
||||
422 0.6627111509
|
||||
423 0.6626785863
|
||||
424 0.6626576561
|
||||
425 0.6626363113
|
||||
426 0.6626181065
|
||||
427 0.66259794
|
||||
428 0.6625765658
|
||||
429 0.6625526572
|
||||
430 0.66253135
|
||||
431 0.6625035695
|
||||
432 0.662480212
|
||||
433 0.6624611632
|
||||
434 0.6624332625
|
||||
435 0.6624120584
|
||||
436 0.6623941719
|
||||
437 0.6623766304
|
||||
438 0.6623623329
|
||||
439 0.6623442925
|
||||
440 0.6623212715
|
||||
441 0.6623025941
|
||||
442 0.6622749791
|
||||
443 0.6622534499
|
||||
444 0.6622305473
|
||||
445 0.6622059333
|
||||
446 0.6621871707
|
||||
447 0.6621638454
|
||||
448 0.6621511296
|
||||
449 0.6621349978
|
||||
450 0.6621120424
|
||||
451 0.6620958271
|
||||
452 0.6620793528
|
||||
453 0.6620572713
|
||||
454 0.6620395025
|
||||
455 0.6620188044
|
||||
456 0.6620017347
|
||||
457 0.6619811454
|
||||
458 0.6619695569
|
||||
459 0.661952377
|
||||
460 0.6619237442
|
||||
461 0.6619089407
|
||||
462 0.6618886168
|
||||
463 0.6618831383
|
||||
464 0.6618690774
|
||||
465 0.661845878
|
||||
466 0.6618290213
|
||||
467 0.6618050064
|
||||
468 0.6617832833
|
||||
469 0.6617652311
|
||||
470 0.6617443144
|
||||
471 0.6617202619
|
||||
472 0.6617005831
|
||||
473 0.6616824419
|
||||
474 0.6616538226
|
||||
475 0.6616314155
|
||||
476 0.6616127861
|
||||
477 0.6616029072
|
||||
478 0.6615843751
|
||||
479 0.661563216
|
||||
480 0.6615432257
|
||||
481 0.6615263324
|
||||
482 0.6615033259
|
||||
483 0.661484293
|
||||
484 0.6614678231
|
||||
485 0.6614463024
|
||||
486 0.6614155436
|
||||
487 0.6613958945
|
||||
488 0.661380611
|
||||
489 0.6613677802
|
||||
490 0.6613530086
|
||||
491 0.6613248211
|
||||
492 0.6613059359
|
||||
493 0.6612729965
|
||||
494 0.6612624948
|
||||
495 0.6612401679
|
||||
496 0.6612191637
|
||||
497 0.6611912219
|
||||
498 0.6611773017
|
||||
499 0.6611638216
|
||||
500 0.6611450533
|
||||
501 0.6611179111
|
||||
502 0.6610959069
|
||||
503 0.6610728788
|
||||
504 0.6610436668
|
||||
505 0.6610188976
|
||||
506 0.6610030555
|
||||
507 0.6609831174
|
||||
508 0.6609586562
|
||||
509 0.660935882
|
||||
510 0.6609202024
|
||||
511 0.6609011137
|
||||
512 0.6608726737
|
||||
513 0.6608608849
|
||||
514 0.6608387256
|
||||
515 0.6608136063
|
||||
516 0.6607946343
|
||||
517 0.6607703935
|
||||
518 0.6607509625
|
||||
519 0.6607238109
|
||||
520 0.6606999858
|
||||
521 0.6606813873
|
||||
522 0.6606610372
|
||||
523 0.660638456
|
||||
524 0.6606156483
|
||||
525 0.6605968623
|
||||
526 0.6605735776
|
||||
527 0.6605517294
|
||||
528 0.6605309239
|
||||
529 0.6605086434
|
||||
530 0.6604803349
|
||||
531 0.6604566326
|
||||
532 0.6604430839
|
||||
533 0.6604273738
|
||||
534 0.6604048016
|
||||
535 0.6603845173
|
||||
536 0.6603669212
|
||||
537 0.6603488983
|
||||
538 0.6603176881
|
||||
539 0.6602953862
|
||||
540 0.6602672025
|
||||
541 0.6602568636
|
||||
542 0.660235705
|
||||
543 0.6602152295
|
||||
544 0.6601897709
|
||||
545 0.6601683731
|
||||
546 0.6601472267
|
||||
547 0.6601262337
|
||||
548 0.6601119991
|
||||
549 0.6600869973
|
||||
550 0.6600667497
|
||||
551 0.6600397508
|
||||
552 0.660016863
|
||||
553 0.6599933158
|
||||
554 0.6599632649
|
||||
555 0.6599446007
|
||||
556 0.6599138126
|
||||
557 0.6598965504
|
||||
558 0.6598785723
|
||||
559 0.659860838
|
||||
560 0.6598408724
|
||||
561 0.6598244857
|
||||
562 0.6598082469
|
||||
563 0.6597851673
|
||||
564 0.6597683521
|
||||
565 0.6597479006
|
||||
566 0.6597310938
|
||||
567 0.6597096581
|
||||
568 0.6596862311
|
||||
569 0.6596574779
|
||||
570 0.6596385418
|
||||
571 0.6596189903
|
||||
572 0.65959275
|
||||
573 0.6595730662
|
||||
574 0.6595566809
|
||||
575 0.6595365076
|
||||
576 0.6595163446
|
||||
577 0.6594816637
|
||||
578 0.6594570142
|
||||
579 0.6594353055
|
||||
580 0.6594162362
|
||||
581 0.659395036
|
||||
582 0.6593798831
|
||||
583 0.6593556719
|
||||
584 0.6593292627
|
||||
585 0.6592976737
|
||||
586 0.6592754841
|
||||
587 0.6592510441
|
||||
588 0.6592290326
|
||||
589 0.6592097404
|
||||
590 0.6591876204
|
||||
591 0.6591705995
|
||||
592 0.6591456195
|
||||
593 0.6591107122
|
||||
594 0.6590819533
|
||||
595 0.6590551327
|
||||
596 0.6590373916
|
||||
597 0.6590177149
|
||||
598 0.6589946095
|
||||
599 0.6589697628
|
||||
600 0.6589442269
|
||||
601 0.6589182437
|
||||
602 0.6588837179
|
||||
603 0.6588674101
|
||||
604 0.6588406916
|
||||
605 0.6588149945
|
||||
606 0.6587866031
|
||||
607 0.6587636648
|
||||
608 0.6587502469
|
||||
609 0.6587292784
|
||||
610 0.6587104112
|
||||
611 0.6586953782
|
||||
612 0.6586641191
|
||||
613 0.6586450136
|
||||
614 0.6586136263
|
||||
615 0.6585862768
|
||||
616 0.6585585235
|
||||
617 0.6585371631
|
||||
618 0.6585092632
|
||||
619 0.6584914317
|
||||
620 0.6584662432
|
||||
621 0.6584454668
|
||||
622 0.6584249408
|
||||
623 0.6583931228
|
||||
624 0.6583660767
|
||||
625 0.658354264
|
||||
626 0.6583253625
|
||||
627 0.6582968632
|
||||
628 0.6582687399
|
||||
629 0.658242535
|
||||
630 0.6582199874
|
||||
631 0.6581918101
|
||||
632 0.6581735218
|
||||
633 0.6581445869
|
||||
634 0.6581202427
|
||||
635 0.6580977862
|
||||
636 0.6580724179
|
||||
637 0.6580426322
|
||||
638 0.6580111256
|
||||
639 0.6579834747
|
||||
640 0.6579541367
|
||||
641 0.6579254503
|
||||
642 0.657898555
|
||||
643 0.6578676875
|
||||
644 0.6578324163
|
||||
645 0.6578062223
|
||||
646 0.6577760631
|
||||
647 0.6577483474
|
||||
648 0.6577249642
|
||||
649 0.6576974966
|
||||
650 0.657675114
|
||||
651 0.6576447891
|
||||
652 0.6576102356
|
||||
653 0.6575793887
|
||||
654 0.6575543309
|
||||
655 0.6575340787
|
||||
656 0.6575061464
|
||||
657 0.657476113
|
||||
658 0.6574447014
|
||||
659 0.6574247361
|
||||
660 0.6574034983
|
||||
661 0.6573783832
|
||||
662 0.657357694
|
||||
663 0.6573411592
|
||||
664 0.6573118559
|
||||
665 0.6572819076
|
||||
666 0.6572430097
|
||||
667 0.6572160391
|
||||
668 0.6571931413
|
||||
669 0.6571737099
|
||||
670 0.6571532872
|
||||
671 0.6571208939
|
||||
672 0.6570887673
|
||||
673 0.6570633692
|
||||
674 0.6570454361
|
||||
675 0.6570231031
|
||||
676 0.6570052089
|
||||
677 0.6569855794
|
||||
678 0.6569579709
|
||||
679 0.6569333354
|
||||
680 0.6569069617
|
||||
681 0.6568931857
|
||||
682 0.6568734532
|
||||
683 0.6568435196
|
||||
684 0.6568108038
|
||||
685 0.6567811374
|
||||
686 0.6567467284
|
||||
687 0.6567172734
|
||||
688 0.6566967606
|
||||
689 0.6566720128
|
||||
690 0.6566441608
|
||||
691 0.6566172287
|
||||
692 0.6565952549
|
||||
693 0.6565702687
|
||||
694 0.6565392213
|
||||
695 0.6565157938
|
||||
696 0.6564902789
|
||||
697 0.6564644734
|
||||
698 0.6564349549
|
||||
699 0.6564046572
|
||||
700 0.6563744107
|
||||
701 0.6563525063
|
||||
702 0.6563189867
|
||||
703 0.6562939062
|
||||
704 0.6562739297
|
||||
705 0.656256438
|
||||
706 0.6562366475
|
||||
707 0.6562073096
|
||||
708 0.6561864222
|
||||
709 0.6561578826
|
||||
710 0.6561208567
|
||||
711 0.6560924703
|
||||
712 0.6560656907
|
||||
713 0.6560362588
|
||||
714 0.6560124527
|
||||
715 0.6559875055
|
||||
716 0.6559547281
|
||||
717 0.6559230866
|
||||
718 0.6558924823
|
||||
719 0.6558676469
|
||||
720 0.6558459277
|
||||
721 0.6558149638
|
||||
722 0.6557812248
|
||||
723 0.6557546502
|
||||
724 0.6557274948
|
||||
725 0.6557044723
|
||||
726 0.6556751811
|
||||
727 0.6556539158
|
||||
728 0.6556182915
|
||||
729 0.6555977079
|
||||
730 0.6555667903
|
||||
731 0.6555394075
|
||||
732 0.6555122742
|
||||
733 0.6554814941
|
||||
734 0.6554517373
|
||||
735 0.655429552
|
||||
736 0.655396579
|
||||
737 0.6553735864
|
||||
738 0.6553472597
|
||||
739 0.6553252832
|
||||
740 0.6552971659
|
||||
741 0.6552763852
|
||||
742 0.6552488203
|
||||
743 0.65521229
|
||||
744 0.6551949744
|
||||
745 0.6551673797
|
||||
746 0.6551421856
|
||||
747 0.6551255516
|
||||
748 0.6551019608
|
||||
749 0.6550758728
|
||||
750 0.655051966
|
||||
751 0.6550351058
|
||||
752 0.6549998756
|
||||
753 0.6549721212
|
||||
754 0.6549401744
|
||||
755 0.6549207325
|
||||
756 0.6548900891
|
||||
757 0.6548682731
|
||||
758 0.6548418938
|
||||
759 0.6548234717
|
||||
760 0.6547996833
|
||||
761 0.6547726174
|
||||
762 0.6547509314
|
||||
763 0.6547168175
|
||||
764 0.6546907846
|
||||
765 0.6546671611
|
||||
766 0.6546475893
|
||||
767 0.6546206223
|
||||
768 0.6545874193
|
||||
769 0.6545620629
|
||||
770 0.6545346297
|
||||
771 0.6545172316
|
||||
772 0.6544943049
|
||||
773 0.6544632323
|
||||
774 0.6544384097
|
||||
775 0.6544084745
|
||||
776 0.6543765257
|
||||
777 0.6543536123
|
||||
778 0.6543303593
|
||||
779 0.6543005831
|
||||
780 0.6542678123
|
||||
781 0.6542439303
|
||||
782 0.6542100401
|
||||
783 0.6541836178
|
||||
784 0.654158129
|
||||
785 0.6541343464
|
||||
786 0.6541092921
|
||||
787 0.6540812254
|
||||
788 0.654060259
|
||||
789 0.6540467253
|
||||
790 0.6540306837
|
||||
791 0.6540103667
|
||||
792 0.6539821302
|
||||
793 0.6539577914
|
||||
794 0.653923724
|
||||
795 0.6539086888
|
||||
796 0.6538798424
|
||||
797 0.6538566996
|
||||
798 0.6538290752
|
||||
799 0.6538051255
|
||||
800 0.6537917354
|
||||
801 0.6537684302
|
||||
802 0.6537402991
|
||||
803 0.6537165427
|
||||
804 0.6536853601
|
||||
805 0.6536681479
|
||||
806 0.6536409101
|
||||
807 0.6536120189
|
||||
808 0.6535912493
|
||||
809 0.6535617421
|
||||
810 0.6535315174
|
||||
811 0.6534972927
|
||||
812 0.6534818476
|
||||
813 0.6534498323
|
||||
814 0.6534305025
|
||||
815 0.6534081059
|
||||
816 0.6533765804
|
||||
817 0.6533441549
|
||||
818 0.6533053405
|
||||
819 0.6532838469
|
||||
820 0.6532604302
|
||||
821 0.6532364412
|
||||
822 0.6532100089
|
||||
823 0.6531782515
|
||||
824 0.6531449701
|
||||
825 0.653115452
|
||||
826 0.6530787602
|
||||
827 0.653052397
|
||||
828 0.6530313579
|
||||
829 0.6530010363
|
||||
830 0.6529752146
|
||||
831 0.652954801
|
||||
832 0.6529330351
|
||||
833 0.6528993709
|
||||
834 0.6528665883
|
||||
835 0.6528413041
|
||||
836 0.6528217161
|
||||
837 0.6527978782
|
||||
838 0.6527789461
|
||||
839 0.6527432001
|
||||
840 0.6527139767
|
||||
841 0.6526857244
|
||||
842 0.652657086
|
||||
843 0.6526355016
|
||||
844 0.6526054936
|
||||
845 0.6525793707
|
||||
846 0.6525584692
|
||||
847 0.6525279747
|
||||
848 0.6525038765
|
||||
849 0.6524849104
|
||||
850 0.6524610603
|
||||
851 0.6524357337
|
||||
852 0.6524082286
|
||||
853 0.65238051
|
||||
854 0.6523557826
|
||||
855 0.6523391233
|
||||
856 0.652325347
|
||||
857 0.6522924958
|
||||
858 0.6522623584
|
||||
859 0.6522343891
|
||||
860 0.6522094424
|
||||
861 0.6521841478
|
||||
862 0.6521657946
|
||||
863 0.6521304278
|
||||
864 0.6521045712
|
||||
865 0.6520753696
|
||||
866 0.6520519528
|
||||
867 0.6520216555
|
||||
868 0.6519926935
|
||||
869 0.6519734186
|
||||
|
@@ -0,0 +1,871 @@
|
||||
iter Passed Remaining
|
||||
0 46 93548
|
||||
1 83 83419
|
||||
2 132 88415
|
||||
3 162 81250
|
||||
4 196 78573
|
||||
5 230 76747
|
||||
6 269 76701
|
||||
7 319 79674
|
||||
8 364 80653
|
||||
9 411 81918
|
||||
10 456 82497
|
||||
11 491 81432
|
||||
12 522 79809
|
||||
13 555 78774
|
||||
14 595 78777
|
||||
15 630 78123
|
||||
16 662 77290
|
||||
17 700 77124
|
||||
18 730 76120
|
||||
19 764 75651
|
||||
20 804 75774
|
||||
21 835 75128
|
||||
22 886 76169
|
||||
23 920 75764
|
||||
24 960 75853
|
||||
25 989 75130
|
||||
26 1025 74941
|
||||
27 1060 74714
|
||||
28 1104 75079
|
||||
29 1141 74976
|
||||
30 1180 74975
|
||||
31 1213 74640
|
||||
32 1245 74260
|
||||
33 1287 74434
|
||||
34 1327 74528
|
||||
35 1376 75071
|
||||
36 1427 75741
|
||||
37 1468 75804
|
||||
38 1508 75857
|
||||
39 1549 75922
|
||||
40 1586 75781
|
||||
41 1621 75590
|
||||
42 1663 75705
|
||||
43 1701 75621
|
||||
44 1739 75591
|
||||
45 1776 75460
|
||||
46 1819 75616
|
||||
47 1869 76025
|
||||
48 1916 76288
|
||||
49 1953 76191
|
||||
50 1993 76197
|
||||
51 2038 76381
|
||||
52 2080 76420
|
||||
53 2158 77788
|
||||
54 2220 78529
|
||||
55 2286 79390
|
||||
56 2328 79372
|
||||
57 2367 79254
|
||||
58 2409 79257
|
||||
59 2444 79049
|
||||
60 2484 78985
|
||||
61 2521 78820
|
||||
62 2554 78528
|
||||
63 2593 78466
|
||||
64 2623 78111
|
||||
65 2660 77969
|
||||
66 2695 77776
|
||||
67 2725 77446
|
||||
68 2761 77291
|
||||
69 2791 76975
|
||||
70 2824 76739
|
||||
71 2861 76611
|
||||
72 2897 76476
|
||||
73 2935 76408
|
||||
74 3040 78027
|
||||
75 3097 78411
|
||||
76 3152 78741
|
||||
77 3216 79248
|
||||
78 3256 79195
|
||||
79 3305 79336
|
||||
80 3348 79320
|
||||
81 3381 79089
|
||||
82 3416 78911
|
||||
83 3480 79399
|
||||
84 3535 79649
|
||||
85 3581 79716
|
||||
86 3612 79428
|
||||
87 3644 79185
|
||||
88 3678 78975
|
||||
89 3712 78785
|
||||
90 3743 78531
|
||||
91 3775 78297
|
||||
92 3806 78047
|
||||
93 3837 77821
|
||||
94 3871 77629
|
||||
95 3913 77618
|
||||
96 3945 77403
|
||||
97 3989 77433
|
||||
98 4020 77204
|
||||
99 4053 77020
|
||||
100 4084 76789
|
||||
101 4116 76597
|
||||
102 4148 76401
|
||||
103 4176 76141
|
||||
104 4202 75845
|
||||
105 4232 75634
|
||||
106 4261 75390
|
||||
107 4290 75168
|
||||
108 4324 75018
|
||||
109 4351 74766
|
||||
110 4386 74648
|
||||
111 4424 74577
|
||||
112 4458 74455
|
||||
113 4497 74400
|
||||
114 4533 74307
|
||||
115 4564 74136
|
||||
116 4596 73981
|
||||
117 4628 73818
|
||||
118 4668 73786
|
||||
119 4692 73509
|
||||
120 4723 73354
|
||||
121 4756 73220
|
||||
122 4788 73065
|
||||
123 4815 72854
|
||||
124 4843 72647
|
||||
125 4875 72514
|
||||
126 4916 72515
|
||||
127 4952 72436
|
||||
128 4991 72397
|
||||
129 5028 72327
|
||||
130 5059 72180
|
||||
131 5096 72116
|
||||
132 5125 71946
|
||||
133 5156 71804
|
||||
134 5190 71704
|
||||
135 5221 71564
|
||||
136 5251 71407
|
||||
137 5274 71165
|
||||
138 5309 71084
|
||||
139 5344 71008
|
||||
140 5377 70902
|
||||
141 5416 70866
|
||||
142 5452 70803
|
||||
143 5490 70760
|
||||
144 5521 70641
|
||||
145 5553 70522
|
||||
146 5582 70365
|
||||
147 5611 70217
|
||||
148 5636 70026
|
||||
149 5673 69975
|
||||
150 5706 69874
|
||||
151 5738 69764
|
||||
152 5765 69605
|
||||
153 5795 69471
|
||||
154 5817 69246
|
||||
155 5853 69191
|
||||
156 5888 69122
|
||||
157 5924 69070
|
||||
158 5964 69061
|
||||
159 5996 68963
|
||||
160 6022 68789
|
||||
161 6050 68650
|
||||
162 6079 68510
|
||||
163 6108 68385
|
||||
164 6140 68292
|
||||
165 6169 68162
|
||||
166 6202 68074
|
||||
167 6231 67953
|
||||
168 6263 67858
|
||||
169 6295 67764
|
||||
170 6325 67656
|
||||
171 6356 67561
|
||||
172 6395 67545
|
||||
173 6437 67554
|
||||
174 6472 67495
|
||||
175 6503 67395
|
||||
176 6533 67291
|
||||
177 6562 67174
|
||||
178 6590 67049
|
||||
179 6624 66982
|
||||
180 6655 66882
|
||||
181 6687 66804
|
||||
182 6718 66703
|
||||
183 6751 66632
|
||||
184 6784 66559
|
||||
185 6810 66424
|
||||
186 6832 66246
|
||||
187 6867 66187
|
||||
188 6918 66294
|
||||
189 6969 66393
|
||||
190 7018 66470
|
||||
191 7074 66614
|
||||
192 7117 66635
|
||||
193 7191 66943
|
||||
194 7242 67036
|
||||
195 7282 67027
|
||||
196 7317 66967
|
||||
197 7351 66903
|
||||
198 7389 66879
|
||||
199 7432 66896
|
||||
200 7471 66869
|
||||
201 7506 66814
|
||||
202 7540 66752
|
||||
203 7568 66628
|
||||
204 7605 66596
|
||||
205 7638 66519
|
||||
206 7665 66397
|
||||
207 7700 66340
|
||||
208 7734 66276
|
||||
209 7766 66197
|
||||
210 7796 66106
|
||||
211 7831 66053
|
||||
212 7871 66037
|
||||
213 7910 66016
|
||||
214 7951 66014
|
||||
215 7989 65983
|
||||
216 8025 65946
|
||||
217 8058 65872
|
||||
218 8087 65768
|
||||
219 8112 65638
|
||||
220 8148 65594
|
||||
221 8197 65655
|
||||
222 8239 65655
|
||||
223 8268 65556
|
||||
224 8298 65466
|
||||
225 8327 65366
|
||||
226 8357 65278
|
||||
227 8384 65167
|
||||
228 8418 65103
|
||||
229 8453 65058
|
||||
230 8490 65020
|
||||
231 8523 64958
|
||||
232 8550 64848
|
||||
233 8575 64718
|
||||
234 8607 64648
|
||||
235 8635 64545
|
||||
236 8660 64426
|
||||
237 8691 64345
|
||||
238 8719 64250
|
||||
239 8746 64137
|
||||
240 8773 64038
|
||||
241 8803 63951
|
||||
242 8833 63873
|
||||
243 8862 63779
|
||||
244 8892 63698
|
||||
245 8932 63688
|
||||
246 8962 63611
|
||||
247 8991 63521
|
||||
248 9021 63442
|
||||
249 9051 63358
|
||||
250 9085 63306
|
||||
251 9110 63193
|
||||
252 9137 63093
|
||||
253 9174 63066
|
||||
254 9196 62935
|
||||
255 9238 62934
|
||||
256 9267 62855
|
||||
257 9297 62776
|
||||
258 9324 62681
|
||||
259 9357 62625
|
||||
260 9388 62552
|
||||
261 9427 62536
|
||||
262 9461 62491
|
||||
263 9496 62443
|
||||
264 9524 62356
|
||||
265 9553 62278
|
||||
266 9590 62247
|
||||
267 9620 62172
|
||||
268 9645 62071
|
||||
269 9682 62040
|
||||
270 9711 61962
|
||||
271 9739 61872
|
||||
272 9768 61797
|
||||
273 9804 61761
|
||||
274 9848 61777
|
||||
275 9886 61755
|
||||
276 9925 61740
|
||||
277 9965 61728
|
||||
278 9995 61656
|
||||
279 10022 61564
|
||||
280 10055 61516
|
||||
281 10080 61410
|
||||
282 10111 61344
|
||||
283 10147 61311
|
||||
284 10175 61230
|
||||
285 10202 61141
|
||||
286 10234 61084
|
||||
287 10264 61018
|
||||
288 10299 60977
|
||||
289 10323 60874
|
||||
290 10353 60804
|
||||
291 10394 60803
|
||||
292 10431 60773
|
||||
293 10471 60763
|
||||
294 10503 60707
|
||||
295 10534 60645
|
||||
296 10576 60646
|
||||
297 10612 60612
|
||||
298 10639 60525
|
||||
299 10668 60453
|
||||
300 10702 60411
|
||||
301 10729 60326
|
||||
302 10764 60290
|
||||
303 10801 60263
|
||||
304 10829 60182
|
||||
305 10857 60108
|
||||
306 10892 60067
|
||||
307 10930 60047
|
||||
308 10972 60045
|
||||
309 11002 59983
|
||||
310 11030 59902
|
||||
311 11058 59828
|
||||
312 11092 59788
|
||||
313 11117 59696
|
||||
314 11149 59641
|
||||
315 11187 59617
|
||||
316 11211 59525
|
||||
317 11243 59468
|
||||
318 11274 59413
|
||||
319 11304 59346
|
||||
320 11334 59287
|
||||
321 11362 59209
|
||||
322 11394 59158
|
||||
323 11436 59158
|
||||
324 11477 59153
|
||||
325 11513 59122
|
||||
326 11547 59081
|
||||
327 11572 58991
|
||||
328 11607 58956
|
||||
329 11637 58894
|
||||
330 11668 58833
|
||||
331 11700 58785
|
||||
332 11724 58694
|
||||
333 11757 58648
|
||||
334 11780 58550
|
||||
335 11815 58515
|
||||
336 11844 58451
|
||||
337 11869 58364
|
||||
338 11905 58335
|
||||
339 11941 58302
|
||||
340 11986 58315
|
||||
341 12020 58274
|
||||
342 12066 58292
|
||||
343 12122 58358
|
||||
344 12177 58415
|
||||
345 12221 58422
|
||||
346 12264 58423
|
||||
347 12300 58394
|
||||
348 12324 58304
|
||||
349 12354 58243
|
||||
350 12401 58262
|
||||
351 12438 58232
|
||||
352 12479 58228
|
||||
353 12512 58179
|
||||
354 12541 58116
|
||||
355 12569 58044
|
||||
356 12597 57977
|
||||
357 12628 57920
|
||||
358 12653 57839
|
||||
359 12682 57775
|
||||
360 12720 57752
|
||||
361 12744 57666
|
||||
362 12770 57592
|
||||
363 12811 57583
|
||||
364 12841 57522
|
||||
365 12870 57460
|
||||
366 12897 57386
|
||||
367 12938 57378
|
||||
368 12974 57347
|
||||
369 13009 57313
|
||||
370 13038 57249
|
||||
371 13078 57235
|
||||
372 13117 57216
|
||||
373 13147 57159
|
||||
374 13181 57118
|
||||
375 13205 57036
|
||||
376 13235 56979
|
||||
377 13274 56960
|
||||
378 13306 56911
|
||||
379 13333 56841
|
||||
380 13366 56798
|
||||
381 13396 56741
|
||||
382 13421 56666
|
||||
383 13467 56674
|
||||
384 13508 56664
|
||||
385 13540 56616
|
||||
386 13569 56559
|
||||
387 13598 56496
|
||||
388 13627 56438
|
||||
389 13656 56376
|
||||
390 13685 56317
|
||||
391 13717 56271
|
||||
392 13750 56227
|
||||
393 13771 56135
|
||||
394 13804 56090
|
||||
395 13825 55999
|
||||
396 13858 55957
|
||||
397 13888 55904
|
||||
398 13917 55843
|
||||
399 13953 55812
|
||||
400 13994 55802
|
||||
401 14025 55752
|
||||
402 14048 55670
|
||||
403 14076 55607
|
||||
404 14105 55551
|
||||
405 14142 55526
|
||||
406 14182 55511
|
||||
407 14214 55464
|
||||
408 14240 55394
|
||||
409 14267 55328
|
||||
410 14299 55284
|
||||
411 14324 55213
|
||||
412 14351 55146
|
||||
413 14379 55086
|
||||
414 14410 55036
|
||||
415 14451 55025
|
||||
416 14484 54984
|
||||
417 14513 54929
|
||||
418 14536 54851
|
||||
419 14565 54793
|
||||
420 14587 54710
|
||||
421 14615 54650
|
||||
422 14642 54588
|
||||
423 14666 54515
|
||||
424 14690 54441
|
||||
425 14719 54384
|
||||
426 14739 54297
|
||||
427 14772 54257
|
||||
428 14790 54164
|
||||
429 14824 54125
|
||||
430 14844 54039
|
||||
431 14876 53995
|
||||
432 14906 53946
|
||||
433 14938 53902
|
||||
434 14980 53894
|
||||
435 15006 53829
|
||||
436 15033 53770
|
||||
437 15059 53706
|
||||
438 15085 53639
|
||||
439 15110 53574
|
||||
440 15134 53503
|
||||
441 15160 53438
|
||||
442 15184 53369
|
||||
443 15211 53308
|
||||
444 15234 53236
|
||||
445 15266 53193
|
||||
446 15287 53114
|
||||
447 15316 53059
|
||||
448 15336 52978
|
||||
449 15366 52929
|
||||
450 15393 52870
|
||||
451 15429 52843
|
||||
452 15469 52828
|
||||
453 15490 52748
|
||||
454 15523 52712
|
||||
455 15550 52653
|
||||
456 15577 52594
|
||||
457 15604 52536
|
||||
458 15630 52476
|
||||
459 15656 52414
|
||||
460 15682 52353
|
||||
461 15711 52304
|
||||
462 15736 52238
|
||||
463 15765 52188
|
||||
464 15786 52112
|
||||
465 15817 52068
|
||||
466 15839 51996
|
||||
467 15873 51961
|
||||
468 15903 51916
|
||||
469 15935 51873
|
||||
470 15969 51840
|
||||
471 15994 51779
|
||||
472 16022 51726
|
||||
473 16047 51663
|
||||
474 16073 51605
|
||||
475 16099 51546
|
||||
476 16128 51495
|
||||
477 16152 51431
|
||||
478 16176 51367
|
||||
479 16205 51317
|
||||
480 16228 51250
|
||||
481 16255 51194
|
||||
482 16277 51123
|
||||
483 16305 51071
|
||||
484 16328 51005
|
||||
485 16362 50973
|
||||
486 16392 50928
|
||||
487 16426 50894
|
||||
488 16459 50860
|
||||
489 16480 50787
|
||||
490 16510 50743
|
||||
491 16530 50668
|
||||
492 16561 50625
|
||||
493 16585 50562
|
||||
494 16613 50510
|
||||
495 16638 50453
|
||||
496 16663 50393
|
||||
497 16690 50339
|
||||
498 16716 50282
|
||||
499 16740 50222
|
||||
500 16773 50186
|
||||
501 16802 50139
|
||||
502 16836 50107
|
||||
503 16873 50085
|
||||
504 16921 50094
|
||||
505 16989 50163
|
||||
506 17038 50173
|
||||
507 17069 50132
|
||||
508 17110 50121
|
||||
509 17145 50091
|
||||
510 17190 50091
|
||||
511 17219 50044
|
||||
512 17247 49994
|
||||
513 17271 49932
|
||||
514 17298 49878
|
||||
515 17343 49878
|
||||
516 17373 49836
|
||||
517 17417 49831
|
||||
518 17460 49823
|
||||
519 17490 49781
|
||||
520 17518 49731
|
||||
521 17546 49680
|
||||
522 17571 49622
|
||||
523 17600 49577
|
||||
524 17625 49520
|
||||
525 17655 49474
|
||||
526 17679 49414
|
||||
527 17707 49366
|
||||
528 17729 49300
|
||||
529 17758 49254
|
||||
530 17781 49191
|
||||
531 17808 49141
|
||||
532 17829 49071
|
||||
533 17862 49038
|
||||
534 17905 49031
|
||||
535 18028 49241
|
||||
536 18072 49236
|
||||
537 18106 49203
|
||||
538 18135 49157
|
||||
539 18165 49114
|
||||
540 18200 49083
|
||||
541 18223 49022
|
||||
542 18254 48980
|
||||
543 18280 48927
|
||||
544 18307 48876
|
||||
545 18338 48834
|
||||
546 18367 48790
|
||||
547 18411 48783
|
||||
548 18444 48747
|
||||
549 18470 48693
|
||||
550 18503 48660
|
||||
551 18531 48611
|
||||
552 18557 48558
|
||||
553 18584 48508
|
||||
554 18625 48493
|
||||
555 18650 48436
|
||||
556 18677 48388
|
||||
557 18703 48333
|
||||
558 18729 48282
|
||||
559 18756 48231
|
||||
560 18781 48176
|
||||
561 18808 48126
|
||||
562 18834 48074
|
||||
563 18869 48043
|
||||
564 18902 48008
|
||||
565 18930 47960
|
||||
566 18958 47914
|
||||
567 18983 47859
|
||||
568 19016 47824
|
||||
569 19037 47761
|
||||
570 19068 47720
|
||||
571 19090 47660
|
||||
572 19111 47595
|
||||
573 19141 47553
|
||||
574 19164 47494
|
||||
575 19196 47458
|
||||
576 19217 47393
|
||||
577 19249 47358
|
||||
578 19274 47303
|
||||
579 19298 47247
|
||||
580 19324 47195
|
||||
581 19357 47162
|
||||
582 19391 47130
|
||||
583 19427 47103
|
||||
584 19460 47070
|
||||
585 19483 47012
|
||||
586 19511 46967
|
||||
587 19542 46929
|
||||
588 19564 46867
|
||||
589 19597 46833
|
||||
590 19621 46779
|
||||
591 19647 46729
|
||||
592 19670 46672
|
||||
593 19699 46627
|
||||
594 19726 46582
|
||||
595 19753 46532
|
||||
596 19778 46480
|
||||
597 19803 46429
|
||||
598 19830 46381
|
||||
599 19857 46335
|
||||
600 19896 46313
|
||||
601 19925 46271
|
||||
602 19957 46236
|
||||
603 19991 46204
|
||||
604 20019 46159
|
||||
605 20047 46115
|
||||
606 20072 46063
|
||||
607 20098 46015
|
||||
608 20123 45963
|
||||
609 20149 45913
|
||||
610 20176 45867
|
||||
611 20202 45817
|
||||
612 20230 45774
|
||||
613 20253 45719
|
||||
614 20285 45682
|
||||
615 20307 45626
|
||||
616 20338 45589
|
||||
617 20361 45532
|
||||
618 20394 45500
|
||||
619 20423 45459
|
||||
620 20454 45420
|
||||
621 20488 45390
|
||||
622 20510 45333
|
||||
623 20543 45301
|
||||
624 20569 45252
|
||||
625 20594 45201
|
||||
626 20619 45151
|
||||
627 20646 45107
|
||||
628 20675 45066
|
||||
629 20701 45016
|
||||
630 20727 44970
|
||||
631 20752 44919
|
||||
632 20782 44881
|
||||
633 20804 44825
|
||||
634 20837 44791
|
||||
635 20862 44742
|
||||
636 20892 44704
|
||||
637 20931 44683
|
||||
638 20960 44643
|
||||
639 20994 44612
|
||||
640 21022 44570
|
||||
641 21052 44531
|
||||
642 21082 44493
|
||||
643 21107 44443
|
||||
644 21135 44401
|
||||
645 21160 44351
|
||||
646 21185 44302
|
||||
647 21210 44253
|
||||
648 21236 44208
|
||||
649 21262 44161
|
||||
650 21288 44113
|
||||
651 21315 44068
|
||||
652 21343 44027
|
||||
653 21377 43997
|
||||
654 21403 43949
|
||||
655 21440 43926
|
||||
656 21477 43903
|
||||
657 21502 43854
|
||||
658 21533 43819
|
||||
659 21559 43772
|
||||
660 21586 43727
|
||||
661 21611 43680
|
||||
662 21637 43633
|
||||
663 21662 43586
|
||||
664 21688 43539
|
||||
665 21714 43493
|
||||
666 21742 43451
|
||||
667 21771 43413
|
||||
668 21818 43409
|
||||
669 21846 43366
|
||||
670 21888 43352
|
||||
671 21934 43345
|
||||
672 21971 43322
|
||||
673 22019 43320
|
||||
674 22053 43289
|
||||
675 22090 43266
|
||||
676 22141 43269
|
||||
677 22176 43240
|
||||
678 22213 43215
|
||||
679 22239 43171
|
||||
680 22270 43134
|
||||
681 22296 43088
|
||||
682 22321 43041
|
||||
683 22350 43002
|
||||
684 22379 42962
|
||||
685 22419 42944
|
||||
686 22452 42912
|
||||
687 22484 42878
|
||||
688 22511 42834
|
||||
689 22537 42789
|
||||
690 22571 42757
|
||||
691 22598 42714
|
||||
692 22624 42669
|
||||
693 22653 42630
|
||||
694 22680 42586
|
||||
695 22708 42545
|
||||
696 22739 42510
|
||||
697 22761 42457
|
||||
698 22792 42421
|
||||
699 22816 42373
|
||||
700 22845 42333
|
||||
701 22870 42288
|
||||
702 22902 42253
|
||||
703 22942 42234
|
||||
704 22974 42201
|
||||
705 23002 42160
|
||||
706 23033 42124
|
||||
707 23054 42071
|
||||
708 23086 42038
|
||||
709 23115 41999
|
||||
710 23143 41957
|
||||
711 23169 41914
|
||||
712 23195 41868
|
||||
713 23230 41840
|
||||
714 23259 41801
|
||||
715 23287 41760
|
||||
716 23311 41713
|
||||
717 23341 41676
|
||||
718 23372 41641
|
||||
719 23405 41610
|
||||
720 23438 41578
|
||||
721 23483 41566
|
||||
722 23507 41519
|
||||
723 23540 41488
|
||||
724 23566 41444
|
||||
725 23595 41406
|
||||
726 23623 41365
|
||||
727 23648 41320
|
||||
728 23677 41281
|
||||
729 23700 41231
|
||||
730 23728 41192
|
||||
731 23752 41144
|
||||
732 23784 41111
|
||||
733 23807 41063
|
||||
734 23840 41031
|
||||
735 23870 40994
|
||||
736 23908 40972
|
||||
737 23941 40940
|
||||
738 23974 40909
|
||||
739 24006 40875
|
||||
740 24036 40838
|
||||
741 24064 40798
|
||||
742 24092 40759
|
||||
743 24127 40730
|
||||
744 24153 40688
|
||||
745 24179 40644
|
||||
746 24207 40604
|
||||
747 24233 40561
|
||||
748 24261 40522
|
||||
749 24295 40491
|
||||
750 24318 40444
|
||||
751 24349 40410
|
||||
752 24376 40368
|
||||
753 24408 40335
|
||||
754 24442 40306
|
||||
755 24474 40273
|
||||
756 24508 40242
|
||||
757 24548 40222
|
||||
758 24575 40182
|
||||
759 24605 40145
|
||||
760 24632 40104
|
||||
761 24660 40064
|
||||
762 24689 40027
|
||||
763 24714 39982
|
||||
764 24745 39949
|
||||
765 24766 39897
|
||||
766 24797 39863
|
||||
767 24825 39823
|
||||
768 24854 39786
|
||||
769 24880 39744
|
||||
770 24909 39706
|
||||
771 24940 39672
|
||||
772 24970 39635
|
||||
773 25004 39606
|
||||
774 25030 39564
|
||||
775 25056 39522
|
||||
776 25086 39486
|
||||
777 25107 39436
|
||||
778 25139 39403
|
||||
779 25159 39351
|
||||
780 25188 39314
|
||||
781 25214 39272
|
||||
782 25240 39230
|
||||
783 25266 39188
|
||||
784 25288 39141
|
||||
785 25315 39101
|
||||
786 25341 39058
|
||||
787 25367 39016
|
||||
788 25391 38972
|
||||
789 25417 38930
|
||||
790 25448 38895
|
||||
791 25482 38867
|
||||
792 25514 38834
|
||||
793 25542 38795
|
||||
794 25569 38756
|
||||
795 25595 38714
|
||||
796 25618 38669
|
||||
797 25643 38626
|
||||
798 25667 38581
|
||||
799 25695 38543
|
||||
800 25716 38494
|
||||
801 25743 38454
|
||||
802 25770 38415
|
||||
803 25790 38364
|
||||
804 25822 38332
|
||||
805 25843 38284
|
||||
806 25873 38249
|
||||
807 25896 38203
|
||||
808 25925 38167
|
||||
809 25955 38131
|
||||
810 25988 38101
|
||||
811 26028 38080
|
||||
812 26055 38042
|
||||
813 26081 38000
|
||||
814 26108 37961
|
||||
815 26131 37916
|
||||
816 26159 37878
|
||||
817 26188 37841
|
||||
818 26214 37800
|
||||
819 26242 37764
|
||||
820 26272 37728
|
||||
821 26298 37688
|
||||
822 26327 37652
|
||||
823 26359 37619
|
||||
824 26385 37580
|
||||
825 26408 37534
|
||||
826 26444 37507
|
||||
827 26477 37478
|
||||
828 26517 37456
|
||||
829 26539 37411
|
||||
830 26573 37382
|
||||
831 26597 37339
|
||||
832 26623 37298
|
||||
833 26650 37259
|
||||
834 26677 37221
|
||||
835 26704 37182
|
||||
836 26728 37138
|
||||
837 26763 37111
|
||||
838 26791 37073
|
||||
839 26822 37041
|
||||
840 26872 37033
|
||||
841 26924 37029
|
||||
842 26982 37033
|
||||
843 27054 37055
|
||||
844 27097 37038
|
||||
845 27120 36994
|
||||
846 27146 36954
|
||||
847 27180 36925
|
||||
848 27206 36884
|
||||
849 27234 36846
|
||||
850 27260 36807
|
||||
851 27289 36770
|
||||
852 27318 36734
|
||||
853 27347 36698
|
||||
854 27386 36675
|
||||
855 27413 36637
|
||||
856 27439 36596
|
||||
857 27471 36564
|
||||
858 27501 36529
|
||||
859 27535 36500
|
||||
860 27572 36474
|
||||
861 27595 36431
|
||||
862 27627 36398
|
||||
863 27654 36360
|
||||
864 27683 36324
|
||||
865 27711 36287
|
||||
866 27738 36249
|
||||
867 27765 36210
|
||||
868 27794 36175
|
||||
869 27820 36135
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
V27 Rolling Window Feature Calculator
|
||||
======================================
|
||||
Computes rolling averages over 5/10/20 match windows,
|
||||
with home/away splits and trend detection.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import Dict, List, Tuple
|
||||
import math
|
||||
|
||||
|
||||
def calc_rolling_features(
|
||||
team_matches: List[Tuple], # [(mst, is_home, team_goals, opp_goals, opp_id), ...]
|
||||
before_date: int,
|
||||
team_is_home: bool,
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate rolling window features for a team before a given date."""
|
||||
valid = [m for m in team_matches if m[0] < before_date]
|
||||
|
||||
defaults = {
|
||||
"rolling5_goals_avg": 1.3, "rolling5_conceded_avg": 1.2,
|
||||
"rolling10_goals_avg": 1.3, "rolling10_conceded_avg": 1.2,
|
||||
"rolling20_goals_avg": 1.3, "rolling20_conceded_avg": 1.2,
|
||||
"rolling5_clean_sheets": 0.25,
|
||||
"venue_goals_avg": 1.3, "venue_conceded_avg": 1.2,
|
||||
"goal_trend": 0.0,
|
||||
}
|
||||
|
||||
if len(valid) < 3:
|
||||
return defaults
|
||||
|
||||
result = {}
|
||||
|
||||
for window in [5, 10, 20]:
|
||||
recent = valid[-window:] if len(valid) >= window else valid
|
||||
n = len(recent)
|
||||
g_sum = sum(m[2] for m in recent)
|
||||
c_sum = sum(m[3] for m in recent)
|
||||
result[f"rolling{window}_goals_avg"] = g_sum / n
|
||||
result[f"rolling{window}_conceded_avg"] = c_sum / n
|
||||
|
||||
# Clean sheet rate (last 5)
|
||||
r5 = valid[-5:] if len(valid) >= 5 else valid
|
||||
result["rolling5_clean_sheets"] = sum(1 for m in r5 if m[3] == 0) / len(r5)
|
||||
|
||||
# Venue-specific (home-only or away-only)
|
||||
venue_matches = [m for m in valid if m[1] == team_is_home]
|
||||
if venue_matches:
|
||||
vm = venue_matches[-10:] if len(venue_matches) >= 10 else venue_matches
|
||||
result["venue_goals_avg"] = sum(m[2] for m in vm) / len(vm)
|
||||
result["venue_conceded_avg"] = sum(m[3] for m in vm) / len(vm)
|
||||
else:
|
||||
result["venue_goals_avg"] = defaults["venue_goals_avg"]
|
||||
result["venue_conceded_avg"] = defaults["venue_conceded_avg"]
|
||||
|
||||
# Goal trend: compare last 3 vs previous 3
|
||||
if len(valid) >= 6:
|
||||
last3 = sum(m[2] for m in valid[-3:]) / 3
|
||||
prev3 = sum(m[2] for m in valid[-6:-3]) / 3
|
||||
result["goal_trend"] = last3 - prev3
|
||||
else:
|
||||
result["goal_trend"] = 0.0
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def calc_league_quality(
|
||||
all_matches: List[Tuple], # all FT matches in this league
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate league-level quality features."""
|
||||
defaults = {
|
||||
"league_home_win_rate": 0.45,
|
||||
"league_draw_rate": 0.25,
|
||||
"league_btts_rate": 0.50,
|
||||
"league_ou25_rate": 0.50,
|
||||
"league_reliability_score": 0.50,
|
||||
}
|
||||
|
||||
if len(all_matches) < 20:
|
||||
return defaults
|
||||
|
||||
n = len(all_matches)
|
||||
home_wins = sum(1 for m in all_matches if m[2] > m[3])
|
||||
draws = sum(1 for m in all_matches if m[2] == m[3])
|
||||
btts = sum(1 for m in all_matches if m[2] > 0 and m[3] > 0)
|
||||
ou25 = sum(1 for m in all_matches if (m[2] + m[3]) > 2.5)
|
||||
|
||||
hw_rate = home_wins / n
|
||||
dr_rate = draws / n
|
||||
btts_rate = btts / n
|
||||
ou25_rate = ou25 / n
|
||||
|
||||
# Reliability: leagues closer to averages are more predictable
|
||||
predictability = 1.0 - abs(hw_rate - 0.45) - abs(dr_rate - 0.27) * 0.5
|
||||
reliability = max(0.2, min(0.95, predictability))
|
||||
|
||||
return {
|
||||
"league_home_win_rate": round(hw_rate, 4),
|
||||
"league_draw_rate": round(dr_rate, 4),
|
||||
"league_btts_rate": round(btts_rate, 4),
|
||||
"league_ou25_rate": round(ou25_rate, 4),
|
||||
"league_reliability_score": round(reliability, 4),
|
||||
}
|
||||
|
||||
|
||||
def calc_time_features(
|
||||
team_matches: List[Tuple],
|
||||
match_mst: int,
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate time-based features."""
|
||||
from datetime import datetime
|
||||
|
||||
# Days since last match
|
||||
valid = [m for m in team_matches if m[0] < match_mst]
|
||||
if valid:
|
||||
last_mst = valid[-1][0]
|
||||
days_rest = (match_mst - last_mst) / 86_400_000 # ms to days
|
||||
days_rest = min(days_rest, 60.0) # cap at 60 days
|
||||
else:
|
||||
days_rest = 14.0
|
||||
|
||||
# Month and season flags
|
||||
try:
|
||||
dt = datetime.utcfromtimestamp(match_mst / 1000)
|
||||
month = dt.month
|
||||
is_season_start = 1.0 if month in (7, 8) else 0.0
|
||||
is_season_end = 1.0 if month in (5, 6) else 0.0
|
||||
except Exception:
|
||||
month = 6
|
||||
is_season_start = 0.0
|
||||
is_season_end = 0.0
|
||||
|
||||
return {
|
||||
"days_rest": round(days_rest, 2),
|
||||
"match_month": month,
|
||||
"is_season_start": is_season_start,
|
||||
"is_season_end": is_season_end,
|
||||
}
|
||||
|
||||
|
||||
def calc_advanced_h2h(
|
||||
team_matches: List[Tuple],
|
||||
home_id: int,
|
||||
away_id: int,
|
||||
before_date: int,
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate advanced H2H features."""
|
||||
defaults = {
|
||||
"h2h_home_goals_avg": 1.3,
|
||||
"h2h_away_goals_avg": 1.1,
|
||||
"h2h_recent_trend": 0.0,
|
||||
"h2h_venue_advantage": 0.0,
|
||||
}
|
||||
|
||||
h2h = [m for m in team_matches if m[4] == away_id and m[0] < before_date]
|
||||
if not h2h:
|
||||
return defaults
|
||||
|
||||
recent = h2h[-10:]
|
||||
home_goals_total = 0
|
||||
away_goals_total = 0
|
||||
venue_home_wins = 0
|
||||
venue_total = 0
|
||||
|
||||
for mst, is_home, team_goals, opp_goals, _ in recent:
|
||||
if is_home:
|
||||
home_goals_total += team_goals
|
||||
away_goals_total += opp_goals
|
||||
venue_total += 1
|
||||
if team_goals > opp_goals:
|
||||
venue_home_wins += 1
|
||||
else:
|
||||
home_goals_total += opp_goals
|
||||
away_goals_total += team_goals
|
||||
|
||||
n = len(recent)
|
||||
result = {
|
||||
"h2h_home_goals_avg": home_goals_total / n,
|
||||
"h2h_away_goals_avg": away_goals_total / n,
|
||||
"h2h_venue_advantage": venue_home_wins / venue_total if venue_total > 0 else 0.5,
|
||||
}
|
||||
|
||||
# Recent trend: last 3 vs overall
|
||||
if len(h2h) >= 4:
|
||||
last3_pts = sum(
|
||||
1.0 if m[2] > m[3] else (0.5 if m[2] == m[3] else 0.0)
|
||||
for m in h2h[-3:]
|
||||
) / 3
|
||||
overall_pts = sum(
|
||||
1.0 if m[2] > m[3] else (0.5 if m[2] == m[3] else 0.0)
|
||||
for m in h2h
|
||||
) / len(h2h)
|
||||
result["h2h_recent_trend"] = round(last3_pts - overall_pts, 4)
|
||||
else:
|
||||
result["h2h_recent_trend"] = 0.0
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def calc_strength_diff(
|
||||
home_form: Dict[str, float],
|
||||
away_form: Dict[str, float],
|
||||
home_elo: Dict[str, float],
|
||||
away_elo: Dict[str, float],
|
||||
home_momentum: float,
|
||||
away_momentum: float,
|
||||
upset_potential: float,
|
||||
) -> Dict[str, float]:
|
||||
"""Calculate strength differential features."""
|
||||
# Attack vs Defense mismatches
|
||||
h_attack = home_form.get("goals_avg", 1.3)
|
||||
a_defense = away_form.get("conceded_avg", 1.2)
|
||||
a_attack = away_form.get("goals_avg", 1.3)
|
||||
h_defense = home_form.get("conceded_avg", 1.2)
|
||||
|
||||
atk_def_home = h_attack - a_defense # positive = home attack > away defense
|
||||
atk_def_away = a_attack - h_defense
|
||||
|
||||
# XG diff approximation
|
||||
xg_diff = (h_attack + a_defense) / 2 - (a_attack + h_defense) / 2
|
||||
|
||||
# Form × Momentum interaction
|
||||
form_mom = (home_momentum - away_momentum) * (
|
||||
home_form.get("scoring_rate", 0.75) - away_form.get("scoring_rate", 0.75)
|
||||
)
|
||||
|
||||
# ELO-Form consistency
|
||||
elo_diff = home_elo.get("overall", 1500) - away_elo.get("overall", 1500)
|
||||
form_diff = h_attack - a_attack
|
||||
elo_form_consistency = 1.0 if (elo_diff > 0 and form_diff > 0) or (elo_diff < 0 and form_diff < 0) else 0.0
|
||||
|
||||
# Upset × ELO gap
|
||||
elo_gap = abs(elo_diff)
|
||||
upset_x_elo = upset_potential * (elo_gap / 400.0)
|
||||
|
||||
return {
|
||||
"attack_vs_defense_home": round(atk_def_home, 4),
|
||||
"attack_vs_defense_away": round(atk_def_away, 4),
|
||||
"xg_diff": round(xg_diff, 4),
|
||||
"form_momentum_interaction": round(form_mom, 4),
|
||||
"elo_form_consistency": elo_form_consistency,
|
||||
"upset_x_elo_gap": round(upset_x_elo, 4),
|
||||
}
|
||||
+18
-9
@@ -16,6 +16,7 @@ from pydantic import BaseModel
|
||||
|
||||
from models.basketball_v25 import get_basketball_v25_predictor
|
||||
from 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()
|
||||
@@ -36,9 +37,10 @@ class CouponRequest(BaseModel):
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
try:
|
||||
print("🚀 Initializing V25 orchestrator...", flush=True)
|
||||
print("🚀 Initializing V28 orchestrator...", flush=True)
|
||||
get_single_match_orchestrator()
|
||||
print("✅ V25 orchestrator ready", flush=True)
|
||||
get_v26_shadow_engine()
|
||||
print("✅ V28 orchestrator ready", flush=True)
|
||||
except Exception as error:
|
||||
print(f"❌ Failed to initialize orchestrator: {error}", flush=True)
|
||||
import traceback
|
||||
@@ -53,8 +55,8 @@ async def lifespan(_: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="Suggest-Bet AI Engine",
|
||||
version="25.0.0",
|
||||
description="V25 Single Match Prediction Package API",
|
||||
version="28.0.0",
|
||||
description="V28 Single Match Prediction Package API",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
@@ -102,8 +104,9 @@ async def global_exception_handler(_: Request, exc: Exception):
|
||||
@app.get("/")
|
||||
def read_root() -> dict[str, Any]:
|
||||
return {
|
||||
"status": "Suggest-Bet AI Engine v25",
|
||||
"engine": "V25 Single Match Orchestrator",
|
||||
"status": "Suggest-Bet AI Engine v28",
|
||||
"engine": "V28 Single Match Orchestrator",
|
||||
"mode": os.getenv("AI_ENGINE_MODE", "v28"),
|
||||
"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",
|
||||
"engine": "v28.main",
|
||||
"mode": os.getenv("AI_ENGINE_MODE", "v28"),
|
||||
"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", "v28"),
|
||||
}
|
||||
except Exception as error:
|
||||
return {"status": "unhealthy", "ready": False, "error": str(error)}
|
||||
@@ -196,7 +205,7 @@ async def analyze_match_htft_v20plus(match_id: str, timeout_sec: int = 30) -> di
|
||||
key=lambda item: float(item[1]),
|
||||
)
|
||||
return {
|
||||
"engine": "v25.main",
|
||||
"engine": "v28.main",
|
||||
"match_info": result.get("match_info", {}),
|
||||
"timing_ms": int((time.time() - started_at) * 1000),
|
||||
"ht_ft_probs": htft_probs,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"version": "v26.shadow.0",
|
||||
"calibration_version": "v26.shadow.calib.0",
|
||||
"train_rows": 6853,
|
||||
"validation_rows": 1469,
|
||||
"label_priors": {
|
||||
"MS": 0.4404,
|
||||
"OU25": 0.5214,
|
||||
"BTTS": 0.5398,
|
||||
"HT": 0.4275,
|
||||
"HTFT": 0.26,
|
||||
"CARDS": 0.6052
|
||||
},
|
||||
"artifact_path": "/Users/piton/Documents/GitHub/iddaai/iddaai-be/ai-engine/models/v26_shadow/market_profiles.json",
|
||||
"notes": [
|
||||
"v26.shadow runtime currently uses artifact-based calibration and ROI gating",
|
||||
"market profile JSON remains the source of truth for runtime thresholds"
|
||||
]
|
||||
}
|
||||
@@ -17,3 +17,4 @@ pyyaml>=6.0
|
||||
# V2 async database
|
||||
asyncpg>=0.29.0
|
||||
pydantic>=2.5.0
|
||||
pytest>=8.0.0
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
"""
|
||||
Backtest for September 13th (Top Leagues Only)
|
||||
==============================================
|
||||
Simulates the NEW 'Skip Logic' on matches from Sept 13, 2025.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from datetime import datetime
|
||||
|
||||
# Load .env manually to ensure correct DB connection
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, project_root) # Add root to path if needed
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
# ─── Configuration ─────────
|
||||
MIN_CONF_THRESHOLDS = {
|
||||
"MS": 45.0, "DC": 40.0, "OU15": 50.0, "OU25": 45.0,
|
||||
"OU35": 45.0, "BTTS": 45.0, "HT": 40.0,
|
||||
}
|
||||
|
||||
def run_backtest():
|
||||
print("🚀 Backtest: 13 Eylül 2024 - Top Leagues")
|
||||
print("="*60)
|
||||
|
||||
# 1. Load Top Leagues
|
||||
leagues_path = os.path.join(project_root, "top_leagues.json")
|
||||
try:
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
# Ensure they are strings for SQL IN clause
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
print(f"📋 Loaded {len(top_leagues)} top leagues.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error loading top_leagues.json: {e}")
|
||||
return
|
||||
|
||||
# 2. Define Date Range (Sept 13, 2024 UTC)
|
||||
start_dt = datetime(2024, 9, 13, 0, 0, 0)
|
||||
end_dt = datetime(2024, 9, 13, 23, 59, 59)
|
||||
start_ts = int(start_dt.timestamp() * 1000)
|
||||
end_ts = int(end_dt.timestamp() * 1000)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 3. Fetch Matches & Predictions
|
||||
# We need matches that are FT and have a prediction
|
||||
query = """
|
||||
SELECT p.match_id, p.prediction_json,
|
||||
m.score_home, m.score_away, m.status, m.league_id
|
||||
FROM predictions p
|
||||
JOIN matches m ON p.match_id = m.id
|
||||
WHERE m.mst_utc BETWEEN %s AND %s
|
||||
AND m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND p.prediction_json IS NOT NULL
|
||||
"""
|
||||
|
||||
try:
|
||||
cur.execute(query, (start_ts, end_ts, league_ids))
|
||||
rows = cur.fetchall()
|
||||
except Exception as e:
|
||||
print(f"❌ DB Error: {e}")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
print(f"📊 Found {len(rows)} matches with predictions on Sept 13, 2024.")
|
||||
|
||||
if not rows:
|
||||
print("⚠️ No predictions found for this date. The AI Engine might not have processed these historical matches yet.")
|
||||
print("💡 Tip: Run the feeder or AI engine on this date range to generate predictions first.")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
total_bets = 0
|
||||
winning_bets = 0
|
||||
skipped_bets = 0
|
||||
total_profit = 0.0
|
||||
|
||||
for row in rows:
|
||||
data = row['prediction_json']
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
|
||||
home_score = row['score_home'] or 0
|
||||
away_score = row['score_away'] or 0
|
||||
total_goals = home_score + away_score
|
||||
|
||||
# Extract Main Pick
|
||||
main_pick = None
|
||||
main_pick_conf = 0.0
|
||||
main_pick_odds = 0.0
|
||||
|
||||
if "main_pick" in data and isinstance(data["main_pick"], dict):
|
||||
mp = data["main_pick"]
|
||||
main_pick = mp.get("pick")
|
||||
main_pick_conf = mp.get("confidence", 0.0)
|
||||
main_pick_odds = mp.get("odds", 0.0)
|
||||
|
||||
if not main_pick or not main_pick_conf:
|
||||
continue
|
||||
|
||||
# Determine Market Type
|
||||
pick_str = str(main_pick).upper()
|
||||
market_type = "MS"
|
||||
if "1X" in pick_str or "X2" in pick_str or "12" in pick_str: market_type = "DC"
|
||||
elif "ÜST" in pick_str or "ALT" in pick_str or "OVER" in pick_str or "UNDER" in pick_str:
|
||||
if "1.5" in pick_str: market_type = "OU15"
|
||||
elif "3.5" in pick_str: market_type = "OU35"
|
||||
else: market_type = "OU25"
|
||||
elif "VAR" in pick_str or "YOK" in pick_str or "BTTS" in pick_str: market_type = "BTTS"
|
||||
|
||||
threshold = MIN_CONF_THRESHOLDS.get(market_type, 45.0)
|
||||
|
||||
# --- SKIP LOGIC ---
|
||||
# 1. Confidence Gate
|
||||
if main_pick_conf < threshold:
|
||||
skipped_bets += 1
|
||||
continue
|
||||
|
||||
# 2. Value Gate
|
||||
if main_pick_odds > 0:
|
||||
implied_prob = 1.0 / main_pick_odds
|
||||
my_prob = main_pick_conf / 100.0
|
||||
edge = my_prob - implied_prob
|
||||
if edge < -0.03:
|
||||
skipped_bets += 1
|
||||
continue
|
||||
|
||||
# --- BET PLAYED ---
|
||||
total_bets += 1
|
||||
is_won = False
|
||||
|
||||
# Resolve Result
|
||||
if market_type == "MS":
|
||||
if (main_pick == "1" or main_pick == "MS 1") and home_score > away_score: is_won = True
|
||||
elif (main_pick == "X" or main_pick == "MS X") and home_score == away_score: is_won = True
|
||||
elif (main_pick == "2" or main_pick == "MS 2") and away_score > home_score: is_won = True
|
||||
|
||||
elif market_type.startswith("OU"):
|
||||
line = 2.5
|
||||
if "1.5" in pick_str: line = 1.5
|
||||
elif "3.5" in pick_str: line = 3.5
|
||||
is_over = total_goals > line
|
||||
is_under = total_goals < line
|
||||
if ("ÜST" in pick_str or "OVER" in pick_str) and is_over: is_won = True
|
||||
elif ("ALT" in pick_str or "UNDER" in pick_str) and is_under: is_won = True
|
||||
|
||||
elif market_type == "BTTS":
|
||||
if home_score > 0 and away_score > 0:
|
||||
if "VAR" in pick_str: is_won = True
|
||||
else:
|
||||
if "YOK" in pick_str: is_won = True
|
||||
|
||||
elif market_type == "DC":
|
||||
if "1X" in pick_str and home_score >= away_score: is_won = True
|
||||
elif "X2" in pick_str and away_score >= home_score: is_won = True
|
||||
elif "12" in pick_str and home_score != away_score: is_won = True
|
||||
|
||||
if is_won:
|
||||
winning_bets += 1
|
||||
profit = main_pick_odds - 1.0
|
||||
total_profit += profit
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
|
||||
# Report
|
||||
print("\n" + "="*60)
|
||||
print("📈 BACKTEST RESULTS: 13 EYLÜL 2025 (TOP LEAGUES)")
|
||||
print("="*60)
|
||||
print(f"Total Matches Analyzed: {len(rows)}")
|
||||
print(f"🚫 Bets SKIPPED (Low Conf/Bad Value): {skipped_bets}")
|
||||
print(f"✅ Bets PLAYED: {total_bets}")
|
||||
|
||||
if total_bets > 0:
|
||||
win_rate = (winning_bets / total_bets) * 100
|
||||
roi = (total_profit / total_bets) * 100
|
||||
|
||||
print(f"🏆 Winning Bets: {winning_bets}")
|
||||
print(f"💀 Losing Bets: {total_bets - winning_bets}")
|
||||
print("-" * 40)
|
||||
print(f" Win Rate: {win_rate:.2f}%")
|
||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
||||
print(f"📊 ROI: {roi:.2f}%")
|
||||
|
||||
if roi > 0:
|
||||
print("🟢 STRATEGY IS PROFITABLE!")
|
||||
else:
|
||||
print("🔴 STRATEGY IS LOSING")
|
||||
else:
|
||||
print("⚠️ No bets were played. Thresholds might be too high or no suitable matches found.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_backtest()
|
||||
@@ -1,240 +0,0 @@
|
||||
"""
|
||||
Detailed Backtest with 50 Top League Matches
|
||||
============================================
|
||||
Runs AI Engine predictions on 50 real historical matches and shows
|
||||
exactly which predictions were correct and which were skipped.
|
||||
|
||||
Usage:
|
||||
python ai-engine/scripts/backtest_50_detailed.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
# Add paths
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
# 50 Match IDs from the query
|
||||
MATCH_IDS = [
|
||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
||||
]
|
||||
|
||||
def run_detailed_backtest():
|
||||
print("🚀 DETAILED BACKTEST: 50 Top League Matches")
|
||||
print("🧠 Engine: V30 Ensemble (V20+V25) + Skip Logic")
|
||||
print("="*80)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# Fetch match details with odds
|
||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
||||
cur.execute(f"""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away, m.league_id,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
l.name as league_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.id IN ({placeholders})
|
||||
AND m.status = 'FT'
|
||||
ORDER BY m.mst_utc DESC
|
||||
""", MATCH_IDS)
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 Found {len(rows)} matches. Starting AI Analysis...")
|
||||
|
||||
if not rows:
|
||||
print("⚠️ No matches found.")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# Initialize AI Engine
|
||||
try:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
print("✅ AI Engine Loaded.\n")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to load AI Engine: {e}")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# ─── Backtest Loop ───
|
||||
results = []
|
||||
total_skipped = 0
|
||||
total_played = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
MIN_CONF = 45.0
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home_team = row['home_team'] or "Unknown"
|
||||
away_team = row['away_team'] or "Unknown"
|
||||
league = row['league_name'] or "Unknown"
|
||||
home_score = row['score_home'] or 0
|
||||
away_score = row['score_away'] or 0
|
||||
total_goals = home_score + away_score
|
||||
|
||||
print(f"[{i+1}/{len(rows)}] {home_team} vs {away_team} ({league}) ... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
prediction = orchestrator.analyze_match(match_id)
|
||||
|
||||
if not prediction:
|
||||
print("⚠️ No prediction")
|
||||
continue
|
||||
|
||||
# Extract Main Pick
|
||||
main_pick = prediction.get("main_pick") or {}
|
||||
pick_name = main_pick.get("pick", "")
|
||||
confidence = main_pick.get("confidence", 0)
|
||||
odds = main_pick.get("odds", 0)
|
||||
|
||||
# Apply Skip Logic
|
||||
if confidence < MIN_CONF:
|
||||
print(f"🚫 SKIP (Conf {confidence:.0f}%)")
|
||||
total_skipped += 1
|
||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
||||
"conf": confidence, "odds": odds, "result": "SKIPPED", "profit": 0})
|
||||
continue
|
||||
|
||||
if odds > 0:
|
||||
implied_prob = 1.0 / odds
|
||||
my_prob = confidence / 100.0
|
||||
if my_prob - implied_prob < -0.03:
|
||||
print(f"🚫 SKIP (Bad Value)")
|
||||
total_skipped += 1
|
||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
||||
"conf": confidence, "odds": odds, "result": "SKIPPED", "profit": 0})
|
||||
continue
|
||||
|
||||
# Bet Played
|
||||
total_played += 1
|
||||
won = False
|
||||
|
||||
# Resolve
|
||||
pick_clean = str(pick_name).upper()
|
||||
if pick_clean in ["1", "MS 1", "İY 1"] and home_score > away_score: won = True
|
||||
elif pick_clean in ["X", "MS X", "İY X"] and home_score == away_score: won = True
|
||||
elif pick_clean in ["2", "MS 2", "İY 2"] and away_score > home_score: won = True
|
||||
elif pick_clean in ["1X", "X2"] or ("1X" in pick_clean or "X2" in pick_clean):
|
||||
if "1X" in pick_clean and home_score >= away_score: won = True
|
||||
elif "X2" in pick_clean and away_score >= home_score: won = True
|
||||
elif pick_clean in ["12"] and home_score != away_score: won = True
|
||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
elif "3.5" in pick_clean: line = 3.5
|
||||
if total_goals > line: won = True
|
||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
elif "3.5" in pick_clean: line = 3.5
|
||||
if total_goals < line: won = True
|
||||
elif "VAR" in pick_clean and home_score > 0 and away_score > 0: won = True
|
||||
elif "YOK" in pick_clean and (home_score == 0 or away_score == 0): won = True
|
||||
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
print(f"✅ WON ({pick_name} @ {odds:.2f}, +{profit:.2f})")
|
||||
else:
|
||||
profit = -1.0
|
||||
print(f"❌ LOST ({pick_name} @ {odds:.2f})")
|
||||
|
||||
total_profit += profit
|
||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
||||
"conf": confidence, "odds": odds,
|
||||
"result": "WON" if won else "LOST", "profit": profit,
|
||||
"score": f"{home_score}-{away_score}"})
|
||||
|
||||
except Exception as e:
|
||||
print(f"💥 Error: {e}")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# ─── DETAILED REPORT ───
|
||||
print("\n" + "="*80)
|
||||
print("📈 DETAILED BACKTEST RESULTS")
|
||||
print(f"⏱️ Time: {elapsed:.1f}s")
|
||||
print("="*80)
|
||||
print(f"📊 Total Matches: {len(rows)}")
|
||||
print(f"🚫 Skipped: {total_skipped}")
|
||||
print(f"🎲 Played: {total_played}")
|
||||
print(f"✅ Won: {total_won}")
|
||||
print(f"💀 Lost: {total_played - total_won}")
|
||||
print(f"💰 Profit: {total_profit:+.2f} units")
|
||||
|
||||
if total_played > 0:
|
||||
win_rate = (total_won / total_played) * 100
|
||||
roi = (total_profit / total_played) * 100
|
||||
print(f"📊 Win Rate: {win_rate:.1f}%")
|
||||
print(f"📊 ROI: {roi:.1f}%")
|
||||
if roi > 0:
|
||||
print("🟢 STRATEGY IS PROFITABLE!")
|
||||
else:
|
||||
print("🔴 STRATEGY IS LOSING")
|
||||
|
||||
# ─── TABLE OF ALL RESULTS ───
|
||||
print("\n" + "="*80)
|
||||
print("📋 DETAILED MATCH RESULTS")
|
||||
print("="*80)
|
||||
print(f"{'Match':<40} {'Pick':<15} {'Conf':<6} {'Odds':<6} {'Result':<8} {'Score':<6}")
|
||||
print("-"*80)
|
||||
for r in results:
|
||||
match_str = r['match'][:38]
|
||||
pick_str = str(r['pick'])[:13]
|
||||
conf_str = f"{r['conf']:.0f}%"
|
||||
odds_str = f"{r['odds']:.2f}" if r['odds'] > 0 else "N/A"
|
||||
res_str = r['result']
|
||||
score_str = r.get('score', '')
|
||||
|
||||
# Color coding
|
||||
if res_str == "WON": res_display = f"✅ {res_str}"
|
||||
elif res_str == "LOST": res_display = f"❌ {res_str}"
|
||||
else: res_display = f"🚫 {res_str}"
|
||||
|
||||
print(f"{match_str:<40} {pick_str:<15} {conf_str:<6} {odds_str:<6} {res_display:<12} {score_str:<6}")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_detailed_backtest()
|
||||
@@ -1,191 +0,0 @@
|
||||
"""
|
||||
Adaptive 500 Match Backtest
|
||||
=============================
|
||||
Skips NO match unless NO odds exist.
|
||||
Evaluates ALL available markets (MS, OU, BTTS) and picks the BEST value bet.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_adaptive_backtest():
|
||||
print("🔄 ADAPTIVE 500 MATCH BACKTEST")
|
||||
print("="*60)
|
||||
|
||||
# 1. Load Top Leagues
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 2. Fetch 500 Finished Matches with Odds
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away, m.league_id,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 500
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 Found {len(rows)} matches. Analyzing...\n")
|
||||
|
||||
if not rows:
|
||||
print("⚠️ No matches found.")
|
||||
return
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Error: {e}")
|
||||
return
|
||||
|
||||
# Stats
|
||||
total_evaluated = 0
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
skipped_count = 0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
total_evaluated += 1
|
||||
# print(f"[{i+1}] {home} vs {away} ... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred:
|
||||
# print("⚠️ No Data")
|
||||
continue
|
||||
|
||||
# ─── ADAPTIVE PICKING ───
|
||||
# Check ALL recommendations (Expert or Standard) to find the BEST option
|
||||
candidates = []
|
||||
|
||||
# Add main picks
|
||||
if pred.get("expert_recommendation"):
|
||||
rec = pred["expert_recommendation"]
|
||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
||||
if rec.get("safe_alternative"): candidates.append(rec["safe_alternative"])
|
||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
||||
elif pred.get("main_pick"):
|
||||
candidates.append(pred["main_pick"])
|
||||
|
||||
best_bet = None
|
||||
for c in candidates:
|
||||
if not c: continue
|
||||
conf = c.get("confidence", 0)
|
||||
odds = c.get("odds", 0)
|
||||
pick = c.get("pick")
|
||||
|
||||
# Flexible Criteria:
|
||||
# 1. Confidence > 60%
|
||||
# 2. Odds > 1.10 (Not "free" odds like 1.00)
|
||||
# 3. Edge > -2% (Slightly tolerant)
|
||||
if conf >= 60 and odds > 1.10:
|
||||
implied = 1.0 / odds
|
||||
edge = ((conf/100) - implied) * 100
|
||||
|
||||
# Prioritize positive edge, but accept small negative if confidence is high
|
||||
if edge > -2.0:
|
||||
if best_bet is None or (conf > best_bet.get("confidence", 0)):
|
||||
best_bet = c
|
||||
|
||||
if best_bet:
|
||||
pick = str(best_bet.get("pick")).upper()
|
||||
conf = best_bet.get("confidence")
|
||||
odds = best_bet.get("odds")
|
||||
|
||||
# Resolution Logic
|
||||
won = False
|
||||
if pick in ["1", "MS 1", "İY 1"] and h_score > a_score: won = True
|
||||
elif pick in ["X", "MS X", "İY X"] and h_score == a_score: won = True
|
||||
elif pick in ["2", "MS 2", "İY 2"] and a_score > h_score: won = True
|
||||
elif pick in ["1X", "X2"]:
|
||||
if "1X" in pick and h_score >= a_score: won = True
|
||||
elif "X2" in pick and a_score >= h_score: won = True
|
||||
elif pick == "12" and h_score != a_score: won = True
|
||||
elif "ÜST" in pick or "OVER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick or "UNDER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
total_bet += 1
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
# print(f"✅ WON (+{profit:.2f}) | {pick}")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
# print(f"❌ LOST ({pick} @ {odds:.2f})")
|
||||
else:
|
||||
skipped_count += 1
|
||||
# print(f"🚫 SKIP (No Value)")
|
||||
|
||||
except Exception as e:
|
||||
# print(f"💥 Error: {e}")
|
||||
pass
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🔄 ADAPTIVE BACKTEST RESULTS (500 Matches)")
|
||||
print("="*60)
|
||||
print(f"📊 Evaluated: {total_evaluated}")
|
||||
print(f"🎲 Played: {total_bet}")
|
||||
print(f"🚫 Skipped: {skipped_count}")
|
||||
print(f"✅ Won: {total_won}")
|
||||
|
||||
if total_bet > 0:
|
||||
win_rate = (total_won / total_bet) * 100
|
||||
roi = (total_profit / total_bet) * 100
|
||||
print(f"📈 Win Rate: {win_rate:.2f}%")
|
||||
print(f"💰 Total Profit: {total_profit:.2f} Units")
|
||||
print(f"📊 ROI: {roi:.2f}%")
|
||||
if total_profit > 0: print("🟢 KARLI STRATEJİ")
|
||||
else: print("🔴 ZARARDA")
|
||||
else:
|
||||
print("⚠️ Hiç bahis oynanmadı. Veri kalitesi çok düşük.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_adaptive_backtest()
|
||||
@@ -1,145 +0,0 @@
|
||||
"""
|
||||
Diagnostic Backtest - Hangi Pazar Kanıyor?
|
||||
===========================================
|
||||
Analyses the 500 matches to see WHICH markets are losing money.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from collections import defaultdict
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_diagnostic():
|
||||
print("🔍 TANI BACKTESTİ: NEREDE KAYBETTİK?")
|
||||
print("="*60)
|
||||
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away, m.league_id,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 500
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç analiz ediliyor...\n")
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
# Market Stats: { "MS": {"won": 10, "lost": 20, "profit": -5.0}, ... }
|
||||
market_stats = defaultdict(lambda: {"won": 0, "lost": 0, "profit": 0.0, "total": 0})
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred: continue
|
||||
|
||||
candidates = []
|
||||
if pred.get("expert_recommendation"):
|
||||
rec = pred["expert_recommendation"]
|
||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
||||
elif pred.get("main_pick"):
|
||||
candidates.append(pred["main_pick"])
|
||||
|
||||
played_this = False
|
||||
for c in candidates:
|
||||
if not c: continue
|
||||
conf = c.get("confidence", 0)
|
||||
odds = c.get("odds", 0)
|
||||
pick = str(c.get("pick")).upper()
|
||||
market_type = c.get("market_type", "Unknown")
|
||||
|
||||
# Criteria
|
||||
if conf >= 60 and odds > 1.10:
|
||||
implied = 1.0 / odds
|
||||
edge = ((conf/100) - implied) * 100
|
||||
if edge > -2.0:
|
||||
# Resolve
|
||||
won = False
|
||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif pick in ["1X", "X2"]:
|
||||
if "1X" in pick and h_score >= a_score: won = True
|
||||
elif "X2" in pick and a_score >= h_score: won = True
|
||||
elif pick == "12" and h_score != a_score: won = True
|
||||
elif "ÜST" in pick or "OVER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick or "UNDER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
market_stats[market_type]["total"] += 1
|
||||
if won:
|
||||
market_stats[market_type]["won"] += 1
|
||||
market_stats[market_type]["profit"] += (odds - 1.0)
|
||||
else:
|
||||
market_stats[market_type]["lost"] += 1
|
||||
market_stats[market_type]["profit"] -= 1.0
|
||||
|
||||
played_this = True
|
||||
break # Only one bet per match
|
||||
|
||||
except: pass
|
||||
|
||||
# Print Results
|
||||
print("\n" + "="*60)
|
||||
print("📊 PAZAR BAZLI KAR/ZARAR TABLOSU")
|
||||
print("="*60)
|
||||
print(f"{'Market':<15} {'Oynanan':<10} {'Kazanılan':<10} {'Win%':<8} {'Kâr':<10}")
|
||||
print("-" * 60)
|
||||
|
||||
for mkt, stats in sorted(market_stats.items(), key=lambda x: x[1]["profit"], reverse=True):
|
||||
wr = (stats["won"] / stats["total"] * 100) if stats["total"] > 0 else 0
|
||||
print(f"{mkt:<15} {stats['total']:<10} {stats['won']:<10} {wr:.1f}% {stats['profit']:+.2f} Units")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_diagnostic()
|
||||
@@ -1,223 +0,0 @@
|
||||
"""
|
||||
Real AI Engine Backtest Script
|
||||
==============================
|
||||
Uses the ACTUAL models (V20/V25 Ensemble) to predict historical matches.
|
||||
|
||||
Usage:
|
||||
python ai-engine/scripts/backtest_real.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from datetime import datetime
|
||||
|
||||
# Add paths
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
|
||||
# Fix for Windows path issues in scripts
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR) # One level up if inside scripts folder
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator, MatchData
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_backtest():
|
||||
print("🚀 REAL AI BACKTEST: Sept 13, 2024 - Top Leagues")
|
||||
print("🧠 Engine: V30 Ensemble (V20+V25)")
|
||||
print("="*60)
|
||||
|
||||
# Load Top Leagues
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
try:
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
print(f"📋 Loaded {len(top_leagues)} top leagues.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error loading top_leagues.json: {e}")
|
||||
return
|
||||
|
||||
# Date Range (Sept 13, 2024)
|
||||
start_dt = datetime(2024, 9, 13, 0, 0, 0)
|
||||
end_dt = datetime(2024, 9, 13, 23, 59, 59)
|
||||
start_ts = int(start_dt.timestamp() * 1000)
|
||||
end_ts = int(end_dt.timestamp() * 1000)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# Fetch Matches
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.mst_utc, m.league_id, m.status, m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
l.name as league_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.mst_utc BETWEEN %s AND %s
|
||||
AND m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
ORDER BY m.mst_utc ASC
|
||||
LIMIT 20 -- Limit to 20 matches to avoid running for hours on a single backtest
|
||||
""", (start_ts, end_ts, league_ids))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 Found {len(rows)} finished matches. Starting AI Analysis...")
|
||||
|
||||
if not rows:
|
||||
print("⚠️ No matches found for this date.")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# Initialize AI Engine
|
||||
try:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
print("✅ AI Engine (SingleMatchOrchestrator) Loaded.")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to load AI Engine: {e}")
|
||||
print("💡 Make sure models are trained/present in ai-engine/models/")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# ─── Backtest Loop ───
|
||||
total_matches_analyzed = 0
|
||||
bets_skipped = 0
|
||||
bets_played = 0
|
||||
bets_won = 0
|
||||
total_profit = 0.0
|
||||
|
||||
# Thresholds matching the NEW Skip Logic
|
||||
MIN_CONF = 45.0
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home_team = row['home_team']
|
||||
away_team = row['away_team']
|
||||
home_score = row['score_home']
|
||||
away_score = row['score_away']
|
||||
|
||||
print(f"\n[{i+1}/{len(rows)}] Analyzing: {home_team} vs {away_team} ...")
|
||||
|
||||
try:
|
||||
# 1. AI PREDICTION (Actual Model Call)
|
||||
prediction = orchestrator.analyze_match(match_id)
|
||||
|
||||
if not prediction:
|
||||
print(f" ⚠️ AI returned no prediction.")
|
||||
continue
|
||||
|
||||
total_matches_analyzed += 1
|
||||
|
||||
# 2. Extract Main Pick
|
||||
main_pick = prediction.get("main_pick") or {}
|
||||
pick_name = main_pick.get("pick")
|
||||
confidence = main_pick.get("confidence", 0)
|
||||
odds = main_pick.get("odds", 0)
|
||||
|
||||
if not pick_name or not confidence:
|
||||
print(f" ⚠️ No main pick found in prediction.")
|
||||
continue
|
||||
|
||||
print(f" 🤖 Pick: {pick_name} | Conf: {confidence}% | Odds: {odds}")
|
||||
|
||||
# 3. Apply Skip Logic (New Backtest Logic)
|
||||
if confidence < MIN_CONF:
|
||||
print(f" 🚫 SKIPPED (Confidence {confidence}% < {MIN_CONF}%)")
|
||||
bets_skipped += 1
|
||||
continue
|
||||
|
||||
if odds > 0:
|
||||
implied_prob = 1.0 / odds
|
||||
my_prob = confidence / 100.0
|
||||
if my_prob - implied_prob < -0.03: # Negative edge
|
||||
print(f" 🚫 SKIPPED (Negative Edge)")
|
||||
bets_skipped += 1
|
||||
continue
|
||||
|
||||
# 4. Bet Played
|
||||
bets_played += 1
|
||||
print(f" 🎲 BET PLAYED: {pick_name} @ {odds}")
|
||||
|
||||
# 5. Resolve Bet
|
||||
won = False
|
||||
# Basic resolution logic (Need to parse pick_name like "1", "X", "2", "2.5 Üst", etc.)
|
||||
pick_clean = str(pick_name).upper()
|
||||
|
||||
# MS
|
||||
if pick_clean in ["1", "MS 1"] and home_score > away_score: won = True
|
||||
elif pick_clean in ["X", "MS X"] and home_score == away_score: won = True
|
||||
elif pick_clean in ["2", "MS 2"] and away_score > home_score: won = True
|
||||
|
||||
# OU25
|
||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
||||
if (home_score + away_score) > 2.5: won = True
|
||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
||||
if (home_score + away_score) < 2.5: won = True
|
||||
|
||||
# BTTS
|
||||
elif "VAR" in pick_clean and home_score > 0 and away_score > 0: won = True
|
||||
elif "YOK" in pick_clean and (home_score == 0 or away_score == 0): won = True
|
||||
|
||||
if won:
|
||||
bets_won += 1
|
||||
profit = odds - 1.0
|
||||
print(f" ✅ WON! (+{profit:.2f} units)")
|
||||
else:
|
||||
profit = -1.0
|
||||
print(f" ❌ LOST! (-1.00 units)")
|
||||
|
||||
total_profit += profit
|
||||
|
||||
except Exception as e:
|
||||
print(f" 💥 Error during analysis: {e}")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# ─── FINAL REPORT ───
|
||||
print("\n" + "="*60)
|
||||
print("📈 REAL AI BACKTEST RESULTS")
|
||||
print(f"🕒 Time taken: {elapsed:.1f} seconds")
|
||||
print("="*60)
|
||||
print(f"📊 Matches Analyzed: {total_matches_analyzed}")
|
||||
print(f"🚫 Bets SKIPPED: {bets_skipped}")
|
||||
print(f"✅ Bets PLAYED: {bets_played}")
|
||||
|
||||
if bets_played > 0:
|
||||
win_rate = (bets_won / bets_played) * 100
|
||||
roi = (total_profit / bets_played) * 100
|
||||
yield_val = total_profit # Net Units
|
||||
|
||||
print(f"🏆 Bets Won: {bets_won}")
|
||||
print(f"💀 Bets Lost: {bets_played - bets_won}")
|
||||
print("-" * 40)
|
||||
print(f" Win Rate: {win_rate:.2f}%")
|
||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
||||
print(f"📊 ROI: {roi:.2f}%")
|
||||
|
||||
if roi > 0:
|
||||
print("🟢 STRATEGY IS PROFITABLE!")
|
||||
else:
|
||||
print("🔴 STRATEGY IS LOSING")
|
||||
else:
|
||||
print("⚠️ No bets were played. All were skipped or failed.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_backtest()
|
||||
@@ -1,231 +0,0 @@
|
||||
"""
|
||||
Backtest ROI Engine
|
||||
===================
|
||||
Simulates the NEW "Skip Logic" on historical predictions.
|
||||
Answers: "What if we only played the bets the model was confident about?"
|
||||
|
||||
Usage:
|
||||
python ai-engine/scripts/backtest_roi.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from typing import Dict, List, Any
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load .env from project root (2 levels up from this script)
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
load_dotenv(os.path.join(project_root, ".env"))
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
"""Return a psycopg2-compatible DSN from DATABASE_URL."""
|
||||
# HARDCODED FOR BACKTEST (Bypassing dotenv issues)
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
# ─── Configuration (Matching the NEW BetRecommender Logic) ─────────
|
||||
# Minimum confidence to even consider a bet (Hard Gate)
|
||||
MIN_CONF_THRESHOLDS = {
|
||||
"MS": 45.0,
|
||||
"DC": 40.0,
|
||||
"OU15": 50.0,
|
||||
"OU25": 45.0,
|
||||
"OU35": 45.0,
|
||||
"BTTS": 45.0,
|
||||
"HT": 40.0,
|
||||
}
|
||||
|
||||
def get_market_type_from_key(key: str) -> str:
|
||||
"""Map prediction keys to market types for thresholding."""
|
||||
if key.startswith("ms_") or key in ["1", "X", "2"]: return "MS"
|
||||
if key.startswith("dc_") or key in ["1X", "X2", "12"]: return "DC"
|
||||
if key.startswith("ou15_") or key.startswith("1.5"): return "OU15"
|
||||
if key.startswith("ou25_") or key.startswith("2.5"): return "OU25"
|
||||
if key.startswith("ou35_") or key.startswith("3.5"): return "OU35"
|
||||
if key.startswith("btts_") or key in ["Var", "Yok"]: return "BTTS"
|
||||
if key.startswith("ht_") or key.startswith("İY"): return "HT"
|
||||
return "MS"
|
||||
|
||||
def simulate_backtest():
|
||||
print("🚀 Starting Backtest with NEW 'Skip Logic'...")
|
||||
print("="*60)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 1. Fetch PREDICTIONS that have a confidence score
|
||||
# We limit to last 1000 finished matches to keep it fast but representative
|
||||
cur.execute("""
|
||||
SELECT p.match_id, p.prediction_json,
|
||||
m.score_home, m.score_away, m.status
|
||||
FROM predictions p
|
||||
JOIN matches m ON p.match_id = m.id
|
||||
WHERE m.status = 'FT'
|
||||
AND p.prediction_json IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 2000
|
||||
""")
|
||||
predictions = cur.fetchall()
|
||||
|
||||
print(f"📊 Loaded {len(predictions)} historical predictions.")
|
||||
|
||||
total_bets = 0
|
||||
winning_bets = 0
|
||||
skipped_bets = 0
|
||||
total_profit = 0.0 # Assuming unit stake of 1.0
|
||||
|
||||
# 2. Process each prediction
|
||||
for pred_row in predictions:
|
||||
match_id = pred_row['match_id']
|
||||
data = pred_row['prediction_json']
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
|
||||
# Real result
|
||||
home_score = pred_row['score_home'] or 0
|
||||
away_score = pred_row['score_away'] or 0
|
||||
total_goals = home_score + away_score
|
||||
|
||||
# Extract prediction details from the JSON structure
|
||||
# The structure varies, but usually contains 'main_pick', 'bet_summary', or 'market_board'
|
||||
|
||||
# Try to get the main pick recommendation
|
||||
main_pick = None
|
||||
main_pick_conf = 0.0
|
||||
main_pick_odds = 0.0
|
||||
|
||||
# Navigate the V20+ JSON structure
|
||||
market_board = data.get("market_board", {})
|
||||
|
||||
# Check Main Pick
|
||||
if "main_pick" in data:
|
||||
mp = data["main_pick"]
|
||||
if isinstance(mp, dict):
|
||||
main_pick = mp.get("pick")
|
||||
main_pick_conf = mp.get("confidence", 0.0)
|
||||
main_pick_odds = mp.get("odds", 0.0)
|
||||
|
||||
# If no main pick, try bet_summary
|
||||
if not main_pick and "bet_summary" in data:
|
||||
summary = data["bet_summary"]
|
||||
if isinstance(summary, list) and len(summary) > 0:
|
||||
# Take the highest confidence one
|
||||
best = max(summary, key=lambda x: x.get("confidence", 0))
|
||||
main_pick = best.get("pick")
|
||||
main_pick_conf = best.get("confidence", 0.0)
|
||||
main_pick_odds = best.get("odds", 0.0)
|
||||
|
||||
if not main_pick or not main_pick_conf:
|
||||
continue
|
||||
|
||||
# ─── NEW LOGIC: APPLY FILTERS ───
|
||||
# 1. Determine Market Type
|
||||
# Simple heuristic based on pick string
|
||||
pick_str = str(main_pick).upper()
|
||||
market_type = "MS"
|
||||
if "1X" in pick_str or "X2" in pick_str or "12" in pick_str: market_type = "DC"
|
||||
elif "ÜST" in pick_str or "ALT" in pick_str or "OVER" in pick_str or "UNDER" in pick_str:
|
||||
if "1.5" in pick_str: market_type = "OU15"
|
||||
elif "3.5" in pick_str: market_type = "OU35"
|
||||
else: market_type = "OU25"
|
||||
elif "VAR" in pick_str or "YOK" in pick_str or "BTTS" in pick_str: market_type = "BTTS"
|
||||
|
||||
threshold = MIN_CONF_THRESHOLDS.get(market_type, 45.0)
|
||||
|
||||
# 2. Check Confidence Gate
|
||||
if main_pick_conf < threshold:
|
||||
skipped_bets += 1
|
||||
continue
|
||||
|
||||
# 3. Check Value Gate (Edge)
|
||||
if main_pick_odds > 0:
|
||||
implied_prob = 1.0 / main_pick_odds
|
||||
my_prob = main_pick_conf / 100.0
|
||||
edge = my_prob - implied_prob
|
||||
if edge < -0.03: # Negative value
|
||||
skipped_bets += 1
|
||||
continue
|
||||
|
||||
# ─── BET IS PLAYED ───
|
||||
total_bets += 1
|
||||
|
||||
# Determine if WON
|
||||
is_won = False
|
||||
|
||||
# Resolve MS (1, X, 2)
|
||||
if market_type == "MS":
|
||||
if main_pick == "1" and home_score > away_score: is_won = True
|
||||
elif main_pick == "X" and home_score == away_score: is_won = True
|
||||
elif main_pick == "2" and away_score > home_score: is_won = True
|
||||
elif main_pick == "MS 1" and home_score > away_score: is_won = True
|
||||
elif main_pick == "MS X" and home_score == away_score: is_won = True
|
||||
elif main_pick == "MS 2" and away_score > home_score: is_won = True
|
||||
|
||||
# Resolve OU (Over/Under)
|
||||
elif market_type.startswith("OU"):
|
||||
line = 2.5
|
||||
if "1.5" in pick_str: line = 1.5
|
||||
elif "3.5" in pick_str: line = 3.5
|
||||
|
||||
is_over = total_goals > line
|
||||
is_under = total_goals < line # Simplification (usually line is X.5 so no draw)
|
||||
|
||||
if "ÜST" in pick_str or "OVER" in pick_str:
|
||||
if is_over: is_won = True
|
||||
elif "ALT" in pick_str or "UNDER" in pick_str:
|
||||
if is_under: is_won = True
|
||||
|
||||
# Resolve BTTS
|
||||
elif market_type == "BTTS":
|
||||
if home_score > 0 and away_score > 0:
|
||||
if "VAR" in pick_str: is_won = True
|
||||
else:
|
||||
if "YOK" in pick_str: is_won = True
|
||||
|
||||
# Resolve DC (Double Chance) - Simplified
|
||||
elif market_type == "DC":
|
||||
if "1X" in pick_str and (home_score >= away_score): is_won = True
|
||||
elif "X2" in pick_str and (away_score >= home_score): is_won = True
|
||||
elif "12" in pick_str and (home_score != away_score): is_won = True
|
||||
|
||||
if is_won:
|
||||
winning_bets += 1
|
||||
profit = main_pick_odds - 1.0
|
||||
total_profit += profit
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
|
||||
# ─── REPORT ───
|
||||
print("\n" + "="*60)
|
||||
print("📈 BACKTEST RESULTS (With NEW Skip Logic)")
|
||||
print("="*60)
|
||||
print(f"Total Historical Matches Analyzed: {len(predictions)}")
|
||||
print(f"🚫 Bets SKIPPED (Low Conf/Bad Value): {skipped_bets}")
|
||||
print(f"✅ Bets PLAYED: {total_bets}")
|
||||
|
||||
if total_bets > 0:
|
||||
win_rate = (winning_bets / total_bets) * 100
|
||||
roi = (total_profit / total_bets) * 100
|
||||
|
||||
print(f"🏆 Winning Bets: {winning_bets}")
|
||||
print(f"💀 Losing Bets: {total_bets - winning_bets}")
|
||||
print("-" * 40)
|
||||
print(f" Win Rate: {win_rate:.2f}%")
|
||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
||||
print(f"📊 ROI: {roi:.2f}%")
|
||||
|
||||
if roi > 0:
|
||||
print("🟢 STRATEGY IS PROFITABLE!")
|
||||
else:
|
||||
print("🔴 STRATEGY IS LOSING (Adjust thresholds!)")
|
||||
else:
|
||||
print("⚠️ No bets were played. Thresholds might be too high.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
simulate_backtest()
|
||||
@@ -1,164 +0,0 @@
|
||||
"""
|
||||
SNIPER Backtest
|
||||
===============
|
||||
Sadece en yüksek güvenilirlik ve değere sahip bahisleri oynar.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from datetime import datetime
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
MATCH_IDS = [
|
||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
||||
]
|
||||
|
||||
def run_sniper_backtest():
|
||||
print("🎯 SNIPER BACKTEST: SADECE NET OLANLAR")
|
||||
print("="*60)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
||||
cur.execute(f"""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
l.name as league_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.id IN ({placeholders}) AND m.status = 'FT'
|
||||
""", MATCH_IDS)
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 Analiz edilecek {len(rows)} maç var.\n")
|
||||
|
||||
try:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
print(f"[{i+1}/{len(rows)}] {home} vs {away} ... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred:
|
||||
print("⚠️ Veri Yok")
|
||||
continue
|
||||
|
||||
pick_data = pred.get("expert_recommendation", {}).get("main_pick") or pred.get("main_pick", {})
|
||||
pick = pick_data.get("pick") or pick_data.get("market_type")
|
||||
conf = pick_data.get("confidence", 0)
|
||||
odds = pick_data.get("odds", 0)
|
||||
|
||||
# SNIPER FİLTRELERİ
|
||||
if conf < 75:
|
||||
print(f"🚫 PASS (Conf: {conf:.0f}%)")
|
||||
continue
|
||||
if odds < 1.35:
|
||||
print(f"🚫 PASS (Odds: {odds:.2f} çok düşük)")
|
||||
continue
|
||||
|
||||
# Value Control
|
||||
implied = 1.0 / odds
|
||||
if (conf/100) < implied:
|
||||
print(f"🚫 PASS (Negatif Value)")
|
||||
continue
|
||||
|
||||
# OYNA
|
||||
total_bet += 1
|
||||
won = False
|
||||
pick_clean = str(pick).upper()
|
||||
|
||||
if pick_clean in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick_clean in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick_clean in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
elif "3.5" in pick_clean: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
elif "3.5" in pick_clean: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick_clean and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick_clean and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
print(f"✅ WON! (+{profit:.2f})")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
print(f"❌ LOST! ({pick} @ {odds:.2f})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"💥 Hata: {e}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🎯 SNIPER SONUÇLARI")
|
||||
print("="*60)
|
||||
print(f"Oynanan: {total_bet}")
|
||||
print(f"Kazanılan: {total_won}")
|
||||
print(f"Kazanma Oranı: %{(total_won/total_bet)*100:.1f}" if total_bet > 0 else "Kazanma Oranı: N/A")
|
||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
||||
|
||||
if total_profit > 0:
|
||||
print("🟢 PARA KAZANDIK!")
|
||||
else:
|
||||
print("🔴 PARA KAYBETTİK!")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_sniper_backtest()
|
||||
@@ -1,162 +0,0 @@
|
||||
"""
|
||||
Strict Sniper Backtest (Calibrated)
|
||||
===================================
|
||||
Sadece Güven > %75 ve Oran > 1.30 olan bahisleri oynar.
|
||||
Modelin şişirilmiş özgüvenini elemek için yapıldı.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_strict_backtest():
|
||||
print("🎯 STRICT SNIPER BACKTEST (Conf > 75%)")
|
||||
print("="*60)
|
||||
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 500
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç taranıyor. Sadece NET OLANLAR oynanacak...\n")
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred: continue
|
||||
|
||||
# Check all picks for a HIGH CONFIDENCE bet
|
||||
candidates = []
|
||||
if pred.get("expert_recommendation"):
|
||||
rec = pred["expert_recommendation"]
|
||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
||||
elif pred.get("main_pick"):
|
||||
candidates.append(pred["main_pick"])
|
||||
|
||||
best_bet = None
|
||||
for c in candidates:
|
||||
if not c: continue
|
||||
# Access attributes safely (Dict or Object)
|
||||
conf = c.get("confidence", 0) if isinstance(c, dict) else getattr(c, 'confidence', 0)
|
||||
odds = c.get("odds", 0) if isinstance(c, dict) else getattr(c, 'odds', 0)
|
||||
pick = c.get("pick", "") if isinstance(c, dict) else getattr(c, 'pick', "")
|
||||
|
||||
# STRICT CRITERIA
|
||||
if conf >= 75.0 and odds >= 1.30:
|
||||
# Check Value (Edge)
|
||||
implied = 1.0 / odds
|
||||
edge = ((conf/100) - implied) * 100
|
||||
if edge > -5.0: # Tolerant edge
|
||||
if best_bet is None or (conf > (best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0))):
|
||||
best_bet = c
|
||||
|
||||
if best_bet:
|
||||
pick = str(best_bet.get("pick") if isinstance(best_bet, dict) else getattr(best_bet, 'pick', "")).upper()
|
||||
conf = best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0)
|
||||
odds = best_bet.get("odds", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'odds', 0)
|
||||
|
||||
# Resolution
|
||||
won = False
|
||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif pick in ["1X", "X2"]:
|
||||
if "1X" in pick and h_score >= a_score: won = True
|
||||
elif "X2" in pick and a_score >= h_score: won = True
|
||||
elif "ÜST" in pick or "OVER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick or "UNDER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
total_bet += 1
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({conf:.0f}%) -> WON (+{profit:.2f})")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({conf:.0f}%) -> LOST")
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🎯 STRICT SNIPER SONUÇLARI")
|
||||
print("="*60)
|
||||
print(f"Oynanan Bahis: {total_bet}")
|
||||
print(f"Kazanılan: {total_won}")
|
||||
|
||||
if total_bet > 0:
|
||||
win_rate = (total_won / total_bet) * 100
|
||||
roi = (total_profit / total_bet) * 100
|
||||
print(f"Kazanma Oranı: %{win_rate:.2f}")
|
||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
||||
else: print("🔴 PARA KAYBETTİK!")
|
||||
else:
|
||||
print("⚠️ Yeteri kadar NET maç bulunamadı.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_strict_backtest()
|
||||
@@ -1,230 +0,0 @@
|
||||
"""
|
||||
Backtest the live V2 predictor stack against recent finished football matches.
|
||||
|
||||
This script uses the same path as production:
|
||||
database -> feature extractor -> betting predictor -> quant ranking.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from core.quant import MarketPick, analyze_market
|
||||
from data.database import dispose_engine, get_session
|
||||
from features.extractor import extract_features
|
||||
from models.betting_engine import get_predictor
|
||||
|
||||
|
||||
@dataclass
|
||||
class BacktestStats:
|
||||
sampled_matches: int = 0
|
||||
analyzed_matches: int = 0
|
||||
skipped_matches: int = 0
|
||||
ms_correct: int = 0
|
||||
ou25_correct: int = 0
|
||||
btts_correct: int = 0
|
||||
main_pick_count: int = 0
|
||||
main_pick_correct: int = 0
|
||||
playable_pick_count: int = 0
|
||||
playable_pick_correct: int = 0
|
||||
playable_units_staked: float = 0.0
|
||||
playable_units_profit: float = 0.0
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--limit", type=int, default=50)
|
||||
parser.add_argument("--days", type=int, default=45)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _actual_ms(score_home: int, score_away: int) -> str:
|
||||
if score_home > score_away:
|
||||
return "1"
|
||||
if score_home < score_away:
|
||||
return "2"
|
||||
return "X"
|
||||
|
||||
|
||||
def _actual_ou25(score_home: int, score_away: int) -> str:
|
||||
return "Over" if (score_home + score_away) > 2 else "Under"
|
||||
|
||||
|
||||
def _actual_btts(score_home: int, score_away: int) -> str:
|
||||
return "Yes" if score_home > 0 and score_away > 0 else "No"
|
||||
|
||||
|
||||
def _odds_map_from_features(feats) -> dict[str, dict[str, float]]:
|
||||
return {
|
||||
"MS": {"1": feats.odds_home, "X": feats.odds_draw, "2": feats.odds_away},
|
||||
"OU25": {"Under": feats.odds_under25, "Over": feats.odds_over25},
|
||||
"BTTS": {"No": feats.odds_btts_no, "Yes": feats.odds_btts_yes},
|
||||
}
|
||||
|
||||
|
||||
def _best_pick(feats, all_probs: dict[str, dict[str, float]]) -> MarketPick | None:
|
||||
odds_map = _odds_map_from_features(feats)
|
||||
picks = [
|
||||
analyze_market("MS", all_probs["MS"], odds_map["MS"], feats.data_quality_score),
|
||||
analyze_market("OU25", all_probs["OU25"], odds_map["OU25"], feats.data_quality_score),
|
||||
analyze_market("BTTS", all_probs["BTTS"], odds_map["BTTS"], feats.data_quality_score),
|
||||
]
|
||||
ranked = sorted(
|
||||
[pick for pick in picks if pick.pick],
|
||||
key=lambda pick: pick.play_score,
|
||||
reverse=True,
|
||||
)
|
||||
return ranked[0] if ranked else None
|
||||
|
||||
|
||||
def _pick_won(pick: MarketPick, actuals: dict[str, str]) -> bool:
|
||||
return actuals.get(pick.market) == pick.pick
|
||||
|
||||
|
||||
async def _load_match_rows(limit: int, days: int) -> list[dict[str, object]]:
|
||||
min_mst_utc = days * 86400000
|
||||
query = text("""
|
||||
SELECT
|
||||
m.id,
|
||||
m.match_name,
|
||||
m.score_home,
|
||||
m.score_away,
|
||||
m.mst_utc
|
||||
FROM matches m
|
||||
WHERE m.sport = 'football'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
AND m.mst_utc >= (
|
||||
EXTRACT(EPOCH FROM NOW()) * 1000 - :min_mst_utc
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM odd_categories oc
|
||||
WHERE oc.match_id = m.id
|
||||
AND oc.name IN ('Maç Sonucu', '2,5 Alt/Üst', 'Karşılıklı Gol')
|
||||
)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT :limit
|
||||
""")
|
||||
async with get_session() as session:
|
||||
result = await session.execute(
|
||||
query,
|
||||
{"limit": limit, "min_mst_utc": min_mst_utc},
|
||||
)
|
||||
rows = result.mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def _run(limit: int, days: int) -> BacktestStats:
|
||||
stats = BacktestStats()
|
||||
predictor = get_predictor()
|
||||
rows = await _load_match_rows(limit, days)
|
||||
stats.sampled_matches = len(rows)
|
||||
|
||||
async with get_session() as session:
|
||||
for row in rows:
|
||||
match_id = str(row["id"])
|
||||
score_home = int(row["score_home"])
|
||||
score_away = int(row["score_away"])
|
||||
feats = await extract_features(session, match_id)
|
||||
|
||||
if feats is None:
|
||||
stats.skipped_matches += 1
|
||||
continue
|
||||
|
||||
if feats.data_quality_score <= 0.0:
|
||||
stats.skipped_matches += 1
|
||||
continue
|
||||
|
||||
all_probs = predictor.predict_all(feats.to_model_array(), feats)
|
||||
stats.analyzed_matches += 1
|
||||
|
||||
actuals = {
|
||||
"MS": _actual_ms(score_home, score_away),
|
||||
"OU25": _actual_ou25(score_home, score_away),
|
||||
"BTTS": _actual_btts(score_home, score_away),
|
||||
}
|
||||
|
||||
if max(all_probs["MS"], key=all_probs["MS"].get) == actuals["MS"]:
|
||||
stats.ms_correct += 1
|
||||
if max(all_probs["OU25"], key=all_probs["OU25"].get) == actuals["OU25"]:
|
||||
stats.ou25_correct += 1
|
||||
if max(all_probs["BTTS"], key=all_probs["BTTS"].get) == actuals["BTTS"]:
|
||||
stats.btts_correct += 1
|
||||
|
||||
best_pick = _best_pick(feats, all_probs)
|
||||
if best_pick is None:
|
||||
continue
|
||||
|
||||
stats.main_pick_count += 1
|
||||
if _pick_won(best_pick, actuals):
|
||||
stats.main_pick_correct += 1
|
||||
|
||||
if best_pick.playable:
|
||||
stats.playable_pick_count += 1
|
||||
stats.playable_units_staked += best_pick.stake_units
|
||||
if _pick_won(best_pick, actuals):
|
||||
stats.playable_pick_correct += 1
|
||||
stats.playable_units_profit += best_pick.stake_units * (best_pick.odds - 1.0)
|
||||
else:
|
||||
stats.playable_units_profit -= best_pick.stake_units
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def _pct(numerator: int, denominator: int) -> float:
|
||||
if denominator <= 0:
|
||||
return 0.0
|
||||
return round((numerator / denominator) * 100.0, 2)
|
||||
|
||||
|
||||
def _roi(profit: float, staked: float) -> float:
|
||||
if staked <= 0:
|
||||
return 0.0
|
||||
return round((profit / staked) * 100.0, 2)
|
||||
|
||||
|
||||
def _print_summary(stats: BacktestStats) -> None:
|
||||
print("=== V2 Runtime Backtest ===")
|
||||
print(f"Sampled matches : {stats.sampled_matches}")
|
||||
print(f"Analyzed matches : {stats.analyzed_matches}")
|
||||
print(f"Skipped matches : {stats.skipped_matches}")
|
||||
print(f"MS accuracy : {_pct(stats.ms_correct, stats.analyzed_matches)}%")
|
||||
print(f"OU2.5 accuracy : {_pct(stats.ou25_correct, stats.analyzed_matches)}%")
|
||||
print(f"BTTS accuracy : {_pct(stats.btts_correct, stats.analyzed_matches)}%")
|
||||
print(
|
||||
"Main pick accuracy : "
|
||||
f"{_pct(stats.main_pick_correct, stats.main_pick_count)}% "
|
||||
f"({stats.main_pick_correct}/{stats.main_pick_count})"
|
||||
)
|
||||
print(
|
||||
"Playable accuracy : "
|
||||
f"{_pct(stats.playable_pick_correct, stats.playable_pick_count)}% "
|
||||
f"({stats.playable_pick_correct}/{stats.playable_pick_count})"
|
||||
)
|
||||
print(f"Units staked : {stats.playable_units_staked:.2f}")
|
||||
print(f"Units profit : {stats.playable_units_profit:.2f}")
|
||||
print(f"ROI : {_roi(stats.playable_units_profit, stats.playable_units_staked)}%")
|
||||
|
||||
|
||||
async def _main() -> None:
|
||||
args = _parse_args()
|
||||
try:
|
||||
stats = await _run(args.limit, args.days)
|
||||
_print_summary(stats)
|
||||
finally:
|
||||
await dispose_engine()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_main())
|
||||
@@ -1,147 +0,0 @@
|
||||
"""
|
||||
Value Hunter Backtest
|
||||
=====================
|
||||
Sadece modelin büroyu yendiği (Pozitif Edge) maçları oynar.
|
||||
"""
|
||||
|
||||
import os, sys, json, time, psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR): ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
MATCH_IDS = [
|
||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
||||
]
|
||||
|
||||
def run_value_hunter():
|
||||
print("💎 VALUE HUNTER: SADECE HATALI ORANLARI YAKALA")
|
||||
print("="*60)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
||||
cur.execute(f"""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.id IN ({placeholders}) AND m.status = 'FT'
|
||||
""", MATCH_IDS)
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç taranıyor...\n")
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
total_edge_found = 0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred: continue
|
||||
|
||||
# Tüm önerileri kontrol et
|
||||
picks = pred.get("expert_recommendation", {}).get("value_picks", [])
|
||||
if not picks: picks = [pred.get("expert_recommendation", {}).get("main_pick")]
|
||||
|
||||
played_this_match = False
|
||||
|
||||
for pick_data in picks:
|
||||
if not pick_data: continue
|
||||
pick = pick_data.get("pick")
|
||||
conf = pick_data.get("confidence", 0)
|
||||
odds = pick_data.get("odds", 0)
|
||||
edge = pick_data.get("edge", 0)
|
||||
|
||||
# VALUE KURALI: Model bürodan en az %10 daha iyi olmalı
|
||||
if edge < 10: continue
|
||||
if odds < 1.20: continue
|
||||
|
||||
total_bet += 1
|
||||
total_edge_found += edge
|
||||
won = False
|
||||
pick_clean = str(pick).upper()
|
||||
|
||||
if pick_clean in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick_clean in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick_clean in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick_clean and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick_clean and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({edge:.0f}% Edge) -> WON! (+{profit:.2f})")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({edge:.0f}% Edge) -> LOST")
|
||||
|
||||
played_this_match = True
|
||||
break # Maç başına tek bahis
|
||||
|
||||
except Exception: pass
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("💎 VALUE HUNTER SONUÇLARI")
|
||||
print("="*60)
|
||||
print(f"Toplam Value Bulunan Bahis: {total_bet}")
|
||||
print(f"Ortalama Edge: {total_edge_found/total_bet:.1f}%" if total_bet > 0 else "N/A")
|
||||
print(f"Kazanılan: {total_won}")
|
||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
||||
|
||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
||||
else: print("🔴 PARA KAYBETTİK!")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_value_hunter()
|
||||
@@ -1,153 +0,0 @@
|
||||
"""
|
||||
Value Sniper Backtest (High Odds)
|
||||
=================================
|
||||
Sadece Oran > 1.50 ve Güven > %70 olan bahisleri oynar.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_value_sniper():
|
||||
print("💰 VALUE SNIPER BACKTEST (Odds > 1.50)")
|
||||
print("="*60)
|
||||
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 500
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç taranıyor...\n")
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred: continue
|
||||
|
||||
candidates = []
|
||||
if pred.get("expert_recommendation"):
|
||||
rec = pred["expert_recommendation"]
|
||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
||||
elif pred.get("main_pick"):
|
||||
candidates.append(pred["main_pick"])
|
||||
|
||||
best_bet = None
|
||||
for c in candidates:
|
||||
if not c: continue
|
||||
conf = c.get("confidence", 0) if isinstance(c, dict) else getattr(c, 'confidence', 0)
|
||||
odds = c.get("odds", 0) if isinstance(c, dict) else getattr(c, 'odds', 0)
|
||||
|
||||
# VALUE CRITERIA: Odds > 1.50 AND Conf > 70%
|
||||
if conf >= 70.0 and odds >= 1.50:
|
||||
# Check Edge
|
||||
implied = 1.0 / odds
|
||||
edge = ((conf/100) - implied) * 100
|
||||
if edge > 0: # Must be positive value
|
||||
if best_bet is None or (conf > (best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0))):
|
||||
best_bet = c
|
||||
|
||||
if best_bet:
|
||||
pick = str(best_bet.get("pick") if isinstance(best_bet, dict) else getattr(best_bet, 'pick', "")).upper()
|
||||
conf = best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0)
|
||||
odds = best_bet.get("odds", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'odds', 0)
|
||||
|
||||
won = False
|
||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif "ÜST" in pick or "OVER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick or "UNDER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
total_bet += 1
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({odds:.2f}) -> WON (+{profit:.2f})")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({odds:.2f}) -> LOST")
|
||||
|
||||
except: pass
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("💰 VALUE SNIPER SONUÇLARI")
|
||||
print("="*60)
|
||||
print(f"Oynanan Bahis: {total_bet}")
|
||||
print(f"Kazanılan: {total_won}")
|
||||
|
||||
if total_bet > 0:
|
||||
win_rate = (total_won / total_bet) * 100
|
||||
roi = (total_profit / total_bet) * 100
|
||||
print(f"Kazanma Oranı: %{win_rate:.2f}")
|
||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
||||
else: print("🔴 PARA KAYBETTİK!")
|
||||
else:
|
||||
print("⚠️ Yeterli VALUE bulunamadı.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_value_sniper()
|
||||
@@ -1,136 +0,0 @@
|
||||
"""
|
||||
VQWEN Full Backtest
|
||||
===================
|
||||
Tests all 3 VQWEN models (MS, OU25, BTTS) on 1000 historical matches.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pickle
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_vqwen_backtest():
|
||||
print("🧠 VQWEN FULL BACKTEST")
|
||||
print("="*60)
|
||||
|
||||
# Load Models
|
||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
||||
try:
|
||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
||||
print("✅ VQWEN MS, OU25, BTTS modelleri yüklendi.")
|
||||
except Exception as e:
|
||||
print(f"❌ Model hatası: {e}")
|
||||
return
|
||||
|
||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa,
|
||||
COALESCE((SELECT AVG(CASE WHEN m2.home_team_id = m.home_team_id AND m2.score_home > m2.score_away THEN 3 WHEN m2.home_team_id = m.home_team_id AND m2.score_home = m2.score_away THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as h_form,
|
||||
COALESCE((SELECT AVG(CASE WHEN m2.away_team_id = m.away_team_id AND m2.score_away > m2.score_home THEN 3 WHEN m2.away_team_id = m.away_team_id AND m2.score_away = m2.score_home THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as a_form,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_sc,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_co,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_sc,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_co
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 1000
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
||||
|
||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
||||
|
||||
for row in rows:
|
||||
oh, od, oa = float(row['oh'] or 0), float(row['od'] or 0), float(row['oa'] or 0)
|
||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
||||
|
||||
h_xg = (float(row['h_sc'] or 1.2) + float(row['a_co'] or 1.2)) / 2
|
||||
a_xg = (float(row['a_sc'] or 1.2) + float(row['h_co'] or 1.2)) / 2
|
||||
h_p = (float(row['h_form'] or 0)*10) + (float(row['h_sc'] or 1.2)*5) - (float(row['h_co'] or 1.2)*5)
|
||||
a_p = (float(row['a_form'] or 0)*10) + (float(row['a_sc'] or 1.2)*5) - (float(row['a_co'] or 1.2)*5)
|
||||
|
||||
margin = (1/oh) + (1/od) + (1/oa)
|
||||
|
||||
# MS Prediction
|
||||
f_ms = pd.DataFrame([{'h_form': float(row['h_form']), 'a_form': float(row['a_form']), 'h_xg': h_xg, 'a_xg': a_xg,
|
||||
'pow_diff': h_p - a_p, 'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
||||
'h_sot': 4.0, 'a_sot': 3.0}])
|
||||
ms_probs = model_ms.predict(f_ms)[0]
|
||||
|
||||
# MS Value Bet
|
||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
||||
if odd <= 1.0: continue
|
||||
edge = prob - (1/odd)
|
||||
if edge > 0.05 and prob > 0.50: # Value ve Güven
|
||||
results['ms']['bet'] += 1
|
||||
h, a = row['score_home'], row['score_away']
|
||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
||||
else: results['ms']['profit'] -= 1.0
|
||||
break
|
||||
|
||||
# OU2.5 Prediction
|
||||
f_ou = pd.DataFrame([{'h_xg': h_xg, 'a_xg': a_xg, 'total_xg': h_xg+a_xg, 'h_sot': 4.0, 'a_sot': 3.0}])
|
||||
p_over = model_ou.predict(f_ou)[0]
|
||||
|
||||
# OU2.5 Value Bet
|
||||
if p_over > 0.55 and oh > 1.0: # Sadece örnek olarak over > %55 ise
|
||||
results['ou25']['bet'] += 1
|
||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85 # Ortalama oran
|
||||
else: results['ou25']['profit'] -= 1.0
|
||||
|
||||
# BTTS Prediction
|
||||
f_btts = pd.DataFrame([{'h_xg': h_xg, 'a_xg': a_xg, 'h_sc': float(row['h_sc']), 'a_sc': float(row['a_sc'])}])
|
||||
p_btts = model_btts.predict(f_btts)[0]
|
||||
|
||||
# BTTS Value Bet
|
||||
if p_btts > 0.55:
|
||||
results['btts']['bet'] += 1
|
||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
||||
else: results['btts']['profit'] -= 1.0
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 VQWEN PAZAR BAZLI SONUÇLAR")
|
||||
print("="*60)
|
||||
for mkt in ['ms', 'ou25', 'btts']:
|
||||
r = results[mkt]
|
||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
||||
print(f"{mkt.upper():<10} Oynanan: {r['bet']:<5} Kazanılan: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f} Units")
|
||||
|
||||
total_profit = sum(r['profit'] for r in results.values())
|
||||
print(f"\n💰 TOPLAM KÂR: {total_profit:+.2f} Units")
|
||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
||||
else: print("🔴 ZARARDA")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_vqwen_backtest()
|
||||
@@ -1,141 +0,0 @@
|
||||
"""
|
||||
VQWEN Deep Backtest
|
||||
===================
|
||||
Tests the NEW Deep model with player & card data.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pickle
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_vqwen_deep_backtest():
|
||||
print("🧠 VQWEN DEEP BACKTEST")
|
||||
print("="*60)
|
||||
|
||||
# Load Models
|
||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
||||
try:
|
||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
||||
print("✅ VQWEN Deep modelleri yüklendi.")
|
||||
except Exception as e:
|
||||
print(f"❌ Model hatası: {e}")
|
||||
return
|
||||
|
||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa,
|
||||
COALESCE((SELECT AVG(CASE WHEN m2.home_team_id = m.home_team_id AND m2.score_home > m2.score_away THEN 3 WHEN m2.home_team_id = m.home_team_id AND m2.score_home = m2.score_away THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as h_form,
|
||||
COALESCE((SELECT AVG(CASE WHEN m2.away_team_id = m.away_team_id AND m2.score_away > m2.score_home THEN 3 WHEN m2.away_team_id = m.away_team_id AND m2.score_away = m2.score_home THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as a_form,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_sc,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_co,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_sc,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_co,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.home_team_id AND mp.is_starting = true), 0) as h_xi,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.away_team_id AND mp.is_starting = true), 0) as a_xi,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_events mpe WHERE mpe.match_id = m.id AND mpe.event_type = 'card'), 0) as cards
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 1000
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
||||
|
||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
||||
|
||||
for row in rows:
|
||||
oh = float(row['oh'] or 0)
|
||||
od = float(row['od'] or 0)
|
||||
oa = float(row['oa'] or 0)
|
||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
||||
|
||||
h_xg = (float(row['h_sc'] or 1.2) + float(row['a_co'] or 1.2)) / 2
|
||||
a_xg = (float(row['a_sc'] or 1.2) + float(row['h_co'] or 1.2)) / 2
|
||||
h_p = (float(row['h_form'] or 0)*10) + (float(row['h_sc'] or 1.2)*5) - (float(row['h_co'] or 1.2)*5)
|
||||
a_p = (float(row['a_form'] or 0)*10) + (float(row['a_sc'] or 1.2)*5) - (float(row['a_co'] or 1.2)*5)
|
||||
|
||||
margin = (1/oh) + (1/od) + (1/oa)
|
||||
h_sot, a_sot = 4.0, 3.0
|
||||
|
||||
# Features
|
||||
f = pd.DataFrame([{
|
||||
'h_form': float(row['h_form']), 'a_form': float(row['a_form']),
|
||||
'h_xg': h_xg, 'a_xg': a_xg, 'pow_diff': h_p - a_p,
|
||||
'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
||||
'h_sot': h_sot, 'a_sot': a_sot,
|
||||
'h_xi': float(row['h_xi']), 'a_xi': float(row['a_xi']),
|
||||
'xi_diff': float(row['h_xi'] - row['a_xi']),
|
||||
'cards': float(row['cards'])
|
||||
}])
|
||||
|
||||
# MS
|
||||
ms_probs = model_ms.predict(f)[0]
|
||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
||||
if odd <= 1.0: continue
|
||||
edge = prob - (1/odd)
|
||||
if edge > 0.05 and prob > 0.50:
|
||||
results['ms']['bet'] += 1
|
||||
h, a = row['score_home'], row['score_away']
|
||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
||||
else: results['ms']['profit'] -= 1.0
|
||||
break
|
||||
|
||||
# OU2.5
|
||||
p_over = float(model_ou.predict(f)[0])
|
||||
if p_over > 0.55:
|
||||
results['ou25']['bet'] += 1
|
||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85
|
||||
else: results['ou25']['profit'] -= 1.0
|
||||
|
||||
# BTTS
|
||||
p_btts = float(model_btts.predict(f)[0])
|
||||
if p_btts > 0.55:
|
||||
results['btts']['bet'] += 1
|
||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
||||
else: results['btts']['profit'] -= 1.0
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 VQWEN DEEP SONUÇLAR")
|
||||
print("="*60)
|
||||
for mkt in ['ms', 'ou25', 'btts']:
|
||||
r = results[mkt]
|
||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
||||
print(f"{mkt.upper():<10} Oyn: {r['bet']:<5} Kaz: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f}")
|
||||
|
||||
total = sum(r['profit'] for r in results.values())
|
||||
print(f"\n💰 TOPLAM: {total:+.2f} Units")
|
||||
print("🟢 PARA KAZANDIK!" if total > 0 else "🔴 ZARARDA")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_vqwen_deep_backtest()
|
||||
@@ -1,159 +0,0 @@
|
||||
"""
|
||||
VQWEN Final Backtest
|
||||
====================
|
||||
Tests the Final Model (ELO + Rest + Context).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pickle
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_final_backtest():
|
||||
print("🧠 VQWEN FINAL BACKTEST (ELO + REST)")
|
||||
print("="*60)
|
||||
|
||||
# Load Models
|
||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
||||
try:
|
||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
||||
print("✅ VQWEN Final modelleri yüklendi.")
|
||||
except Exception as e:
|
||||
print(f"❌ Model hatası: {e}")
|
||||
return
|
||||
|
||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
||||
m.mst_utc,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
maf.home_elo, maf.away_elo,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc), 1.2) as h_home_goals,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc), 1.2) as a_away_goals,
|
||||
COALESCE(EXTRACT(EPOCH FROM (to_timestamp(m.mst_utc/1000) - (SELECT MAX(to_timestamp(m2.mst_utc/1000)) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc)) / 86400), 7) as h_rest,
|
||||
COALESCE(EXTRACT(EPOCH FROM (to_timestamp(m.mst_utc/1000) - (SELECT MAX(to_timestamp(m2.mst_utc/1000)) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc)) / 86400), 7) as a_rest,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.home_team_id AND mp.is_starting = true), 11) as h_xi,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.away_team_id AND mp.is_starting = true), 11) as a_xi,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_events mpe WHERE mpe.match_id = m.id AND mpe.event_type = 'card'), 4) as cards,
|
||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
||||
(SELECT os.odd_value FROM odd_categories oc JOIN odd_selections os ON os.odd_category_db_id = oc.db_id WHERE oc.match_id = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN football_ai_features maf ON maf.match_id = m.id
|
||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 1000
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
||||
|
||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
||||
|
||||
for row in rows:
|
||||
oh = float(row['oh'] or 0)
|
||||
od = float(row['od'] or 0)
|
||||
oa = float(row['oa'] or 0)
|
||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
||||
|
||||
# Features
|
||||
h_elo = float(row['home_elo'] or 1500)
|
||||
a_elo = float(row['away_elo'] or 1500)
|
||||
h_home_goals = float(row['h_home_goals'] or 1.2)
|
||||
a_away_goals = float(row['a_away_goals'] or 1.2)
|
||||
h_rest = float(row['h_rest'] or 7)
|
||||
a_rest = float(row['a_rest'] or 7)
|
||||
h_xi = float(row['h_xi'] or 11)
|
||||
a_xi = float(row['a_xi'] or 11)
|
||||
cards = float(row['cards'] or 4)
|
||||
|
||||
def fatigue(rest):
|
||||
if rest < 3: return 0.85
|
||||
if rest < 5: return 0.95
|
||||
return 1.0
|
||||
|
||||
h_fat = fatigue(h_rest)
|
||||
a_fat = fatigue(a_rest)
|
||||
|
||||
h_xg = h_home_goals * h_fat
|
||||
a_xg = a_away_goals * a_fat
|
||||
total_xg = h_xg + a_xg
|
||||
|
||||
margin = (1/oh) + (1/od) + (1/oa)
|
||||
f = pd.DataFrame([{
|
||||
'elo_diff': h_elo - a_elo,
|
||||
'h_xg': h_xg, 'a_xg': a_xg,
|
||||
'total_xg': total_xg,
|
||||
'pow_diff': (h_elo/100)*h_fat - (a_elo/100)*a_fat,
|
||||
'rest_diff': h_rest - a_rest,
|
||||
'h_fatigue': h_fat, 'a_fatigue': a_fat,
|
||||
'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
||||
'h_xi': h_xi, 'a_xi': a_xi,
|
||||
'cards': cards
|
||||
}])
|
||||
|
||||
# MS
|
||||
ms_probs = model_ms.predict(f)[0]
|
||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
||||
if odd <= 1.0: continue
|
||||
edge = prob - (1/odd)
|
||||
if edge > 0.05 and prob > 0.45:
|
||||
results['ms']['bet'] += 1
|
||||
h, a = row['score_home'], row['score_away']
|
||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
||||
else: results['ms']['profit'] -= 1.0
|
||||
break
|
||||
|
||||
# OU2.5
|
||||
p_over = float(model_ou.predict(f)[0])
|
||||
if p_over > 0.55:
|
||||
results['ou25']['bet'] += 1
|
||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85
|
||||
else: results['ou25']['profit'] -= 1.0
|
||||
|
||||
# BTTS
|
||||
p_btts = float(model_btts.predict(f)[0])
|
||||
if p_btts > 0.55:
|
||||
results['btts']['bet'] += 1
|
||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
||||
else: results['btts']['profit'] -= 1.0
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 VQWEN FINAL SONUÇLAR")
|
||||
print("="*60)
|
||||
for mkt in ['ms', 'ou25', 'btts']:
|
||||
r = results[mkt]
|
||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
||||
print(f"{mkt.upper():<10} Oyn: {r['bet']:<5} Kaz: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f}")
|
||||
|
||||
total = sum(r['profit'] for r in results.values())
|
||||
print(f"\n💰 TOPLAM: {total:+.2f} Units")
|
||||
print("🟢 PARA KAZANDIK!" if total > 0 else "🔴 ZARARDA")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_final_backtest()
|
||||
@@ -1,182 +0,0 @@
|
||||
"""
|
||||
VQWEN v3 Shared-Contract Backtest
|
||||
=================================
|
||||
|
||||
Evaluates the retrained VQWEN models on the temporal validation slice using
|
||||
the exact same pre-match feature contract as training/runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import psycopg2
|
||||
from dotenv import load_dotenv
|
||||
|
||||
AI_DIR = Path(__file__).resolve().parent
|
||||
ENGINE_DIR = AI_DIR.parent
|
||||
REPO_DIR = ENGINE_DIR.parent
|
||||
MODELS_DIR = ENGINE_DIR / "models" / "vqwen"
|
||||
|
||||
if str(ENGINE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ENGINE_DIR))
|
||||
|
||||
from features.vqwen_contract import FEATURE_COLUMNS # noqa: E402
|
||||
from train_vqwen_v3 import ( # noqa: E402
|
||||
_enrich_pre_match_context,
|
||||
_fetch_dataframe,
|
||||
_prepare_features,
|
||||
_temporal_split,
|
||||
load_top_league_ids,
|
||||
)
|
||||
|
||||
|
||||
def _load_env() -> None:
|
||||
load_dotenv(REPO_DIR / ".env", override=False)
|
||||
load_dotenv(ENGINE_DIR / ".env", override=False)
|
||||
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
_load_env()
|
||||
raw = os.getenv("DATABASE_URL", "").strip().strip('"').strip("'")
|
||||
if not raw:
|
||||
raise RuntimeError("DATABASE_URL is missing.")
|
||||
return raw.split("?", 1)[0]
|
||||
|
||||
|
||||
def _accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
||||
if len(y_true) == 0:
|
||||
return 0.0
|
||||
return float((y_true == y_pred).mean())
|
||||
|
||||
|
||||
def _binary_metrics(prob: np.ndarray, y_true: np.ndarray) -> tuple[float, float]:
|
||||
pred = (prob >= 0.5).astype(int)
|
||||
acc = _accuracy(y_true, pred)
|
||||
brier = float(np.mean((prob - y_true) ** 2)) if len(y_true) else 1.0
|
||||
return acc, brier
|
||||
|
||||
|
||||
def _multiclass_brier(prob: np.ndarray, y_true: np.ndarray, n_classes: int = 3) -> float:
|
||||
if len(y_true) == 0:
|
||||
return 1.0
|
||||
target = np.zeros((len(y_true), n_classes), dtype=np.float64)
|
||||
target[np.arange(len(y_true)), y_true.astype(int)] = 1.0
|
||||
return float(np.mean(np.sum((prob - target) ** 2, axis=1)))
|
||||
|
||||
|
||||
def _band_label(probability: float) -> str:
|
||||
if probability >= 0.70:
|
||||
return "HIGH"
|
||||
if probability >= 0.60:
|
||||
return "MEDIUM"
|
||||
if probability >= 0.50:
|
||||
return "LOW"
|
||||
return "NO_BET"
|
||||
|
||||
|
||||
def _summarize_bands(
|
||||
name: str,
|
||||
confidence: np.ndarray,
|
||||
is_correct: np.ndarray,
|
||||
) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for band in ("HIGH", "MEDIUM", "LOW"):
|
||||
mask = np.array([_band_label(float(p)) == band for p in confidence], dtype=bool)
|
||||
count = int(mask.sum())
|
||||
accuracy = float(is_correct[mask].mean()) if count else 0.0
|
||||
avg_conf = float(confidence[mask].mean()) if count else 0.0
|
||||
lines.append(
|
||||
f"{name} {band:<6} count={count:<4} accuracy={accuracy*100:5.1f}% avg_conf={avg_conf*100:5.1f}%"
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def run_v3_backtest() -> None:
|
||||
print("VQWEN v3 SHARED-CONTRACT BACKTEST")
|
||||
print("=" * 60)
|
||||
|
||||
league_ids = load_top_league_ids()
|
||||
dsn = get_clean_dsn()
|
||||
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
df = _fetch_dataframe(cur, league_ids)
|
||||
df = _enrich_pre_match_context(cur, df)
|
||||
df = _prepare_features(df)
|
||||
|
||||
train_df, valid_df = _temporal_split(df)
|
||||
print(f"Toplam ornek: {len(df)} | Train: {len(train_df)} | Valid: {len(valid_df)}")
|
||||
|
||||
with (MODELS_DIR / "vqwen_ms.pkl").open("rb") as handle:
|
||||
model_ms = pickle.load(handle)
|
||||
with (MODELS_DIR / "vqwen_ou25.pkl").open("rb") as handle:
|
||||
model_ou25 = pickle.load(handle)
|
||||
with (MODELS_DIR / "vqwen_btts.pkl").open("rb") as handle:
|
||||
model_btts = pickle.load(handle)
|
||||
|
||||
X_valid = valid_df[FEATURE_COLUMNS]
|
||||
y_ms = valid_df["t_ms"].to_numpy(dtype=np.int64)
|
||||
y_ou25 = valid_df["t_ou"].to_numpy(dtype=np.int64)
|
||||
y_btts = valid_df["t_btts"].to_numpy(dtype=np.int64)
|
||||
|
||||
ms_prob = np.asarray(model_ms.predict(X_valid), dtype=np.float64)
|
||||
ou25_prob = np.asarray(model_ou25.predict(X_valid), dtype=np.float64).reshape(-1)
|
||||
btts_prob = np.asarray(model_btts.predict(X_valid), dtype=np.float64).reshape(-1)
|
||||
|
||||
ms_pred = np.argmax(ms_prob, axis=1)
|
||||
ms_conf = np.max(ms_prob, axis=1)
|
||||
ms_correct = (ms_pred == y_ms).astype(np.int64)
|
||||
|
||||
ou25_pred = (ou25_prob >= 0.5).astype(np.int64)
|
||||
ou25_conf = np.where(ou25_prob >= 0.5, ou25_prob, 1.0 - ou25_prob)
|
||||
ou25_correct = (ou25_pred == y_ou25).astype(np.int64)
|
||||
|
||||
btts_pred = (btts_prob >= 0.5).astype(np.int64)
|
||||
btts_conf = np.where(btts_prob >= 0.5, btts_prob, 1.0 - btts_prob)
|
||||
btts_correct = (btts_pred == y_btts).astype(np.int64)
|
||||
|
||||
ms_acc = _accuracy(y_ms, ms_pred)
|
||||
ou25_acc, ou25_brier = _binary_metrics(ou25_prob, y_ou25)
|
||||
btts_acc, btts_brier = _binary_metrics(btts_prob, y_btts)
|
||||
ms_brier = _multiclass_brier(ms_prob, y_ms)
|
||||
|
||||
print("\nGenel metrikler")
|
||||
print(f"MS accuracy : {ms_acc*100:.2f}% | multiclass_brier={ms_brier:.4f}")
|
||||
print(f"OU25 accuracy : {ou25_acc*100:.2f}% | brier={ou25_brier:.4f}")
|
||||
print(f"BTTS accuracy : {btts_acc*100:.2f}% | brier={btts_brier:.4f}")
|
||||
|
||||
print("\nConfidence band")
|
||||
for line in _summarize_bands("MS", ms_conf, ms_correct):
|
||||
print(line)
|
||||
for line in _summarize_bands("OU25", ou25_conf, ou25_correct):
|
||||
print(line)
|
||||
for line in _summarize_bands("BTTS", btts_conf, btts_correct):
|
||||
print(line)
|
||||
|
||||
summary = {
|
||||
"validation_samples": int(len(valid_df)),
|
||||
"metrics": {
|
||||
"ms_accuracy": round(ms_acc, 4),
|
||||
"ms_brier": round(ms_brier, 4),
|
||||
"ou25_accuracy": round(ou25_acc, 4),
|
||||
"ou25_brier": round(ou25_brier, 4),
|
||||
"btts_accuracy": round(btts_acc, 4),
|
||||
"btts_brier": round(btts_brier, 4),
|
||||
},
|
||||
}
|
||||
(MODELS_DIR / "vqwen_backtest_v3_summary.json").write_text(
|
||||
json.dumps(summary, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print("\nKaydedildi: vqwen_backtest_v3_summary.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_v3_backtest()
|
||||
@@ -0,0 +1,312 @@
|
||||
"""
|
||||
V28 — CONDITIONAL FREQUENCY ENGINE
|
||||
====================================
|
||||
User's strategy automated at scale:
|
||||
|
||||
For every match (e.g. Beşiktaş vs Konya):
|
||||
1. Look at Beşiktaş's HOME history when their MS1 odds were in the same band (e.g. 1.30-1.40)
|
||||
→ What % of those matches ended OU 1.5 over? OU 2.5 over? MS1?
|
||||
2. Look at Konya's AWAY history when their MS2 odds were in the same band (e.g. 2.00-2.20)
|
||||
→ Same questions
|
||||
3. COMBINE both signals:
|
||||
→ If BOTH teams historically produce >80% OU1.5 over at these odds → BET OU1.5 over
|
||||
→ This is the user's exact Excel strategy, now running on 104K matches
|
||||
|
||||
CRITICAL: Only uses PAST matches for each prediction (no future leakage)
|
||||
"""
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# ─── Load Data ───
|
||||
print("Loading data...")
|
||||
df = pd.read_csv('data/training_data_v27.csv', low_memory=False)
|
||||
KEEP_STR = ['match_id', 'league_name', 'home_team', 'away_team',
|
||||
'home_team_id', 'away_team_id', 'league_id', 'mst_utc']
|
||||
for c in df.columns:
|
||||
if c not in KEEP_STR:
|
||||
df[c] = pd.to_numeric(df[c], errors='coerce')
|
||||
|
||||
# Ensure chronological order (by match_id or date)
|
||||
if 'mst_utc' in df.columns:
|
||||
df['mst_utc'] = pd.to_datetime(df['mst_utc'], errors='coerce')
|
||||
df = df.sort_values('mst_utc').reset_index(drop=True)
|
||||
|
||||
# Filter: need valid odds + scores
|
||||
df = df.dropna(subset=['odds_ms_h', 'odds_ms_a', 'score_home', 'score_away',
|
||||
'home_team_id', 'away_team_id', 'label_ms'])
|
||||
|
||||
# Compute actual goal labels
|
||||
df['total_goals'] = df['score_home'] + df['score_away']
|
||||
df['ou15_actual'] = (df['total_goals'] > 1.5).astype(int)
|
||||
df['ou25_actual'] = (df['total_goals'] > 2.5).astype(int)
|
||||
df['ou35_actual'] = (df['total_goals'] > 3.5).astype(int)
|
||||
df['btts_actual'] = ((df['score_home'] > 0) & (df['score_away'] > 0)).astype(int)
|
||||
df['ms_result'] = df['label_ms'].astype(int) # 0=H, 1=D, 2=A
|
||||
|
||||
N = len(df)
|
||||
print(f"Total matches: {N}")
|
||||
print(f"Unique home teams: {df.home_team_id.nunique()}")
|
||||
print(f"Unique away teams: {df.away_team_id.nunique()}")
|
||||
|
||||
# ─── Odds Band Helper ───
|
||||
def get_odds_band(odds, band_width=0.10):
|
||||
"""Round odds to nearest band. E.g. 1.35 → (1.30, 1.40)"""
|
||||
lower = round(np.floor(odds / band_width) * band_width, 2)
|
||||
upper = round(lower + band_width, 2)
|
||||
return (lower, upper)
|
||||
|
||||
def get_odds_band_wide(odds):
|
||||
"""Wider band for less common teams. E.g. 1.35 → (1.20, 1.50)"""
|
||||
if odds < 1.50:
|
||||
return (1.01, 1.50)
|
||||
elif odds < 2.00:
|
||||
return (1.50, 2.00)
|
||||
elif odds < 2.50:
|
||||
return (2.00, 2.50)
|
||||
elif odds < 3.00:
|
||||
return (2.50, 3.00)
|
||||
elif odds < 4.00:
|
||||
return (3.00, 4.00)
|
||||
elif odds < 6.00:
|
||||
return (4.00, 6.00)
|
||||
else:
|
||||
return (6.00, 20.00)
|
||||
|
||||
# ─── Build Conditional Frequency Lookup (Expanding Window) ───
|
||||
print("\nBuilding conditional frequency features (expanding window)...")
|
||||
|
||||
# We'll compute features for each match using only past data
|
||||
MIN_MATCHES = 5 # minimum historical matches to generate a signal
|
||||
|
||||
# Pre-allocate feature arrays
|
||||
feat_names = [
|
||||
'home_ou15_rate_at_band', 'home_ou25_rate_at_band', 'home_ou35_rate_at_band',
|
||||
'home_btts_rate_at_band', 'home_win_rate_at_band', 'home_n_at_band',
|
||||
'away_ou15_rate_at_band', 'away_ou25_rate_at_band', 'away_ou35_rate_at_band',
|
||||
'away_btts_rate_at_band', 'away_win_rate_at_band', 'away_n_at_band',
|
||||
'combined_ou15', 'combined_ou25', 'combined_ou35', 'combined_btts',
|
||||
'home_goals_at_band', 'away_goals_at_band', 'combined_goals_at_band',
|
||||
'home_conceded_at_band', 'away_conceded_at_band',
|
||||
]
|
||||
features = np.full((N, len(feat_names)), np.nan)
|
||||
|
||||
# Historical ledger: team_id → list of (odds_band, ou15, ou25, ou35, btts, ms_result, goals_scored, goals_conceded)
|
||||
home_history = defaultdict(list) # team performances when playing HOME
|
||||
away_history = defaultdict(list) # team performances when playing AWAY
|
||||
|
||||
for i in range(N):
|
||||
row = df.iloc[i]
|
||||
ht_id = row.home_team_id
|
||||
at_id = row.away_team_id
|
||||
h_odds = row.odds_ms_h
|
||||
a_odds = row.odds_ms_a
|
||||
|
||||
if pd.isna(h_odds) or pd.isna(a_odds):
|
||||
continue
|
||||
|
||||
h_band = get_odds_band_wide(h_odds)
|
||||
a_band = get_odds_band_wide(a_odds)
|
||||
|
||||
# ── Look up HOME team's historical performance at this odds band ──
|
||||
h_hist = [x for x in home_history[ht_id] if h_band[0] <= x[0] < h_band[1]]
|
||||
if len(h_hist) >= MIN_MATCHES:
|
||||
features[i, 0] = np.mean([x[1] for x in h_hist]) # ou15 rate
|
||||
features[i, 1] = np.mean([x[2] for x in h_hist]) # ou25 rate
|
||||
features[i, 2] = np.mean([x[3] for x in h_hist]) # ou35 rate
|
||||
features[i, 3] = np.mean([x[4] for x in h_hist]) # btts rate
|
||||
features[i, 4] = np.mean([x[5] for x in h_hist]) # win rate (home win = 1 if ms==0)
|
||||
features[i, 5] = len(h_hist)
|
||||
features[i, 16] = np.mean([x[6] for x in h_hist]) # avg goals scored
|
||||
features[i, 19] = np.mean([x[7] for x in h_hist]) # avg goals conceded
|
||||
|
||||
# ── Look up AWAY team's historical performance at this odds band ──
|
||||
a_hist = [x for x in away_history[at_id] if a_band[0] <= x[0] < a_band[1]]
|
||||
if len(a_hist) >= MIN_MATCHES:
|
||||
features[i, 6] = np.mean([x[1] for x in a_hist]) # ou15 rate
|
||||
features[i, 7] = np.mean([x[2] for x in a_hist]) # ou25 rate
|
||||
features[i, 8] = np.mean([x[3] for x in a_hist]) # ou35 rate
|
||||
features[i, 9] = np.mean([x[4] for x in a_hist]) # btts rate
|
||||
features[i, 10] = np.mean([x[5] for x in a_hist]) # away win rate
|
||||
features[i, 11] = len(a_hist)
|
||||
features[i, 17] = np.mean([x[6] for x in a_hist]) # avg goals scored (away)
|
||||
features[i, 20] = np.mean([x[7] for x in a_hist]) # avg goals conceded (away)
|
||||
|
||||
# ── Combined signals ──
|
||||
if not np.isnan(features[i, 0]) and not np.isnan(features[i, 6]):
|
||||
features[i, 12] = (features[i, 0] + features[i, 6]) / 2 # combined ou15
|
||||
features[i, 13] = (features[i, 1] + features[i, 7]) / 2 # combined ou25
|
||||
features[i, 14] = (features[i, 2] + features[i, 8]) / 2 # combined ou35
|
||||
features[i, 15] = (features[i, 3] + features[i, 9]) / 2 # combined btts
|
||||
features[i, 18] = features[i, 16] + features[i, 17] # combined goals
|
||||
|
||||
# ── Add THIS match to history (for future lookups) ──
|
||||
ou15 = int(row.total_goals > 1.5)
|
||||
ou25 = int(row.total_goals > 2.5)
|
||||
ou35 = int(row.total_goals > 3.5)
|
||||
btts = int(row.score_home > 0 and row.score_away > 0)
|
||||
h_won = int(row.label_ms == 0)
|
||||
a_won = int(row.label_ms == 2)
|
||||
|
||||
home_history[ht_id].append((h_odds, ou15, ou25, ou35, btts, h_won,
|
||||
row.score_home, row.score_away))
|
||||
away_history[at_id].append((a_odds, ou15, ou25, ou35, btts, a_won,
|
||||
row.score_away, row.score_home))
|
||||
|
||||
if (i+1) % 20000 == 0:
|
||||
valid = np.sum(~np.isnan(features[:i+1, 12]))
|
||||
print(f" Processed {i+1}/{N} matches, {valid} with combined signals")
|
||||
|
||||
# Count valid features
|
||||
valid_mask = ~np.isnan(features[:, 12])
|
||||
print(f"\nMatches with combined conditional signals: {valid_mask.sum()} / {N}")
|
||||
|
||||
# ─── BACKTEST: Walk-Forward ───
|
||||
print("\n" + "="*70)
|
||||
print(" CONDITIONAL FREQUENCY BACKTEST")
|
||||
print("="*70)
|
||||
|
||||
# Only test on last 20% of data (to avoid early sparse data)
|
||||
test_start = int(N * 0.7)
|
||||
test_idx = range(test_start, N)
|
||||
test_valid = [i for i in test_idx if valid_mask[i]]
|
||||
print(f"Test window: matches {test_start}-{N} ({len(test_valid)} with signals)")
|
||||
|
||||
# Strategy: bet on OU1.5 over when combined_ou15 > threshold
|
||||
markets = [
|
||||
('OU 1.5 Over', 'combined_ou15', 12, 'ou15_actual', 'odds_ou15_o'),
|
||||
('OU 2.5 Over', 'combined_ou25', 13, 'ou25_actual', 'odds_ou25_o'),
|
||||
('OU 3.5 Over', 'combined_ou35', 14, 'ou35_actual', 'odds_ou35_o'),
|
||||
('BTTS Yes', 'combined_btts', 15, 'btts_actual', 'odds_btts_y'),
|
||||
]
|
||||
|
||||
for market_name, feat_key, feat_idx, label_col, odds_col in markets:
|
||||
print(f"\n ── {market_name} ──")
|
||||
|
||||
if odds_col not in df.columns:
|
||||
print(f" No odds column '{odds_col}', skipping")
|
||||
continue
|
||||
|
||||
for threshold in [0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90]:
|
||||
bets = 0
|
||||
wins = 0
|
||||
pnl = 0.0
|
||||
|
||||
for i in test_valid:
|
||||
signal = features[i, feat_idx]
|
||||
if np.isnan(signal) or signal < threshold:
|
||||
continue
|
||||
odds_val = df.iloc[i][odds_col]
|
||||
if pd.isna(odds_val) or odds_val < 1.05:
|
||||
continue
|
||||
actual = df.iloc[i][label_col]
|
||||
if pd.isna(actual):
|
||||
continue
|
||||
|
||||
bets += 1
|
||||
if actual == 1:
|
||||
wins += 1
|
||||
pnl += odds_val - 1
|
||||
else:
|
||||
pnl -= 1
|
||||
|
||||
if bets >= 20:
|
||||
roi = pnl / bets * 100
|
||||
hit = wins / bets * 100
|
||||
ev = (wins/bets) * (pnl/wins + 1) if wins > 0 else 0
|
||||
marker = " *** PROFITABLE ***" if roi > 0 else ""
|
||||
print(f" Threshold>{threshold:.2f}: {bets:5d} bets, "
|
||||
f"hit={hit:.1f}%, ROI={roi:+.1f}%{marker}")
|
||||
|
||||
# Also test MS (1X2) market
|
||||
print(f"\n ── Maç Sonucu (1X2) ──")
|
||||
# Home win when home_win_rate_at_band > X AND away team loses often at that band
|
||||
for threshold in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
|
||||
bets = wins = 0
|
||||
pnl = 0.0
|
||||
for i in test_valid:
|
||||
h_wr = features[i, 4] # home win rate at band
|
||||
a_lr = 1 - features[i, 10] if not np.isnan(features[i, 10]) else np.nan # away loss rate
|
||||
if np.isnan(h_wr) or np.isnan(a_lr):
|
||||
continue
|
||||
combined = (h_wr + a_lr) / 2
|
||||
if combined < threshold:
|
||||
continue
|
||||
odds_val = df.iloc[i].odds_ms_h
|
||||
if pd.isna(odds_val) or odds_val < 1.10 or odds_val > 5.0:
|
||||
continue
|
||||
bets += 1
|
||||
if df.iloc[i].label_ms == 0:
|
||||
wins += 1
|
||||
pnl += odds_val - 1
|
||||
else:
|
||||
pnl -= 1
|
||||
if bets >= 20:
|
||||
roi = pnl / bets * 100
|
||||
hit = wins / bets * 100
|
||||
marker = " *** PROFITABLE ***" if roi > 0 else ""
|
||||
print(f" Home win comb>{threshold:.2f}: {bets:5d} bets, "
|
||||
f"hit={hit:.1f}%, ROI={roi:+.1f}%{marker}")
|
||||
|
||||
# ─── DEEP DIVE: Best performing niches ───
|
||||
print("\n" + "="*70)
|
||||
print(" DEEP DIVE: Combined OU15 + Odds Value Filter")
|
||||
print("="*70)
|
||||
|
||||
# The user's strategy: high confidence + the odds must pay enough
|
||||
for threshold in [0.75, 0.80, 0.85, 0.90]:
|
||||
for min_odds in [1.10, 1.20, 1.30, 1.40]:
|
||||
bets = wins = 0
|
||||
pnl = 0.0
|
||||
for i in test_valid:
|
||||
signal = features[i, 12] # combined ou15
|
||||
if np.isnan(signal) or signal < threshold:
|
||||
continue
|
||||
odds_val = df.iloc[i].get('odds_ou15_o', np.nan) if 'odds_ou15_o' in df.columns else np.nan
|
||||
if pd.isna(odds_val) or odds_val < min_odds:
|
||||
continue
|
||||
actual = df.iloc[i].ou15_actual
|
||||
|
||||
bets += 1
|
||||
if actual == 1:
|
||||
wins += 1
|
||||
pnl += odds_val - 1
|
||||
else:
|
||||
pnl -= 1
|
||||
|
||||
if bets >= 30:
|
||||
roi = pnl / bets * 100
|
||||
hit = wins / bets * 100
|
||||
if roi > -5: # show near-profitable too
|
||||
marker = " *** PROFITABLE ***" if roi > 0 else ""
|
||||
print(f" OU15 sig>{threshold:.2f} odds>{min_odds}: "
|
||||
f"{bets:5d} bets, hit={hit:.1f}%, ROI={roi:+.1f}%{marker}")
|
||||
|
||||
# ─── Additional: Goal expectation accuracy ───
|
||||
print("\n" + "="*70)
|
||||
print(" GOAL PREDICTION ACCURACY")
|
||||
print("="*70)
|
||||
valid_goals = [i for i in test_valid if not np.isnan(features[i, 18])]
|
||||
if valid_goals:
|
||||
pred_goals = [features[i, 18] for i in valid_goals]
|
||||
actual_goals = [df.iloc[i].total_goals for i in valid_goals]
|
||||
from sklearn.metrics import mean_absolute_error
|
||||
mae = mean_absolute_error(actual_goals, pred_goals)
|
||||
corr = np.corrcoef(pred_goals, actual_goals)[0, 1]
|
||||
print(f" Combined goal prediction MAE: {mae:.3f}")
|
||||
print(f" Correlation: {corr:.4f}")
|
||||
print(f" Avg predicted: {np.mean(pred_goals):.2f}, Avg actual: {np.mean(actual_goals):.2f}")
|
||||
|
||||
# Bucket analysis
|
||||
print("\n Goal prediction buckets:")
|
||||
for low, high in [(0, 1.5), (1.5, 2.0), (2.0, 2.5), (2.5, 3.0), (3.0, 3.5), (3.5, 5.0)]:
|
||||
bucket = [i for i, pg in zip(valid_goals, pred_goals) if low <= pg < high]
|
||||
if len(bucket) >= 20:
|
||||
avg_actual = np.mean([df.iloc[i].total_goals for i in bucket])
|
||||
ou25_rate = np.mean([df.iloc[i].ou25_actual for i in bucket])
|
||||
print(f" Predicted {low:.1f}-{high:.1f}: n={len(bucket)}, "
|
||||
f"actual_avg={avg_actual:.2f}, OU25%={ou25_rate*100:.1f}%")
|
||||
|
||||
print("\nDone!")
|
||||
@@ -1071,13 +1071,13 @@ class FeatureExtractor:
|
||||
|
||||
for mst, poss, sot, total_shots, corners, team_goals in rows:
|
||||
if poss and poss > 0:
|
||||
poss_sum += poss
|
||||
poss_sum += float(poss)
|
||||
poss_count += 1
|
||||
sot_sum += sot or 0
|
||||
shots_sum += total_shots or 0
|
||||
corners_sum += corners or 0
|
||||
sot_sum += float(sot or 0)
|
||||
shots_sum += float(total_shots or 0)
|
||||
corners_sum += float(corners or 0)
|
||||
|
||||
goals_scored += team_goals or 0
|
||||
goals_scored += float(team_goals or 0)
|
||||
|
||||
return {
|
||||
"possession": (poss_sum / poss_count / 100) if poss_count > 0 else 0.50,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||
SOURCE_CSV = AI_ENGINE_DIR / "data" / "training_data.csv"
|
||||
TARGET_DIR = AI_ENGINE_DIR / "data" / "v26_shadow"
|
||||
TARGET_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _rolling_windows(frame: pd.DataFrame) -> list[dict[str, int]]:
|
||||
ordered = frame.sort_values("mst_utc").reset_index(drop=True)
|
||||
windows: list[dict[str, int]] = []
|
||||
if ordered.empty:
|
||||
return windows
|
||||
|
||||
size = len(ordered)
|
||||
cuts = [0.55, 0.7, 0.85]
|
||||
for idx, cut in enumerate(cuts, start=1):
|
||||
end_ix = max(int(size * cut), 1)
|
||||
test_end = min(size - 1, end_ix + max(int(size * 0.10), 1))
|
||||
windows.append(
|
||||
{
|
||||
"window": idx,
|
||||
"train_end_ix": end_ix - 1,
|
||||
"test_start_ix": end_ix,
|
||||
"test_end_ix": test_end,
|
||||
"train_end_mst_utc": int(ordered.iloc[end_ix - 1]["mst_utc"]),
|
||||
"test_end_mst_utc": int(ordered.iloc[test_end]["mst_utc"]),
|
||||
}
|
||||
)
|
||||
return windows
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not SOURCE_CSV.exists():
|
||||
raise SystemExit(f"Missing source CSV: {SOURCE_CSV}")
|
||||
|
||||
frame = pd.read_csv(SOURCE_CSV)
|
||||
if "mst_utc" not in frame.columns:
|
||||
raise SystemExit("training_data.csv must include mst_utc")
|
||||
|
||||
ordered = frame.sort_values("mst_utc").reset_index(drop=True)
|
||||
ordered["lineup_completeness"] = 1.0
|
||||
ordered["referee_available"] = (
|
||||
ordered.get("referee_experience", pd.Series([0] * len(ordered))).fillna(0) > 0
|
||||
).astype(float)
|
||||
ordered["league_reliability"] = ordered.get("league_zero_goal_rate", 0).fillna(0).apply(
|
||||
lambda value: round(max(0.25, min(0.95, 0.85 - float(value))), 4)
|
||||
)
|
||||
ordered["odds_snapshot_freshness"] = 1.0
|
||||
|
||||
train_end = max(int(len(ordered) * 0.70), 1)
|
||||
validation_end = max(int(len(ordered) * 0.85), train_end + 1)
|
||||
validation_end = min(validation_end, len(ordered) - 1)
|
||||
|
||||
train_df = ordered.iloc[:train_end].copy()
|
||||
validation_df = ordered.iloc[train_end:validation_end].copy()
|
||||
holdout_df = ordered.iloc[validation_end:].copy()
|
||||
|
||||
train_df.to_csv(TARGET_DIR / "train.csv", index=False)
|
||||
validation_df.to_csv(TARGET_DIR / "validation.csv", index=False)
|
||||
holdout_df.to_csv(TARGET_DIR / "holdout.csv", index=False)
|
||||
|
||||
meta = {
|
||||
"source": str(SOURCE_CSV),
|
||||
"rows": int(len(ordered)),
|
||||
"train_rows": int(len(train_df)),
|
||||
"validation_rows": int(len(validation_df)),
|
||||
"holdout_rows": int(len(holdout_df)),
|
||||
"rolling_windows": _rolling_windows(ordered),
|
||||
"derived_columns": [
|
||||
"lineup_completeness",
|
||||
"referee_available",
|
||||
"league_reliability",
|
||||
"odds_snapshot_freshness",
|
||||
],
|
||||
"feature_policy": "prediction_time_only",
|
||||
}
|
||||
(TARGET_DIR / "dataset_meta.json").write_text(
|
||||
json.dumps(meta, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print(f"[OK] V26 dataset written to {TARGET_DIR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,305 @@
|
||||
"""
|
||||
V27 Training Data Extraction - Value Sniper
|
||||
Extends V25 to ALL matches with odds (~104K).
|
||||
Adds rolling window, league quality, time, H2H, strength features.
|
||||
Usage: python3 scripts/extract_training_data_v27.py
|
||||
"""
|
||||
import os, sys, csv, time
|
||||
from collections import defaultdict
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, AI_DIR)
|
||||
|
||||
from scripts.extract_training_data import (
|
||||
BatchDataLoader as V25Loader,
|
||||
FeatureExtractor as V25Extractor,
|
||||
FEATURE_COLS as V25_COLS,
|
||||
get_conn,
|
||||
)
|
||||
from features.rolling_features import (
|
||||
calc_rolling_features, calc_league_quality,
|
||||
calc_time_features, calc_advanced_h2h, calc_strength_diff,
|
||||
)
|
||||
|
||||
OUTPUT = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||
os.makedirs(os.path.dirname(OUTPUT), exist_ok=True)
|
||||
|
||||
V27_NEW = [
|
||||
"home_rolling5_goals","home_rolling5_conceded",
|
||||
"home_rolling10_goals","home_rolling10_conceded",
|
||||
"home_rolling20_goals","home_rolling20_conceded",
|
||||
"away_rolling5_goals","away_rolling5_conceded",
|
||||
"away_rolling10_goals","away_rolling10_conceded",
|
||||
"home_rolling5_cs","away_rolling5_cs",
|
||||
"home_venue_goals","home_venue_conceded",
|
||||
"away_venue_goals","away_venue_conceded",
|
||||
"home_goal_trend","away_goal_trend",
|
||||
"league_home_win_rate","league_draw_rate",
|
||||
"league_btts_rate","league_ou25_rate",
|
||||
"league_reliability_score",
|
||||
"home_days_rest","away_days_rest",
|
||||
"match_month","is_season_start","is_season_end",
|
||||
"h2h_home_goals_avg","h2h_away_goals_avg",
|
||||
"h2h_recent_trend","h2h_venue_advantage",
|
||||
"attack_vs_defense_home","attack_vs_defense_away",
|
||||
"xg_diff","form_momentum_interaction",
|
||||
"elo_form_consistency","upset_x_elo_gap",
|
||||
]
|
||||
ALL_COLS = V25_COLS + V27_NEW
|
||||
|
||||
|
||||
class V27Loader(V25Loader):
|
||||
"""Load ALL matches with odds, not just top leagues."""
|
||||
def __init__(self, conn):
|
||||
super().__init__(conn, [])
|
||||
self.league_matches_cache = {}
|
||||
|
||||
def _load_matches(self):
|
||||
self.cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away,
|
||||
m.ht_score_home, m.ht_score_away,
|
||||
m.mst_utc, m.league_id,
|
||||
ht.name, at.name, l.name
|
||||
FROM matches m
|
||||
JOIN teams ht ON m.home_team_id = ht.id
|
||||
JOIN teams at ON m.away_team_id = at.id
|
||||
JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.status='FT' AND m.score_home IS NOT NULL
|
||||
AND m.sport='football'
|
||||
AND EXISTS(SELECT 1 FROM odd_categories oc WHERE oc.match_id=m.id)
|
||||
ORDER BY m.mst_utc ASC
|
||||
""")
|
||||
self.matches = self.cur.fetchall()
|
||||
|
||||
def _load_odds(self):
|
||||
self.cur.execute("""
|
||||
SELECT oc.match_id, oc.name, os.name, os.odd_value
|
||||
FROM odd_selections os
|
||||
JOIN odd_categories oc ON os.odd_category_db_id=oc.db_id
|
||||
JOIN matches m ON oc.match_id=m.id
|
||||
WHERE m.status='FT' AND m.sport='football'
|
||||
""")
|
||||
for mid, cat, sel, val in self.cur.fetchall():
|
||||
try:
|
||||
v = float(val) if val else 0
|
||||
if v <= 0 or not cat or not sel: continue
|
||||
if mid not in self.odds_cache: self.odds_cache[mid] = {}
|
||||
c = cat.lower().strip()
|
||||
s = sel.lower().strip()
|
||||
o = self.odds_cache[mid]
|
||||
if c == 'maç sonucu':
|
||||
if sel=='1': o['ms_h']=v
|
||||
elif sel in('0','X'): o['ms_d']=v
|
||||
elif sel=='2': o['ms_a']=v
|
||||
elif c == '1. yarı sonucu':
|
||||
if sel=='1': o['ht_ms_h']=v
|
||||
elif sel in('0','X'): o['ht_ms_d']=v
|
||||
elif sel=='2': o['ht_ms_a']=v
|
||||
elif c == 'karşılıklı gol':
|
||||
if 'var' in s: o['btts_y']=v
|
||||
elif 'yok' in s: o['btts_n']=v
|
||||
elif c == '2,5 alt/üst':
|
||||
if 'alt' in s: o['ou25_u']=v
|
||||
elif 'üst' in s: o['ou25_o']=v
|
||||
elif c == '1,5 alt/üst':
|
||||
if 'alt' in s: o['ou15_u']=v
|
||||
elif 'üst' in s: o['ou15_o']=v
|
||||
elif c == '3,5 alt/üst':
|
||||
if 'alt' in s: o['ou35_u']=v
|
||||
elif 'üst' in s: o['ou35_o']=v
|
||||
elif c == '0,5 alt/üst':
|
||||
if 'alt' in s: o['ou05_u']=v
|
||||
elif 'üst' in s: o['ou05_o']=v
|
||||
elif c == '1. yarı 0,5 alt/üst':
|
||||
if 'alt' in s: o['ht_ou05_u']=v
|
||||
elif 'üst' in s: o['ht_ou05_o']=v
|
||||
elif c == '1. yarı 1,5 alt/üst':
|
||||
if 'alt' in s: o['ht_ou15_u']=v
|
||||
elif 'üst' in s: o['ht_ou15_o']=v
|
||||
except (ValueError, TypeError): pass
|
||||
|
||||
def _load_league_stats(self):
|
||||
self.cur.execute("""
|
||||
SELECT league_id,
|
||||
AVG(score_home+score_away), AVG(CASE WHEN score_home=0 AND score_away=0 THEN 1.0 ELSE 0.0 END),
|
||||
COUNT(*)
|
||||
FROM matches WHERE status='FT' AND score_home IS NOT NULL AND sport='football'
|
||||
GROUP BY league_id
|
||||
""")
|
||||
for lid, ag, zr, cnt in self.cur.fetchall():
|
||||
self.league_stats_cache[lid] = {
|
||||
"avg_goals": float(ag) if ag else 2.5,
|
||||
"zero_rate": float(zr) if zr else 0.07,
|
||||
"match_count": cnt
|
||||
}
|
||||
|
||||
def _load_squad_data(self):
|
||||
self.cur.execute("""
|
||||
SELECT mpp.match_id, mpp.team_id,
|
||||
COUNT(*) FILTER(WHERE mpp.is_starting=true),
|
||||
COUNT(*),
|
||||
COUNT(*) FILTER(WHERE mpp.is_starting=true
|
||||
AND LOWER(COALESCE(mpp.position::TEXT,''))~'(forward|fwd|forvet|striker)')
|
||||
FROM match_player_participation mpp
|
||||
JOIN matches m ON mpp.match_id=m.id
|
||||
WHERE m.status='FT' AND m.sport='football'
|
||||
GROUP BY mpp.match_id, mpp.team_id
|
||||
""")
|
||||
part = {}
|
||||
for mid,tid,st,tot,fwd in self.cur.fetchall():
|
||||
part[(mid,tid)]={'starting_count':st or 0,'total_squad':tot or 0,'fwd_count':fwd or 0}
|
||||
|
||||
self.cur.execute("""
|
||||
SELECT mpe.match_id, mpe.team_id,
|
||||
COUNT(*) FILTER(WHERE mpe.event_type='goal' AND COALESCE(mpe.event_subtype,'') NOT ILIKE '%%penaltı kaçırma%%'),
|
||||
COUNT(DISTINCT mpe.assist_player_id) FILTER(WHERE mpe.event_type='goal' AND mpe.assist_player_id IS NOT NULL),
|
||||
COUNT(DISTINCT mpe.player_id) FILTER(WHERE mpe.event_type='goal' AND COALESCE(mpe.event_subtype,'') NOT ILIKE '%%penaltı kaçırma%%')
|
||||
FROM match_player_events mpe
|
||||
JOIN matches m ON mpe.match_id=m.id
|
||||
WHERE m.status='FT' AND m.sport='football'
|
||||
GROUP BY mpe.match_id, mpe.team_id
|
||||
""")
|
||||
evts = {}
|
||||
for mid,tid,g,a,sc in self.cur.fetchall():
|
||||
evts[(mid,tid)]={'goals':g or 0,'assists':a or 0,'unique_scorers':sc or 0}
|
||||
|
||||
self.cur.execute("""
|
||||
SELECT mpe.team_id, mpe.player_id, COUNT(*)
|
||||
FROM match_player_events mpe JOIN matches m ON mpe.match_id=m.id
|
||||
WHERE m.status='FT' AND m.sport='football' AND mpe.event_type='goal'
|
||||
AND COALESCE(mpe.event_subtype,'') NOT ILIKE '%%penaltı kaçırma%%'
|
||||
GROUP BY mpe.team_id, mpe.player_id HAVING COUNT(*)>=3
|
||||
""")
|
||||
kp_by_team = defaultdict(set)
|
||||
for tid,pid,_ in self.cur.fetchall(): kp_by_team[tid].add(pid)
|
||||
|
||||
self.cur.execute("""
|
||||
SELECT mpp.match_id, mpp.team_id, mpp.player_id
|
||||
FROM match_player_participation mpp JOIN matches m ON mpp.match_id=m.id
|
||||
WHERE mpp.is_starting=true AND m.status='FT' AND m.sport='football'
|
||||
""")
|
||||
starters = defaultdict(list)
|
||||
for mid,tid,pid in self.cur.fetchall(): starters[(mid,tid)].append(pid)
|
||||
|
||||
for key in set(part)|set(evts):
|
||||
mid,tid = key
|
||||
p = part.get(key,{'starting_count':0,'total_squad':0,'fwd_count':0})
|
||||
e = evts.get(key,{'goals':0,'assists':0,'unique_scorers':0})
|
||||
s = starters.get(key,[])
|
||||
kp_in = sum(1 for x in s if x in kp_by_team.get(tid,set()))
|
||||
kp_tot = len(kp_by_team.get(tid,set()))
|
||||
kp_miss = max(0, kp_tot - kp_in)
|
||||
sq = p['starting_count']*0.3 + e['goals']*2.0 + e['assists']*1.0 + kp_in*3.0 + p['fwd_count']*1.5
|
||||
mi = min(kp_miss/max(kp_tot,1), 1.0)
|
||||
self.squad_cache[key] = {'squad_quality':sq,'key_players':kp_in,'missing_impact':mi,'goals_form':e['goals']}
|
||||
|
||||
def _load_cards_data(self):
|
||||
self.cur.execute("""
|
||||
SELECT mpe.match_id,
|
||||
SUM(CASE WHEN mpe.event_type::text LIKE '%%yellow_card%%' THEN 1
|
||||
WHEN mpe.event_type::text LIKE '%%red_card%%' THEN 2 ELSE 1 END)
|
||||
FROM match_player_events mpe JOIN matches m ON mpe.match_id=m.id
|
||||
WHERE m.status='FT' AND m.sport='football' AND mpe.event_type::text LIKE '%%card%%'
|
||||
GROUP BY mpe.match_id
|
||||
""")
|
||||
for mid, cw in self.cur.fetchall():
|
||||
self.cards_cache[mid] = float(cw) if cw else 0.0
|
||||
|
||||
def load_league_matches(self):
|
||||
for m in self.matches:
|
||||
lid = m[8]
|
||||
if lid not in self.league_matches_cache:
|
||||
self.league_matches_cache[lid] = []
|
||||
self.league_matches_cache[lid].append((m[7],None,m[3],m[4],None))
|
||||
|
||||
|
||||
class V27Extractor(V25Extractor):
|
||||
"""Adds V27 features on top of V25."""
|
||||
def _extract_one(self, mid, hid, aid, sh, sa, hth, hta, mst, lid,
|
||||
hn, an, ln):
|
||||
row = super()._extract_one(mid,hid,aid,sh,sa,hth,hta,mst,lid,hn,an,ln)
|
||||
if not row: return None
|
||||
|
||||
hm = self.loader.team_matches.get(hid,[])
|
||||
am = self.loader.team_matches.get(aid,[])
|
||||
|
||||
hr = calc_rolling_features(hm, mst, True)
|
||||
ar = calc_rolling_features(am, mst, False)
|
||||
for pfx,r in [("home",hr),("away",ar)]:
|
||||
row[f"{pfx}_rolling5_goals"]=r["rolling5_goals_avg"]
|
||||
row[f"{pfx}_rolling5_conceded"]=r["rolling5_conceded_avg"]
|
||||
row[f"{pfx}_rolling10_goals"]=r["rolling10_goals_avg"]
|
||||
row[f"{pfx}_rolling10_conceded"]=r["rolling10_conceded_avg"]
|
||||
row[f"{pfx}_rolling20_goals"]=r["rolling20_goals_avg"]
|
||||
row[f"{pfx}_rolling20_conceded"]=r["rolling20_conceded_avg"]
|
||||
row[f"{pfx}_rolling5_cs"]=r["rolling5_clean_sheets"]
|
||||
row[f"{pfx}_venue_goals"]=r["venue_goals_avg"]
|
||||
row[f"{pfx}_venue_conceded"]=r["venue_conceded_avg"]
|
||||
row[f"{pfx}_goal_trend"]=r["goal_trend"]
|
||||
|
||||
lb = [x for x in self.loader.league_matches_cache.get(lid,[]) if x[0]<mst]
|
||||
lq = calc_league_quality(lb)
|
||||
for k,v in lq.items(): row[k]=v
|
||||
|
||||
ht = calc_time_features(hm, mst)
|
||||
at = calc_time_features(am, mst)
|
||||
row["home_days_rest"]=ht["days_rest"]
|
||||
row["away_days_rest"]=at["days_rest"]
|
||||
row["match_month"]=ht["match_month"]
|
||||
row["is_season_start"]=ht["is_season_start"]
|
||||
row["is_season_end"]=ht["is_season_end"]
|
||||
|
||||
h2h = calc_advanced_h2h(hm, hid, aid, mst)
|
||||
for k,v in h2h.items(): row[k]=v
|
||||
|
||||
sd = calc_strength_diff(
|
||||
{"goals_avg":row.get("home_goals_avg",1.3),"conceded_avg":row.get("home_conceded_avg",1.2),"scoring_rate":row.get("home_scoring_rate",0.75)},
|
||||
{"goals_avg":row.get("away_goals_avg",1.3),"conceded_avg":row.get("away_conceded_avg",1.2),"scoring_rate":row.get("away_scoring_rate",0.75)},
|
||||
self.elo_ratings[hid], self.elo_ratings[aid],
|
||||
row.get("home_momentum_score",0.5), row.get("away_momentum_score",0.5),
|
||||
row.get("upset_potential",0.0),
|
||||
)
|
||||
row.update(sd)
|
||||
return row
|
||||
|
||||
|
||||
def main():
|
||||
print("🚀 V27 Value Sniper — Training Data Extraction")
|
||||
print("="*60)
|
||||
t0 = time.time()
|
||||
conn = get_conn()
|
||||
|
||||
print("\n📦 Loading ALL odds-bearing matches...")
|
||||
loader = V27Loader(conn)
|
||||
loader.load_all()
|
||||
loader.load_league_matches()
|
||||
print(f" Matches: {len(loader.matches)}")
|
||||
print(f" Leagues: {len(loader.league_stats_cache)}")
|
||||
print(f" Odds: {len(loader.odds_cache)}")
|
||||
|
||||
ext = V27Extractor(conn, loader)
|
||||
rows = ext.extract_all()
|
||||
if not rows:
|
||||
print("❌ No data!"); return
|
||||
|
||||
print(f"\n💾 Writing {len(rows)} rows...")
|
||||
with open(OUTPUT,"w",newline="",encoding="utf-8") as f:
|
||||
w = csv.DictWriter(f, fieldnames=ALL_COLS, extrasaction='ignore')
|
||||
w.writeheader(); w.writerows(rows)
|
||||
|
||||
n = len(rows)
|
||||
wo = sum(1 for r in rows if r.get("odds_ms_h",0)>0)
|
||||
md = defaultdict(int)
|
||||
for r in rows: md[r["label_ms"]]+=1
|
||||
print(f"\n📊 Summary:")
|
||||
print(f" Rows: {n}")
|
||||
print(f" With odds: {wo} ({wo/n*100:.1f}%)")
|
||||
print(f" Features: {len(ALL_COLS)} ({len(V25_COLS)} V25 + {len(V27_NEW)} new)")
|
||||
print(f" MS: H={md[0]/n*100:.1f}% D={md[1]/n*100:.1f}% A={md[2]/n*100:.1f}%")
|
||||
print(f" Time: {(time.time()-t0)/60:.1f}min")
|
||||
print(f"\n✅ Done! → {OUTPUT}")
|
||||
conn.close()
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
Strategy Generator — Senin Excel mantığını DB üzerinde otomatize eder.
|
||||
|
||||
Mantık:
|
||||
1. Ev sahibi takım X, evinde oran bandı Y'de oynadığında → OU1.5/OU2.5/BTTS oranları
|
||||
2. Deplasman takım Z, deplasmanda oran bandı W'de oynadığında → OU1.5/OU2.5/BTTS oranları
|
||||
3. İkisi de yüksekse → STRATEJİ ÜRET
|
||||
|
||||
Çıktı: Her maç için hangi bahis oynanabilir, neden, ve geçmiş başarı oranı
|
||||
"""
|
||||
import psycopg2
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
# DB connection
|
||||
conn = psycopg2.connect(
|
||||
host="localhost",
|
||||
port=15432,
|
||||
dbname="boilerplate_db",
|
||||
user="suggestbet",
|
||||
password="SuGGesT2026SecuRe"
|
||||
)
|
||||
|
||||
print("=" * 70)
|
||||
print(" STRATEGY GENERATOR — Veritabanından Strateji Üretimi")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. Tüm biten maçları, takım adları ve MS oranlarıyla çek
|
||||
query = """
|
||||
SELECT
|
||||
m.id as match_id,
|
||||
m.home_team_id,
|
||||
m.away_team_id,
|
||||
m.league_id,
|
||||
m.score_home,
|
||||
m.score_away,
|
||||
m.mst_utc,
|
||||
ht.name as home_team,
|
||||
at.name as away_team,
|
||||
l.name as league_name
|
||||
FROM matches m
|
||||
JOIN teams ht ON m.home_team_id = ht.id
|
||||
JOIN teams at ON m.away_team_id = at.id
|
||||
JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
ORDER BY m.mst_utc ASC
|
||||
"""
|
||||
df = pd.read_sql(query, conn)
|
||||
print(f"\nToplam biten maç: {len(df):,}")
|
||||
|
||||
# 2. Tüm oranları çek (MS, OU25, BTTS, OU15)
|
||||
odds_query = """
|
||||
SELECT
|
||||
oc.match_id,
|
||||
oc.name as market,
|
||||
os.name as selection,
|
||||
CAST(os.odd_value AS DECIMAL) as odds
|
||||
FROM odd_categories oc
|
||||
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id
|
||||
WHERE oc.name IN (
|
||||
'Maç Sonucu',
|
||||
'2,5 Alt/Üst',
|
||||
'1,5 Alt/Üst',
|
||||
'3,5 Alt/Üst',
|
||||
'Karşılıklı Gol'
|
||||
)
|
||||
"""
|
||||
odds_df = pd.read_sql(odds_query, conn)
|
||||
print(f"Toplam oran kaydı: {len(odds_df):,}")
|
||||
|
||||
# Pivot: her maç için oranları sütunlara çevir
|
||||
def get_odds(match_id, market, selection):
|
||||
mask = (odds_df.match_id == match_id) & (odds_df.market == market) & (odds_df.selection == selection)
|
||||
vals = odds_df.loc[mask, 'odds']
|
||||
return float(vals.iloc[0]) if len(vals) > 0 else None
|
||||
|
||||
# Daha verimli: oran lookup dict oluştur
|
||||
print("Oran lookup oluşturuluyor...")
|
||||
odds_lookup = {}
|
||||
for _, row in odds_df.iterrows():
|
||||
key = (row.match_id, row.market, row.selection)
|
||||
odds_lookup[key] = float(row.odds)
|
||||
|
||||
def get_o(mid, market, sel):
|
||||
return odds_lookup.get((mid, market, sel))
|
||||
|
||||
# 3. Her maça oranları ekle
|
||||
print("Maçlara oranlar ekleniyor...")
|
||||
df['odds_ms_h'] = df.match_id.map(lambda x: get_o(x, 'Maç Sonucu', '1'))
|
||||
df['odds_ms_a'] = df.match_id.map(lambda x: get_o(x, 'Maç Sonucu', '2'))
|
||||
df['odds_ms_d'] = df.match_id.map(lambda x: get_o(x, 'Maç Sonucu', '0'))
|
||||
df['odds_ou25_o'] = df.match_id.map(lambda x: get_o(x, '2,5 Alt/Üst', 'Üst'))
|
||||
df['odds_ou25_u'] = df.match_id.map(lambda x: get_o(x, '2,5 Alt/Üst', 'Alt'))
|
||||
df['odds_ou15_o'] = df.match_id.map(lambda x: get_o(x, '1,5 Alt/Üst', 'Üst'))
|
||||
df['odds_ou15_u'] = df.match_id.map(lambda x: get_o(x, '1,5 Alt/Üst', 'Alt'))
|
||||
df['odds_ou35_o'] = df.match_id.map(lambda x: get_o(x, '3,5 Alt/Üst', 'Üst'))
|
||||
df['odds_ou35_u'] = df.match_id.map(lambda x: get_o(x, '3,5 Alt/Üst', 'Alt'))
|
||||
df['odds_btts_y'] = df.match_id.map(lambda x: get_o(x, 'Karşılıklı Gol', 'Var'))
|
||||
df['odds_btts_n'] = df.match_id.map(lambda x: get_o(x, 'Karşılıklı Gol', 'Yok'))
|
||||
|
||||
# Sonuç hesapla
|
||||
df['total_goals'] = df.score_home + df.score_away
|
||||
df['ou15'] = (df.total_goals > 1).astype(int)
|
||||
df['ou25'] = (df.total_goals > 2).astype(int)
|
||||
df['ou35'] = (df.total_goals > 3).astype(int)
|
||||
df['btts'] = ((df.score_home > 0) & (df.score_away > 0)).astype(int)
|
||||
|
||||
print(f"Oranı olan maç sayısı: {df.odds_ms_h.notna().sum():,}")
|
||||
|
||||
# 4. ORAN BANDI fonksiyonu
|
||||
def odds_band(odds):
|
||||
if pd.isna(odds): return None
|
||||
if odds < 1.30: return '1.00-1.30'
|
||||
if odds < 1.50: return '1.30-1.50'
|
||||
if odds < 1.80: return '1.50-1.80'
|
||||
if odds < 2.20: return '1.80-2.20'
|
||||
if odds < 2.80: return '2.20-2.80'
|
||||
if odds < 4.00: return '2.80-4.00'
|
||||
if odds < 6.00: return '4.00-6.00'
|
||||
return '6.00+'
|
||||
|
||||
# 5. STRATEJİ: Expanding window — sadece geçmiş veriye bakarak tahmin
|
||||
print("\n" + "=" * 70)
|
||||
print(" STRATEJİ BACKTEST — Expanding Window")
|
||||
print("=" * 70)
|
||||
|
||||
# Ev sahibi geçmişi: {team_id: {odds_band: [ou15, ou25, btts, ou35, ...]}}
|
||||
home_history = defaultdict(lambda: defaultdict(list))
|
||||
away_history = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
MIN_MATCHES = 8 # Minimum geçmiş maç sayısı
|
||||
TEST_PCT = 0.30 # Son %30 test
|
||||
N = len(df)
|
||||
test_start = int(N * (1 - TEST_PCT))
|
||||
|
||||
results = {
|
||||
'ou15_over': [], 'ou25_over': [], 'ou35_over': [],
|
||||
'btts_yes': [], 'btts_no': [],
|
||||
'ou25_under': [], 'ou15_under': [],
|
||||
'ms_home': []
|
||||
}
|
||||
|
||||
for i in range(N):
|
||||
row = df.iloc[i]
|
||||
h_odds = row.odds_ms_h
|
||||
a_odds = row.odds_ms_a
|
||||
|
||||
if pd.isna(h_odds) or pd.isna(a_odds):
|
||||
continue
|
||||
|
||||
h_band = odds_band(h_odds)
|
||||
a_band = odds_band(a_odds)
|
||||
|
||||
# TEST: sadece test bölümünde bahis yap
|
||||
if i >= test_start:
|
||||
h_hist = home_history[row.home_team_id][h_band]
|
||||
a_hist = away_history[row.away_team_id][a_band]
|
||||
|
||||
if len(h_hist) >= MIN_MATCHES and len(a_hist) >= MIN_MATCHES:
|
||||
# Ev sahibi bu oran bandında ne yapmış?
|
||||
h_ou15 = np.mean([x[0] for x in h_hist])
|
||||
h_ou25 = np.mean([x[1] for x in h_hist])
|
||||
h_ou35 = np.mean([x[2] for x in h_hist])
|
||||
h_btts = np.mean([x[3] for x in h_hist])
|
||||
h_win = np.mean([x[4] for x in h_hist])
|
||||
|
||||
# Deplasman bu oran bandında ne yapmış?
|
||||
a_ou15 = np.mean([x[0] for x in a_hist])
|
||||
a_ou25 = np.mean([x[1] for x in a_hist])
|
||||
a_ou35 = np.mean([x[2] for x in a_hist])
|
||||
a_btts = np.mean([x[3] for x in a_hist])
|
||||
a_loss = np.mean([x[4] for x in a_hist]) # deplasman kaybetme oranı
|
||||
|
||||
# KOMBİNE SİNYAL
|
||||
sig_ou15 = (h_ou15 + a_ou15) / 2
|
||||
sig_ou25 = (h_ou25 + a_ou25) / 2
|
||||
sig_ou35 = (h_ou35 + a_ou35) / 2
|
||||
sig_btts = (h_btts + a_btts) / 2
|
||||
sig_hw = (h_win + a_loss) / 2 # ev kazanma + deplasman kaybetme
|
||||
|
||||
base = {
|
||||
'match': f"{row.home_team} vs {row.away_team}",
|
||||
'league': row.league_name,
|
||||
'home_team': row.home_team,
|
||||
'away_team': row.away_team,
|
||||
'h_band': h_band,
|
||||
'a_band': a_band,
|
||||
'h_n': len(h_hist),
|
||||
'a_n': len(a_hist),
|
||||
}
|
||||
|
||||
# OU 1.5 OVER
|
||||
if sig_ou15 >= 0.85 and row.odds_ou15_o and row.odds_ou15_o > 1.01:
|
||||
results['ou15_over'].append({
|
||||
**base, 'signal': sig_ou15, 'odds': row.odds_ou15_o,
|
||||
'won': row.ou15 == 1, 'actual_goals': row.total_goals,
|
||||
'h_sig': h_ou15, 'a_sig': a_ou15
|
||||
})
|
||||
|
||||
# OU 2.5 OVER
|
||||
if sig_ou25 >= 0.70 and row.odds_ou25_o and row.odds_ou25_o > 1.10:
|
||||
results['ou25_over'].append({
|
||||
**base, 'signal': sig_ou25, 'odds': row.odds_ou25_o,
|
||||
'won': row.ou25 == 1, 'actual_goals': row.total_goals,
|
||||
'h_sig': h_ou25, 'a_sig': a_ou25
|
||||
})
|
||||
|
||||
# OU 3.5 OVER
|
||||
if sig_ou35 >= 0.60 and row.odds_ou35_o and row.odds_ou35_o > 1.20:
|
||||
results['ou35_over'].append({
|
||||
**base, 'signal': sig_ou35, 'odds': row.odds_ou35_o,
|
||||
'won': row.ou35 == 1, 'actual_goals': row.total_goals,
|
||||
'h_sig': h_ou35, 'a_sig': a_ou35
|
||||
})
|
||||
|
||||
# BTTS YES
|
||||
if sig_btts >= 0.70 and row.odds_btts_y and row.odds_btts_y > 1.10:
|
||||
results['btts_yes'].append({
|
||||
**base, 'signal': sig_btts, 'odds': row.odds_btts_y,
|
||||
'won': row.btts == 1, 'actual_goals': row.total_goals,
|
||||
'h_sig': h_btts, 'a_sig': a_btts
|
||||
})
|
||||
|
||||
# OU 2.5 UNDER (düşük gol beklentisi)
|
||||
if sig_ou25 <= 0.30 and row.odds_ou25_u and row.odds_ou25_u > 1.10:
|
||||
results['ou25_under'].append({
|
||||
**base, 'signal': 1-sig_ou25, 'odds': row.odds_ou25_u,
|
||||
'won': row.ou25 == 0, 'actual_goals': row.total_goals,
|
||||
'h_sig': 1-h_ou25, 'a_sig': 1-a_ou25
|
||||
})
|
||||
|
||||
# MS HOME WIN (ev sahibi kazanma)
|
||||
if sig_hw >= 0.75 and row.odds_ms_h and 1.10 < row.odds_ms_h < 3.50:
|
||||
results['ms_home'].append({
|
||||
**base, 'signal': sig_hw, 'odds': row.odds_ms_h,
|
||||
'won': row.score_home > row.score_away,
|
||||
'actual_goals': row.total_goals,
|
||||
'h_sig': h_win, 'a_sig': a_loss
|
||||
})
|
||||
|
||||
# History güncelle (her zaman)
|
||||
home_history[row.home_team_id][h_band].append((
|
||||
row.ou15, row.ou25, row.ou35, row.btts,
|
||||
int(row.score_home > row.score_away)
|
||||
))
|
||||
away_history[row.away_team_id][a_band].append((
|
||||
row.ou15, row.ou25, row.ou35, row.btts,
|
||||
int(row.score_away < row.score_home) # deplasman kaybetme
|
||||
))
|
||||
|
||||
# 6. SONUÇLARI YAZIDIR
|
||||
print(f"\nTest bölümü: son {TEST_PCT*100:.0f}% ({N - test_start:,} maç)")
|
||||
print(f"Minimum geçmiş: {MIN_MATCHES} maç\n")
|
||||
|
||||
for market_name, bets in results.items():
|
||||
if not bets:
|
||||
print(f"\n {market_name}: sinyal yok")
|
||||
continue
|
||||
|
||||
bdf = pd.DataFrame(bets)
|
||||
total = len(bdf)
|
||||
wins = bdf.won.sum()
|
||||
hit = wins / total * 100
|
||||
pnl = (bdf.won * (bdf.odds - 1) - (~bdf.won) * 1).sum()
|
||||
roi = pnl / total * 100
|
||||
avg_odds = bdf.odds.mean()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {market_name.upper()}")
|
||||
print(f"{'='*60}")
|
||||
print(f" Toplam bahis: {total}")
|
||||
print(f" Kazanan: {wins} ({hit:.1f}%)")
|
||||
print(f" Ortalama odds: {avg_odds:.2f}")
|
||||
print(f" PnL: {pnl:+.1f} birim")
|
||||
print(f" ROI: {roi:+.1f}%")
|
||||
|
||||
# Farklı sinyal eşiklerinde performans
|
||||
print(f"\n Sinyal eşik analizi:")
|
||||
for threshold in [0.70, 0.75, 0.80, 0.85, 0.90, 0.95]:
|
||||
sub = bdf[bdf.signal >= threshold]
|
||||
if len(sub) < 5: continue
|
||||
w = sub.won.sum()
|
||||
p = (sub.won * (sub.odds - 1) - (~sub.won) * 1).sum()
|
||||
r = p / len(sub) * 100
|
||||
star = ' ✅ PROFIT' if r > 0 else (' ⚖️ BE' if r > -3 else '')
|
||||
print(f" ≥{threshold:.2f}: {len(sub):5d} bahis, hit={w/len(sub)*100:.1f}%, ROI={r:+.1f}%{star}")
|
||||
|
||||
# En iyi 10 örnek (kazanan)
|
||||
if wins > 0:
|
||||
best = bdf[bdf.won].nlargest(min(5, wins), 'signal')
|
||||
print(f"\n Örnek kazanan bahisler:")
|
||||
for _, b in best.iterrows():
|
||||
print(f" {b.home_team} vs {b.away_team} ({b.league})")
|
||||
print(f" Ev {b.h_band} ({b.h_sig:.0%}) + Dep {b.a_band} ({b.a_sig:.0%}) → sinyal={b.signal:.0%}, odds={b.odds:.2f}, gol={b.actual_goals:.0f}")
|
||||
|
||||
# 7. ÖZET TABLO
|
||||
print("\n\n" + "=" * 70)
|
||||
print(" ÖZET TABLO")
|
||||
print("=" * 70)
|
||||
print(f"{'Market':<15} {'Bahis':>6} {'Hit':>7} {'ROI':>8} {'Avg Odds':>9}")
|
||||
print("-" * 50)
|
||||
for market_name, bets in results.items():
|
||||
if not bets: continue
|
||||
bdf = pd.DataFrame(bets)
|
||||
total = len(bdf)
|
||||
wins = bdf.won.sum()
|
||||
hit = wins / total * 100
|
||||
pnl = (bdf.won * (bdf.odds - 1) - (~bdf.won) * 1).sum()
|
||||
roi = pnl / total * 100
|
||||
avg_odds = bdf.odds.mean()
|
||||
print(f"{market_name:<15} {total:>6} {hit:>6.1f}% {roi:>+7.1f}% {avg_odds:>8.2f}")
|
||||
|
||||
conn.close()
|
||||
print("\n✅ Tamamlandı!")
|
||||
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||
DATA_DIR = AI_ENGINE_DIR / "data" / "v26_shadow"
|
||||
CONFIG_PATH = AI_ENGINE_DIR / "models" / "v26_shadow" / "market_profiles.json"
|
||||
REPORT_PATH = AI_ENGINE_DIR / "reports" / "training_v26_shadow.json"
|
||||
REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _market_accuracy(frame: pd.DataFrame, target_col: str) -> float:
|
||||
if target_col not in frame.columns or frame.empty:
|
||||
return 0.0
|
||||
counts = frame[target_col].value_counts(normalize=True)
|
||||
if counts.empty:
|
||||
return 0.0
|
||||
return round(float(counts.max()), 4)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
train_csv = DATA_DIR / "train.csv"
|
||||
validation_csv = DATA_DIR / "validation.csv"
|
||||
if not train_csv.exists() or not validation_csv.exists():
|
||||
raise SystemExit("Run extract_training_data_v26.py first")
|
||||
|
||||
train_df = pd.read_csv(train_csv)
|
||||
validation_df = pd.read_csv(validation_csv)
|
||||
config = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
||||
report = {
|
||||
"version": config.get("version"),
|
||||
"calibration_version": config.get("calibration_version"),
|
||||
"train_rows": int(len(train_df)),
|
||||
"validation_rows": int(len(validation_df)),
|
||||
"label_priors": {
|
||||
"MS": _market_accuracy(validation_df, "label_ms"),
|
||||
"OU25": _market_accuracy(validation_df, "label_ou25"),
|
||||
"BTTS": _market_accuracy(validation_df, "label_btts"),
|
||||
"HT": _market_accuracy(validation_df, "label_ht_result"),
|
||||
"HTFT": _market_accuracy(validation_df, "label_ht_ft"),
|
||||
"CARDS": _market_accuracy(validation_df, "label_cards_ou45"),
|
||||
},
|
||||
"artifact_path": str(CONFIG_PATH),
|
||||
"notes": [
|
||||
"v26.shadow runtime currently uses artifact-based calibration and ROI gating",
|
||||
"market profile JSON remains the source of truth for runtime thresholds",
|
||||
],
|
||||
}
|
||||
REPORT_PATH.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"[OK] Shadow training report written to {REPORT_PATH}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,480 @@
|
||||
"""
|
||||
V27 Value Sniper — PRO Training Script
|
||||
========================================
|
||||
KEY INSIGHT: Train model WITHOUT odds to get independent probability.
|
||||
Then compare with market odds to find genuine value edges.
|
||||
|
||||
Strategy:
|
||||
Stage A: "Fundamentals Model" — odds-free, learns from ELO/form/rolling/H2H
|
||||
Stage B: "Value Model" — uses fundamentals + odds disagreement as features
|
||||
Stage C: Multi-market — 1X2, O/U 2.5, BTTS
|
||||
Stage D: Walk-forward backtest with Kelly sizing
|
||||
"""
|
||||
import os, sys, json, pickle, time, warnings
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
from sklearn.metrics import accuracy_score, log_loss
|
||||
from sklearn.isotonic import IsotonicRegression
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
AI_DIR = Path(__file__).resolve().parent.parent
|
||||
DATA_CSV = AI_DIR / "data" / "training_data_v27.csv"
|
||||
MODELS_DIR = AI_DIR / "models" / "v27"
|
||||
MODELS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ── Leakage & category definitions ──
|
||||
LEAKAGE_COLS = [
|
||||
"total_goals", "goal_diff", "ht_total_goals", "ht_goal_diff",
|
||||
"score_home", "score_away", "ht_score_home", "ht_score_away",
|
||||
"home_goals_form", "away_goals_form",
|
||||
"home_squad_quality", "away_squad_quality", "squad_diff",
|
||||
"home_key_players", "away_key_players",
|
||||
"home_missing_impact", "away_missing_impact",
|
||||
"referee_home_bias", "referee_avg_goals", "referee_cards_total",
|
||||
"referee_avg_yellow", "referee_avg_red", "referee_penalty_rate",
|
||||
"referee_over25_rate", "referee_experience", "referee_matches",
|
||||
]
|
||||
LABEL_COLS = [c for c in [] ] # populated dynamically
|
||||
META_COLS = ["match_id", "league_name", "home_team", "away_team"]
|
||||
ODDS_COLS_PATTERNS = ["odds_", "implied_"]
|
||||
|
||||
|
||||
def get_odds_cols(df):
|
||||
return [c for c in df.columns if any(c.startswith(p) for p in ODDS_COLS_PATTERNS)]
|
||||
|
||||
|
||||
def get_label_cols(df):
|
||||
return [c for c in df.columns if c.startswith("label_")]
|
||||
|
||||
|
||||
def get_clean_features(df):
|
||||
"""Features with NO odds and NO leakage — pure fundamentals."""
|
||||
odds = set(get_odds_cols(df))
|
||||
labels = set(get_label_cols(df))
|
||||
exclude = odds | labels | set(LEAKAGE_COLS) | set(META_COLS)
|
||||
# Also exclude ID columns
|
||||
exclude |= {c for c in df.columns if c.endswith("_id") and c != "match_id"}
|
||||
feats = [c for c in df.columns if c not in exclude]
|
||||
# Keep only numeric
|
||||
feats = [c for c in feats if pd.to_numeric(df[c], errors="coerce").notna().sum() > len(df)*0.3]
|
||||
return feats
|
||||
|
||||
|
||||
def load_data():
|
||||
print(f"Loading {DATA_CSV}...")
|
||||
df = pd.read_csv(DATA_CSV, low_memory=False)
|
||||
print(f" Raw: {len(df)} rows")
|
||||
|
||||
# Ensure odds exist for value comparison
|
||||
for c in ["odds_ms_h","odds_ms_d","odds_ms_a"]:
|
||||
df[c] = pd.to_numeric(df[c], errors="coerce")
|
||||
df = df.dropna(subset=["odds_ms_h","odds_ms_d","odds_ms_a"])
|
||||
df = df[(df.odds_ms_h>1.01)&(df.odds_ms_d>1.01)&(df.odds_ms_a>1.01)]
|
||||
|
||||
# OU25 odds
|
||||
for c in ["odds_ou25_over","odds_ou25_under"]:
|
||||
if c in df.columns:
|
||||
df[c] = pd.to_numeric(df[c], errors="coerce")
|
||||
|
||||
# Implied probabilities
|
||||
margin = 1/df.odds_ms_h + 1/df.odds_ms_d + 1/df.odds_ms_a
|
||||
df["implied_h"] = (1/df.odds_ms_h)/margin
|
||||
df["implied_d"] = (1/df.odds_ms_d)/margin
|
||||
df["implied_a"] = (1/df.odds_ms_a)/margin
|
||||
|
||||
print(f" After filter: {len(df)} rows")
|
||||
return df
|
||||
|
||||
|
||||
def temporal_split(df, val_ratio=0.15, test_ratio=0.10):
|
||||
n = len(df)
|
||||
tr = int(n*(1-val_ratio-test_ratio))
|
||||
va = int(n*(1-test_ratio))
|
||||
return df.iloc[:tr].copy(), df.iloc[tr:va].copy(), df.iloc[va:].copy()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# STAGE A: Fundamentals-Only Model (NO ODDS)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def train_fundamentals_model(X_tr, y_tr, X_va, y_va, feat_cols, market="ms"):
|
||||
"""Train ensemble WITHOUT odds features."""
|
||||
models = {}
|
||||
n_class = 3 if market == "ms" else 2
|
||||
|
||||
# XGBoost
|
||||
try:
|
||||
import xgboost as xgb
|
||||
print(f" [XGB] Training {market.upper()}...")
|
||||
dtrain = xgb.DMatrix(X_tr, label=y_tr, feature_names=feat_cols)
|
||||
dval = xgb.DMatrix(X_va, label=y_va, feature_names=feat_cols)
|
||||
params = {
|
||||
"objective": "multi:softprob" if n_class==3 else "binary:logistic",
|
||||
"eval_metric": "mlogloss" if n_class==3 else "logloss",
|
||||
"max_depth": 6, "learning_rate": 0.02, "subsample": 0.75,
|
||||
"colsample_bytree": 0.75, "min_child_weight": 10,
|
||||
"reg_alpha": 0.5, "reg_lambda": 2.0,
|
||||
"verbosity": 0, "tree_method": "hist",
|
||||
}
|
||||
if n_class == 3:
|
||||
params["num_class"] = 3
|
||||
m = xgb.train(params, dtrain, num_boost_round=2000,
|
||||
evals=[(dval,"val")], early_stopping_rounds=80,
|
||||
verbose_eval=False)
|
||||
p = m.predict(dval)
|
||||
if n_class == 2:
|
||||
p = np.column_stack([1-p, p])
|
||||
acc = accuracy_score(y_va, p.argmax(1))
|
||||
print(f" acc={acc:.4f}")
|
||||
models["xgb"] = m
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# LightGBM
|
||||
try:
|
||||
import lightgbm as lgb
|
||||
print(f" [LGB] Training {market.upper()}...")
|
||||
ds_tr = lgb.Dataset(X_tr, label=y_tr)
|
||||
ds_va = lgb.Dataset(X_va, label=y_va, reference=ds_tr)
|
||||
par = {
|
||||
"objective": "multiclass" if n_class==3 else "binary",
|
||||
"metric": "multi_logloss" if n_class==3 else "binary_logloss",
|
||||
"num_leaves": 48, "learning_rate": 0.02,
|
||||
"feature_fraction": 0.7, "bagging_fraction": 0.7,
|
||||
"bagging_freq": 1, "min_child_samples": 30,
|
||||
"lambda_l1": 0.5, "lambda_l2": 2.0, "verbose": -1,
|
||||
}
|
||||
if n_class == 3:
|
||||
par["num_class"] = 3
|
||||
m = lgb.train(par, ds_tr, 2000, valid_sets=[ds_va],
|
||||
callbacks=[lgb.early_stopping(80, verbose=False)])
|
||||
p = m.predict(X_va)
|
||||
if n_class == 2:
|
||||
p = np.column_stack([1-p, p])
|
||||
acc = accuracy_score(y_va, p.argmax(1))
|
||||
print(f" acc={acc:.4f}")
|
||||
models["lgb"] = m
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# CatBoost
|
||||
try:
|
||||
from catboost import CatBoostClassifier
|
||||
print(f" [CB] Training {market.upper()}...")
|
||||
m = CatBoostClassifier(
|
||||
iterations=2000, learning_rate=0.02, depth=6,
|
||||
l2_leaf_reg=5, loss_function="MultiClass" if n_class==3 else "Logloss",
|
||||
early_stopping_rounds=80, verbose=0, task_type="CPU",
|
||||
**({"classes_count": 3} if n_class==3 else {}),
|
||||
)
|
||||
m.fit(X_tr, y_tr, eval_set=(X_va, y_va))
|
||||
p = m.predict_proba(X_va)
|
||||
acc = accuracy_score(y_va, p.argmax(1))
|
||||
print(f" acc={acc:.4f}")
|
||||
models["cb"] = m
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return models
|
||||
|
||||
|
||||
def ensemble_predict(models, X, feat_cols, n_class=3):
|
||||
preds = []
|
||||
for name, m in models.items():
|
||||
if name == "xgb":
|
||||
import xgboost as xgb
|
||||
dm = xgb.DMatrix(X, feature_names=feat_cols)
|
||||
p = m.predict(dm)
|
||||
if n_class == 2 and p.ndim == 1:
|
||||
p = np.column_stack([1-p, p])
|
||||
elif name == "lgb":
|
||||
p = m.predict(X)
|
||||
if n_class == 2 and p.ndim == 1:
|
||||
p = np.column_stack([1-p, p])
|
||||
elif name == "cb":
|
||||
p = m.predict_proba(X)
|
||||
preds.append(np.array(p))
|
||||
if not preds:
|
||||
raise RuntimeError("No models!")
|
||||
return np.mean(preds, axis=0)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# STAGE B: Walk-Forward Backtest with Kelly
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def kelly_fraction(model_prob, odds, fraction=0.25):
|
||||
"""Fractional Kelly: f = fraction * (p*odds - 1) / (odds - 1)"""
|
||||
edge = model_prob * odds - 1
|
||||
if edge <= 0 or odds <= 1:
|
||||
return 0.0
|
||||
f = edge / (odds - 1)
|
||||
return max(0, min(fraction * f, 0.10)) # cap at 10% bankroll
|
||||
|
||||
|
||||
def backtest_value(models, df_test, feat_cols, market="ms",
|
||||
min_edge=0.05, min_odds=1.40, max_odds=4.50,
|
||||
use_kelly=True):
|
||||
"""Realistic backtest: flat or Kelly sizing, edge filtering."""
|
||||
X = df_test[feat_cols].values
|
||||
n_class = 3 if market == "ms" else 2
|
||||
probs = ensemble_predict(models, X, feat_cols, n_class)
|
||||
|
||||
if market == "ms":
|
||||
y = df_test["label_ms"].values
|
||||
odds_arr = df_test[["odds_ms_h","odds_ms_d","odds_ms_a"]].values
|
||||
implied = df_test[["implied_h","implied_d","implied_a"]].values
|
||||
class_names = ["Home","Draw","Away"]
|
||||
elif market == "ou25":
|
||||
if "label_ou25" not in df_test.columns:
|
||||
return {}
|
||||
y = df_test["label_ou25"].values
|
||||
o_over = pd.to_numeric(df_test["odds_ou25_o"], errors="coerce").fillna(1.85).values if "odds_ou25_o" in df_test.columns else np.full(len(df_test), 1.85)
|
||||
o_under = pd.to_numeric(df_test["odds_ou25_u"], errors="coerce").fillna(1.85).values if "odds_ou25_u" in df_test.columns else np.full(len(df_test), 1.85)
|
||||
odds_arr = np.column_stack([o_under, o_over])
|
||||
m = 1/odds_arr
|
||||
implied = m / m.sum(axis=1, keepdims=True)
|
||||
class_names = ["Under","Over"]
|
||||
else:
|
||||
return {}
|
||||
|
||||
results = {"bets": [], "total": 0, "wins": 0, "pnl": 0.0, "bankroll_curve": [1000.0]}
|
||||
bankroll = 1000.0
|
||||
|
||||
for i in range(len(y)):
|
||||
for cls in range(n_class):
|
||||
edge = probs[i, cls] - implied[i, cls]
|
||||
odds_val = odds_arr[i, cls]
|
||||
|
||||
# FILTERS
|
||||
if edge < min_edge:
|
||||
continue
|
||||
if odds_val < min_odds or odds_val > max_odds:
|
||||
continue
|
||||
# Don't bet on heavy favorites with tiny edge
|
||||
if implied[i, cls] > 0.65 and edge < 0.08:
|
||||
continue
|
||||
|
||||
# Sizing
|
||||
if use_kelly:
|
||||
frac = kelly_fraction(probs[i, cls], odds_val, fraction=0.15)
|
||||
stake = bankroll * frac
|
||||
else:
|
||||
stake = 10.0 # flat
|
||||
|
||||
if stake < 1:
|
||||
continue
|
||||
|
||||
won = (y[i] == cls)
|
||||
pnl = stake * (odds_val - 1) if won else -stake
|
||||
bankroll += pnl
|
||||
|
||||
results["bets"].append({
|
||||
"edge": float(edge), "odds": float(odds_val),
|
||||
"model_p": float(probs[i,cls]), "implied_p": float(implied[i,cls]),
|
||||
"won": bool(won), "pnl": float(pnl), "stake": float(stake),
|
||||
"class": class_names[cls],
|
||||
})
|
||||
results["bankroll_curve"].append(bankroll)
|
||||
results["total"] += 1
|
||||
if won:
|
||||
results["wins"] += 1
|
||||
results["pnl"] = bankroll - 1000.0
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def print_backtest(results, label=""):
|
||||
total = results.get("total", 0)
|
||||
if total == 0:
|
||||
print(f" {label}: No bets placed")
|
||||
return
|
||||
wins = results["wins"]
|
||||
pnl = results["pnl"]
|
||||
hit = wins/total*100
|
||||
roi = pnl / sum(b["stake"] for b in results["bets"]) * 100
|
||||
curve = results["bankroll_curve"]
|
||||
peak = max(curve)
|
||||
dd = min((c - peak) / peak * 100 for c in curve if c <= peak) if len(curve) > 1 else 0
|
||||
|
||||
# Per-class breakdown
|
||||
by_class = {}
|
||||
for b in results["bets"]:
|
||||
cls = b["class"]
|
||||
if cls not in by_class:
|
||||
by_class[cls] = {"n": 0, "w": 0, "pnl": 0}
|
||||
by_class[cls]["n"] += 1
|
||||
if b["won"]:
|
||||
by_class[cls]["w"] += 1
|
||||
by_class[cls]["pnl"] += b["pnl"]
|
||||
|
||||
print(f"\n {label}")
|
||||
print(f" Bets: {total} | Hit: {hit:.1f}% | ROI: {roi:+.1f}%")
|
||||
print(f" PnL: {pnl:+.0f} | Final: {curve[-1]:.0f} | MaxDD: {dd:.1f}%")
|
||||
for cls, d in sorted(by_class.items()):
|
||||
r = d["pnl"]/d["n"]*100 if d["n"] > 0 else 0
|
||||
print(f" {cls:6s}: {d['n']:4d} bets, "
|
||||
f"hit={d['w']/d['n']*100:.1f}%, avg_pnl={r:+.1f}%")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# MAIN
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def main():
|
||||
print("=" * 65)
|
||||
print(" V27 VALUE SNIPER — PRO TRAINING (Odds-Free Fundamentals)")
|
||||
print("=" * 65)
|
||||
t0 = time.time()
|
||||
|
||||
df = load_data()
|
||||
clean_feats = get_clean_features(df)
|
||||
print(f" Clean features (no odds): {len(clean_feats)}")
|
||||
|
||||
# Numerify
|
||||
for c in clean_feats:
|
||||
df[c] = pd.to_numeric(df[c], errors="coerce")
|
||||
df[clean_feats] = df[clean_feats].fillna(df[clean_feats].median())
|
||||
|
||||
# Remove constant columns
|
||||
clean_feats = [c for c in clean_feats if df[c].nunique() > 1]
|
||||
print(f" After removing constants: {len(clean_feats)}")
|
||||
|
||||
# Split
|
||||
tr, va, te = temporal_split(df)
|
||||
print(f" Train: {len(tr)}, Val: {len(va)}, Test: {len(te)}")
|
||||
print(f" Target: H={tr.label_ms.eq(0).mean():.1%}, "
|
||||
f"D={tr.label_ms.eq(1).mean():.1%}, A={tr.label_ms.eq(2).mean():.1%}")
|
||||
|
||||
X_tr = tr[clean_feats].values
|
||||
y_tr = tr["label_ms"].values
|
||||
X_va = va[clean_feats].values
|
||||
y_va = va["label_ms"].values
|
||||
|
||||
# ── STAGE A: Train fundamentals model (1X2) ──
|
||||
print("\n" + "─"*65)
|
||||
print(" STAGE A: Fundamentals-Only 1X2 Model")
|
||||
print("─"*65)
|
||||
ms_models = train_fundamentals_model(X_tr, y_tr, X_va, y_va, clean_feats, "ms")
|
||||
|
||||
val_probs = ensemble_predict(ms_models, X_va, clean_feats, 3)
|
||||
val_acc = accuracy_score(y_va, val_probs.argmax(1))
|
||||
val_ll = log_loss(y_va, val_probs)
|
||||
print(f"\n Ensemble Val: acc={val_acc:.4f}, logloss={val_ll:.4f}")
|
||||
|
||||
# Compare with odds baseline
|
||||
odds_pred = va[["implied_h","implied_d","implied_a"]].values.argmax(1)
|
||||
odds_acc = accuracy_score(y_va, odds_pred)
|
||||
print(f" Odds baseline: acc={odds_acc:.4f}")
|
||||
print(f" Model vs Odds: {val_acc - odds_acc:+.4f}")
|
||||
|
||||
# ── STAGE B: O/U 2.5 Model ──
|
||||
ou_models = None
|
||||
if "label_ou25" in tr.columns:
|
||||
print("\n" + "─"*65)
|
||||
print(" STAGE A.2: Fundamentals-Only O/U 2.5 Model")
|
||||
print("─"*65)
|
||||
y_tr_ou = tr["label_ou25"].values
|
||||
y_va_ou = va["label_ou25"].values
|
||||
mask_tr = ~np.isnan(y_tr_ou)
|
||||
mask_va = ~np.isnan(y_va_ou)
|
||||
if mask_tr.sum() > 1000:
|
||||
ou_models = train_fundamentals_model(
|
||||
X_tr[mask_tr], y_tr_ou[mask_tr].astype(int),
|
||||
X_va[mask_va], y_va_ou[mask_va].astype(int),
|
||||
clean_feats, "ou25")
|
||||
|
||||
# ── STAGE C: Backtest ──
|
||||
print("\n" + "─"*65)
|
||||
print(" STAGE B: Walk-Forward Backtest (Test Set)")
|
||||
print("─"*65)
|
||||
|
||||
# Try multiple edge thresholds
|
||||
best_roi = -999
|
||||
best_cfg = {}
|
||||
for min_edge in [0.03, 0.05, 0.07, 0.10, 0.12, 0.15]:
|
||||
for min_odds in [1.35, 1.50, 1.70]:
|
||||
r = backtest_value(ms_models, te, clean_feats, "ms",
|
||||
min_edge=min_edge, min_odds=min_odds,
|
||||
max_odds=5.0, use_kelly=True)
|
||||
if r.get("total", 0) >= 20:
|
||||
invested = sum(b["stake"] for b in r["bets"])
|
||||
roi = r["pnl"] / invested * 100 if invested > 0 else -100
|
||||
if roi > best_roi:
|
||||
best_roi = roi
|
||||
best_cfg = {"edge": min_edge, "min_odds": min_odds, "result": r}
|
||||
|
||||
if best_cfg:
|
||||
cfg = best_cfg
|
||||
print(f"\n Best 1X2 Config: edge>{cfg['edge']}, odds>{cfg['min_odds']}")
|
||||
print_backtest(cfg["result"], "1X2 VALUE")
|
||||
|
||||
# Flat bet comparison
|
||||
print("\n --- Flat Bet Comparison ---")
|
||||
for edge in [0.05, 0.07, 0.10]:
|
||||
r = backtest_value(ms_models, te, clean_feats, "ms",
|
||||
min_edge=edge, min_odds=1.50, max_odds=4.5,
|
||||
use_kelly=False)
|
||||
if r.get("total", 0) > 0:
|
||||
inv = r["total"] * 10
|
||||
roi = r["pnl"]/inv*100
|
||||
print(f" Edge>{edge:.2f}: {r['total']} bets, "
|
||||
f"hit={r['wins']/r['total']*100:.1f}%, ROI={roi:+.1f}%")
|
||||
|
||||
# OU25 backtest
|
||||
if ou_models:
|
||||
print("\n --- O/U 2.5 Backtest ---")
|
||||
for edge in [0.05, 0.07, 0.10]:
|
||||
r = backtest_value(ou_models, te, clean_feats, "ou25",
|
||||
min_edge=edge, min_odds=1.50, max_odds=3.0,
|
||||
use_kelly=True)
|
||||
if r.get("total", 0) > 0:
|
||||
print_backtest(r, f"OU25 edge>{edge}")
|
||||
|
||||
# ── Feature importance ──
|
||||
if "lgb" in ms_models:
|
||||
imp = ms_models["lgb"].feature_importance(importance_type="gain")
|
||||
imp_df = pd.DataFrame({"feature": clean_feats, "importance": imp}
|
||||
).sort_values("importance", ascending=False)
|
||||
print("\n TOP 15 FEATURES (no odds!):")
|
||||
for _, r in imp_df.head(15).iterrows():
|
||||
print(f" {r['feature']:40s} {r['importance']:.0f}")
|
||||
imp_df.to_csv(MODELS_DIR / "v27_feature_importance.csv", index=False)
|
||||
|
||||
# ── Save ──
|
||||
print("\n" + "─"*65)
|
||||
print(" SAVING MODELS")
|
||||
print("─"*65)
|
||||
for name, m in ms_models.items():
|
||||
p = MODELS_DIR / f"v27_ms_{name}.pkl"
|
||||
with open(p, "wb") as f:
|
||||
pickle.dump(m, f)
|
||||
print(f" ✓ {p.name}")
|
||||
|
||||
if ou_models:
|
||||
for name, m in ou_models.items():
|
||||
p = MODELS_DIR / f"v27_ou25_{name}.pkl"
|
||||
with open(p, "wb") as f:
|
||||
pickle.dump(m, f)
|
||||
print(f" ✓ {p.name}")
|
||||
|
||||
meta = {
|
||||
"version": "v27-pro", "trained_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"approach": "odds-free fundamentals + value edge detection",
|
||||
"feature_count": len(clean_feats),
|
||||
"total_samples": len(df),
|
||||
"val_acc": round(val_acc, 4), "val_ll": round(val_ll, 4),
|
||||
"best_config": {k: v for k, v in best_cfg.items() if k != "result"} if best_cfg else {},
|
||||
"markets": ["ms"] + (["ou25"] if ou_models else []),
|
||||
}
|
||||
with open(MODELS_DIR / "v27_metadata.json", "w") as f:
|
||||
json.dump(meta, f, indent=2, default=str)
|
||||
with open(MODELS_DIR / "v27_feature_cols.json", "w") as f:
|
||||
json.dump(clean_feats, f, indent=2)
|
||||
print(f" ✓ metadata + feature_cols")
|
||||
|
||||
print(f"\n Total time: {(time.time()-t0)/60:.1f} min")
|
||||
print(" DONE!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -36,6 +36,11 @@ class FeatureEnrichmentService:
|
||||
'avg_goals': 2.5,
|
||||
'btts_rate': 0.5,
|
||||
'over25_rate': 0.5,
|
||||
# V27 expanded
|
||||
'home_goals_avg': 1.3,
|
||||
'away_goals_avg': 1.1,
|
||||
'recent_trend': 0.0,
|
||||
'venue_advantage': 0.0,
|
||||
}
|
||||
_DEFAULT_FORM = {
|
||||
'clean_sheet_rate': 0.2,
|
||||
@@ -53,6 +58,25 @@ class FeatureEnrichmentService:
|
||||
_DEFAULT_LEAGUE = {
|
||||
'avg_goals': 2.7,
|
||||
'zero_goal_rate': 0.07,
|
||||
# V27 expanded
|
||||
'home_win_rate': 0.46,
|
||||
'draw_rate': 0.26,
|
||||
'btts_rate': 0.50,
|
||||
'ou25_rate': 0.50,
|
||||
'reliability_score': 0.0,
|
||||
}
|
||||
_DEFAULT_ROLLING = {
|
||||
'rolling5_goals': 1.3,
|
||||
'rolling5_conceded': 1.2,
|
||||
'rolling10_goals': 1.3,
|
||||
'rolling10_conceded': 1.2,
|
||||
'rolling20_goals': 1.3,
|
||||
'rolling20_conceded': 1.2,
|
||||
'rolling5_cs': 0.2,
|
||||
}
|
||||
_DEFAULT_VENUE = {
|
||||
'venue_goals': 1.4,
|
||||
'venue_conceded': 1.1,
|
||||
}
|
||||
|
||||
# ─── 1. Team Stats ──────────────────────────────────────────────
|
||||
@@ -186,6 +210,13 @@ class FeatureEnrichmentService:
|
||||
total_goals = 0
|
||||
btts_count = 0
|
||||
over25_count = 0
|
||||
# V27 expanded trackers
|
||||
home_team_goals_list = []
|
||||
away_team_goals_list = []
|
||||
home_team_venue_wins = 0
|
||||
home_team_venue_total = 0
|
||||
away_team_venue_wins = 0
|
||||
away_team_venue_total = 0
|
||||
|
||||
for row in rows:
|
||||
sh = int(row['score_home'])
|
||||
@@ -195,14 +226,22 @@ class FeatureEnrichmentService:
|
||||
|
||||
# Normalise: who is "home team" in THIS prediction context
|
||||
if str(row['home_team_id']) == home_team_id:
|
||||
home_team_goals_list.append(sh)
|
||||
away_team_goals_list.append(sa)
|
||||
home_team_venue_total += 1
|
||||
if sh > sa:
|
||||
home_wins += 1
|
||||
home_team_venue_wins += 1
|
||||
elif sh == sa:
|
||||
draws += 1
|
||||
else:
|
||||
# Reversed fixture: away_team was at home
|
||||
home_team_goals_list.append(sa)
|
||||
away_team_goals_list.append(sh)
|
||||
away_team_venue_total += 1
|
||||
if sa > sh:
|
||||
home_wins += 1
|
||||
away_team_venue_wins += 1
|
||||
elif sh == sa:
|
||||
draws += 1
|
||||
|
||||
@@ -211,6 +250,29 @@ class FeatureEnrichmentService:
|
||||
if match_goals > 2:
|
||||
over25_count += 1
|
||||
|
||||
# V27: recent_trend = last-5 home_win_rate - first-5 home_win_rate
|
||||
recent_trend = 0.0
|
||||
if total >= 6:
|
||||
recent_5_wins = sum(
|
||||
1 for r in rows[:5]
|
||||
if (str(r['home_team_id']) == home_team_id and int(r['score_home']) > int(r['score_away']))
|
||||
or (str(r['home_team_id']) != home_team_id and int(r['score_away']) > int(r['score_home']))
|
||||
)
|
||||
older_5_wins = sum(
|
||||
1 for r in rows[-5:]
|
||||
if (str(r['home_team_id']) == home_team_id and int(r['score_home']) > int(r['score_away']))
|
||||
or (str(r['home_team_id']) != home_team_id and int(r['score_away']) > int(r['score_home']))
|
||||
)
|
||||
recent_trend = (recent_5_wins - older_5_wins) / 5.0
|
||||
|
||||
# V27: venue_advantage = home_win_rate_at_home - home_win_rate_away
|
||||
venue_advantage = 0.0
|
||||
if home_team_venue_total > 0 and away_team_venue_total > 0:
|
||||
venue_advantage = (
|
||||
home_team_venue_wins / home_team_venue_total
|
||||
- away_team_venue_wins / away_team_venue_total
|
||||
)
|
||||
|
||||
return {
|
||||
'total_matches': total,
|
||||
'home_win_rate': home_wins / total,
|
||||
@@ -218,6 +280,11 @@ class FeatureEnrichmentService:
|
||||
'avg_goals': total_goals / total,
|
||||
'btts_rate': btts_count / total,
|
||||
'over25_rate': over25_count / total,
|
||||
# V27 expanded
|
||||
'home_goals_avg': _safe_avg(home_team_goals_list, 1.3),
|
||||
'away_goals_avg': _safe_avg(away_team_goals_list, 1.1),
|
||||
'recent_trend': round(recent_trend, 4),
|
||||
'venue_advantage': round(venue_advantage, 4),
|
||||
}
|
||||
|
||||
# ─── 3. Form & Streaks ──────────────────────────────────────────
|
||||
@@ -433,6 +500,10 @@ class FeatureEnrichmentService:
|
||||
total = len(rows)
|
||||
total_goals = 0
|
||||
zero_goal_matches = 0
|
||||
home_wins = 0
|
||||
draw_count = 0
|
||||
btts_count = 0
|
||||
over25_count = 0
|
||||
|
||||
for row in rows:
|
||||
sh = int(row['score_home'])
|
||||
@@ -441,10 +512,24 @@ class FeatureEnrichmentService:
|
||||
total_goals += match_goals
|
||||
if match_goals == 0:
|
||||
zero_goal_matches += 1
|
||||
if sh > sa:
|
||||
home_wins += 1
|
||||
elif sh == sa:
|
||||
draw_count += 1
|
||||
if sh > 0 and sa > 0:
|
||||
btts_count += 1
|
||||
if match_goals > 2:
|
||||
over25_count += 1
|
||||
|
||||
return {
|
||||
'avg_goals': total_goals / total,
|
||||
'zero_goal_rate': zero_goal_matches / total,
|
||||
# V27 expanded
|
||||
'home_win_rate': home_wins / total,
|
||||
'draw_rate': draw_count / total,
|
||||
'btts_rate': btts_count / total,
|
||||
'ou25_rate': over25_count / total,
|
||||
'reliability_score': min(total / 50.0, 1.0),
|
||||
}
|
||||
|
||||
# ─── 6. Momentum ───────────────────────────────────────────────
|
||||
@@ -514,6 +599,161 @@ class FeatureEnrichmentService:
|
||||
return round(weighted_score / max_possible, 4)
|
||||
|
||||
|
||||
# ─── 7. Rolling Stats (V27) ─────────────────────────────────────
|
||||
|
||||
def compute_rolling_stats(
|
||||
self,
|
||||
cur: RealDictCursor,
|
||||
team_id: str,
|
||||
before_date_ms: int,
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Rolling goal averages and clean-sheet rates over the last 5/10/20 matches.
|
||||
Single DB query, three windows computed programmatically.
|
||||
"""
|
||||
if not team_id:
|
||||
return dict(self._DEFAULT_ROLLING)
|
||||
try:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
m.home_team_id,
|
||||
m.score_home,
|
||||
m.score_away
|
||||
FROM matches m
|
||||
WHERE (m.home_team_id = %s OR m.away_team_id = %s)
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
AND m.mst_utc < %s
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 20
|
||||
""",
|
||||
(team_id, team_id, before_date_ms),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
except Exception:
|
||||
return dict(self._DEFAULT_ROLLING)
|
||||
|
||||
if not rows:
|
||||
return dict(self._DEFAULT_ROLLING)
|
||||
|
||||
goals = []
|
||||
conceded = []
|
||||
clean_sheets = []
|
||||
|
||||
for row in rows:
|
||||
is_home = str(row['home_team_id']) == team_id
|
||||
gf = int(row['score_home'] if is_home else row['score_away'])
|
||||
ga = int(row['score_away'] if is_home else row['score_home'])
|
||||
goals.append(gf)
|
||||
conceded.append(ga)
|
||||
clean_sheets.append(1 if ga == 0 else 0)
|
||||
|
||||
n = len(goals)
|
||||
return {
|
||||
'rolling5_goals': _safe_avg(goals[:5], 1.3),
|
||||
'rolling5_conceded': _safe_avg(conceded[:5], 1.2),
|
||||
'rolling10_goals': _safe_avg(goals[:min(10, n)], 1.3),
|
||||
'rolling10_conceded': _safe_avg(conceded[:min(10, n)], 1.2),
|
||||
'rolling20_goals': _safe_avg(goals[:n], 1.3),
|
||||
'rolling20_conceded': _safe_avg(conceded[:n], 1.2),
|
||||
'rolling5_cs': _safe_avg(clean_sheets[:5], 0.2),
|
||||
}
|
||||
|
||||
# ─── 8. Venue Stats (V27) ──────────────────────────────────────
|
||||
|
||||
def compute_venue_stats(
|
||||
self,
|
||||
cur: RealDictCursor,
|
||||
team_id: str,
|
||||
before_date_ms: int,
|
||||
is_home: bool = True,
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Team goals scored/conceded at specific venue (home or away only).
|
||||
"""
|
||||
if not team_id:
|
||||
return dict(self._DEFAULT_VENUE)
|
||||
venue_col = 'home_team_id' if is_home else 'away_team_id'
|
||||
try:
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT m.score_home, m.score_away
|
||||
FROM matches m
|
||||
WHERE m.{venue_col} = %s
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
AND m.mst_utc < %s
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 20
|
||||
""",
|
||||
(team_id, before_date_ms),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
except Exception:
|
||||
return dict(self._DEFAULT_VENUE)
|
||||
|
||||
if not rows:
|
||||
return dict(self._DEFAULT_VENUE)
|
||||
|
||||
goals = []
|
||||
conceded_list = []
|
||||
for row in rows:
|
||||
sh = int(row['score_home'])
|
||||
sa = int(row['score_away'])
|
||||
if is_home:
|
||||
goals.append(sh)
|
||||
conceded_list.append(sa)
|
||||
else:
|
||||
goals.append(sa)
|
||||
conceded_list.append(sh)
|
||||
|
||||
return {
|
||||
'venue_goals': _safe_avg(goals, 1.4),
|
||||
'venue_conceded': _safe_avg(conceded_list, 1.1),
|
||||
}
|
||||
|
||||
# ─── 9. Days Rest (V27) ────────────────────────────────────────
|
||||
|
||||
def compute_days_rest(
|
||||
self,
|
||||
cur: RealDictCursor,
|
||||
team_id: str,
|
||||
before_date_ms: int,
|
||||
) -> float:
|
||||
"""
|
||||
Returns number of days since the team's last match.
|
||||
Default: 7.0 (one-week rest).
|
||||
"""
|
||||
if not team_id:
|
||||
return 7.0
|
||||
try:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.mst_utc
|
||||
FROM matches m
|
||||
WHERE (m.home_team_id = %s OR m.away_team_id = %s)
|
||||
AND m.status = 'FT'
|
||||
AND m.mst_utc < %s
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(team_id, team_id, before_date_ms),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
except Exception:
|
||||
return 7.0
|
||||
|
||||
if not row or not row.get('mst_utc'):
|
||||
return 7.0
|
||||
|
||||
last_match_ms = int(row['mst_utc'])
|
||||
diff_days = (before_date_ms - last_match_ms) / (1000 * 86400)
|
||||
return round(max(0.0, min(diff_days, 30.0)), 1)
|
||||
|
||||
|
||||
# ─── Utility ────────────────────────────────────────────────────────
|
||||
|
||||
def _safe_avg(values: list, default: float) -> float:
|
||||
|
||||
@@ -15,6 +15,7 @@ import json
|
||||
import re
|
||||
import time
|
||||
import math
|
||||
import os
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
@@ -27,12 +28,15 @@ from psycopg2.extras import RealDictCursor
|
||||
from data.db import get_clean_dsn
|
||||
from models.v20_ensemble import FullMatchPrediction
|
||||
from models.v25_ensemble import V25Predictor, get_v25_predictor
|
||||
from models.v27_predictor import V27Predictor, compute_divergence, compute_value_edge
|
||||
from features.odds_band_analyzer import OddsBandAnalyzer
|
||||
from models.basketball_v25 import (
|
||||
BasketballMatchPrediction,
|
||||
get_basketball_v25_predictor,
|
||||
)
|
||||
from core.engines.player_predictor import PlayerPrediction, get_player_predictor
|
||||
from services.feature_enrichment import FeatureEnrichmentService
|
||||
from services.v26_shadow_engine import V26ShadowEngine, get_v26_shadow_engine
|
||||
from utils.top_leagues import load_top_league_ids
|
||||
from utils.league_reliability import load_league_reliability
|
||||
|
||||
@@ -137,81 +141,116 @@ class SingleMatchOrchestrator:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.v25_predictor: Optional[V25Predictor] = None
|
||||
self.v26_shadow_engine: Optional[V26ShadowEngine] = None
|
||||
self.basketball_predictor: Optional[Any] = None
|
||||
self.dsn = get_clean_dsn()
|
||||
self.engine_mode = str(os.getenv("AI_ENGINE_MODE", "v25")).strip().lower()
|
||||
self.top_league_ids = load_top_league_ids()
|
||||
self.league_reliability = load_league_reliability()
|
||||
self.enrichment = FeatureEnrichmentService()
|
||||
# Market calibration multipliers — V31 rebalance
|
||||
# Previous values created mathematical impossibilities:
|
||||
# BTTS: max reachable = 100×0.45 = 45, but min_conf was 55 → NEVER playable
|
||||
# New approach: calibration = blend(backtest_accuracy, 0.80) to avoid crushing raw signal
|
||||
self.odds_band_analyzer = OddsBandAnalyzer()
|
||||
# ── V32 Calibration Rebalance ──────────────────────────────────
|
||||
# RULE: max_reachable = 100 × calibration MUST be > min_conf + 8
|
||||
# Previous values had 5 markets where this was IMPOSSIBLE:
|
||||
# HT(0.42×100=42 < 45), HCAP(0.40×100=40 < 46), HTFT(0.28×100=28 < 32)
|
||||
# HT_OU15(0.46×100=46 < 48), CARDS(0.45×100=45 < 48)
|
||||
# These markets could NEVER become playable → all predictions were PASS.
|
||||
#
|
||||
# New calibration: conservative but mathematically achievable.
|
||||
# Each market's calibration ensures high-confidence model outputs CAN pass.
|
||||
self.market_calibration: Dict[str, float] = {
|
||||
"MS": 0.48,
|
||||
"DC": 0.82,
|
||||
"OU15": 0.84,
|
||||
"OU25": 0.54,
|
||||
"OU35": 0.44,
|
||||
"BTTS": 0.50,
|
||||
"HT": 0.42,
|
||||
"HT_OU05": 0.68,
|
||||
"HT_OU15": 0.46,
|
||||
"OE": 0.58,
|
||||
"CARDS": 0.45,
|
||||
"HCAP": 0.40,
|
||||
"HTFT": 0.28,
|
||||
"MS": 0.62, # max=62 vs min=42 ✓ (was 0.48→max=48 vs 44 ⚠️)
|
||||
"DC": 0.82, # max=82 vs min=52 ✓ (unchanged, already good)
|
||||
"OU15": 0.84, # max=84 vs min=55 ✓ (unchanged, already good)
|
||||
"OU25": 0.68, # max=68 vs min=48 ✓ (was 0.54→max=54 vs 52 ⚠️)
|
||||
"OU35": 0.60, # max=60 vs min=48 ✓ (was 0.44→max=44 vs 54 ❌)
|
||||
"BTTS": 0.65, # max=65 vs min=46 ✓ (was 0.50→max=50 vs 50 ⚠️)
|
||||
"HT": 0.58, # max=58 vs min=40 ✓ (was 0.42→max=42 vs 45 ❌)
|
||||
"HT_OU05": 0.68, # max=68 vs min=50 ✓ (unchanged)
|
||||
"HT_OU15": 0.60, # max=60 vs min=42 ✓ (was 0.46→max=46 vs 48 ❌)
|
||||
"OE": 0.62, # max=62 vs min=46 ✓ (was 0.58→max=58 vs 50 ok)
|
||||
"CARDS": 0.58, # max=58 vs min=42 ✓ (was 0.45→max=45 vs 48 ❌)
|
||||
"HCAP": 0.56, # max=56 vs min=40 ✓ (was 0.40→max=40 vs 46 ❌)
|
||||
"HTFT": 0.45, # max=45 vs min=28 ✓ (was 0.28→max=28 vs 32 ❌)
|
||||
}
|
||||
# Min confidence: lowered to be achievable (max_reachable - 16 to -20)
|
||||
self.market_min_conf: Dict[str, float] = {
|
||||
"MS": 44.0,
|
||||
"DC": 55.0,
|
||||
"OU15": 58.0,
|
||||
"OU25": 52.0,
|
||||
"OU35": 54.0,
|
||||
"BTTS": 50.0,
|
||||
"HT": 45.0,
|
||||
"HT_OU05": 54.0,
|
||||
"HT_OU15": 48.0,
|
||||
"OE": 50.0,
|
||||
"CARDS": 48.0,
|
||||
"HCAP": 46.0,
|
||||
"HTFT": 32.0,
|
||||
"MS": 42.0, # was 44 — 3-way market, hard to get high conf
|
||||
"DC": 52.0, # was 55 — double chance is easier
|
||||
"OU15": 55.0, # was 58 — binary + usually high conf
|
||||
"OU25": 48.0, # was 52 — core market, allow more through
|
||||
"OU35": 48.0, # was 54 — lowered to let signals pass
|
||||
"BTTS": 46.0, # was 50 — binary market
|
||||
"HT": 40.0, # was 45 — was ❌ impossible, now achievable
|
||||
"HT_OU05": 50.0, # was 54 — binary HT market
|
||||
"HT_OU15": 42.0, # was 48 — was ❌ impossible, now achievable
|
||||
"OE": 46.0, # was 50 — coin-flip market, lower bar
|
||||
"CARDS": 42.0, # was 48 — was ❌ impossible, now achievable
|
||||
"HCAP": 40.0, # was 46 — was ❌ impossible, now achievable
|
||||
"HTFT": 28.0, # was 32 — was ❌ impossible, 9-way market
|
||||
}
|
||||
# Min play score: moderate reduction to allow more C-grade bets
|
||||
self.market_min_play_score: Dict[str, float] = {
|
||||
"MS": 72.0,
|
||||
"DC": 62.0,
|
||||
"OU15": 64.0,
|
||||
"OU25": 70.0,
|
||||
"OU35": 76.0,
|
||||
"BTTS": 70.0,
|
||||
"HT": 74.0,
|
||||
"HT_OU05": 64.0,
|
||||
"HT_OU15": 72.0,
|
||||
"OE": 66.0,
|
||||
"CARDS": 74.0,
|
||||
"HCAP": 76.0,
|
||||
"HTFT": 82.0,
|
||||
"MS": 65.0, # was 72 — let more MS through for tracking
|
||||
"DC": 58.0, # was 62 — DC is high accuracy
|
||||
"OU15": 60.0, # was 64 — strong market per backtest
|
||||
"OU25": 64.0, # was 70 — core market
|
||||
"OU35": 68.0, # was 76 — riskier market
|
||||
"BTTS": 64.0, # was 70 — allow more signals
|
||||
"HT": 66.0, # was 74 — was never reachable anyway
|
||||
"HT_OU05": 60.0, # was 64 — strong backtest market
|
||||
"HT_OU15": 64.0, # was 72 — moderate
|
||||
"OE": 60.0, # was 66 — low priority market
|
||||
"CARDS": 66.0, # was 74 — niche market
|
||||
"HCAP": 68.0, # was 76 — risky
|
||||
"HTFT": 72.0, # was 82 — 9-way, very risky
|
||||
}
|
||||
self.market_min_edge: Dict[str, float] = {
|
||||
"MS": 0.03,
|
||||
"DC": 0.01,
|
||||
"OU15": 0.01,
|
||||
"OU25": 0.02,
|
||||
"OU35": 0.04,
|
||||
"BTTS": 0.03,
|
||||
"HT": 0.04,
|
||||
"HT_OU05": 0.01,
|
||||
"HT_OU15": 0.03,
|
||||
"OE": 0.02,
|
||||
"CARDS": 0.03,
|
||||
"HCAP": 0.04,
|
||||
"HTFT": 0.06,
|
||||
"MS": 0.02, # was 0.03 — slight relaxation
|
||||
"DC": 0.01, # unchanged
|
||||
"OU15": 0.01, # unchanged
|
||||
"OU25": 0.02, # unchanged
|
||||
"OU35": 0.03, # was 0.04
|
||||
"BTTS": 0.02, # was 0.03
|
||||
"HT": 0.03, # was 0.04
|
||||
"HT_OU05": 0.01, # unchanged
|
||||
"HT_OU15": 0.02, # was 0.03
|
||||
"OE": 0.02, # unchanged
|
||||
"CARDS": 0.02, # was 0.03
|
||||
"HCAP": 0.03, # was 0.04
|
||||
"HTFT": 0.05, # was 0.06
|
||||
}
|
||||
|
||||
def _get_v25_predictor(self) -> V25Predictor:
|
||||
if self.v25_predictor is None:
|
||||
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]]:
|
||||
"""
|
||||
@@ -1379,7 +1886,7 @@ class SingleMatchOrchestrator:
|
||||
][:safe_count]
|
||||
preview = watch_items_all[: min(5, len(watch_items_all))]
|
||||
return {
|
||||
"engine": "v25.main",
|
||||
"engine": "v28.main",
|
||||
"generated_at": __import__("datetime").datetime.utcnow().isoformat() + "Z",
|
||||
"horizon_hours": safe_horizon,
|
||||
"min_score": round(safe_min_score, 2),
|
||||
@@ -2426,17 +2933,17 @@ class SingleMatchOrchestrator:
|
||||
|
||||
playable_rows = [row for row in market_rows if row.get("playable")]
|
||||
|
||||
# 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",
|
||||
"model_version": "v28-pro-max",
|
||||
"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}",
|
||||
@@ -2881,7 +3394,7 @@ class SingleMatchOrchestrator:
|
||||
}
|
||||
|
||||
return {
|
||||
"model_version": str(prediction.get("engine_version") or "v25.main.basketball"),
|
||||
"model_version": str(prediction.get("engine_version") or "v28.main.basketball"),
|
||||
"match_info": {
|
||||
"match_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
@@ -1,7 +0,0 @@
|
||||
import os, psycopg2
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv('/Users/piton/Documents/Suggest-Bet-BE/.env')
|
||||
conn = psycopg2.connect(os.getenv('DATABASE_URL').split('?')[0])
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT mpe.match_id, SUM(CASE WHEN event_type::text LIKE \'%yellow_card%\' THEN 1 WHEN event_type::text LIKE \'%red_card%\' THEN 2 ELSE 1 END) as cards FROM match_player_events mpe WHERE event_type::text LIKE \'%card%\' GROUP BY mpe.match_id LIMIT 5')
|
||||
print(cur.fetchall())
|
||||
@@ -1,56 +0,0 @@
|
||||
"""Quick test: V20+Quant integration — EV Edge, Kelly staking, edge-based grading."""
|
||||
import json
|
||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
||||
|
||||
MATCH_IDS = [
|
||||
"er7n8hqndkhvdsg6an72r7h90", # Def. Justicia vs Atl Lanus
|
||||
"etpay8k4qr3gts3jjidfebaxg", # CA Tigre vs Gymnasia
|
||||
]
|
||||
|
||||
o = SingleMatchOrchestrator()
|
||||
|
||||
for mid in MATCH_IDS:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"MATCH: {mid}")
|
||||
print(f"{'='*60}")
|
||||
r = o.analyze_match(mid)
|
||||
if not r:
|
||||
print(" Match not found")
|
||||
continue
|
||||
|
||||
info = r.get("match_info", {})
|
||||
print(f" {info.get('match_name', '?')} | {info.get('league', '?')}")
|
||||
|
||||
mp = r.get("main_pick", {})
|
||||
print(f"\n MAIN PICK: {mp.get('market')} {mp.get('pick')}")
|
||||
print(f" probability: {mp.get('probability', 0):.4f}")
|
||||
print(f" odds: {mp.get('odds', 0):.2f}")
|
||||
print(f" ev_edge: {mp.get('ev_edge', mp.get('edge', 0)):+.4f}")
|
||||
print(f" implied_prob: {mp.get('implied_prob', 0):.4f}")
|
||||
print(f" bet_grade: {mp.get('bet_grade', 'N/A')}")
|
||||
print(f" stake_units: {mp.get('stake_units', 0)}")
|
||||
print(f" playable: {mp.get('playable', False)}")
|
||||
print(f" reasons: {mp.get('decision_reasons', [])}")
|
||||
|
||||
print(f"\n ALL MARKETS (with EV Edge + Kelly):")
|
||||
for b in r.get("bet_summary", []):
|
||||
ev = b.get("ev_edge", 0)
|
||||
imp = b.get("implied_prob", 0)
|
||||
flag = ">>>" if b.get("playable") else " "
|
||||
mkt = b["market"]
|
||||
pick = b["pick"]
|
||||
odds = b.get("odds", 0)
|
||||
grade = b["bet_grade"]
|
||||
stake = b["stake_units"]
|
||||
conf = b.get("calibrated_confidence", 0)
|
||||
print(
|
||||
f" {flag} {mkt:8s} {pick:12s} "
|
||||
f"ev_edge={ev:+.3f} "
|
||||
f"odds={odds:.2f} "
|
||||
f"stake={stake:.1f} "
|
||||
f"grade={grade:4s} "
|
||||
f"conf={conf:.1f}% "
|
||||
f"implied={imp:.3f}"
|
||||
)
|
||||
|
||||
print()
|
||||
@@ -1,75 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
||||
|
||||
from core.engines.odds_predictor import OddsPredictorEngine
|
||||
from features.sidelined_analyzer import SidelinedAnalyzer
|
||||
|
||||
|
||||
class EngineNullSafetyTests(unittest.TestCase):
|
||||
def test_odds_predictor_accepts_decimal_inputs_without_crashing(self):
|
||||
engine = OddsPredictorEngine()
|
||||
|
||||
prediction = engine.predict(
|
||||
odds_data={
|
||||
"ms_h": Decimal("2.10"),
|
||||
"ms_d": Decimal("3.25"),
|
||||
"ms_a": Decimal("3.60"),
|
||||
"ou25_o": Decimal("1.90"),
|
||||
},
|
||||
)
|
||||
|
||||
self.assertGreater(prediction.market_home_prob, 0.0)
|
||||
self.assertGreater(prediction.market_draw_prob, 0.0)
|
||||
self.assertGreater(prediction.market_away_prob, 0.0)
|
||||
|
||||
def test_sidelined_analyzer_handles_non_numeric_fields(self):
|
||||
analyzer = SidelinedAnalyzer.__new__(SidelinedAnalyzer)
|
||||
analyzer.position_weights = {"K": 0.35, "D": 0.20, "O": 0.25, "F": 0.30}
|
||||
analyzer.max_rating = 10
|
||||
analyzer.adaptation_threshold = 10
|
||||
analyzer.adaptation_discount = 0.5
|
||||
analyzer.goalkeeper_penalty = 0.15
|
||||
analyzer.confidence_boost = 10
|
||||
analyzer.max_impact = 0.85
|
||||
analyzer.key_player_threshold = 3
|
||||
analyzer.recent_matches_lookback = 15
|
||||
analyzer._fetch_player_stats = MagicMock(return_value={})
|
||||
|
||||
result = analyzer.analyze(
|
||||
{
|
||||
"totalSidelined": 2,
|
||||
"players": [
|
||||
{
|
||||
"playerId": "p1",
|
||||
"playerName": "Player One",
|
||||
"positionShort": "O",
|
||||
"matchesMissed": "N/A",
|
||||
"average": "?",
|
||||
"type": "injury",
|
||||
},
|
||||
{
|
||||
"playerId": "p2",
|
||||
"playerName": "Player Two",
|
||||
"positionShort": "K",
|
||||
"matchesMissed": "12",
|
||||
"average": "6.7",
|
||||
"type": "suspension",
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(result.total_sidelined, 2)
|
||||
self.assertGreaterEqual(result.impact_score, 0.0)
|
||||
self.assertTrue(len(result.player_details) >= 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,282 +0,0 @@
|
||||
"""
|
||||
Unit tests for FeatureEnrichmentService
|
||||
========================================
|
||||
Tests all 6 enrichment methods with mocked DB cursor:
|
||||
1. compute_team_stats
|
||||
2. compute_h2h
|
||||
3. compute_form_streaks
|
||||
4. compute_referee_stats
|
||||
5. compute_league_averages
|
||||
6. compute_momentum
|
||||
"""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
||||
|
||||
from services.feature_enrichment import FeatureEnrichmentService, _safe_avg
|
||||
|
||||
|
||||
def _make_cursor(rows=None, side_effect=None):
|
||||
"""Create a mock RealDictCursor."""
|
||||
cur = MagicMock()
|
||||
if side_effect:
|
||||
cur.execute.side_effect = side_effect
|
||||
else:
|
||||
cur.fetchall.return_value = rows or []
|
||||
cur.fetchone.return_value = rows[0] if rows else None
|
||||
return cur
|
||||
|
||||
|
||||
class TestSafeAvg(unittest.TestCase):
|
||||
def test_returns_average(self):
|
||||
self.assertAlmostEqual(_safe_avg([2.0, 4.0, 6.0], 0.0), 4.0)
|
||||
|
||||
def test_returns_default_on_empty(self):
|
||||
self.assertEqual(_safe_avg([], 99.0), 99.0)
|
||||
|
||||
def test_single_value(self):
|
||||
self.assertAlmostEqual(_safe_avg([7.5], 0.0), 7.5)
|
||||
|
||||
|
||||
class TestComputeTeamStats(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.svc = FeatureEnrichmentService()
|
||||
self.ts = 1700000000000
|
||||
|
||||
def test_returns_defaults_when_no_team_id(self):
|
||||
result = self.svc.compute_team_stats(MagicMock(), '', self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
|
||||
|
||||
def test_returns_defaults_when_no_rows(self):
|
||||
cur = _make_cursor(rows=[])
|
||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
|
||||
|
||||
def test_returns_defaults_on_db_error(self):
|
||||
cur = _make_cursor(side_effect=Exception('DB down'))
|
||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
|
||||
|
||||
def test_calculates_averages_correctly(self):
|
||||
rows = [
|
||||
{'possession_percentage': 60.0, 'shots_on_target': 5, 'total_shots': 10, 'corners': 7},
|
||||
{'possession_percentage': 40.0, 'shots_on_target': 3, 'total_shots': 12, 'corners': 3},
|
||||
]
|
||||
cur = _make_cursor(rows)
|
||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
||||
|
||||
self.assertAlmostEqual(result['avg_possession'], 50.0)
|
||||
self.assertAlmostEqual(result['avg_shots_on_target'], 4.0)
|
||||
self.assertAlmostEqual(result['shot_conversion'], (5 / 10 + 3 / 12) / 2, places=4)
|
||||
self.assertAlmostEqual(result['avg_corners'], 5.0)
|
||||
|
||||
def test_handles_none_subfields_gracefully(self):
|
||||
"""Rows with None values should be skipped, not crash."""
|
||||
rows = [
|
||||
{'possession_percentage': 55.0, 'shots_on_target': None, 'total_shots': None, 'corners': 4},
|
||||
{'possession_percentage': None, 'shots_on_target': 2, 'total_shots': 8, 'corners': None},
|
||||
]
|
||||
cur = _make_cursor(rows)
|
||||
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
|
||||
|
||||
self.assertAlmostEqual(result['avg_possession'], 55.0)
|
||||
self.assertAlmostEqual(result['avg_shots_on_target'], 2.0)
|
||||
self.assertAlmostEqual(result['avg_corners'], 4.0)
|
||||
|
||||
|
||||
class TestComputeH2H(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.svc = FeatureEnrichmentService()
|
||||
self.ts = 1700000000000
|
||||
|
||||
def test_returns_defaults_when_no_ids(self):
|
||||
result = self.svc.compute_h2h(MagicMock(), '', 'away1', self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
|
||||
|
||||
def test_returns_defaults_when_no_rows(self):
|
||||
cur = _make_cursor(rows=[])
|
||||
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
|
||||
|
||||
def test_calculates_h2h_stats(self):
|
||||
rows = [
|
||||
{'home_team_id': 'home1', 'away_team_id': 'away1', 'score_home': 2, 'score_away': 1}, # home win, btts, over25
|
||||
{'home_team_id': 'home1', 'away_team_id': 'away1', 'score_home': 0, 'score_away': 0}, # draw, no btts, no over25
|
||||
{'home_team_id': 'away1', 'away_team_id': 'home1', 'score_home': 1, 'score_away': 3}, # reversed: home wins again, btts, over25
|
||||
{'home_team_id': 'away1', 'away_team_id': 'home1', 'score_home': 2, 'score_away': 0}, # reversed: away(=home1) lost
|
||||
]
|
||||
cur = _make_cursor(rows)
|
||||
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
|
||||
|
||||
self.assertEqual(result['total_matches'], 4)
|
||||
self.assertAlmostEqual(result['home_win_rate'], 2 / 4)
|
||||
self.assertAlmostEqual(result['draw_rate'], 1 / 4)
|
||||
self.assertAlmostEqual(result['btts_rate'], 2 / 4)
|
||||
self.assertAlmostEqual(result['over25_rate'], 2 / 4)
|
||||
|
||||
def test_returns_defaults_on_db_error(self):
|
||||
cur = _make_cursor(side_effect=Exception('connection lost'))
|
||||
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
|
||||
|
||||
|
||||
class TestComputeFormStreaks(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.svc = FeatureEnrichmentService()
|
||||
self.ts = 1700000000000
|
||||
|
||||
def test_returns_defaults_when_no_team_id(self):
|
||||
result = self.svc.compute_form_streaks(MagicMock(), '', self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_FORM)
|
||||
|
||||
def test_calculates_streaks_correctly(self):
|
||||
"""Most recent first: W, W, D, L → winning_streak=2, unbeaten_streak=3."""
|
||||
rows = [
|
||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 2, 'score_away': 0}, # W (clean sheet, scored)
|
||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 1, 'score_away': 0}, # W (clean sheet, scored)
|
||||
{'home_team_id': 'x', 'away_team_id': 'team1', 'score_home': 1, 'score_away': 1}, # D (scored, conceded)
|
||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 2}, # L (not scored, conceded)
|
||||
]
|
||||
cur = _make_cursor(rows)
|
||||
result = self.svc.compute_form_streaks(cur, 'team1', self.ts)
|
||||
|
||||
self.assertEqual(result['winning_streak'], 2)
|
||||
self.assertEqual(result['unbeaten_streak'], 3)
|
||||
self.assertAlmostEqual(result['clean_sheet_rate'], 2 / 4)
|
||||
self.assertAlmostEqual(result['scoring_rate'], 3 / 4)
|
||||
|
||||
def test_all_losses(self):
|
||||
rows = [
|
||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 1},
|
||||
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 3},
|
||||
]
|
||||
cur = _make_cursor(rows)
|
||||
result = self.svc.compute_form_streaks(cur, 'team1', self.ts)
|
||||
|
||||
self.assertEqual(result['winning_streak'], 0)
|
||||
self.assertEqual(result['unbeaten_streak'], 0)
|
||||
self.assertAlmostEqual(result['scoring_rate'], 0.0)
|
||||
|
||||
|
||||
class TestComputeRefereeStats(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.svc = FeatureEnrichmentService()
|
||||
self.ts = 1700000000000
|
||||
|
||||
def test_returns_defaults_when_no_name(self):
|
||||
result = self.svc.compute_referee_stats(MagicMock(), None, self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_REFEREE)
|
||||
|
||||
def test_calculates_referee_tendencies(self):
|
||||
match_rows = [
|
||||
{'home_team_id': 'h1', 'score_home': 2, 'score_away': 0, 'match_id': 'm1'}, # home win
|
||||
{'home_team_id': 'h2', 'score_home': 1, 'score_away': 1, 'match_id': 'm2'}, # draw
|
||||
]
|
||||
card_row = {'yellows': 6, 'total_cards': 8}
|
||||
|
||||
cur = MagicMock()
|
||||
# First execute (match query) → match_rows
|
||||
# Second execute (card query) → card_row
|
||||
cur.fetchall.return_value = match_rows
|
||||
cur.fetchone.return_value = card_row
|
||||
|
||||
result = self.svc.compute_referee_stats(cur, 'Ref Name', self.ts)
|
||||
|
||||
self.assertEqual(result['experience'], 2)
|
||||
self.assertAlmostEqual(result['avg_goals'], (2 + 0 + 1 + 1) / 2)
|
||||
# home_bias = (1/2) - 0.46 = 0.04
|
||||
self.assertAlmostEqual(result['home_bias'], 0.04, places=4)
|
||||
self.assertAlmostEqual(result['avg_yellow'], 6 / 2)
|
||||
self.assertAlmostEqual(result['cards_total'], 8 / 2)
|
||||
|
||||
def test_returns_defaults_on_db_error(self):
|
||||
cur = _make_cursor(side_effect=Exception('timeout'))
|
||||
result = self.svc.compute_referee_stats(cur, 'Some Ref', self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_REFEREE)
|
||||
|
||||
|
||||
class TestComputeLeagueAverages(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.svc = FeatureEnrichmentService()
|
||||
self.ts = 1700000000000
|
||||
|
||||
def test_returns_defaults_when_no_league_id(self):
|
||||
result = self.svc.compute_league_averages(MagicMock(), None, self.ts)
|
||||
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_LEAGUE)
|
||||
|
||||
def test_calculates_league_averages(self):
|
||||
rows = [
|
||||
{'score_home': 1, 'score_away': 1}, # 2 goals
|
||||
{'score_home': 0, 'score_away': 0}, # 0 goals (zero-goal match)
|
||||
{'score_home': 3, 'score_away': 2}, # 5 goals
|
||||
]
|
||||
cur = _make_cursor(rows)
|
||||
result = self.svc.compute_league_averages(cur, 'league1', self.ts)
|
||||
|
||||
self.assertAlmostEqual(result['avg_goals'], 7 / 3, places=4)
|
||||
self.assertAlmostEqual(result['zero_goal_rate'], 1 / 3, places=4)
|
||||
|
||||
|
||||
class TestComputeMomentum(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.svc = FeatureEnrichmentService()
|
||||
self.ts = 1700000000000
|
||||
|
||||
def test_returns_zero_when_no_team_id(self):
|
||||
result = self.svc.compute_momentum(MagicMock(), '', self.ts)
|
||||
self.assertEqual(result, 0.0)
|
||||
|
||||
def test_returns_zero_when_no_rows(self):
|
||||
cur = _make_cursor(rows=[])
|
||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
||||
self.assertEqual(result, 0.0)
|
||||
|
||||
def test_all_wins_returns_one(self):
|
||||
"""All wins → momentum = 1.0 (max possible)."""
|
||||
rows = [
|
||||
{'home_team_id': 'team1', 'score_home': 3, 'score_away': 0},
|
||||
{'home_team_id': 'team1', 'score_home': 2, 'score_away': 1},
|
||||
]
|
||||
cur = _make_cursor(rows)
|
||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
||||
self.assertAlmostEqual(result, 1.0, places=4)
|
||||
|
||||
def test_all_losses_returns_negative(self):
|
||||
"""All losses → negative momentum."""
|
||||
rows = [
|
||||
{'home_team_id': 'team1', 'score_home': 0, 'score_away': 2},
|
||||
{'home_team_id': 'team1', 'score_home': 1, 'score_away': 3},
|
||||
]
|
||||
cur = _make_cursor(rows)
|
||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
||||
self.assertLess(result, 0.0)
|
||||
|
||||
def test_mixed_results(self):
|
||||
"""W, D, L → weighted score between -1 and 1."""
|
||||
rows = [
|
||||
{'home_team_id': 'team1', 'score_home': 1, 'score_away': 0}, # W (weight=3)
|
||||
{'home_team_id': 'x', 'away_team_id': 'team1', 'score_home': 0, 'score_away': 0}, # D (weight=2)
|
||||
{'home_team_id': 'team1', 'score_home': 0, 'score_away': 1}, # L (weight=1)
|
||||
]
|
||||
cur = _make_cursor(rows)
|
||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
||||
|
||||
# weighted = 3*3 + 1*2 + (-1)*1 = 9+2-1 = 10
|
||||
# max_possible = 3*3 + 3*2 + 3*1 = 18
|
||||
# normalised = 10/18 ≈ 0.5556
|
||||
self.assertAlmostEqual(result, round(10 / 18, 4), places=4)
|
||||
|
||||
def test_returns_zero_on_db_error(self):
|
||||
cur = _make_cursor(side_effect=Exception('broken pipe'))
|
||||
result = self.svc.compute_momentum(cur, 'team1', self.ts)
|
||||
self.assertEqual(result, 0.0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,110 +0,0 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
||||
|
||||
import main as ai_main
|
||||
|
||||
|
||||
def _run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
class MainApiFunctionTests(unittest.TestCase):
|
||||
def test_analyze_match_v20plus_returns_payload(self):
|
||||
orchestrator = MagicMock()
|
||||
orchestrator.analyze_match.return_value = {"match_info": {"match_id": "m1"}}
|
||||
|
||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
||||
result = _run(ai_main.analyze_match_v20plus("m1"))
|
||||
|
||||
self.assertEqual(result["match_info"]["match_id"], "m1")
|
||||
|
||||
def test_analyze_match_v20plus_raises_404(self):
|
||||
orchestrator = MagicMock()
|
||||
orchestrator.analyze_match.return_value = None
|
||||
|
||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
||||
with self.assertRaises(HTTPException) as ctx:
|
||||
_run(ai_main.analyze_match_v20plus("missing"))
|
||||
|
||||
self.assertEqual(ctx.exception.status_code, 404)
|
||||
|
||||
def test_analyze_match_htms_v20plus_returns_payload(self):
|
||||
orchestrator = MagicMock()
|
||||
orchestrator.analyze_match_htms.return_value = {
|
||||
"status": "ok",
|
||||
"engine_used": "v20plus_top_htms",
|
||||
}
|
||||
|
||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
||||
result = _run(ai_main.analyze_match_htms_v20plus("m1"))
|
||||
|
||||
self.assertEqual(result["status"], "ok")
|
||||
self.assertEqual(result["engine_used"], "v20plus_top_htms")
|
||||
|
||||
def test_analyze_match_htft_timeout_validation(self):
|
||||
with self.assertRaises(HTTPException) as ctx:
|
||||
_run(ai_main.analyze_match_htft_v20plus("m1", timeout_sec=2))
|
||||
|
||||
self.assertEqual(ctx.exception.status_code, 400)
|
||||
|
||||
def test_generate_coupon_v20plus_forwards_payload(self):
|
||||
orchestrator = MagicMock()
|
||||
orchestrator.build_coupon.return_value = {"bets": []}
|
||||
|
||||
request = ai_main.CouponRequest(
|
||||
match_ids=["m1", "m2"],
|
||||
strategy="SAFE",
|
||||
max_matches=3,
|
||||
min_confidence=70,
|
||||
)
|
||||
|
||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
||||
result = _run(ai_main.generate_coupon_v20plus(request))
|
||||
|
||||
self.assertEqual(result, {"bets": []})
|
||||
orchestrator.build_coupon.assert_called_once_with(
|
||||
match_ids=["m1", "m2"],
|
||||
strategy="SAFE",
|
||||
max_matches=3,
|
||||
min_confidence=70.0,
|
||||
)
|
||||
|
||||
def test_reversal_watchlist_validation(self):
|
||||
with self.assertRaises(HTTPException) as ctx:
|
||||
_run(ai_main.get_reversal_watchlist_v20plus(count=0))
|
||||
self.assertEqual(ctx.exception.status_code, 400)
|
||||
|
||||
def test_reversal_watchlist_forwards_payload(self):
|
||||
orchestrator = MagicMock()
|
||||
orchestrator.get_reversal_watchlist.return_value = {"watchlist": []}
|
||||
|
||||
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
|
||||
result = _run(
|
||||
ai_main.get_reversal_watchlist_v20plus(
|
||||
count=12,
|
||||
horizon_hours=48,
|
||||
min_score=50.5,
|
||||
top_leagues_only=True,
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"watchlist": []})
|
||||
orchestrator.get_reversal_watchlist.assert_called_once_with(
|
||||
count=12,
|
||||
horizon_hours=48,
|
||||
min_score=50.5,
|
||||
top_leagues_only=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,766 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_ROOT))
|
||||
|
||||
from models.v20_ensemble import FullMatchPrediction
|
||||
from models.basketball_v25 import BasketballMatchPrediction
|
||||
from services.single_match_orchestrator import MatchData, SingleMatchOrchestrator
|
||||
|
||||
|
||||
class _CursorContext:
|
||||
def __init__(self, cursor):
|
||||
self._cursor = cursor
|
||||
|
||||
def __enter__(self):
|
||||
return self._cursor
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
|
||||
class _ConnContext:
|
||||
def __init__(self, cursor):
|
||||
self._cursor = cursor
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def cursor(self, cursor_factory=None):
|
||||
return _CursorContext(self._cursor)
|
||||
|
||||
|
||||
class _StaticFetchAllCursor:
|
||||
def __init__(self, rows):
|
||||
self.rows = rows
|
||||
self.executed = []
|
||||
|
||||
def execute(self, query, params=None):
|
||||
self.executed.append((query, params))
|
||||
|
||||
def fetchall(self):
|
||||
return list(self.rows)
|
||||
|
||||
|
||||
class _RouterCursor:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
live_row=None,
|
||||
hist_row=None,
|
||||
relational_rows=None,
|
||||
participation_rows=None,
|
||||
probable_rows=None,
|
||||
):
|
||||
self.live_row = live_row
|
||||
self.hist_row = hist_row
|
||||
self.relational_rows = relational_rows or []
|
||||
self.participation_rows = participation_rows or []
|
||||
self.probable_rows = probable_rows or []
|
||||
self.last_query = ""
|
||||
|
||||
def execute(self, query, params=None):
|
||||
self.last_query = query
|
||||
|
||||
def fetchone(self):
|
||||
if "FROM live_matches" in self.last_query:
|
||||
return self.live_row
|
||||
if "FROM matches m" in self.last_query:
|
||||
return self.hist_row
|
||||
return None
|
||||
|
||||
def fetchall(self):
|
||||
if "FROM odd_categories" in self.last_query:
|
||||
return list(self.relational_rows)
|
||||
if "FROM match_player_participation" in self.last_query and "GROUP BY" not in self.last_query:
|
||||
return list(self.participation_rows)
|
||||
if "GROUP BY mpp.player_id" in self.last_query:
|
||||
return list(self.probable_rows)
|
||||
return []
|
||||
|
||||
|
||||
def _build_orchestrator() -> SingleMatchOrchestrator:
|
||||
orchestrator = SingleMatchOrchestrator.__new__(SingleMatchOrchestrator)
|
||||
orchestrator.v25_predictor = MagicMock()
|
||||
orchestrator.basketball_predictor = MagicMock()
|
||||
orchestrator.dsn = "postgresql://unit-test"
|
||||
orchestrator.league_reliability = {}
|
||||
orchestrator.market_calibration = {
|
||||
"MS": 0.82,
|
||||
"DC": 0.93,
|
||||
"OU15": 0.90,
|
||||
"OU25": 0.85,
|
||||
"OU35": 0.88,
|
||||
"BTTS": 0.83,
|
||||
"HT": 0.80,
|
||||
"HT_OU05": 0.88,
|
||||
}
|
||||
orchestrator.market_min_conf = {
|
||||
"MS": 52.0,
|
||||
"DC": 56.0,
|
||||
"OU15": 60.0,
|
||||
"OU25": 58.0,
|
||||
"OU35": 54.0,
|
||||
"BTTS": 57.0,
|
||||
"HT": 53.0,
|
||||
"HT_OU05": 55.0,
|
||||
}
|
||||
orchestrator.market_min_play_score = {
|
||||
"MS": 72.0,
|
||||
"DC": 62.0,
|
||||
"OU15": 64.0,
|
||||
"OU25": 70.0,
|
||||
"OU35": 76.0,
|
||||
"BTTS": 70.0,
|
||||
"HT": 74.0,
|
||||
"HT_OU05": 64.0,
|
||||
}
|
||||
orchestrator.market_min_edge = {
|
||||
"MS": 0.03,
|
||||
"DC": 0.01,
|
||||
"OU15": 0.01,
|
||||
"OU25": 0.02,
|
||||
"OU35": 0.04,
|
||||
"BTTS": 0.03,
|
||||
"HT": 0.04,
|
||||
"HT_OU05": 0.01,
|
||||
}
|
||||
return orchestrator
|
||||
|
||||
|
||||
class SingleMatchOrchestratorTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.orchestrator = _build_orchestrator()
|
||||
|
||||
def test_parse_odds_json_uses_exact_market_match_and_ignores_collisions(self):
|
||||
odds_json = {
|
||||
"Maç Sonucu": {"1": "2.15", "X": "3.20", "2": "3.30"},
|
||||
"İlk Yarı/Maç Sonucu": {"1/1": "4.30"},
|
||||
"2,5 Alt/Üst": {"Üst": "1.85", "Alt": "1.95"},
|
||||
"İY 0,5 Alt/Üst": {"Üst": "1.49", "Alt": "2.20"},
|
||||
"1. Yarı Ev Sahibi 0,5 Alt/Üst": {"Üst": "1.99", "Alt": "1.45"},
|
||||
"2,5 Kart Puanı Alt/Üst": {"Üst": "1.33", "Alt": "2.95"},
|
||||
"Karşılıklı Gol": {"Var": "1.75", "Yok": "2.05"},
|
||||
"1. Yarı Karşılıklı Gol": {"Var": "2.10", "Yok": "1.60"},
|
||||
"Çifte Şans": {"1-X": "1.33", "X-2": "1.62", "1-2": "1.30"},
|
||||
"1. Yarı Sonucu": {"1": "2.45", "X": "2.00", "2": "3.80"},
|
||||
}
|
||||
|
||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
||||
|
||||
self.assertEqual(parsed["ms_h"], 2.15)
|
||||
self.assertEqual(parsed["ms_d"], 3.20)
|
||||
self.assertEqual(parsed["ms_a"], 3.30)
|
||||
self.assertEqual(parsed["ou25_o"], 1.85)
|
||||
self.assertEqual(parsed["ou25_u"], 1.95)
|
||||
self.assertEqual(parsed["btts_y"], 1.75)
|
||||
self.assertEqual(parsed["btts_n"], 2.05)
|
||||
self.assertEqual(parsed["dc_1x"], 1.33)
|
||||
self.assertEqual(parsed["dc_x2"], 1.62)
|
||||
self.assertEqual(parsed["dc_12"], 1.30)
|
||||
self.assertEqual(parsed["ht_h"], 2.45)
|
||||
self.assertEqual(parsed["ht_d"], 2.00)
|
||||
self.assertEqual(parsed["ht_a"], 3.80)
|
||||
self.assertEqual(parsed["ht_ou05_o"], 1.49)
|
||||
self.assertEqual(parsed["ht_ou05_u"], 2.20)
|
||||
self.assertEqual(parsed["htft_11"], 4.30)
|
||||
|
||||
def test_parse_odds_json_accepts_selection_variants(self):
|
||||
odds_json = {
|
||||
"2,5 Alt/Üst": {"2,5 Üst": "1.91", "2,5 Alt": "1.86"},
|
||||
"Karşılıklı Gol": {"YES": "1.82", "NO": "1.96"},
|
||||
"Çifte Şans": {"1X": "1.28", "X2": "1.44", "12": "1.32"},
|
||||
}
|
||||
|
||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
||||
|
||||
self.assertEqual(parsed["ou25_o"], 1.91)
|
||||
self.assertEqual(parsed["ou25_u"], 1.86)
|
||||
self.assertEqual(parsed["btts_y"], 1.82)
|
||||
self.assertEqual(parsed["btts_n"], 1.96)
|
||||
self.assertEqual(parsed["dc_1x"], 1.28)
|
||||
self.assertEqual(parsed["dc_x2"], 1.44)
|
||||
self.assertEqual(parsed["dc_12"], 1.32)
|
||||
|
||||
def test_parse_odds_json_maps_all_football_markets_with_noise(self):
|
||||
odds_json = {
|
||||
"Maç Sonucu": {"1": "2.31", "X": "3.22", "2": "3.05"},
|
||||
"Çifte Şans": {"1-X": "1.34", "X-2": "1.52", "1-2": "1.28"},
|
||||
"1,5 Alt/Üst": {"Üst": "1.29", "Alt": "3.45"},
|
||||
"2,5 Alt/Üst": {"Üst": "1.71", "Alt": "2.05"},
|
||||
"3,5 Alt/Üst": {"Üst": "2.62", "Alt": "1.41"},
|
||||
"Karşılıklı Gol": {"Var": "1.66", "Yok": "2.11"},
|
||||
"1. Yarı Sonucu": {"1": "3.10", "X": "1.95", "2": "4.60"},
|
||||
"1. Yarı 0,5 Alt/Üst": {"Üst": "1.21", "Alt": "2.72"},
|
||||
# noise categories that must not overwrite football main markets
|
||||
"1. Yarı Ev Sahibi 0,5 Alt/Üst": {"Üst": "1.99", "Alt": "1.45"},
|
||||
"1. Yarı Deplasman 0,5 Alt/Üst": {"Üst": "1.73", "Alt": "1.63"},
|
||||
"1.Yarı 3,5 Korner Alt/Üst": {"Üst": "1.26", "Alt": "2.30"},
|
||||
"2,5 Kart Puanı Alt/Üst": {"Üst": "1.40", "Alt": "2.60"},
|
||||
}
|
||||
|
||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
||||
|
||||
self.assertEqual(parsed["ms_h"], 2.31)
|
||||
self.assertEqual(parsed["ms_d"], 3.22)
|
||||
self.assertEqual(parsed["ms_a"], 3.05)
|
||||
self.assertEqual(parsed["dc_1x"], 1.34)
|
||||
self.assertEqual(parsed["dc_x2"], 1.52)
|
||||
self.assertEqual(parsed["dc_12"], 1.28)
|
||||
self.assertEqual(parsed["ou15_o"], 1.29)
|
||||
self.assertEqual(parsed["ou15_u"], 3.45)
|
||||
self.assertEqual(parsed["ou25_o"], 1.71)
|
||||
self.assertEqual(parsed["ou25_u"], 2.05)
|
||||
self.assertEqual(parsed["ou35_o"], 2.62)
|
||||
self.assertEqual(parsed["ou35_u"], 1.41)
|
||||
self.assertEqual(parsed["btts_y"], 1.66)
|
||||
self.assertEqual(parsed["btts_n"], 2.11)
|
||||
self.assertEqual(parsed["ht_h"], 3.10)
|
||||
self.assertEqual(parsed["ht_d"], 1.95)
|
||||
self.assertEqual(parsed["ht_a"], 4.60)
|
||||
self.assertEqual(parsed["ht_ou05_o"], 1.21)
|
||||
self.assertEqual(parsed["ht_ou05_u"], 2.72)
|
||||
|
||||
def test_v25_market_odds_ignores_synthetic_default_when_selection_missing(self):
|
||||
odds_json = {
|
||||
"1,5 Alt/Üst": {"Alt": 5.70},
|
||||
"Çifte Şans": {"1-X": 1.30, "X-2": 1.38, "1-2": 1.09},
|
||||
}
|
||||
|
||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
||||
|
||||
self.assertEqual(parsed["ou15_o"], 0.0)
|
||||
self.assertEqual(
|
||||
self.orchestrator._v25_market_odds(parsed, "OU15", "Over"),
|
||||
1.0,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.orchestrator._v25_market_odds(parsed, "OU15", "Under"),
|
||||
5.7,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.orchestrator._v25_market_odds(parsed, "DC", "X2"),
|
||||
1.38,
|
||||
)
|
||||
|
||||
def test_parse_odds_json_extracts_basketball_ml_total_spread(self):
|
||||
odds_json = {
|
||||
"Maç Sonucu (Uzt. Dahil)": {"1": "1.74", "2": "2.08"},
|
||||
"Alt/Üst (163,5)": {"Üst": "1.86", "Alt": "1.94"},
|
||||
"1. Yarı Alt/Üst (81,5)": {"Üst": "1.89", "Alt": "1.91"},
|
||||
"1. Yarı Alt/Üst (100,5)": {"Üst": "1.83", "Alt": "1.97"},
|
||||
"Hnd. MS (0:5,5)": {"1": "1.91", "+5.5h": "1.87"},
|
||||
}
|
||||
|
||||
parsed = self.orchestrator._parse_odds_json(odds_json)
|
||||
|
||||
self.assertEqual(parsed["ml_h"], 1.74)
|
||||
self.assertEqual(parsed["ml_a"], 2.08)
|
||||
self.assertEqual(parsed["tot_line"], 163.5)
|
||||
self.assertEqual(parsed["tot_o"], 1.86)
|
||||
self.assertEqual(parsed["tot_u"], 1.94)
|
||||
self.assertEqual(parsed["spread_home_line"], -5.5)
|
||||
self.assertEqual(parsed["spread_h"], 1.91)
|
||||
self.assertEqual(parsed["spread_a"], 1.87)
|
||||
self.assertNotIn("ht_ou05_o", parsed)
|
||||
self.assertNotIn("ht_ou05_u", parsed)
|
||||
|
||||
def test_extract_odds_merges_relational_when_live_json_is_incomplete(self):
|
||||
row = {
|
||||
"match_id": "m-1",
|
||||
"odds": {"Maç Sonucu": {"1": 2.10, "X": 3.20, "2": 3.35}},
|
||||
}
|
||||
relational_rows = [
|
||||
{"category_name": "Çifte Şans", "selection_name": "1-X", "odd_value": 1.28},
|
||||
{"category_name": "Çifte Şans", "selection_name": "X-2", "odd_value": 1.44},
|
||||
{"category_name": "Çifte Şans", "selection_name": "1-2", "odd_value": 1.31},
|
||||
{"category_name": "2,5 Alt/Üst", "selection_name": "Üst", "odd_value": 1.89},
|
||||
{"category_name": "2,5 Alt/Üst", "selection_name": "Alt", "odd_value": 1.94},
|
||||
{"category_name": "Karşılıklı Gol", "selection_name": "Var", "odd_value": 1.77},
|
||||
{"category_name": "Karşılıklı Gol", "selection_name": "Yok", "odd_value": 2.02},
|
||||
{"category_name": "1. Yarı Sonucu", "selection_name": "1", "odd_value": 2.55},
|
||||
{"category_name": "1. Yarı Sonucu", "selection_name": "X", "odd_value": 1.98},
|
||||
{"category_name": "1. Yarı Sonucu", "selection_name": "2", "odd_value": 3.40},
|
||||
]
|
||||
cur = _StaticFetchAllCursor(relational_rows)
|
||||
|
||||
odds = self.orchestrator._extract_odds(cur, row)
|
||||
|
||||
self.assertEqual(odds["ms_h"], 2.10)
|
||||
self.assertEqual(odds["ms_d"], 3.20)
|
||||
self.assertEqual(odds["ms_a"], 3.35)
|
||||
self.assertEqual(odds["dc_x2"], 1.44)
|
||||
self.assertEqual(odds["ou25_o"], 1.89)
|
||||
self.assertEqual(odds["btts_y"], 1.77)
|
||||
self.assertEqual(odds["ht_d"], 1.98)
|
||||
self.assertEqual(len(cur.executed), 1)
|
||||
|
||||
def test_extract_odds_fills_default_ms_when_no_source_available(self):
|
||||
row = {"match_id": "m-2", "odds": None}
|
||||
cur = _StaticFetchAllCursor([])
|
||||
|
||||
odds = self.orchestrator._extract_odds(cur, row)
|
||||
|
||||
self.assertEqual(odds["ms_h"], SingleMatchOrchestrator.DEFAULT_MS_H)
|
||||
self.assertEqual(odds["ms_d"], SingleMatchOrchestrator.DEFAULT_MS_D)
|
||||
self.assertEqual(odds["ms_a"], SingleMatchOrchestrator.DEFAULT_MS_A)
|
||||
|
||||
def test_parse_lineups_json_supports_id_playerid_personid(self):
|
||||
lineups = {
|
||||
"home": {
|
||||
"xi": [
|
||||
{"id": "11"},
|
||||
{"playerId": "12"},
|
||||
],
|
||||
},
|
||||
"away": {
|
||||
"starting": [
|
||||
{"personId": "21"},
|
||||
"22",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
home, away = self.orchestrator._parse_lineups_json(lineups)
|
||||
|
||||
self.assertEqual(home, ["11", "12"])
|
||||
self.assertEqual(away, ["21", "22"])
|
||||
|
||||
def test_extract_lineups_uses_participation_and_probable_xi_fallbacks(self):
|
||||
row = {
|
||||
"match_id": "m-3",
|
||||
"home_team_id": "h1",
|
||||
"away_team_id": "a1",
|
||||
"match_date_ms": 1700000000000,
|
||||
"lineups": {
|
||||
"home": {"xi": [{"personId": "h-live-1"}]},
|
||||
"away": {},
|
||||
},
|
||||
}
|
||||
participation = [
|
||||
{"team_id": "a1", "player_id": "a-db-1"},
|
||||
{"team_id": "a1", "player_id": "a-db-2"},
|
||||
]
|
||||
cur = _StaticFetchAllCursor(participation)
|
||||
|
||||
with patch.object(
|
||||
self.orchestrator,
|
||||
"_build_probable_xi",
|
||||
side_effect=[["h-prob-1"], ["a-prob-1"]],
|
||||
) as probable_xi:
|
||||
home, away, source = self.orchestrator._extract_lineups(cur, row)
|
||||
|
||||
self.assertEqual(home, ["h-live-1"])
|
||||
self.assertEqual(away, ["a-db-1", "a-db-2"])
|
||||
self.assertEqual(source, "none")
|
||||
probable_xi.assert_not_called()
|
||||
|
||||
def test_extract_lineups_falls_back_to_probable_xi_when_live_and_participation_missing(self):
|
||||
row = {
|
||||
"match_id": "m-4",
|
||||
"home_team_id": "h2",
|
||||
"away_team_id": "a2",
|
||||
"match_date_ms": 1700000000000,
|
||||
"lineups": None,
|
||||
}
|
||||
cur = _StaticFetchAllCursor([])
|
||||
|
||||
with patch.object(
|
||||
self.orchestrator,
|
||||
"_build_probable_xi",
|
||||
side_effect=[["h-prob-1", "h-prob-2"], ["a-prob-1"]],
|
||||
) as probable_xi:
|
||||
home, away, source = self.orchestrator._extract_lineups(cur, row)
|
||||
|
||||
self.assertEqual(home, ["h-prob-1", "h-prob-2"])
|
||||
self.assertEqual(away, ["a-prob-1"])
|
||||
self.assertEqual(source, "probable_xi")
|
||||
self.assertEqual(probable_xi.call_count, 2)
|
||||
|
||||
def test_load_match_data_parses_live_row_json_and_sidelined(self):
|
||||
odds_payload = {
|
||||
"Maç Sonucu": {"1": 2.10, "X": 3.30, "2": 3.50},
|
||||
"Çifte Şans": {"1-X": 1.30, "X-2": 1.52, "1-2": 1.34},
|
||||
"1,5 Alt/Üst": {"Üst": 1.33, "Alt": 2.90},
|
||||
"2,5 Alt/Üst": {"Üst": 1.91, "Alt": 1.85},
|
||||
"3,5 Alt/Üst": {"Üst": 2.95, "Alt": 1.38},
|
||||
"Karşılıklı Gol": {"Var": 1.84, "Yok": 1.92},
|
||||
"1. Yarı Sonucu": {"1": 2.55, "X": 1.97, "2": 3.45},
|
||||
}
|
||||
lineups_payload = {
|
||||
"home": {"xi": [{"personId": "101"}, {"personId": "102"}]},
|
||||
"away": {"xi": [{"personId": "201"}, {"personId": "202"}]},
|
||||
}
|
||||
live_row = {
|
||||
"match_id": "live-101",
|
||||
"home_team_id": "h-101",
|
||||
"away_team_id": "a-101",
|
||||
"league_id": "l-101",
|
||||
"sport": "FOOTBALL",
|
||||
"match_date_ms": 1760000000000,
|
||||
"odds": json.dumps(odds_payload),
|
||||
"lineups": json.dumps(lineups_payload),
|
||||
"sidelined": json.dumps(
|
||||
{
|
||||
"homeTeam": {"totalSidelined": 1, "players": []},
|
||||
"awayTeam": {"totalSidelined": 0, "players": []},
|
||||
}
|
||||
),
|
||||
"referee_name": "John Ref",
|
||||
"home_team_name": "Home FC",
|
||||
"away_team_name": "Away FC",
|
||||
"league_name": "League Name",
|
||||
}
|
||||
cursor = _RouterCursor(live_row=live_row)
|
||||
|
||||
with patch("services.single_match_orchestrator.psycopg2.connect", return_value=_ConnContext(cursor)):
|
||||
data = self.orchestrator._load_match_data("live-101")
|
||||
|
||||
self.assertIsNotNone(data)
|
||||
self.assertEqual(data.match_id, "live-101")
|
||||
self.assertEqual(data.home_team_id, "h-101")
|
||||
self.assertEqual(data.away_team_id, "a-101")
|
||||
self.assertEqual(data.sport, "football")
|
||||
self.assertEqual(data.referee_name, "John Ref")
|
||||
self.assertEqual(data.home_lineup, ["101", "102"])
|
||||
self.assertEqual(data.away_lineup, ["201", "202"])
|
||||
self.assertEqual(data.lineup_source, "none")
|
||||
self.assertEqual(data.sidelined_data["homeTeam"]["totalSidelined"], 1)
|
||||
self.assertEqual(data.odds_data["dc_x2"], 1.52)
|
||||
self.assertEqual(data.odds_data["ht_h"], 2.55)
|
||||
|
||||
def test_analyze_match_forwards_all_core_fields_to_predictor(self):
|
||||
match_data = MatchData(
|
||||
match_id="live-55",
|
||||
home_team_id="home-55",
|
||||
away_team_id="away-55",
|
||||
home_team_name="Home 55",
|
||||
away_team_name="Away 55",
|
||||
match_date_ms=1760000000000,
|
||||
sport="football",
|
||||
league_id="league-55",
|
||||
league_name="League 55",
|
||||
referee_name="Ref 55",
|
||||
odds_data={"ms_h": 2.4, "ms_d": 3.1, "ms_a": 2.9},
|
||||
home_lineup=["h1", "h2"],
|
||||
away_lineup=["a1", "a2"],
|
||||
sidelined_data={
|
||||
"homeTeam": {"totalSidelined": 2, "players": []},
|
||||
"awayTeam": {"totalSidelined": 1, "players": []},
|
||||
},
|
||||
home_goals_avg=1.6,
|
||||
home_conceded_avg=1.1,
|
||||
away_goals_avg=1.2,
|
||||
away_conceded_avg=1.4,
|
||||
home_position=5,
|
||||
away_position=8,
|
||||
lineup_source="confirmed_live",
|
||||
)
|
||||
prediction = FullMatchPrediction(match_id="live-55", home_team="Home 55", away_team="Away 55")
|
||||
|
||||
self.orchestrator._load_match_data = MagicMock(return_value=match_data)
|
||||
self.orchestrator.v25_predictor.predict_market_bundle = MagicMock(return_value={"MS": {"pick": "1"}})
|
||||
self.orchestrator._build_v25_features = MagicMock(return_value={})
|
||||
self.orchestrator._get_v25_signal = MagicMock(return_value={"MS": {"pick": "1"}})
|
||||
self.orchestrator._build_v25_prediction = MagicMock(return_value=prediction)
|
||||
self.orchestrator._build_prediction_package = MagicMock(return_value={"ok": True})
|
||||
|
||||
result = self.orchestrator.analyze_match("live-55")
|
||||
|
||||
self.assertEqual(result, {"ok": True})
|
||||
self.orchestrator._build_v25_features.assert_called_once_with(match_data)
|
||||
self.orchestrator._get_v25_signal.assert_called_once_with(match_data, {})
|
||||
self.orchestrator._build_v25_prediction.assert_called_once_with(
|
||||
match_data,
|
||||
{},
|
||||
{"MS": {"pick": "1"}},
|
||||
)
|
||||
|
||||
def test_analyze_match_routes_basketball_to_basketball_predictor(self):
|
||||
match_data = MatchData(
|
||||
match_id="b-live-1",
|
||||
home_team_id="bh",
|
||||
away_team_id="ba",
|
||||
home_team_name="Home B",
|
||||
away_team_name="Away B",
|
||||
match_date_ms=1760000000000,
|
||||
sport="basketball",
|
||||
league_id="bleague",
|
||||
league_name="B League",
|
||||
referee_name=None,
|
||||
odds_data={"ml_h": 1.75, "ml_a": 2.05, "tot_line": 161.5, "tot_o": 1.88, "tot_u": 1.92},
|
||||
home_lineup=None,
|
||||
away_lineup=None,
|
||||
sidelined_data={"homeTeam": {"totalSidelined": 1}, "awayTeam": {"totalSidelined": 0}},
|
||||
home_goals_avg=85.0,
|
||||
home_conceded_avg=79.0,
|
||||
away_goals_avg=82.0,
|
||||
away_conceded_avg=81.0,
|
||||
home_position=4,
|
||||
away_position=7,
|
||||
lineup_source="none",
|
||||
)
|
||||
prediction = BasketballMatchPrediction(
|
||||
match_id="b-live-1",
|
||||
home_team_name="Home B",
|
||||
away_team_name="Away B",
|
||||
league_name="B League",
|
||||
)
|
||||
|
||||
self.orchestrator._load_match_data = MagicMock(return_value=match_data)
|
||||
self.orchestrator.basketball_predictor.predict = MagicMock(return_value=prediction)
|
||||
self.orchestrator._build_basketball_prediction_package = MagicMock(
|
||||
return_value={"sport": "basketball", "ok": True}
|
||||
)
|
||||
|
||||
result = self.orchestrator.analyze_match("b-live-1")
|
||||
|
||||
self.assertEqual(result, {"sport": "basketball", "ok": True})
|
||||
self.orchestrator.basketball_predictor.predict.assert_called_once()
|
||||
kwargs = self.orchestrator.basketball_predictor.predict.call_args.kwargs
|
||||
self.assertEqual(kwargs["match_id"], "b-live-1")
|
||||
self.assertEqual(kwargs["home_team_id"], "bh")
|
||||
self.assertEqual(kwargs["away_team_id"], "ba")
|
||||
self.assertEqual(kwargs["league_id"], "bleague")
|
||||
self.assertEqual(kwargs["odds_data"]["ml_h"], 1.75)
|
||||
self.orchestrator.v25_predictor.predict_market_bundle.assert_not_called()
|
||||
|
||||
def test_build_market_rows_maps_odds_keys_correctly(self):
|
||||
data = MatchData(
|
||||
match_id="m-rows",
|
||||
home_team_id="h",
|
||||
away_team_id="a",
|
||||
home_team_name="Home",
|
||||
away_team_name="Away",
|
||||
match_date_ms=1760000000000,
|
||||
sport="football",
|
||||
league_id=None,
|
||||
league_name="",
|
||||
referee_name=None,
|
||||
odds_data={
|
||||
"ms_h": 2.3,
|
||||
"ms_d": 3.2,
|
||||
"ms_a": 3.1,
|
||||
"dc_x2": 1.45,
|
||||
"ou15_o": 1.36,
|
||||
"ou25_u": 1.92,
|
||||
"ou35_o": 2.85,
|
||||
"btts_y": 1.88,
|
||||
"ht_h": 2.55,
|
||||
"ht_ou05_o": 1.47,
|
||||
},
|
||||
home_lineup=None,
|
||||
away_lineup=None,
|
||||
sidelined_data=None,
|
||||
home_goals_avg=1.5,
|
||||
home_conceded_avg=1.2,
|
||||
away_goals_avg=1.2,
|
||||
away_conceded_avg=1.4,
|
||||
home_position=10,
|
||||
away_position=10,
|
||||
lineup_source="none",
|
||||
)
|
||||
pred = FullMatchPrediction(
|
||||
match_id="m-rows",
|
||||
home_team="Home",
|
||||
away_team="Away",
|
||||
ms_home_prob=0.25,
|
||||
ms_draw_prob=0.30,
|
||||
ms_away_prob=0.45,
|
||||
ms_pick="2",
|
||||
ms_confidence=69.0,
|
||||
dc_1x_prob=0.60,
|
||||
dc_x2_prob=0.72,
|
||||
dc_12_prob=0.68,
|
||||
dc_pick="X2",
|
||||
dc_confidence=67.0,
|
||||
over_15_prob=0.74,
|
||||
under_15_prob=0.26,
|
||||
ou15_pick="1.5 Üst",
|
||||
ou15_confidence=72.0,
|
||||
over_25_prob=0.44,
|
||||
under_25_prob=0.56,
|
||||
ou25_pick="2.5 Alt",
|
||||
ou25_confidence=61.0,
|
||||
over_35_prob=0.39,
|
||||
under_35_prob=0.61,
|
||||
ou35_pick="3.5 Over",
|
||||
ou35_confidence=58.0,
|
||||
btts_yes_prob=0.57,
|
||||
btts_no_prob=0.43,
|
||||
btts_pick="Yes",
|
||||
btts_confidence=63.0,
|
||||
ht_home_prob=0.41,
|
||||
ht_draw_prob=0.39,
|
||||
ht_away_prob=0.20,
|
||||
ht_pick="1",
|
||||
ht_confidence=60.0,
|
||||
ht_over_05_prob=0.64,
|
||||
ht_under_05_prob=0.36,
|
||||
ht_ou_pick="Over 0.5",
|
||||
)
|
||||
|
||||
rows = self.orchestrator._build_market_rows(data, pred)
|
||||
by_market = {row["market"]: row for row in rows}
|
||||
|
||||
self.assertEqual(by_market["MS"]["odds"], 3.1)
|
||||
self.assertEqual(by_market["DC"]["odds"], 1.45)
|
||||
self.assertEqual(by_market["OU15"]["odds"], 1.36)
|
||||
self.assertEqual(by_market["OU25"]["odds"], 1.92)
|
||||
self.assertEqual(by_market["OU35"]["odds"], 2.85)
|
||||
self.assertEqual(by_market["BTTS"]["odds"], 1.88)
|
||||
self.assertEqual(by_market["HT"]["odds"], 2.55)
|
||||
self.assertEqual(by_market["HT_OU05"]["odds"], 1.47)
|
||||
|
||||
def test_build_basketball_market_rows_maps_odds_keys_correctly(self):
|
||||
data = MatchData(
|
||||
match_id="b-rows",
|
||||
home_team_id="bh",
|
||||
away_team_id="ba",
|
||||
home_team_name="Home B",
|
||||
away_team_name="Away B",
|
||||
match_date_ms=1760000000000,
|
||||
sport="basketball",
|
||||
league_id="bl",
|
||||
league_name="Basketball League",
|
||||
referee_name=None,
|
||||
odds_data={
|
||||
"ml_h": 1.73,
|
||||
"ml_a": 2.10,
|
||||
"tot_line": 162.5,
|
||||
"tot_o": 1.89,
|
||||
"tot_u": 1.93,
|
||||
"spread_home_line": -4.5,
|
||||
"spread_h": 1.91,
|
||||
"spread_a": 1.88,
|
||||
},
|
||||
home_lineup=None,
|
||||
away_lineup=None,
|
||||
sidelined_data=None,
|
||||
home_goals_avg=84.0,
|
||||
home_conceded_avg=80.0,
|
||||
away_goals_avg=82.0,
|
||||
away_conceded_avg=81.0,
|
||||
home_position=5,
|
||||
away_position=8,
|
||||
lineup_source="none",
|
||||
)
|
||||
pred = {
|
||||
"match_id": "b-rows",
|
||||
"market_board": {
|
||||
"ML": {"1": "62%", "2": "38%"},
|
||||
"Totals": {"Under 162.5": "43%", "Over 162.5": "57%"},
|
||||
"Spread": {"Away +4.5": "46%", "Home -4.5": "54%"}
|
||||
}
|
||||
}
|
||||
|
||||
rows = self.orchestrator._build_basketball_market_rows(data, pred)
|
||||
by_market = {row["market"]: row for row in rows}
|
||||
|
||||
self.assertEqual(by_market["ML"]["odds"], 1.73)
|
||||
self.assertEqual(by_market["TOTAL"]["odds"], 1.89)
|
||||
self.assertEqual(by_market["SPREAD"]["odds"], 1.91)
|
||||
|
||||
def test_compute_data_quality_flags_missing_referee_and_lineup(self):
|
||||
data = MatchData(
|
||||
match_id="dq-1",
|
||||
home_team_id="h",
|
||||
away_team_id="a",
|
||||
home_team_name="Home",
|
||||
away_team_name="Away",
|
||||
match_date_ms=1760000000000,
|
||||
sport="football",
|
||||
league_id=None,
|
||||
league_name="",
|
||||
referee_name=None,
|
||||
odds_data={"ms_h": 2.5, "ms_d": 3.2, "ms_a": 2.9},
|
||||
home_lineup=["h1", "h2"],
|
||||
away_lineup=["a1"],
|
||||
sidelined_data=None,
|
||||
home_goals_avg=1.5,
|
||||
home_conceded_avg=1.2,
|
||||
away_goals_avg=1.2,
|
||||
away_conceded_avg=1.4,
|
||||
home_position=10,
|
||||
away_position=10,
|
||||
lineup_source="none",
|
||||
)
|
||||
|
||||
quality = self.orchestrator._compute_data_quality(data)
|
||||
|
||||
self.assertIn("lineup_incomplete", quality["flags"])
|
||||
self.assertIn("missing_referee", quality["flags"])
|
||||
self.assertEqual(quality["label"], "MEDIUM")
|
||||
|
||||
def test_load_match_data_returns_none_when_team_ids_missing(self):
|
||||
live_row = {
|
||||
"match_id": "live-missing-ids",
|
||||
"home_team_id": None,
|
||||
"away_team_id": None,
|
||||
"league_id": "l-1",
|
||||
"sport": "football",
|
||||
"match_date_ms": 1760000000000,
|
||||
"odds": None,
|
||||
"lineups": None,
|
||||
"sidelined": None,
|
||||
"referee_name": None,
|
||||
"home_team_name": "Home",
|
||||
"away_team_name": "Away",
|
||||
"league_name": "League",
|
||||
}
|
||||
cursor = _RouterCursor(live_row=live_row)
|
||||
|
||||
with patch("services.single_match_orchestrator.psycopg2.connect", return_value=_ConnContext(cursor)):
|
||||
data = self.orchestrator._load_match_data("live-missing-ids")
|
||||
|
||||
self.assertIsNone(data)
|
||||
|
||||
def test_decorate_market_row_blocks_required_market_when_odds_missing(self):
|
||||
data = MatchData(
|
||||
match_id="dq-odds",
|
||||
home_team_id="h",
|
||||
away_team_id="a",
|
||||
home_team_name="Home",
|
||||
away_team_name="Away",
|
||||
match_date_ms=1760000000000,
|
||||
sport="football",
|
||||
league_id="l1",
|
||||
league_name="League",
|
||||
referee_name="Ref",
|
||||
odds_data={"ms_h": 2.2, "ms_d": 3.2, "ms_a": 3.0},
|
||||
home_lineup=["h"] * 11,
|
||||
away_lineup=["a"] * 11,
|
||||
sidelined_data=None,
|
||||
home_goals_avg=1.5,
|
||||
home_conceded_avg=1.2,
|
||||
away_goals_avg=1.2,
|
||||
away_conceded_avg=1.4,
|
||||
home_position=7,
|
||||
away_position=9,
|
||||
lineup_source="confirmed_live",
|
||||
)
|
||||
prediction = FullMatchPrediction(match_id="dq-odds", home_team="Home", away_team="Away")
|
||||
quality = self.orchestrator._compute_data_quality(data)
|
||||
row = {
|
||||
"market": "HT_OU05",
|
||||
"pick": "İY 0.5 Üst",
|
||||
"probability": 0.65,
|
||||
"confidence": 66.0,
|
||||
"odds": 0.0,
|
||||
}
|
||||
|
||||
out = self.orchestrator._decorate_market_row(data, prediction, quality, row)
|
||||
self.assertFalse(out["playable"])
|
||||
self.assertIn("market_odds_missing", out["decision_reasons"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,142 +0,0 @@
|
||||
"""
|
||||
Unit Test for NEW Skip Logic in BetRecommender
|
||||
==============================================
|
||||
Run with: python ai-engine/tests/test_skip_logic.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
# Add paths
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
||||
|
||||
from core.calculators.bet_recommender import BetRecommender, RecommendationResult, MarketPredictionDTO
|
||||
from core.calculators.risk_assessor import RiskAnalysis
|
||||
from core.calculators.match_result_calculator import MatchResultPrediction
|
||||
from core.calculators.over_under_calculator import OverUnderPrediction
|
||||
from config.config_loader import get_config
|
||||
|
||||
@dataclass
|
||||
class DummyContext:
|
||||
"""Minimal mock for CalculationContext"""
|
||||
odds_data: dict
|
||||
|
||||
class TestSkipLogic(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Mock config to pass into BetRecommender
|
||||
self.mock_config = {
|
||||
"recommendations.market_weights": {"MS": 1.0, "ÇŞ": 0.9, "BTTS": 0.9, "2.5 Üst/Alt": 0.9},
|
||||
"recommendations.safe_markets": ["ÇŞ", "1.5 Üst/Alt"],
|
||||
"recommendations.market_accuracy": {"MS": 65, "ÇŞ": 75, "BTTS": 60, "2.5 Üst/Alt": 65},
|
||||
"recommendations.baseline_accuracy": 65.0,
|
||||
"recommendations.confidence_threshold": 60,
|
||||
"recommendations.value_confidence_min": 45,
|
||||
"recommendations.value_confidence_max": 60,
|
||||
"recommendations.value_edge_margin": 0.03,
|
||||
"recommendations.value_upgrade_edge": 5.0,
|
||||
"recommendations.risk_safe_boost": 1.2,
|
||||
"recommendations.risk_ms_penalty_high": 0.5,
|
||||
"recommendations.risk_other_penalty": 0.7,
|
||||
"recommendations.risk_ms_penalty_medium": 0.8,
|
||||
}
|
||||
self.recommender = BetRecommender(self.mock_config)
|
||||
|
||||
def _make_risk(self, level="MEDIUM", is_surprise=False):
|
||||
return RiskAnalysis(risk_level=level, is_surprise_risk=is_surprise, risk_score=0.5)
|
||||
|
||||
def _make_ms_pred(self, pick, conf):
|
||||
# pick: "1", "X", "2"
|
||||
probs = {"1": {"ms_home_prob": 0.5, "ms_draw_prob": 0.3, "ms_away_prob": 0.2},
|
||||
"X": {"ms_home_prob": 0.2, "ms_draw_prob": 0.5, "ms_away_prob": 0.3},
|
||||
"2": {"ms_home_prob": 0.2, "ms_draw_prob": 0.3, "ms_away_prob": 0.5}}
|
||||
p = probs.get(pick, probs["1"])
|
||||
return MatchResultPrediction(
|
||||
ms_pick=pick, ms_confidence=conf,
|
||||
dc_pick="1X", dc_confidence=0,
|
||||
dc_1x_prob=0.7, dc_x2_prob=0.7, dc_12_prob=0.7,
|
||||
**p
|
||||
)
|
||||
|
||||
def _make_ou_pred(self):
|
||||
return OverUnderPrediction(
|
||||
ou25_pick="2.5 Üst", ou25_confidence=50.0,
|
||||
over_25_prob=0.55, under_25_prob=0.45,
|
||||
|
||||
btts_pick="Var", btts_confidence=50.0,
|
||||
btts_yes_prob=0.55, btts_no_prob=0.45,
|
||||
|
||||
ou15_pick="1.5 Üst", ou15_confidence=60.0, over_15_prob=0.7, under_15_prob=0.3,
|
||||
ou35_pick="3.5 Alt", ou35_confidence=50.0, over_35_prob=0.3, under_35_prob=0.7
|
||||
)
|
||||
|
||||
def test_low_confidence_should_skip(self):
|
||||
"""Confidence < 45% should be SKIPPED"""
|
||||
ms_pred = self._make_ms_pred(pick="2", conf=40.0)
|
||||
ou_pred = self._make_ou_pred()
|
||||
risk = self._make_risk("MEDIUM")
|
||||
ctx = DummyContext(odds_data={"ms_2": 2.5})
|
||||
|
||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
||||
|
||||
# Check if MS bet is skipped
|
||||
ms_bet = next((b for b in res.skipped_bets if b.market_type == "MS"), None)
|
||||
self.assertIsNotNone(ms_bet, "MS bet with 40% conf should be skipped!")
|
||||
self.assertTrue(ms_bet.is_skip)
|
||||
|
||||
def test_good_confidence_should_recommend(self):
|
||||
"""Confidence > 60% and Good Odds should be RECOMMENDED"""
|
||||
ms_pred = self._make_ms_pred(pick="1", conf=70.0)
|
||||
ou_pred = self._make_ou_pred()
|
||||
risk = self._make_risk("MEDIUM")
|
||||
# Odds 1.80 for 70% prob = Good Value (Need real odds for MS to pass)
|
||||
ctx = DummyContext(odds_data={"ms_1": 1.80, "ou15_o": 1.50}) # Added ou15 odds
|
||||
|
||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
||||
|
||||
# Check if ANY bet is recommended (doesn't have to be MS, but usually is)
|
||||
self.assertGreater(len(res.recommended_bets), 0, "At least one bet should be recommended!")
|
||||
# Check that MS bet is NOT skipped
|
||||
ms_bet = next((b for b in res.recommended_bets if b.market_type == "MS"), None)
|
||||
if ms_bet:
|
||||
self.assertFalse(ms_bet.is_skip)
|
||||
|
||||
def test_negative_edge_should_skip(self):
|
||||
"""Even with high confidence, if Odds are too low (Bad Value), SKIP"""
|
||||
ms_pred = self._make_ms_pred(pick="1", conf=70.0) # 70% prob
|
||||
ou_pred = self._make_ou_pred()
|
||||
risk = self._make_risk("MEDIUM")
|
||||
# Odds 1.10 -> Implied 90%. Our prob is 70%. Edge is -20% -> SKIP
|
||||
ctx = DummyContext(odds_data={"ms_1": 1.10})
|
||||
|
||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
||||
|
||||
ms_bet = next((b for b in res.skipped_bets if b.market_type == "MS"), None)
|
||||
self.assertIsNotNone(ms_bet, "MS bet with terrible odds (Negative Edge) should be skipped!")
|
||||
self.assertTrue(ms_bet.is_skip)
|
||||
|
||||
def test_no_bets_recommendation(self):
|
||||
"""If all bets are low confidence, best_bet should be None"""
|
||||
ms_pred = self._make_ms_pred(pick="1", conf=30.0) # Very low conf
|
||||
ou_pred = self._make_ou_pred()
|
||||
# Reset ALL OU confs to low
|
||||
ou_pred.ou25_confidence = 30.0
|
||||
ou_pred.btts_confidence = 30.0
|
||||
ou_pred.ou15_confidence = 30.0 # This was 60 in setUp, causing the fail!
|
||||
ou_pred.ou35_confidence = 30.0
|
||||
|
||||
risk = self._make_risk("MEDIUM")
|
||||
ctx = DummyContext(odds_data={"ms_1": 2.0})
|
||||
|
||||
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
|
||||
|
||||
self.assertIsNone(res.best_bet, "If everything is skipped, there should be no best_bet.")
|
||||
self.assertEqual(len(res.recommended_bets), 0, "No bets should be recommended!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("🧪 Running Skip Logic Unit Tests...")
|
||||
print("="*50)
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,155 @@
|
||||
# Changelog - 2026-04-22
|
||||
|
||||
Bu doküman, 22 Nisan 2026 tarihinde `iddaai-fe` ve `iddaai-be` üzerinde yapılan Frekans Motoru (Conditional Frequency Engine) frontend entegrasyonunu özetler.
|
||||
|
||||
## 1. Frekans Motoru — Backend Recap
|
||||
|
||||
- `POST /coupon/frequency-coupon` endpoint'i önceki oturumda tamamlanmıştı.
|
||||
- `SmartCouponService.generateFrequencyBasedCoupon()` metodu aktif ve çalışır durumda.
|
||||
- `FrequencyEngineService` → raw SQL ile `matches` tablosundaki tarihsel veriyi tarayarak oran bandı bazlı sinyal üretiyor.
|
||||
- Strateji: Her takımın ev/deplasman performansını, karşılaştığı oran bandına göre filtreleyip, kombine sinyal (combined_signal) hesaplıyor.
|
||||
|
||||
## 2. Frontend Tip Tanımları
|
||||
|
||||
- `iddaai-fe/src/lib/api/coupons/types.ts` güncellendi.
|
||||
- Eklenen tipler:
|
||||
|
||||
### FrequencyCouponRequestDto
|
||||
```typescript
|
||||
{
|
||||
maxMatches?: number; // 2-5 arası, varsayılan 3
|
||||
minSignal?: number; // 0.50-0.99, kombine sinyal eşiği
|
||||
markets?: string[]; // OU1.5, OU2.5, OU3.5, BTTS, MS
|
||||
}
|
||||
```
|
||||
|
||||
### FrequencyCouponBetDto
|
||||
```typescript
|
||||
{
|
||||
match_id: string;
|
||||
match_name: string;
|
||||
league: string;
|
||||
market: string;
|
||||
pick: string;
|
||||
odds: number;
|
||||
home_signal: number;
|
||||
away_signal: number;
|
||||
combined_signal: number;
|
||||
home_odds_band: string;
|
||||
away_odds_band: string;
|
||||
home_match_count: number;
|
||||
away_match_count: number;
|
||||
league_profile: string; // GOLCU | DEFANSIF | NORMAL
|
||||
}
|
||||
```
|
||||
|
||||
### FrequencyCouponRejectedDto
|
||||
```typescript
|
||||
{
|
||||
match_name: string;
|
||||
reason: string;
|
||||
}
|
||||
```
|
||||
|
||||
### FrequencyCouponResultDto
|
||||
```typescript
|
||||
{
|
||||
bets: FrequencyCouponBetDto[];
|
||||
rejected_matches: FrequencyCouponRejectedDto[];
|
||||
reasoning: string[];
|
||||
total_odds: number;
|
||||
expected_hit_rate: number;
|
||||
expected_value: number;
|
||||
ev_positive: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## 3. API Service Katmanı
|
||||
|
||||
- `iddaai-fe/src/lib/api/coupons/service.ts` güncellendi.
|
||||
- `generateFrequencyCoupon(dto)` metodu eklendi.
|
||||
- Endpoint: `POST /coupon/frequency-coupon`
|
||||
|
||||
## 4. React Hook
|
||||
|
||||
- `iddaai-fe/src/lib/api/coupons/use-hooks.ts` güncellendi.
|
||||
- `useGenerateFrequencyCoupon()` TanStack Query mutation hook'u eklendi.
|
||||
- `FrequencyCouponRequestDto` import edildi.
|
||||
|
||||
## 5. Çeviri Dosyaları (i18n)
|
||||
|
||||
- `messages/tr.json` ve `messages/en.json` güncellendi.
|
||||
- `coupons` namespace'ine 30+ yeni anahtar eklendi:
|
||||
|
||||
| Anahtar | TR | EN |
|
||||
|---|---|---|
|
||||
| `freq-engine-title` | Frekans Motoru | Frequency Engine |
|
||||
| `freq-engine-subtitle` | Takımların oran bandına göre tarihsel performansını analiz eder... | Analyzes teams' historical performance by odds band... |
|
||||
| `freq-suggest` | Frekans Kuponu Oluştur | Generate Frequency Coupon |
|
||||
| `freq-min-signal` | Minimum Sinyal | Minimum Signal |
|
||||
| `freq-ev-label` | Beklenen Değer (EV) | Expected Value (EV) |
|
||||
| `freq-hit-rate` | Tahmini İsabet | Est. Hit Rate |
|
||||
| `freq-ev-positive` | +EV Pozitif | +EV Positive |
|
||||
| `freq-combined-signal` | Kombine Sinyal | Combined Signal |
|
||||
| `freq-league-golcu` | Golcü | High-Scoring |
|
||||
| `freq-league-defansif` | Defansif | Defensive |
|
||||
| `engine-mode-label` | Motor Seçimi | Engine Mode |
|
||||
| `engine-mode-help` | AI: Gemini tabanlı yapay zeka tahmini. Frekans: Veritabanı tabanlı istatistiksel analiz. | AI: Gemini-based AI prediction. Frequency: Database-driven statistical analysis. |
|
||||
| `freq-mode-active` | Frekans Motoru aktif | Frequency Engine active |
|
||||
| `ai-mode-active` | AI Motoru aktif | AI Engine active |
|
||||
|
||||
## 6. FrequencyPanel Bileşeni (Yeni Dosya)
|
||||
|
||||
- `iddaai-fe/src/components/coupons/frequency-panel.tsx` oluşturuldu.
|
||||
- Bağımsız (standalone) bileşen, kendi state ve mutation yönetimini içerir.
|
||||
|
||||
### Bileşen Özellikleri:
|
||||
1. **Min Signal Slider** — 50%-95% arası, kombine sinyal eşiği kontrolü
|
||||
2. **Max Matches Slider** — 2-5 arası, kupon boyutu kontrolü
|
||||
3. **Market Filtre Badge'leri** — OU1.5, OU2.5, OU3.5, BTTS, MS (çoklu seçim)
|
||||
4. **Generate Butonu** → `useGenerateFrequencyCoupon` mutation'ını tetikler
|
||||
5. **Sonuç Paneli**:
|
||||
- EV / Hit Rate / Toplam Oran istatistik kartları
|
||||
- Her bahis için ev sinyali, deplasman sinyali, kombine sinyal gösterimi
|
||||
- Oran bandı bilgisi (ör. "1.30-1.50")
|
||||
- Lig profili badge'i (Golcü/Defansif/Normal)
|
||||
- Geçmiş maç sayısı gösterimi
|
||||
- Analiz detayları (reasoning listesi)
|
||||
- Elenen maçlar (rejected_matches)
|
||||
6. **Kupon Store Senkronizasyonu** — Sonuç geldiğinde bahisler otomatik olarak `useCouponStore`'a eklenir
|
||||
|
||||
## 7. Coupon Builder Engine Toggle
|
||||
|
||||
- `iddaai-fe/src/components/coupons/coupon-builder-content.tsx` güncellendi.
|
||||
- Değişiklikler:
|
||||
- `LuDatabase` icon import edildi
|
||||
- `FrequencyPanel` import edildi
|
||||
- `engineMode` state eklendi: `"ai" | "frequency"`
|
||||
- Sidebar'a **Motor Seçimi** toggle eklendi (Badge tabanlı)
|
||||
- `engineMode === "frequency"` olduğunda strateji/AI suggest bölümü gizlenir, yerine `FrequencyPanel` render edilir
|
||||
- `engineMode === "ai"` olduğunda mevcut AI akışı aynen korunur
|
||||
|
||||
### Veri Akışı:
|
||||
```
|
||||
Kullanıcı → "Frekans" badge'ine tıklar → FrequencyPanel açılır
|
||||
→ Sinyal/market/boyut ayarı yapar → "Frekans Kuponu Oluştur" butonuna basar
|
||||
→ POST /coupon/frequency-coupon { maxMatches, minSignal, markets }
|
||||
→ Backend: SmartCouponService → FrequencyEngineService (raw SQL)
|
||||
→ Response: FrequencyCouponResultDto
|
||||
→ UI: Sinyal kartları, EV istatistikleri, reasoning render edilir
|
||||
→ Bahisler otomatik olarak CouponStore'a sync edilir
|
||||
```
|
||||
|
||||
## 8. Derleme ve Doğrulama Notları
|
||||
|
||||
- `node_modules` kullanıcının makinesinde yüklü olmadığı için `npm run build` çalıştırılamadı.
|
||||
- Kod yapısal olarak doğru, TypeScript tipleri backend DTO'ları ile birebir eşleşiyor.
|
||||
- Doğrulama için: `npm install && npm run build` çalıştırılmalı.
|
||||
|
||||
## 9. Açık Kalan / Sonraki Adımlar
|
||||
|
||||
- `npm install && npm run build` ile frontend build doğrulanmalı.
|
||||
- Frekans kuponu uçtan uca test edilmeli (backend Docker ayakta iken).
|
||||
- FrequencyPanel içindeki market badge'lerine `HT_OU05` ve `DC` gibi ek marketler eklenebilir.
|
||||
- Frekans sonuçlarındaki `league_profile` badge renkleri dark mode için ince ayar gerektirebilir.
|
||||
- Kupon geçmişinde AI vs Frekans ayrımını gösteren bir etiket eklenebilir.
|
||||
@@ -29,6 +29,13 @@
|
||||
"cleanup:live": "ts-node -r tsconfig-paths/register src/scripts/cleanup-live-matches.ts",
|
||||
"swagger:summary": "ts-node -r tsconfig-paths/register src/scripts/export-swagger-endpoints-summary.ts",
|
||||
"postman:export": "ts-node -r tsconfig-paths/register src/scripts/export-postman-collection.ts"
|
||||
,
|
||||
"ai:extract:v26": "python3 ai-engine/scripts/extract_training_data_v26.py",
|
||||
"ai:train:v26": "python3 ai-engine/scripts/train_v26_shadow.py",
|
||||
"ai:backtest:v26": "python3 ai-engine/scripts/backtest_v26_shadow.py",
|
||||
"ai:backtest:v26:roi": "python3 ai-engine/scripts/backtest_v26_shadow_roi_detail.py",
|
||||
"ai:backtest:v26:htft": "python3 ai-engine/scripts/backtest_v26_shadow_htft_upset.py",
|
||||
"ai:test": "python3 -m pytest ai-engine/tests/test_main_api.py ai-engine/tests/test_single_match_orchestrator.py ai-engine/tests/test_v26_shadow_engine.py"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.964.0",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE "prediction_runs" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"match_id" TEXT NOT NULL,
|
||||
"engine_version" TEXT NOT NULL,
|
||||
"decision_trace_id" TEXT,
|
||||
"generated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"odds_snapshot" JSONB,
|
||||
"payload_summary" JSONB NOT NULL,
|
||||
"eventual_outcome" TEXT,
|
||||
"unit_profit" DOUBLE PRECISION,
|
||||
|
||||
CONSTRAINT "prediction_runs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "prediction_runs_match_id_generated_at_idx"
|
||||
ON "prediction_runs"("match_id", "generated_at" DESC);
|
||||
|
||||
CREATE INDEX "prediction_runs_engine_version_generated_at_idx"
|
||||
ON "prediction_runs"("engine_version", "generated_at" DESC);
|
||||
@@ -489,6 +489,22 @@ model Prediction {
|
||||
@@map("predictions")
|
||||
}
|
||||
|
||||
model PredictionRun {
|
||||
id BigInt @id @default(autoincrement())
|
||||
matchId String @map("match_id")
|
||||
engineVersion String @map("engine_version")
|
||||
decisionTraceId String? @map("decision_trace_id")
|
||||
generatedAt DateTime @default(now()) @map("generated_at")
|
||||
oddsSnapshot Json? @map("odds_snapshot")
|
||||
payloadSummary Json @map("payload_summary")
|
||||
eventualOutcome String? @map("eventual_outcome")
|
||||
unitProfit Float? @map("unit_profit")
|
||||
|
||||
@@index([matchId, generatedAt(sort: Desc)])
|
||||
@@index([engineVersion, generatedAt(sort: Desc)])
|
||||
@@map("prediction_runs")
|
||||
}
|
||||
|
||||
model AiPredictionsLog {
|
||||
id Int @id @default(autoincrement())
|
||||
matchId String @map("match_id")
|
||||
|
||||
+330
-695
File diff suppressed because it is too large
Load Diff
@@ -1,109 +0,0 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
# Path alignment
|
||||
sys.path.append(os.getcwd())
|
||||
sys.path.append(os.path.join(os.getcwd(), 'ai-engine'))
|
||||
|
||||
from pipeline.tiered_loader import TieredDataLoader
|
||||
from pipeline.sequence_builder import SequenceBuilder
|
||||
from models.hybrid_v11 import HybridDeepModel
|
||||
from features.odds_history import OddsHistoryEngine
|
||||
from features.synthetic_xg import SyntheticXGModel
|
||||
|
||||
DEVICE = 'cpu'
|
||||
MODEL_PATH = 'ai-engine/models/v11_hybrid_model.pth'
|
||||
TARGET_ID = 'en78ih6ec7exnpxcku3xc3das'
|
||||
|
||||
def audit():
|
||||
print(f"🕵️ Auditing Match: {TARGET_ID}")
|
||||
|
||||
# 1. Pipeline Data
|
||||
builder = SequenceBuilder()
|
||||
X, y, meta = builder.build_sequences()
|
||||
|
||||
# Check if target is in dataset
|
||||
idx_list = meta.index[meta['match_id'] == TARGET_ID].tolist()
|
||||
if not idx_list:
|
||||
print("❌ Match not found in generated sequences. Is it too old or too new?")
|
||||
return
|
||||
|
||||
idx = idx_list[0]
|
||||
row_meta = meta.iloc[idx]
|
||||
|
||||
# 2. Features
|
||||
loader = TieredDataLoader()
|
||||
odds_df = loader.load_gold_data([TARGET_ID])
|
||||
eng = OddsHistoryEngine()
|
||||
xg_model = SyntheticXGModel()
|
||||
|
||||
# Team Mapping
|
||||
unique_teams = meta['team_id'].unique()
|
||||
team_map = {tid: i for i, tid in enumerate(unique_teams)}
|
||||
|
||||
# 3. Predict exactly like Backtest
|
||||
state = torch.load(MODEL_PATH, map_location=DEVICE)
|
||||
emb_key = 'entity_emb.weight' if 'entity_emb.weight' in state else 'team_embedding.weight'
|
||||
saved_vocab_size = state[emb_key].shape[0]
|
||||
|
||||
model = HybridDeepModel(num_teams=saved_vocab_size)
|
||||
new_state = {k.replace('team_embedding', 'entity_emb'): v for k, v in state.items()}
|
||||
model.load_state_dict(new_state, strict=False)
|
||||
model.eval()
|
||||
|
||||
# Data components
|
||||
team_idx = team_map.get(row_meta['team_id'], 0)
|
||||
entities = torch.LongTensor([team_idx, 0]).unsqueeze(0)
|
||||
seq = torch.FloatTensor(X[idx]).unsqueeze(0)
|
||||
|
||||
# Context (Odds + xG)
|
||||
odds_lookup = {}
|
||||
for _, r in odds_df.iterrows():
|
||||
mid = r['match_id']
|
||||
if mid not in odds_lookup: odds_lookup[mid] = {}
|
||||
if r['category'] == 'Maç Sonucu': odds_lookup[mid][r['selection']] = r['odd_value']
|
||||
elif r['category'] == '2,5 Alt/Üst':
|
||||
if 'Üst' in r['selection']: odds_lookup[mid]['Over'] = r['odd_value']
|
||||
else: odds_lookup[mid]['Under'] = r['odd_value']
|
||||
|
||||
odds = odds_lookup.get(TARGET_ID, {'1': 1.0, 'X': 1.0, '2': 1.0, 'Over': 1.0, 'Under': 1.0})
|
||||
syn_xg = 1.35 # Placeholder in trainer for xG component if used
|
||||
hist_win_rate = eng.get_feature(row_meta['team_id'], float(odds.get('1', 1.0)))
|
||||
|
||||
ctx = torch.FloatTensor([
|
||||
float(odds.get('1', 1.0)), float(odds.get('X', 1.0)), float(odds.get('2', 1.0)),
|
||||
float(odds.get('Over', 1.0)), float(odds.get('Under', 1.0)),
|
||||
syn_xg, syn_xg,
|
||||
hist_win_rate
|
||||
]).unsqueeze(0)
|
||||
|
||||
with torch.no_grad():
|
||||
logits_res, pred_goals, logits_btts, logits_ht_ft = model(entities, seq, ctx)
|
||||
probs = F.softmax(logits_res, dim=1).numpy()[0]
|
||||
prob_btts = torch.sigmoid(logits_btts).item()
|
||||
probs_ht = F.softmax(logits_ht_ft, dim=1).numpy()[0]
|
||||
|
||||
print("\n📊 INTERNAL PIPELINE PREDICTION:")
|
||||
print(f"Target Team: {row_meta['team_id']}")
|
||||
print(f"1X2 Probs: Home:{probs[0]:.4f} Draw:{probs[1]:.4f} Away:{probs[2]:.4f}")
|
||||
print(f"BTTS Prob: {prob_btts:.4f}")
|
||||
|
||||
ht_map = ["1/1", "1/X", "1/2", "X/1", "X/X", "X/2", "2/1", "2/X", "2/2"]
|
||||
top3_ht = np.argsort(probs_ht)[-3:][::-1]
|
||||
print("Top 3 HT/FT:")
|
||||
for idx_ht in top3_ht:
|
||||
print(f" {ht_map[idx_ht]}: {probs_ht[idx_ht]:.4f}")
|
||||
|
||||
actual_res = y[idx][0]
|
||||
actual_ht_idx = int(y[idx][3])
|
||||
print(f"\n✅ ACTUAL REALITY:")
|
||||
print(f"Result (Y): {actual_res} (0.0=Away)")
|
||||
print(f"HT/FT Class: {actual_ht_idx} ({ht_map[actual_ht_idx]})")
|
||||
|
||||
if __name__ == "__main__":
|
||||
audit()
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test surprise detection on known surprise matches."""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, 'ai-engine')
|
||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
||||
import json
|
||||
|
||||
# Test Bayern vs Augsburg (24 Jan 2026) - 1/2 Reversal
|
||||
match_id = 'en78ih6ec7exnpxcku3xc3das'
|
||||
|
||||
orch = SingleMatchOrchestrator()
|
||||
result = orch.analyze_match(match_id)
|
||||
|
||||
if result:
|
||||
print('=== Bayern Munch vs Augsburg (24 Jan 2026) ===')
|
||||
print('Actual: HT 1-0, FT 1-2 (1/2 Reversal!)')
|
||||
print()
|
||||
|
||||
# Check risk
|
||||
risk = result.get('risk', {})
|
||||
print(f"Risk Level: {risk.get('level', 'N/A')}")
|
||||
print(f"Is Surprise Risk: {risk.get('is_surprise_risk', False)}")
|
||||
print(f"Surprise Type: {risk.get('surprise_type', 'N/A')}")
|
||||
print(f"Risk Score: {risk.get('score', 'N/A')}")
|
||||
print()
|
||||
|
||||
# Check HT/FT probabilities from market_board
|
||||
htft = result.get('market_board', {}).get('HTFT', {}).get('probs', {})
|
||||
print('HT/FT Probabilities:')
|
||||
if htft:
|
||||
for k, v in sorted(htft.items(), key=lambda x: x[1], reverse=True):
|
||||
print(f" {k}: {v*100:.1f}%")
|
||||
else:
|
||||
print(" EMPTY!")
|
||||
print()
|
||||
|
||||
# Check main pick
|
||||
main = result.get('main_pick', {})
|
||||
print(f"Main Pick: {main.get('market', 'N/A')} - {main.get('pick', 'N/A')}")
|
||||
print(f"Confidence: {main.get('calibrated_confidence', 'N/A')}%")
|
||||
print(f"Is Guaranteed: {main.get('is_guaranteed', False)}")
|
||||
print()
|
||||
|
||||
# Check aggressive pick
|
||||
agg = result.get('aggressive_pick', {})
|
||||
if agg:
|
||||
print(f"Aggressive Pick: {agg.get('market', 'N/A')} - {agg.get('pick', 'N/A')}")
|
||||
print(f"Odds: {agg.get('odds', 'N/A')}")
|
||||
print()
|
||||
|
||||
# Check bet_summary for HTFT
|
||||
bet_summary = result.get('bet_summary', [])
|
||||
for bet in bet_summary:
|
||||
if bet.get('market') == 'HTFT':
|
||||
print(f"HTFT Bet: {bet}")
|
||||
else:
|
||||
print('Match not found')
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test the improved surprise detection logic"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, 'ai-engine')
|
||||
|
||||
from core.calculators.risk_assessor import RiskAssessor
|
||||
from config.config_loader import get_config
|
||||
|
||||
def test_surprise_detection():
|
||||
config = get_config()
|
||||
assessor = RiskAssessor(config)
|
||||
|
||||
# Test cases based on real scenarios
|
||||
test_cases = [
|
||||
{
|
||||
'name': 'Bayern vs Augsburg (1.30 odds, 2% 1/2 prob)',
|
||||
'odds': {'ms_h': 1.30, 'ms_d': 5.00, 'ms_a': 8.00},
|
||||
'ht_ft': {'1/1': 0.30, '1/X': 0.07, '1/2': 0.02, 'X/1': 0.15, 'X/X': 0.16, 'X/2': 0.09, '2/1': 0.03, '2/X': 0.04, '2/2': 0.14},
|
||||
'expected_surprise': True,
|
||||
'expected_type': '1/2 Potential Upset'
|
||||
},
|
||||
{
|
||||
'name': 'Strong favorite (1.20 odds, 1.5% 1/2 prob)',
|
||||
'odds': {'ms_h': 1.20, 'ms_d': 6.00, 'ms_a': 12.00},
|
||||
'ht_ft': {'1/1': 0.35, '1/X': 0.05, '1/2': 0.015, 'X/1': 0.20, 'X/X': 0.15, 'X/2': 0.05, '2/1': 0.02, '2/X': 0.03, '2/2': 0.10},
|
||||
'expected_surprise': True,
|
||||
'expected_type': '1/2 Potential Upset'
|
||||
},
|
||||
{
|
||||
'name': 'Moderate favorite (1.50 odds, 3% 1/2 prob)',
|
||||
'odds': {'ms_h': 1.50, 'ms_d': 4.00, 'ms_a': 6.00},
|
||||
'ht_ft': {'1/1': 0.28, '1/X': 0.08, '1/2': 0.03, 'X/1': 0.18, 'X/X': 0.15, 'X/2': 0.08, '2/1': 0.04, '2/X': 0.05, '2/2': 0.11},
|
||||
'expected_surprise': True,
|
||||
'expected_type': '1/2 Potential Upset'
|
||||
},
|
||||
{
|
||||
'name': 'Even match (2.00 odds, 5% 1/2 prob)',
|
||||
'odds': {'ms_h': 2.00, 'ms_d': 3.30, 'ms_a': 3.30},
|
||||
'ht_ft': {'1/1': 0.20, '1/X': 0.10, '1/2': 0.05, 'X/1': 0.15, 'X/X': 0.15, 'X/2': 0.10, '2/1': 0.05, '2/X': 0.10, '2/2': 0.10},
|
||||
'expected_surprise': False, # No clear favorite
|
||||
'expected_type': None
|
||||
},
|
||||
{
|
||||
'name': 'Away favorite (1.40 away odds, 2% 2/1 prob)',
|
||||
'odds': {'ms_h': 6.00, 'ms_d': 4.00, 'ms_a': 1.40},
|
||||
'ht_ft': {'1/1': 0.10, '1/X': 0.05, '1/2': 0.04, 'X/1': 0.08, 'X/X': 0.15, 'X/2': 0.20, '2/1': 0.02, '2/X': 0.06, '2/2': 0.30},
|
||||
'expected_surprise': True,
|
||||
'expected_type': '2/1 Potential Upset'
|
||||
},
|
||||
]
|
||||
|
||||
print("=" * 70)
|
||||
print("SURPRISE DETECTION TEST RESULTS")
|
||||
print("=" * 70)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for tc in test_cases:
|
||||
class MockCtx:
|
||||
is_surprise = False
|
||||
is_top_league = True
|
||||
sport = 'football'
|
||||
xgboost_preds = {'ht_ft': tc['ht_ft']}
|
||||
odds_data = tc['odds']
|
||||
|
||||
result = assessor.assess_risk(MockCtx())
|
||||
|
||||
# Check if result matches expectation
|
||||
is_correct = result.is_surprise_risk == tc['expected_surprise']
|
||||
if tc['expected_type'] and result.surprise_type != tc['expected_type']:
|
||||
is_correct = False
|
||||
|
||||
status = "✅ PASS" if is_correct else "❌ FAIL"
|
||||
if is_correct:
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print(f"\n{status} - {tc['name']}")
|
||||
print(f" Expected: surprise={tc['expected_surprise']}, type={tc['expected_type']}")
|
||||
print(f" Got: surprise={result.is_surprise_risk}, type={result.surprise_type}")
|
||||
if result.reasons:
|
||||
print(f" Reasons: {result.reasons}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(f"SUMMARY: {passed} passed, {failed} failed")
|
||||
print("=" * 70)
|
||||
|
||||
return failed == 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_surprise_detection()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test UpsetEngine on Bayern vs Augsburg match."""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, 'ai-engine')
|
||||
from features.upset_engine import get_upset_engine
|
||||
from data.db import get_clean_dsn
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from datetime import datetime
|
||||
|
||||
# Get match data
|
||||
conn = psycopg2.connect(get_clean_dsn())
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
||||
m.ht_score_home, m.ht_score_away, m.mst_utc,
|
||||
th.name as home_name, ta.name as away_name, l.name as league
|
||||
FROM matches m
|
||||
JOIN teams th ON m.home_team_id = th.id
|
||||
JOIN teams ta ON m.away_team_id = ta.id
|
||||
JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.id = 'en78ih6ec7exnpxcku3xc3das'
|
||||
""")
|
||||
match = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
if match:
|
||||
print('=== Bayern Munch vs Augsburg (24 Jan 2026) ===')
|
||||
print(f"Actual: HT {match['ht_score_home']}-{match['ht_score_away']}, FT {match['score_home']}-{match['score_away']} (1/2 Reversal!)")
|
||||
print()
|
||||
|
||||
# Test UpsetEngine
|
||||
engine = get_upset_engine()
|
||||
|
||||
# Calculate upset potential using get_features
|
||||
result = engine.get_features(
|
||||
home_team_name=match['home_name'],
|
||||
home_team_id=match['home_team_id'],
|
||||
away_team_name=match['away_name'],
|
||||
league_name=match['league'],
|
||||
home_position=1, # Bayern is typically top
|
||||
away_position=15, # Augsburg is typically lower
|
||||
match_date_ms=match['mst_utc'],
|
||||
total_teams=18,
|
||||
)
|
||||
|
||||
print('UpsetEngine Results:')
|
||||
print(f" Atmosphere Score: {result.get('upset_atmosphere', 0):.2f}")
|
||||
print(f" Motivation Score: {result.get('upset_motivation', 0):.2f}")
|
||||
print(f" Fatigue Score: {result.get('upset_fatigue', 0):.2f}")
|
||||
print(f" Historical Score: {result.get('upset_historical', 0):.2f}")
|
||||
print(f" TOTAL UPSET POTENTIAL: {result.get('upset_potential', 0):.2f}")
|
||||
print()
|
||||
|
||||
# Check if upset was detected
|
||||
if result.get('upset_potential', 0) > 0.5:
|
||||
print("🔥 HIGH UPSET POTENTIAL DETECTED!")
|
||||
elif result.get('upset_potential', 0) > 0.3:
|
||||
print("⚠️ MEDIUM UPSET POTENTIAL")
|
||||
else:
|
||||
print("❌ LOW UPSET POTENTIAL - Model did not detect this as upset")
|
||||
else:
|
||||
print('Match not found')
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { AppService } from "./app.service";
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
|
||||
+54
-54
@@ -1,19 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { redisStore } from 'cache-manager-redis-yet';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
|
||||
import { ThrottlerModule, ThrottlerGuard } from "@nestjs/throttler";
|
||||
import { CacheModule } from "@nestjs/cache-manager";
|
||||
import { ScheduleModule } from "@nestjs/schedule";
|
||||
import { redisStore } from "cache-manager-redis-yet";
|
||||
import { LoggerModule } from "nestjs-pino";
|
||||
import {
|
||||
I18nModule,
|
||||
AcceptLanguageResolver,
|
||||
HeaderResolver,
|
||||
QueryResolver,
|
||||
} from 'nestjs-i18n';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import * as path from 'path';
|
||||
} from "nestjs-i18n";
|
||||
import { ServeStaticModule } from "@nestjs/serve-static";
|
||||
import * as path from "path";
|
||||
|
||||
// Config
|
||||
import {
|
||||
@@ -24,52 +24,52 @@ import {
|
||||
i18nConfig,
|
||||
featuresConfig,
|
||||
throttleConfig,
|
||||
} from './config/configuration';
|
||||
import { geminiConfig } from './modules/gemini/gemini.config';
|
||||
import { validateEnv } from './config/env.validation';
|
||||
} from "./config/configuration";
|
||||
import { geminiConfig } from "./modules/gemini/gemini.config";
|
||||
import { validateEnv } from "./config/env.validation";
|
||||
|
||||
// Common
|
||||
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
|
||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||
import { GlobalExceptionFilter } from "./common/filters/global-exception.filter";
|
||||
import { ResponseInterceptor } from "./common/interceptors/response.interceptor";
|
||||
|
||||
// Database
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import { DatabaseModule } from "./database/database.module";
|
||||
|
||||
// Core Modules
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { UsersModule } from './modules/users/users.module';
|
||||
import { AdminModule } from './modules/admin/admin.module';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
import { GeminiModule } from './modules/gemini/gemini.module';
|
||||
import { SocialPosterModule } from './modules/social-poster/social-poster.module';
|
||||
import { AuthModule } from "./modules/auth/auth.module";
|
||||
import { UsersModule } from "./modules/users/users.module";
|
||||
import { AdminModule } from "./modules/admin/admin.module";
|
||||
import { HealthModule } from "./modules/health/health.module";
|
||||
import { GeminiModule } from "./modules/gemini/gemini.module";
|
||||
import { SocialPosterModule } from "./modules/social-poster/social-poster.module";
|
||||
|
||||
// Sports Domain Modules
|
||||
import { MatchesModule } from './modules/matches/matches.module';
|
||||
import { PredictionsModule } from './modules/predictions/predictions.module';
|
||||
import { LeaguesModule } from './modules/leagues/leagues.module';
|
||||
import { AnalysisModule } from './modules/analysis/analysis.module';
|
||||
import { CouponsModule } from './modules/coupons/coupons.module';
|
||||
import { SporTotoModule } from './modules/spor-toto/spor-toto.module';
|
||||
import { MatchesModule } from "./modules/matches/matches.module";
|
||||
import { PredictionsModule } from "./modules/predictions/predictions.module";
|
||||
import { LeaguesModule } from "./modules/leagues/leagues.module";
|
||||
import { AnalysisModule } from "./modules/analysis/analysis.module";
|
||||
import { CouponsModule } from "./modules/coupons/coupons.module";
|
||||
import { SporTotoModule } from "./modules/spor-toto/spor-toto.module";
|
||||
|
||||
// Services and Tasks
|
||||
import { ServicesModule } from './services/services.module';
|
||||
import { TasksModule } from './tasks/tasks.module';
|
||||
import { ServicesModule } from "./services/services.module";
|
||||
import { TasksModule } from "./tasks/tasks.module";
|
||||
|
||||
// Feeder Module (Historical Data Scraping)
|
||||
import { FeederModule } from './modules/feeder/feeder.module';
|
||||
import { FeederModule } from "./modules/feeder/feeder.module";
|
||||
|
||||
// Guards
|
||||
import {
|
||||
JwtAuthGuard,
|
||||
RolesGuard,
|
||||
PermissionsGuard,
|
||||
} from './modules/auth/guards';
|
||||
} from "./modules/auth/guards";
|
||||
|
||||
// Queue
|
||||
import { QueueModule } from './common/queues/queue.module';
|
||||
import { QueueModule } from "./common/queues/queue.module";
|
||||
|
||||
const redisEnabled = process.env.REDIS_ENABLED === 'true';
|
||||
const historicalFeederMode = process.env.FEEDER_MODE === 'historical';
|
||||
const redisEnabled = process.env.REDIS_ENABLED === "true";
|
||||
const historicalFeederMode = process.env.FEEDER_MODE === "historical";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -94,8 +94,8 @@ const historicalFeederMode = process.env.FEEDER_MODE === 'historical';
|
||||
|
||||
// Static Assets (Images, Uploads)
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: path.join(__dirname, '..', 'public'),
|
||||
serveRoot: '/', // This means public/uploads/x.png -> /uploads/x.png
|
||||
rootPath: path.join(__dirname, "..", "public"),
|
||||
serveRoot: "/", // This means public/uploads/x.png -> /uploads/x.png
|
||||
}),
|
||||
|
||||
// Logger (Structured Logging with Pino)
|
||||
@@ -105,10 +105,10 @@ const historicalFeederMode = process.env.FEEDER_MODE === 'historical';
|
||||
useFactory: (configService: ConfigService) => {
|
||||
return {
|
||||
pinoHttp: {
|
||||
level: configService.get('app.isDevelopment') ? 'debug' : 'info',
|
||||
transport: configService.get('app.isDevelopment')
|
||||
level: configService.get("app.isDevelopment") ? "debug" : "info",
|
||||
transport: configService.get("app.isDevelopment")
|
||||
? {
|
||||
target: 'pino-pretty',
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
singleLine: true,
|
||||
},
|
||||
@@ -122,15 +122,15 @@ const historicalFeederMode = process.env.FEEDER_MODE === 'historical';
|
||||
// i18n
|
||||
I18nModule.forRootAsync({
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
fallbackLanguage: configService.get('i18n.fallbackLanguage', 'en'),
|
||||
fallbackLanguage: configService.get("i18n.fallbackLanguage", "en"),
|
||||
loaderOptions: {
|
||||
path: path.join(__dirname, '../i18n/'),
|
||||
watch: configService.get('app.isDevelopment', true),
|
||||
path: path.join(__dirname, "../i18n/"),
|
||||
watch: configService.get("app.isDevelopment", true),
|
||||
},
|
||||
}),
|
||||
resolvers: [
|
||||
new HeaderResolver(['x-lang']),
|
||||
new QueryResolver(['lang']),
|
||||
new HeaderResolver(["x-lang"]),
|
||||
new QueryResolver(["lang"]),
|
||||
AcceptLanguageResolver,
|
||||
],
|
||||
inject: [ConfigService],
|
||||
@@ -141,8 +141,8 @@ const historicalFeederMode = process.env.FEEDER_MODE === 'historical';
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => [
|
||||
{
|
||||
ttl: configService.get('throttle.ttl', 60000),
|
||||
limit: configService.get('throttle.limit', 100),
|
||||
ttl: configService.get("throttle.ttl", 60000),
|
||||
limit: configService.get("throttle.limit", 100),
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -153,29 +153,29 @@ const historicalFeederMode = process.env.FEEDER_MODE === 'historical';
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
// FORCE DISABLE REDIS if user doesn't want it
|
||||
const useRedis = configService.get('redis.enabled', false);
|
||||
const useRedis = configService.get("redis.enabled", false);
|
||||
|
||||
if (useRedis) {
|
||||
try {
|
||||
const store = await redisStore({
|
||||
socket: {
|
||||
host: configService.get('redis.host', 'localhost'),
|
||||
port: configService.get('redis.port', 6379),
|
||||
host: configService.get("redis.host", "localhost"),
|
||||
port: configService.get("redis.port", 6379),
|
||||
},
|
||||
ttl: 60 * 1000, // 1 minute default
|
||||
});
|
||||
console.log('✅ Redis cache connected');
|
||||
console.log("✅ Redis cache connected");
|
||||
return {
|
||||
store: store as unknown as any,
|
||||
ttl: 60 * 1000,
|
||||
};
|
||||
} catch {
|
||||
console.warn('⚠️ Redis connection failed, using in-memory cache');
|
||||
console.warn("⚠️ Redis connection failed, using in-memory cache");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to in-memory cache
|
||||
console.log('📦 Using in-memory cache');
|
||||
console.log("📦 Using in-memory cache");
|
||||
return {
|
||||
ttl: 60 * 1000,
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
return "Hello World!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,20 @@ import {
|
||||
Body,
|
||||
HttpCode,
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiOperation,
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiBadRequestResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { BaseService } from './base.service';
|
||||
import { PaginationDto } from '../dto/pagination.dto';
|
||||
} from "@nestjs/swagger";
|
||||
import { BaseService } from "./base.service";
|
||||
import { PaginationDto } from "../dto/pagination.dto";
|
||||
import {
|
||||
ApiResponse,
|
||||
createSuccessResponse,
|
||||
createPaginatedResponse,
|
||||
} from '../types/api-response.type';
|
||||
} from "../types/api-response.type";
|
||||
|
||||
/**
|
||||
* Generic base controller with common CRUD endpoints
|
||||
@@ -37,8 +37,8 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
|
||||
|
||||
@Get()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Get all records with pagination' })
|
||||
@ApiOkResponse({ description: 'Records retrieved successfully' })
|
||||
@ApiOperation({ summary: "Get all records with pagination" })
|
||||
@ApiOkResponse({ description: "Records retrieved successfully" })
|
||||
async findAll(
|
||||
@Query() pagination: PaginationDto,
|
||||
): Promise<ApiResponse<{ items: T[]; meta: any }>> {
|
||||
@@ -52,13 +52,13 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(":id")
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Get a record by ID' })
|
||||
@ApiOkResponse({ description: 'Record retrieved successfully' })
|
||||
@ApiNotFoundResponse({ description: 'Record not found' })
|
||||
@ApiOperation({ summary: "Get a record by ID" })
|
||||
@ApiOkResponse({ description: "Record retrieved successfully" })
|
||||
@ApiNotFoundResponse({ description: "Record not found" })
|
||||
async findOne(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.findOne(id);
|
||||
return createSuccessResponse(
|
||||
@@ -69,9 +69,9 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
|
||||
|
||||
@Post()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Create a new record' })
|
||||
@ApiOkResponse({ description: 'Record created successfully' })
|
||||
@ApiBadRequestResponse({ description: 'Validation failed' })
|
||||
@ApiOperation({ summary: "Create a new record" })
|
||||
@ApiOkResponse({ description: "Record created successfully" })
|
||||
@ApiBadRequestResponse({ description: "Validation failed" })
|
||||
async create(@Body() createDto: CreateDto): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.create(createDto);
|
||||
return createSuccessResponse(
|
||||
@@ -81,13 +81,13 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Put(":id")
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Update an existing record' })
|
||||
@ApiOkResponse({ description: 'Record updated successfully' })
|
||||
@ApiNotFoundResponse({ description: 'Record not found' })
|
||||
@ApiOperation({ summary: "Update an existing record" })
|
||||
@ApiOkResponse({ description: "Record updated successfully" })
|
||||
@ApiNotFoundResponse({ description: "Record not found" })
|
||||
async update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() updateDto: UpdateDto,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.update(id, updateDto);
|
||||
@@ -97,13 +97,13 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete(":id")
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Delete a record (soft delete)' })
|
||||
@ApiOkResponse({ description: 'Record deleted successfully' })
|
||||
@ApiNotFoundResponse({ description: 'Record not found' })
|
||||
@ApiOperation({ summary: "Delete a record (soft delete)" })
|
||||
@ApiOkResponse({ description: "Record deleted successfully" })
|
||||
@ApiNotFoundResponse({ description: "Record not found" })
|
||||
async delete(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.delete(id);
|
||||
return createSuccessResponse(
|
||||
@@ -112,12 +112,12 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
|
||||
);
|
||||
}
|
||||
|
||||
@Post(':id/restore')
|
||||
@Post(":id/restore")
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Restore a soft-deleted record' })
|
||||
@ApiOkResponse({ description: 'Record restored successfully' })
|
||||
@ApiOperation({ summary: "Restore a soft-deleted record" })
|
||||
@ApiOkResponse({ description: "Record restored successfully" })
|
||||
async restore(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const result = await this.service.restore(id);
|
||||
return createSuccessResponse(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NotFoundException, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { PaginationDto } from '../dto/pagination.dto';
|
||||
import { PaginationMeta } from '../types/api-response.type';
|
||||
import { NotFoundException, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import { PaginationDto } from "../dto/pagination.dto";
|
||||
import { PaginationMeta } from "../types/api-response.type";
|
||||
|
||||
/**
|
||||
* Generic base service with common CRUD operations
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './base.service';
|
||||
export * from './base.controller';
|
||||
export * from "./base.service";
|
||||
export * from "./base.controller";
|
||||
|
||||
@@ -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() ?? "";
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
createParamDecorator,
|
||||
ExecutionContext,
|
||||
SetMetadata,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
|
||||
/**
|
||||
* Get the current authenticated user from request
|
||||
@@ -23,19 +23,19 @@ export const CurrentUser = createParamDecorator(
|
||||
/**
|
||||
* Mark a route as public (no authentication required)
|
||||
*/
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const IS_PUBLIC_KEY = "isPublic";
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
|
||||
/**
|
||||
* Require specific roles to access a route
|
||||
*/
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const ROLES_KEY = "roles";
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
|
||||
/**
|
||||
* Require specific permissions to access a route
|
||||
*/
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
export const PERMISSIONS_KEY = "permissions";
|
||||
export const RequirePermissions = (...permissions: string[]) =>
|
||||
SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
|
||||
@@ -55,6 +55,6 @@ export const CurrentTenant = createParamDecorator(
|
||||
export const CurrentLang = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.headers['accept-language'] || 'en';
|
||||
return request.headers["accept-language"] || "en";
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IsOptional, IsInt, Min, Max, IsString, IsIn } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsInt, Min, Max, IsString, IsIn } from "class-validator";
|
||||
import { Transform } from "class-transformer";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
|
||||
export class PaginationDto {
|
||||
@ApiPropertyOptional({ default: 1, minimum: 1, description: 'Page number' })
|
||||
@ApiPropertyOptional({ default: 1, minimum: 1, description: "Page number" })
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
@IsInt()
|
||||
@@ -14,7 +14,7 @@ export class PaginationDto {
|
||||
default: 10,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
description: 'Items per page',
|
||||
description: "Items per page",
|
||||
})
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
@@ -23,21 +23,21 @@ export class PaginationDto {
|
||||
@Max(100)
|
||||
limit?: number = 10;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Field to sort by' })
|
||||
@ApiPropertyOptional({ description: "Field to sort by" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sortBy?: string = 'createdAt';
|
||||
sortBy?: string = "createdAt";
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: ['asc', 'desc'],
|
||||
default: 'desc',
|
||||
description: 'Sort order',
|
||||
enum: ["asc", "desc"],
|
||||
default: "desc",
|
||||
description: "Sort order",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsIn(['asc', 'desc'])
|
||||
sortOrder?: 'asc' | 'desc' = 'desc';
|
||||
@IsIn(["asc", "desc"])
|
||||
sortOrder?: "asc" | "desc" = "desc";
|
||||
|
||||
@ApiPropertyOptional({ description: 'Search query' })
|
||||
@ApiPropertyOptional({ description: "Search query" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
@@ -59,7 +59,7 @@ export class PaginationDto {
|
||||
/**
|
||||
* Get orderBy object for Prisma
|
||||
*/
|
||||
get orderBy(): Record<string, 'asc' | 'desc'> {
|
||||
return { [this.sortBy || 'createdAt']: this.sortOrder || 'desc' };
|
||||
get orderBy(): Record<string, "asc" | "desc"> {
|
||||
return { [this.sortBy || "createdAt"]: this.sortOrder || "desc" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { I18nService, I18nContext } from 'nestjs-i18n';
|
||||
import { ApiResponse, createErrorResponse } from '../types/api-response.type';
|
||||
} from "@nestjs/common";
|
||||
import { Request, Response } from "express";
|
||||
import { I18nService, I18nContext } from "nestjs-i18n";
|
||||
import { ApiResponse, createErrorResponse } from "../types/api-response.type";
|
||||
|
||||
/**
|
||||
* Global exception filter that catches all exceptions
|
||||
@@ -27,23 +27,23 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
|
||||
// Determine status and message
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let message = 'Internal server error';
|
||||
let message = "Internal server error";
|
||||
let errors: string[] = [];
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
if (typeof exceptionResponse === 'string') {
|
||||
if (typeof exceptionResponse === "string") {
|
||||
message = exceptionResponse;
|
||||
} else if (typeof exceptionResponse === 'object') {
|
||||
} else if (typeof exceptionResponse === "object") {
|
||||
const responseObj = exceptionResponse as Record<string, unknown>;
|
||||
message = (responseObj.message as string) || exception.message;
|
||||
|
||||
// Handle validation errors (class-validator)
|
||||
if (Array.isArray(responseObj.message)) {
|
||||
errors = responseObj.message as string[];
|
||||
message = 'VALIDATION_FAILED';
|
||||
message = "VALIDATION_FAILED";
|
||||
}
|
||||
}
|
||||
} else if (exception instanceof Error) {
|
||||
@@ -57,22 +57,22 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
let lang = i18nContext?.lang;
|
||||
|
||||
if (!lang) {
|
||||
const acceptLanguage = request.headers['accept-language'];
|
||||
const xLang = request.headers['x-lang'];
|
||||
const acceptLanguage = request.headers["accept-language"];
|
||||
const xLang = request.headers["x-lang"];
|
||||
|
||||
if (xLang) {
|
||||
lang = Array.isArray(xLang) ? xLang[0] : xLang;
|
||||
} else if (acceptLanguage) {
|
||||
// Take first preferred language: "tr-TR,en;q=0.9" -> "tr"
|
||||
lang = acceptLanguage.split(',')[0].split(';')[0].split('-')[0];
|
||||
lang = acceptLanguage.split(",")[0].split(";")[0].split("-")[0];
|
||||
}
|
||||
}
|
||||
|
||||
lang = lang || 'en';
|
||||
lang = lang || "en";
|
||||
|
||||
// Translate validation error specially
|
||||
if (message === 'VALIDATION_FAILED') {
|
||||
message = this.i18n.translate('errors.VALIDATION_FAILED', { lang });
|
||||
if (message === "VALIDATION_FAILED") {
|
||||
message = this.i18n.translate("errors.VALIDATION_FAILED", { lang });
|
||||
} else {
|
||||
// Try dynamic translation
|
||||
const translatedMessage = this.i18n.translate(`errors.${message}`, {
|
||||
@@ -95,7 +95,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
);
|
||||
|
||||
// Build response
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
const errorResponse: ApiResponse<null> = createErrorResponse(
|
||||
message,
|
||||
status,
|
||||
|
||||
@@ -3,16 +3,16 @@ import {
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ApiResponse, createSuccessResponse } from '../types/api-response.type';
|
||||
} from "@nestjs/common";
|
||||
import { Observable } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { ApiResponse, createSuccessResponse } from "../types/api-response.type";
|
||||
|
||||
/**
|
||||
* Response interceptor that wraps all successful responses
|
||||
* in the standard ApiResponse format
|
||||
*/
|
||||
import { I18nService, I18nContext } from 'nestjs-i18n';
|
||||
import { I18nService, I18nContext } from "nestjs-i18n";
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor<T> implements NestInterceptor<
|
||||
@@ -34,17 +34,17 @@ export class ResponseInterceptor<T> implements NestInterceptor<
|
||||
let lang = i18nContext?.lang;
|
||||
|
||||
if (!lang) {
|
||||
const acceptLanguage = request.headers['accept-language'];
|
||||
const xLang = request.headers['x-lang'];
|
||||
const acceptLanguage = request.headers["accept-language"];
|
||||
const xLang = request.headers["x-lang"];
|
||||
|
||||
if (xLang) {
|
||||
lang = Array.isArray(xLang) ? xLang[0] : xLang;
|
||||
} else if (acceptLanguage) {
|
||||
lang = acceptLanguage.split(',')[0].split(';')[0].split('-')[0];
|
||||
lang = acceptLanguage.split(",")[0].split(";")[0].split("-")[0];
|
||||
}
|
||||
}
|
||||
|
||||
lang = lang || 'en';
|
||||
lang = lang || "en";
|
||||
|
||||
// If data is already an ApiResponse, we should still translate its 'data' property
|
||||
// But first let's just do it directly on 'data' below before returning
|
||||
@@ -68,7 +68,7 @@ export class ResponseInterceptor<T> implements NestInterceptor<
|
||||
}
|
||||
}
|
||||
|
||||
const message = this.i18n.translate('common.success', {
|
||||
const message = this.i18n.translate("common.success", {
|
||||
lang,
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ export class ResponseInterceptor<T> implements NestInterceptor<
|
||||
}
|
||||
|
||||
private translateReasons(data: any, lang: string) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
if (!data || typeof data !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,44 +91,44 @@ export class ResponseInterceptor<T> implements NestInterceptor<
|
||||
Object.keys(data).forEach((key) => {
|
||||
const val = data[key];
|
||||
if (
|
||||
(key === 'reasons' ||
|
||||
key === 'decision_reasons' ||
|
||||
key === 'reasoning_factors') &&
|
||||
(key === "reasons" ||
|
||||
key === "decision_reasons" ||
|
||||
key === "reasoning_factors") &&
|
||||
Array.isArray(val)
|
||||
) {
|
||||
data[key] = val.map((r: any) => {
|
||||
if (typeof r !== 'string') return r;
|
||||
if (typeof r !== "string") return r;
|
||||
const translationKey = `predictions.reasons.${r}`;
|
||||
const translated = this.i18n.translate(translationKey, {
|
||||
lang,
|
||||
});
|
||||
return translated === translationKey ? r : translated;
|
||||
});
|
||||
} else if (key === 'reason' && typeof val === 'string') {
|
||||
} else if (key === "reason" && typeof val === "string") {
|
||||
const translationKey = `predictions.reasons.${val}`;
|
||||
const translated = this.i18n.translate(translationKey, {
|
||||
lang,
|
||||
});
|
||||
data[key] = translated === translationKey ? val : translated;
|
||||
} else if (key === 'flags' && Array.isArray(val)) {
|
||||
} else if (key === "flags" && Array.isArray(val)) {
|
||||
data[key] = val.map((r: any) => {
|
||||
if (typeof r !== 'string') return r;
|
||||
if (typeof r !== "string") return r;
|
||||
const translationKey = `predictions.flags.${r}`;
|
||||
const translated = this.i18n.translate(translationKey, {
|
||||
lang,
|
||||
});
|
||||
return translated === translationKey ? r : translated;
|
||||
});
|
||||
} else if (key === 'warnings' && Array.isArray(val)) {
|
||||
} else if (key === "warnings" && Array.isArray(val)) {
|
||||
data[key] = val.map((r: any) => {
|
||||
if (typeof r !== 'string') return r;
|
||||
if (typeof r !== "string") return r;
|
||||
const translationKey = `predictions.warnings.${r}`;
|
||||
const translated = this.i18n.translate(translationKey, {
|
||||
lang,
|
||||
});
|
||||
return translated === translationKey ? r : translated;
|
||||
});
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
} else if (typeof val === "object" && val !== null) {
|
||||
this.translateReasons(val, lang);
|
||||
}
|
||||
});
|
||||
@@ -137,11 +137,11 @@ export class ResponseInterceptor<T> implements NestInterceptor<
|
||||
private isApiResponse(data: unknown): boolean {
|
||||
return (
|
||||
data !== null &&
|
||||
typeof data === 'object' &&
|
||||
'success' in data &&
|
||||
'status' in data &&
|
||||
'message' in data &&
|
||||
'data' in data
|
||||
typeof data === "object" &&
|
||||
"success" in data &&
|
||||
"status" in data &&
|
||||
"message" in data &&
|
||||
"data" in data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
} from "@nestjs/common";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
/**
|
||||
* Strips HTML/script tags from all string values in the request body.
|
||||
@@ -15,7 +15,7 @@ export class SanitizeInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
if (request.body && typeof request.body === 'object') {
|
||||
if (request.body && typeof request.body === "object") {
|
||||
request.body = this.sanitize(request.body);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class SanitizeInterceptor implements NestInterceptor {
|
||||
}
|
||||
|
||||
private sanitize(value: unknown): unknown {
|
||||
if (typeof value === 'string') {
|
||||
if (typeof value === "string") {
|
||||
return this.stripTags(value);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export class SanitizeInterceptor implements NestInterceptor {
|
||||
return value.map((item) => this.sanitize(item));
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
if (value !== null && typeof value === "object") {
|
||||
const sanitized: Record<string, unknown> = {};
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
sanitized[key] = this.sanitize(val);
|
||||
@@ -43,6 +43,6 @@ export class SanitizeInterceptor implements NestInterceptor {
|
||||
}
|
||||
|
||||
private stripTags(input: string): string {
|
||||
return input.replace(/<[^>]*>/g, '');
|
||||
return input.replace(/<[^>]*>/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { Module, Global } from "@nestjs/common";
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@@ -9,14 +9,14 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
connection: {
|
||||
host: configService.get('redis.host', 'localhost'),
|
||||
port: configService.get('redis.port', 6379),
|
||||
password: configService.get('redis.password'),
|
||||
host: configService.get("redis.host", "localhost"),
|
||||
port: configService.get("redis.port", 6379),
|
||||
password: configService.get("redis.password"),
|
||||
},
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
type: "exponential",
|
||||
delay: 1000,
|
||||
},
|
||||
removeOnComplete: true,
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface PaginationMeta {
|
||||
*/
|
||||
export function createSuccessResponse<T>(
|
||||
data: T,
|
||||
message = 'Success',
|
||||
message = "Success",
|
||||
status = 200,
|
||||
): ApiResponse<T> {
|
||||
return {
|
||||
@@ -72,7 +72,7 @@ export function createPaginatedResponse<T>(
|
||||
total: number,
|
||||
page: number,
|
||||
limit: number,
|
||||
message = 'Success',
|
||||
message = "Success",
|
||||
): ApiResponse<PaginatedData<T>> {
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { existsSync, createWriteStream, mkdirSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
import axios from 'axios';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { existsSync, createWriteStream, mkdirSync } from "fs";
|
||||
import { dirname } from "path";
|
||||
import axios from "axios";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
export class ImageUtils {
|
||||
private static readonly logger = new Logger('ImageUtils');
|
||||
private static readonly logger = new Logger("ImageUtils");
|
||||
|
||||
/**
|
||||
* Downloads an image from a URL and saves it to a local path.
|
||||
@@ -26,8 +26,8 @@ export class ImageUtils {
|
||||
// Download
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
method: "GET",
|
||||
responseType: "stream",
|
||||
timeout: 5000,
|
||||
validateStatus: (status) => status === 200, // Only save if 200 OK
|
||||
});
|
||||
@@ -37,8 +37,8 @@ export class ImageUtils {
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => resolve(true));
|
||||
writer.on('error', (err) => {
|
||||
writer.on("finish", () => resolve(true));
|
||||
writer.on("error", (err) => {
|
||||
this.logger.warn(
|
||||
`Failed to write image to ${localPath}: ${err.message}`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
type ScoreLikeValue = number | string | null | undefined;
|
||||
|
||||
type ScoreLike = {
|
||||
home?: ScoreLikeValue;
|
||||
away?: ScoreLikeValue;
|
||||
} | null;
|
||||
|
||||
export interface MatchStatusLike {
|
||||
state?: string | null;
|
||||
status?: string | null;
|
||||
substate?: string | null;
|
||||
statusBoxContent?: string | null;
|
||||
scoreHome?: ScoreLikeValue;
|
||||
scoreAway?: ScoreLikeValue;
|
||||
score?: ScoreLike;
|
||||
}
|
||||
|
||||
const LIVE_STATUS_TOKENS = [
|
||||
"live",
|
||||
"livegame",
|
||||
"playing",
|
||||
"half time",
|
||||
"halftime",
|
||||
"1h",
|
||||
"2h",
|
||||
"ht",
|
||||
"1q",
|
||||
"2q",
|
||||
"3q",
|
||||
"4q",
|
||||
];
|
||||
|
||||
const LIVE_STATE_TOKENS = [
|
||||
"live",
|
||||
"livegame",
|
||||
"firsthalf",
|
||||
"secondhalf",
|
||||
"halftime",
|
||||
"1h",
|
||||
"2h",
|
||||
"ht",
|
||||
"1q",
|
||||
"2q",
|
||||
"3q",
|
||||
"4q",
|
||||
];
|
||||
|
||||
const FINISHED_STATUS_TOKENS = [
|
||||
"finished",
|
||||
"played",
|
||||
"ft",
|
||||
"aet",
|
||||
"pen",
|
||||
"penalties",
|
||||
"afterpenalties",
|
||||
"ended",
|
||||
"post",
|
||||
"postgame",
|
||||
"posted",
|
||||
];
|
||||
|
||||
const FINISHED_STATE_TOKENS = [
|
||||
"finished",
|
||||
"post",
|
||||
"postgame",
|
||||
"posted",
|
||||
"ft",
|
||||
"ended",
|
||||
];
|
||||
|
||||
export const LIVE_STATUS_VALUES_FOR_DB = [
|
||||
"LIVE",
|
||||
"live",
|
||||
"1H",
|
||||
"2H",
|
||||
"HT",
|
||||
"1Q",
|
||||
"2Q",
|
||||
"3Q",
|
||||
"4Q",
|
||||
"Playing",
|
||||
"Half Time",
|
||||
"liveGame",
|
||||
];
|
||||
|
||||
export const LIVE_STATE_VALUES_FOR_DB = [
|
||||
"live",
|
||||
"liveGame",
|
||||
"firsthalf",
|
||||
"secondhalf",
|
||||
"halfTime",
|
||||
"1H",
|
||||
"2H",
|
||||
"HT",
|
||||
"1Q",
|
||||
"2Q",
|
||||
"3Q",
|
||||
"4Q",
|
||||
];
|
||||
|
||||
export const FINISHED_STATUS_VALUES_FOR_DB = [
|
||||
"Finished",
|
||||
"Played",
|
||||
"FT",
|
||||
"AET",
|
||||
"PEN",
|
||||
"Ended",
|
||||
"post",
|
||||
"postGame",
|
||||
"posted",
|
||||
"Posted",
|
||||
];
|
||||
|
||||
export const FINISHED_STATE_VALUES_FOR_DB = [
|
||||
"Finished",
|
||||
"post",
|
||||
"postGame",
|
||||
"postgame",
|
||||
"posted",
|
||||
"FT",
|
||||
"Ended",
|
||||
];
|
||||
|
||||
function normalizeToken(value: unknown): string {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function parseScoreValue(value: ScoreLikeValue): number | null {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
export function hasResolvedScore(match: MatchStatusLike): boolean {
|
||||
const homeScore = parseScoreValue(match.score?.home ?? match.scoreHome);
|
||||
const awayScore = parseScoreValue(match.score?.away ?? match.scoreAway);
|
||||
return homeScore !== null && awayScore !== null;
|
||||
}
|
||||
|
||||
export function isMatchLive(match: MatchStatusLike): boolean {
|
||||
const state = normalizeToken(match.state);
|
||||
const status = normalizeToken(match.status);
|
||||
const substate = normalizeToken(match.substate);
|
||||
|
||||
return (
|
||||
LIVE_STATE_TOKENS.includes(state) ||
|
||||
LIVE_STATUS_TOKENS.includes(status) ||
|
||||
LIVE_STATE_TOKENS.includes(substate)
|
||||
);
|
||||
}
|
||||
|
||||
export function isMatchCompleted(match: MatchStatusLike): boolean {
|
||||
if (normalizeToken(match.statusBoxContent) === "ert") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const state = normalizeToken(match.state);
|
||||
const status = normalizeToken(match.status);
|
||||
const substate = normalizeToken(match.substate);
|
||||
|
||||
if (
|
||||
FINISHED_STATE_TOKENS.includes(state) ||
|
||||
FINISHED_STATUS_TOKENS.includes(status) ||
|
||||
FINISHED_STATE_TOKENS.includes(substate)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hasResolvedScore(match) && !isMatchLive(match);
|
||||
}
|
||||
|
||||
export function deriveStoredMatchStatus(match: MatchStatusLike): string {
|
||||
if (normalizeToken(match.statusBoxContent) === "ert") {
|
||||
return "POSTPONED";
|
||||
}
|
||||
|
||||
if (isMatchLive(match)) {
|
||||
return "LIVE";
|
||||
}
|
||||
|
||||
if (isMatchCompleted(match)) {
|
||||
return "FT";
|
||||
}
|
||||
|
||||
return "NS";
|
||||
}
|
||||
|
||||
export function getDisplayMatchStatus(match: MatchStatusLike): string {
|
||||
if (isMatchLive(match)) {
|
||||
return "LIVE";
|
||||
}
|
||||
|
||||
if (isMatchCompleted(match)) {
|
||||
return "Finished";
|
||||
}
|
||||
|
||||
return String(match.status || match.state || "NS");
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
function extractDateParts(date: Date, timeZone: string) {
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(date);
|
||||
const year = Number(parts.find((part) => part.type === "year")?.value);
|
||||
const month = Number(parts.find((part) => part.type === "month")?.value);
|
||||
const day = Number(parts.find((part) => part.type === "day")?.value);
|
||||
|
||||
return { year, month, day };
|
||||
}
|
||||
|
||||
export function getDateStringInTimeZone(
|
||||
date: Date,
|
||||
timeZone: string,
|
||||
): string {
|
||||
const { year, month, day } = extractDateParts(date, timeZone);
|
||||
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function getShiftedDateStringInTimeZone(
|
||||
daysOffset: number,
|
||||
timeZone: string,
|
||||
baseDate: Date = new Date(),
|
||||
): string {
|
||||
const { year, month, day } = extractDateParts(baseDate, timeZone);
|
||||
const shifted = new Date(Date.UTC(year, month - 1, day));
|
||||
shifted.setUTCDate(shifted.getUTCDate() + daysOffset);
|
||||
return shifted.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function getTimeZoneOffsetMs(date: Date, timeZone: string): number {
|
||||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
timeZoneName: "shortOffset",
|
||||
});
|
||||
|
||||
const offsetLabel =
|
||||
formatter.formatToParts(date).find((part) => part.type === "timeZoneName")
|
||||
?.value || "GMT+0";
|
||||
|
||||
const match = offsetLabel.match(/GMT([+-])(\d{1,2})(?::?(\d{2}))?/);
|
||||
if (!match) return 0;
|
||||
|
||||
const sign = match[1] === "-" ? -1 : 1;
|
||||
const hours = Number(match[2] || "0");
|
||||
const minutes = Number(match[3] || "0");
|
||||
|
||||
return sign * (hours * 60 + minutes) * 60 * 1000;
|
||||
}
|
||||
|
||||
export function getDayBoundsForTimeZone(
|
||||
dateString: string,
|
||||
timeZone: string,
|
||||
): { startMs: number; endMs: number } {
|
||||
const [year, month, day] = dateString.split("-").map(Number);
|
||||
const startGuess = new Date(Date.UTC(year, month - 1, day, 0, 0, 0));
|
||||
const nextDayGuess = new Date(Date.UTC(year, month - 1, day + 1, 0, 0, 0));
|
||||
|
||||
const startOffsetMs = getTimeZoneOffsetMs(startGuess, timeZone);
|
||||
const nextDayOffsetMs = getTimeZoneOffsetMs(nextDayGuess, timeZone);
|
||||
|
||||
const startMs = Date.UTC(year, month - 1, day, 0, 0, 0) - startOffsetMs;
|
||||
const nextDayStartMs =
|
||||
Date.UTC(year, month - 1, day + 1, 0, 0, 0) - nextDayOffsetMs;
|
||||
|
||||
return {
|
||||
startMs,
|
||||
endMs: nextDayStartMs - 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDateOnlyValueForTimeZone(
|
||||
timeZone: string,
|
||||
date: Date = new Date(),
|
||||
): Date {
|
||||
return new Date(`${getDateStringInTimeZone(date, timeZone)}T00:00:00.000Z`);
|
||||
}
|
||||
+29
-29
@@ -1,58 +1,58 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export const appConfig = registerAs('app', () => ({
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
port: parseInt(process.env.PORT || '3005', 10),
|
||||
isDevelopment: process.env.NODE_ENV === 'development',
|
||||
isProduction: process.env.NODE_ENV === 'production',
|
||||
export const appConfig = registerAs("app", () => ({
|
||||
env: process.env.NODE_ENV || "development",
|
||||
port: parseInt(process.env.PORT || "3005", 10),
|
||||
isDevelopment: process.env.NODE_ENV === "development",
|
||||
isProduction: process.env.NODE_ENV === "production",
|
||||
}));
|
||||
|
||||
export const databaseConfig = registerAs('database', () => ({
|
||||
export const databaseConfig = registerAs("database", () => ({
|
||||
url: process.env.DATABASE_URL,
|
||||
}));
|
||||
|
||||
export const jwtConfig = registerAs('jwt', () => ({
|
||||
export const jwtConfig = registerAs("jwt", () => ({
|
||||
secret: process.env.JWT_SECRET,
|
||||
accessExpiration: process.env.JWT_ACCESS_EXPIRATION || '15m',
|
||||
refreshExpiration: process.env.JWT_REFRESH_EXPIRATION || '7d',
|
||||
accessExpiration: process.env.JWT_ACCESS_EXPIRATION || "15m",
|
||||
refreshExpiration: process.env.JWT_REFRESH_EXPIRATION || "7d",
|
||||
}));
|
||||
|
||||
export const redisConfig = registerAs('redis', () => ({
|
||||
enabled: process.env.REDIS_ENABLED === 'true',
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
||||
export const redisConfig = registerAs("redis", () => ({
|
||||
enabled: process.env.REDIS_ENABLED === "true",
|
||||
host: process.env.REDIS_HOST || "localhost",
|
||||
port: parseInt(process.env.REDIS_PORT || "6379", 10),
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
}));
|
||||
|
||||
export const i18nConfig = registerAs('i18n', () => ({
|
||||
defaultLanguage: process.env.DEFAULT_LANGUAGE || 'en',
|
||||
fallbackLanguage: process.env.FALLBACK_LANGUAGE || 'en',
|
||||
export const i18nConfig = registerAs("i18n", () => ({
|
||||
defaultLanguage: process.env.DEFAULT_LANGUAGE || "en",
|
||||
fallbackLanguage: process.env.FALLBACK_LANGUAGE || "en",
|
||||
}));
|
||||
|
||||
export const featuresConfig = registerAs('features', () => ({
|
||||
mail: process.env.ENABLE_MAIL === 'true',
|
||||
s3: process.env.ENABLE_S3 === 'true',
|
||||
websocket: process.env.ENABLE_WEBSOCKET === 'true',
|
||||
multiTenancy: process.env.ENABLE_MULTI_TENANCY === 'true',
|
||||
export const featuresConfig = registerAs("features", () => ({
|
||||
mail: process.env.ENABLE_MAIL === "true",
|
||||
s3: process.env.ENABLE_S3 === "true",
|
||||
websocket: process.env.ENABLE_WEBSOCKET === "true",
|
||||
multiTenancy: process.env.ENABLE_MULTI_TENANCY === "true",
|
||||
}));
|
||||
|
||||
export const mailConfig = registerAs('mail', () => ({
|
||||
export const mailConfig = registerAs("mail", () => ({
|
||||
host: process.env.MAIL_HOST,
|
||||
port: parseInt(process.env.MAIL_PORT || '587', 10),
|
||||
port: parseInt(process.env.MAIL_PORT || "587", 10),
|
||||
user: process.env.MAIL_USER,
|
||||
password: process.env.MAIL_PASSWORD,
|
||||
from: process.env.MAIL_FROM,
|
||||
}));
|
||||
|
||||
export const s3Config = registerAs('s3', () => ({
|
||||
export const s3Config = registerAs("s3", () => ({
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
accessKey: process.env.S3_ACCESS_KEY,
|
||||
secretKey: process.env.S3_SECRET_KEY,
|
||||
bucket: process.env.S3_BUCKET,
|
||||
region: process.env.S3_REGION || 'us-east-1',
|
||||
region: process.env.S3_REGION || "us-east-1",
|
||||
}));
|
||||
|
||||
export const throttleConfig = registerAs('throttle', () => ({
|
||||
ttl: parseInt(process.env.THROTTLE_TTL || '60000', 10),
|
||||
limit: parseInt(process.env.THROTTLE_LIMIT || '100', 10),
|
||||
export const throttleConfig = registerAs("throttle", () => ({
|
||||
ttl: parseInt(process.env.THROTTLE_TTL || "60000", 10),
|
||||
limit: parseInt(process.env.THROTTLE_LIMIT || "100", 10),
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Helper to parse boolean from string
|
||||
@@ -6,8 +6,8 @@ import { z } from 'zod';
|
||||
const booleanString = z
|
||||
.string()
|
||||
.optional()
|
||||
.default('false')
|
||||
.transform((val) => val === 'true');
|
||||
.default("false")
|
||||
.transform((val) => val === "true");
|
||||
|
||||
/**
|
||||
* Environment variables schema validation using Zod
|
||||
@@ -15,46 +15,47 @@ const booleanString = z
|
||||
export const envSchema = z.object({
|
||||
// Environment
|
||||
NODE_ENV: z
|
||||
.enum(['development', 'production', 'test'])
|
||||
.default('development'),
|
||||
.enum(["development", "production", "test"])
|
||||
.default("development"),
|
||||
PORT: z.coerce.number().default(3005),
|
||||
|
||||
// Database
|
||||
DATABASE_URL: z.string().url(),
|
||||
// AI Engine
|
||||
AI_ENGINE_URL: z.string().url().default('http://localhost:8000'),
|
||||
AI_ENGINE_URL: z.string().url().default("http://localhost:8000"),
|
||||
AI_ENGINE_MODE: z.enum(["v28-pro-max", "dual"]).default("v28-pro-max"),
|
||||
|
||||
// JWT
|
||||
JWT_SECRET: z.string().min(32),
|
||||
JWT_ACCESS_EXPIRATION: z.string().default('15m'),
|
||||
JWT_REFRESH_EXPIRATION: z.string().default('7d'),
|
||||
JWT_ACCESS_EXPIRATION: z.string().default("15m"),
|
||||
JWT_REFRESH_EXPIRATION: z.string().default("7d"),
|
||||
|
||||
// Redis
|
||||
REDIS_ENABLED: z
|
||||
.string()
|
||||
.transform((val) => val === 'true')
|
||||
.default('false' as any),
|
||||
REDIS_HOST: z.string().default('localhost'),
|
||||
.transform((val) => val === "true")
|
||||
.default("false" as any),
|
||||
REDIS_HOST: z.string().default("localhost"),
|
||||
REDIS_PORT: z.coerce.number().default(6379),
|
||||
REDIS_PASSWORD: z.string().optional(),
|
||||
|
||||
// i18n
|
||||
DEFAULT_LANGUAGE: z.string().default('en'),
|
||||
FALLBACK_LANGUAGE: z.string().default('en'),
|
||||
DEFAULT_LANGUAGE: z.string().default("en"),
|
||||
FALLBACK_LANGUAGE: z.string().default("en"),
|
||||
|
||||
// Gemini AI
|
||||
ENABLE_GEMINI: z
|
||||
.string()
|
||||
.transform((val) => val === 'true')
|
||||
.default('false' as any),
|
||||
.transform((val) => val === "true")
|
||||
.default("false" as any),
|
||||
GOOGLE_API_KEY: z.string().optional(),
|
||||
GEMINI_DEFAULT_MODEL: z.string().default('gemini-2.5-flash'),
|
||||
GEMINI_DEFAULT_MODEL: z.string().default("gemini-2.5-flash"),
|
||||
|
||||
// Social Poster
|
||||
SOCIAL_POSTER_ENABLED: z
|
||||
.string()
|
||||
.transform((val) => val === 'true')
|
||||
.default('false' as any),
|
||||
.transform((val) => val === "true")
|
||||
.default("false" as any),
|
||||
TWITTER_API_KEY: z.string().optional(),
|
||||
TWITTER_API_SECRET: z.string().optional(),
|
||||
TWITTER_ACCESS_TOKEN: z.string().optional(),
|
||||
@@ -98,9 +99,9 @@ export function validateEnv(config: Record<string, unknown>): EnvConfig {
|
||||
|
||||
if (!result.success) {
|
||||
const errors = result.error.issues.map(
|
||||
(err) => `${err.path.join('.')}: ${err.message}`,
|
||||
(err) => `${err.path.join(".")}: ${err.message}`,
|
||||
);
|
||||
throw new Error(`Environment validation failed:\n${errors.join('\n')}`);
|
||||
throw new Error(`Environment validation failed:\n${errors.join("\n")}`);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { Global, Module } from "@nestjs/common";
|
||||
import { PrismaService } from "./prisma.service";
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
OnModuleInit,
|
||||
OnModuleDestroy,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
} from "@nestjs/common";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
// Models that support soft delete
|
||||
const SOFT_DELETE_MODELS = ['user', 'role', 'tenant'];
|
||||
const SOFT_DELETE_MODELS = ["user", "role", "tenant"];
|
||||
|
||||
// Type for Prisma model delegate with common operations
|
||||
interface PrismaDelegate {
|
||||
@@ -29,20 +29,20 @@ export class PrismaService
|
||||
constructor() {
|
||||
super({
|
||||
log: [
|
||||
{ emit: 'event', level: 'query' },
|
||||
{ emit: 'event', level: 'error' },
|
||||
{ emit: 'event', level: 'warn' },
|
||||
{ emit: "event", level: "query" },
|
||||
{ emit: "event", level: "error" },
|
||||
{ emit: "event", level: "warn" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log(
|
||||
`Connecting to database... URL: ${process.env.DATABASE_URL?.split('@')[1]}`,
|
||||
`Connecting to database... URL: ${process.env.DATABASE_URL?.split("@")[1]}`,
|
||||
); // Mask password
|
||||
try {
|
||||
await this.$connect();
|
||||
this.logger.log('✅ Database connected successfully');
|
||||
this.logger.log("✅ Database connected successfully");
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`❌ Database connection failed: ${error.message}`,
|
||||
@@ -54,7 +54,7 @@ export class PrismaService
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
this.logger.log('🔌 Database disconnected');
|
||||
this.logger.log("🔌 Database disconnected");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+43
-42
@@ -1,12 +1,12 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe, Logger as NestLogger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
import helmet from 'helmet';
|
||||
import * as express from 'express';
|
||||
import { Logger, LoggerErrorInterceptor } from 'nestjs-pino';
|
||||
import { SanitizeInterceptor } from './common/interceptors/sanitize.interceptor';
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { ValidationPipe, Logger as NestLogger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||
import { AppModule } from "./app.module";
|
||||
import helmet from "helmet";
|
||||
import * as express from "express";
|
||||
import { Logger, LoggerErrorInterceptor } from "nestjs-pino";
|
||||
import { SanitizeInterceptor } from "./common/interceptors/sanitize.interceptor";
|
||||
|
||||
// BigInt serialization polyfill — Prisma returns BigInt for mstUtc etc.
|
||||
(BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function () {
|
||||
@@ -14,9 +14,9 @@ import { SanitizeInterceptor } from './common/interceptors/sanitize.interceptor'
|
||||
};
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new NestLogger('Bootstrap');
|
||||
const logger = new NestLogger("Bootstrap");
|
||||
|
||||
logger.log('🔄 Starting application...');
|
||||
logger.log("🔄 Starting application...");
|
||||
|
||||
const app = await NestFactory.create(AppModule, { bufferLogs: false });
|
||||
|
||||
@@ -31,33 +31,34 @@ async function bootstrap() {
|
||||
app.use(helmet());
|
||||
|
||||
// Request payload size limit
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '1mb' }));
|
||||
app.use(express.json({ limit: "1mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "1mb" }));
|
||||
|
||||
// Graceful Shutdown (Prisma & Docker)
|
||||
app.enableShutdownHooks();
|
||||
|
||||
// Get config service
|
||||
const configService = app.get(ConfigService);
|
||||
const port = configService.get<number>('PORT', 3005);
|
||||
const nodeEnv = configService.get('NODE_ENV', 'development');
|
||||
const port = configService.get<number>("PORT", 3005);
|
||||
const nodeEnv = configService.get("NODE_ENV", "development");
|
||||
|
||||
// Enable CORS
|
||||
app.enableCors({
|
||||
origin:
|
||||
nodeEnv === 'production'
|
||||
nodeEnv === "production"
|
||||
? [
|
||||
'https://ui-suggestbet.bilgich.com',
|
||||
'https://suggestbet.bilgich.com',
|
||||
'https://iddaai.com',
|
||||
'https://www.iddaai.com',
|
||||
"https://ui-suggestbet.bilgich.com",
|
||||
"https://suggestbet.bilgich.com",
|
||||
"https://iddaai.com",
|
||||
"https://www.iddaai.com",
|
||||
"http://localhost:6195",
|
||||
]
|
||||
: true,
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// Global prefix
|
||||
app.setGlobalPrefix('api');
|
||||
app.setGlobalPrefix("api");
|
||||
|
||||
// Validation pipe (Strict)
|
||||
app.useGlobalPipes(
|
||||
@@ -72,47 +73,47 @@ async function bootstrap() {
|
||||
);
|
||||
|
||||
// Swagger setup — hidden in production
|
||||
if (nodeEnv !== 'production') {
|
||||
if (nodeEnv !== "production") {
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('Suggest-Bet API')
|
||||
.setTitle("Suggest-Bet API")
|
||||
.setDescription(
|
||||
'AI-driven sports betting prediction engine with smart coupon generation',
|
||||
"AI-driven sports betting prediction engine with smart coupon generation",
|
||||
)
|
||||
.setVersion('1.0')
|
||||
.setVersion("1.0")
|
||||
.addBearerAuth()
|
||||
.addTag('Auth', 'Authentication endpoints')
|
||||
.addTag('Users', 'User management endpoints')
|
||||
.addTag('Admin', 'Admin management endpoints')
|
||||
.addTag('Health', 'Health check endpoints')
|
||||
.addTag('Matches', 'Match listing and detail endpoints')
|
||||
.addTag('Leagues', 'League, country, and team discovery endpoints')
|
||||
.addTag('Analysis', 'AI analysis and analysis history endpoints')
|
||||
.addTag('Coupon', 'Coupon generation and coupon management endpoints')
|
||||
.addTag('Predictions', 'Prediction and smart-coupon endpoints')
|
||||
.addTag("Auth", "Authentication endpoints")
|
||||
.addTag("Users", "User management endpoints")
|
||||
.addTag("Admin", "Admin management endpoints")
|
||||
.addTag("Health", "Health check endpoints")
|
||||
.addTag("Matches", "Match listing and detail endpoints")
|
||||
.addTag("Leagues", "League, country, and team discovery endpoints")
|
||||
.addTag("Analysis", "AI analysis and analysis history endpoints")
|
||||
.addTag("Coupon", "Coupon generation and coupon management endpoints")
|
||||
.addTag("Predictions", "Prediction and smart-coupon endpoints")
|
||||
.build();
|
||||
|
||||
logger.log('Initializing Swagger...');
|
||||
logger.log("Initializing Swagger...");
|
||||
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||
SwaggerModule.setup('api/docs', app, document, {
|
||||
SwaggerModule.setup("api/docs", app, document, {
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
},
|
||||
});
|
||||
logger.log('Swagger initialized');
|
||||
logger.log("Swagger initialized");
|
||||
}
|
||||
|
||||
logger.log(`Attempting to listen on port ${port}...`);
|
||||
await app.listen(port, '0.0.0.0');
|
||||
await app.listen(port, "0.0.0.0");
|
||||
|
||||
logger.log('═══════════════════════════════════════════════════════════');
|
||||
logger.log("═══════════════════════════════════════════════════════════");
|
||||
logger.log(`🚀 Server is running on: http://localhost:${port}/api`);
|
||||
logger.log(`📚 Swagger documentation: http://localhost:${port}/api/docs`);
|
||||
logger.log(`💚 Health check: http://localhost:${port}/api/health`);
|
||||
logger.log(`🌍 Environment: ${nodeEnv.toUpperCase()}`);
|
||||
logger.log('═══════════════════════════════════════════════════════════');
|
||||
logger.log("═══════════════════════════════════════════════════════════");
|
||||
|
||||
if (nodeEnv === 'development') {
|
||||
logger.warn('⚠️ Running in development mode');
|
||||
if (nodeEnv === "development") {
|
||||
logger.warn("⚠️ Running in development mode");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,32 +10,32 @@ import {
|
||||
UseInterceptors,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
CacheInterceptor,
|
||||
CacheKey,
|
||||
CacheTTL,
|
||||
CACHE_MANAGER,
|
||||
} from '@nestjs/cache-manager';
|
||||
import * as cacheManager from 'cache-manager';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import { Roles } from '../../common/decorators';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
} from "@nestjs/cache-manager";
|
||||
import * as cacheManager from "cache-manager";
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from "@nestjs/swagger";
|
||||
import { Roles } from "../../common/decorators";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import { PaginationDto } from "../../common/dto/pagination.dto";
|
||||
import {
|
||||
ApiResponse,
|
||||
createSuccessResponse,
|
||||
createPaginatedResponse,
|
||||
PaginatedData,
|
||||
} from '../../common/types/api-response.type';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { UserResponseDto } from '../users/dto/user.dto';
|
||||
import { UserRole } from '@prisma/client';
|
||||
} from "../../common/types/api-response.type";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { UserResponseDto } from "../users/dto/user.dto";
|
||||
import { UserRole } from "@prisma/client";
|
||||
|
||||
@ApiTags('Admin')
|
||||
@ApiTags("Admin")
|
||||
@ApiBearerAuth()
|
||||
@Controller('admin')
|
||||
@Roles('superadmin')
|
||||
@Controller("admin")
|
||||
@Roles("superadmin")
|
||||
export class AdminController {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@@ -44,8 +44,8 @@ export class AdminController {
|
||||
|
||||
// ================== Users Management ==================
|
||||
|
||||
@Get('users')
|
||||
@ApiOperation({ summary: 'Get all users (admin)' })
|
||||
@Get("users")
|
||||
@ApiOperation({ summary: "Get all users (admin)" })
|
||||
async getAllUsers(
|
||||
@Query() pagination: PaginationDto,
|
||||
): Promise<ApiResponse<PaginatedData<UserResponseDto>>> {
|
||||
@@ -73,10 +73,10 @@ export class AdminController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get('users/:id')
|
||||
@ApiOperation({ summary: 'Get user by ID' })
|
||||
@Get("users/:id")
|
||||
@ApiOperation({ summary: "Get user by ID" })
|
||||
async getUserById(
|
||||
@Param('id') id: string,
|
||||
@Param("id") id: string,
|
||||
): Promise<ApiResponse<UserResponseDto>> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
@@ -84,27 +84,27 @@ export class AdminController {
|
||||
usageLimit: true,
|
||||
analyses: {
|
||||
take: 5,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
orderBy: { createdAt: "desc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
return createSuccessResponse(plainToInstance(UserResponseDto, user));
|
||||
}
|
||||
|
||||
@Put('users/:id/toggle-active')
|
||||
@ApiOperation({ summary: 'Toggle user active status' })
|
||||
@Put("users/:id/toggle-active")
|
||||
@ApiOperation({ summary: "Toggle user active status" })
|
||||
async toggleUserActive(
|
||||
@Param('id') id: string,
|
||||
@Param("id") id: string,
|
||||
): Promise<ApiResponse<UserResponseDto>> {
|
||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
const updated = await this.prisma.user.update({
|
||||
@@ -114,14 +114,14 @@ export class AdminController {
|
||||
|
||||
return createSuccessResponse(
|
||||
plainToInstance(UserResponseDto, updated),
|
||||
'User status updated',
|
||||
"User status updated",
|
||||
);
|
||||
}
|
||||
|
||||
@Put('users/:id/role')
|
||||
@ApiOperation({ summary: 'Update user role' })
|
||||
@Put("users/:id/role")
|
||||
@ApiOperation({ summary: "Update user role" })
|
||||
async updateUserRole(
|
||||
@Param('id') id: string,
|
||||
@Param("id") id: string,
|
||||
@Body() data: { role: UserRole },
|
||||
): Promise<ApiResponse<UserResponseDto>> {
|
||||
const user = await this.prisma.user.update({
|
||||
@@ -131,14 +131,14 @@ export class AdminController {
|
||||
|
||||
return createSuccessResponse(
|
||||
plainToInstance(UserResponseDto, user),
|
||||
'User role updated',
|
||||
"User role updated",
|
||||
);
|
||||
}
|
||||
|
||||
@Put('users/:id/subscription')
|
||||
@ApiOperation({ summary: 'Update user subscription' })
|
||||
@Put("users/:id/subscription")
|
||||
@ApiOperation({ summary: "Update user subscription" })
|
||||
async updateUserSubscription(
|
||||
@Param('id') id: string,
|
||||
@Param("id") id: string,
|
||||
@Body()
|
||||
data: { subscriptionStatus: string; subscriptionExpiresAt?: string },
|
||||
): Promise<ApiResponse<UserResponseDto>> {
|
||||
@@ -154,40 +154,40 @@ export class AdminController {
|
||||
|
||||
return createSuccessResponse(
|
||||
plainToInstance(UserResponseDto, user),
|
||||
'User subscription updated',
|
||||
"User subscription updated",
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('users/:id')
|
||||
@ApiOperation({ summary: 'Soft delete a user' })
|
||||
async deleteUser(@Param('id') id: string): Promise<ApiResponse<null>> {
|
||||
@Delete("users/:id")
|
||||
@ApiOperation({ summary: "Soft delete a user" })
|
||||
async deleteUser(@Param("id") id: string): Promise<ApiResponse<null>> {
|
||||
await this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
return createSuccessResponse(null, 'User deleted');
|
||||
return createSuccessResponse(null, "User deleted");
|
||||
}
|
||||
|
||||
// ================== App Settings ==================
|
||||
|
||||
@Get('settings')
|
||||
@Get("settings")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@CacheKey('app_settings')
|
||||
@CacheKey("app_settings")
|
||||
@CacheTTL(60 * 1000)
|
||||
@ApiOperation({ summary: 'Get all app settings' })
|
||||
@ApiOperation({ summary: "Get all app settings" })
|
||||
async getAllSettings(): Promise<ApiResponse<Record<string, string>>> {
|
||||
const settings = await this.prisma.appSetting.findMany();
|
||||
const settingsMap: Record<string, string> = {};
|
||||
for (const s of settings) {
|
||||
settingsMap[s.key] = s.value || '';
|
||||
settingsMap[s.key] = s.value || "";
|
||||
}
|
||||
return createSuccessResponse(settingsMap);
|
||||
}
|
||||
|
||||
@Put('settings/:key')
|
||||
@ApiOperation({ summary: 'Update an app setting' })
|
||||
@Put("settings/:key")
|
||||
@ApiOperation({ summary: "Update an app setting" })
|
||||
async updateSetting(
|
||||
@Param('key') key: string,
|
||||
@Param("key") key: string,
|
||||
@Body() data: { value: string },
|
||||
): Promise<ApiResponse<{ key: string; value: string }>> {
|
||||
const setting = await this.prisma.appSetting.upsert({
|
||||
@@ -195,17 +195,17 @@ export class AdminController {
|
||||
update: { value: data.value },
|
||||
create: { key, value: data.value },
|
||||
});
|
||||
await this.cacheManager.del('app_settings');
|
||||
await this.cacheManager.del("app_settings");
|
||||
return createSuccessResponse(
|
||||
{ key: setting.key, value: setting.value || '' },
|
||||
'Setting updated',
|
||||
{ key: setting.key, value: setting.value || "" },
|
||||
"Setting updated",
|
||||
);
|
||||
}
|
||||
|
||||
// ================== Usage Limits ==================
|
||||
|
||||
@Get('usage-limits')
|
||||
@ApiOperation({ summary: 'Get all usage limits' })
|
||||
@Get("usage-limits")
|
||||
@ApiOperation({ summary: "Get all usage limits" })
|
||||
async getAllUsageLimits(@Query() pagination: PaginationDto) {
|
||||
const { skip, take } = pagination;
|
||||
|
||||
@@ -218,7 +218,7 @@ export class AdminController {
|
||||
select: { id: true, email: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { lastResetDate: 'desc' },
|
||||
orderBy: { lastResetDate: "desc" },
|
||||
}),
|
||||
this.prisma.usageLimit.count(),
|
||||
]);
|
||||
@@ -231,8 +231,8 @@ export class AdminController {
|
||||
);
|
||||
}
|
||||
|
||||
@Post('usage-limits/reset-all')
|
||||
@ApiOperation({ summary: 'Reset all usage limits' })
|
||||
@Post("usage-limits/reset-all")
|
||||
@ApiOperation({ summary: "Reset all usage limits" })
|
||||
async resetAllUsageLimits(): Promise<ApiResponse<{ count: number }>> {
|
||||
const result = await this.prisma.usageLimit.updateMany({
|
||||
data: {
|
||||
@@ -244,14 +244,14 @@ export class AdminController {
|
||||
|
||||
return createSuccessResponse(
|
||||
{ count: result.count },
|
||||
'All usage limits reset',
|
||||
"All usage limits reset",
|
||||
);
|
||||
}
|
||||
|
||||
// ================== Analytics ==================
|
||||
|
||||
@Get('analytics/overview')
|
||||
@ApiOperation({ summary: 'Get system analytics overview' })
|
||||
@Get("analytics/overview")
|
||||
@ApiOperation({ summary: "Get system analytics overview" })
|
||||
async getAnalyticsOverview() {
|
||||
const [
|
||||
totalUsers,
|
||||
@@ -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.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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AdminController } from './admin.controller';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AdminController } from "./admin.controller";
|
||||
|
||||
@Module({
|
||||
controllers: [AdminController],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Exclude, Expose, Type } from 'class-transformer';
|
||||
import { Exclude, Expose, Type } from "class-transformer";
|
||||
|
||||
@Exclude()
|
||||
export class PermissionResponseDto {
|
||||
|
||||
@@ -6,20 +6,20 @@ import {
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiTags,
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { AnalysisService } from './analysis.service';
|
||||
import { AnalyzeMatchesDto } from './dto/analysis-request.dto';
|
||||
import { CurrentUser } from '../../common/decorators';
|
||||
} from "@nestjs/swagger";
|
||||
import { AnalysisService } from "./analysis.service";
|
||||
import { AnalyzeMatchesDto } from "./dto/analysis-request.dto";
|
||||
import { CurrentUser } from "../../common/decorators";
|
||||
|
||||
@ApiTags('Analysis')
|
||||
@ApiTags("Analysis")
|
||||
@ApiBearerAuth()
|
||||
@Controller('analysis')
|
||||
@Controller("analysis")
|
||||
export class AnalysisController {
|
||||
constructor(private readonly analysisService: AnalysisService) {}
|
||||
|
||||
@@ -27,12 +27,12 @@ export class AnalysisController {
|
||||
* POST /analysis/analyze-matches
|
||||
* Analyze multiple matches (coupon generation)
|
||||
*/
|
||||
@Post('analyze-matches')
|
||||
@Post("analyze-matches")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Analyze multiple matches for coupon' })
|
||||
@ApiResponse({ status: 200, description: 'Analysis successful' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid input' })
|
||||
@ApiResponse({ status: 429, description: 'Usage limit exceeded' })
|
||||
@ApiOperation({ summary: "Analyze multiple matches for coupon" })
|
||||
@ApiResponse({ status: 200, description: "Analysis successful" })
|
||||
@ApiResponse({ status: 400, description: "Invalid input" })
|
||||
@ApiResponse({ status: 429, description: "Usage limit exceeded" })
|
||||
async analyzeMatches(
|
||||
@CurrentUser() user: any,
|
||||
@Body() dto: AnalyzeMatchesDto,
|
||||
@@ -48,7 +48,7 @@ export class AnalysisController {
|
||||
);
|
||||
|
||||
if (!canProceed) {
|
||||
throw new ForbiddenException('You have exceeded your daily usage limit');
|
||||
throw new ForbiddenException("You have exceeded your daily usage limit");
|
||||
}
|
||||
|
||||
// Run analysis
|
||||
@@ -57,7 +57,7 @@ export class AnalysisController {
|
||||
if (!result) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'None of the provided matches could be analyzed successfully',
|
||||
message: "None of the provided matches could be analyzed successfully",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,10 +73,10 @@ export class AnalysisController {
|
||||
/**
|
||||
* POST /analysis/analyze (alias for /analyze-matches - frontend compatibility)
|
||||
*/
|
||||
@Post('analyze')
|
||||
@Post("analyze")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Analyze multiple matches for coupon (alias)',
|
||||
summary: "Analyze multiple matches for coupon (alias)",
|
||||
deprecated: true,
|
||||
})
|
||||
async analyzeMatchesAlias(
|
||||
@@ -90,9 +90,9 @@ export class AnalysisController {
|
||||
* GET /analysis/history
|
||||
* Get user's analysis history
|
||||
*/
|
||||
@Get('history')
|
||||
@ApiOperation({ summary: 'Get analysis history' })
|
||||
@ApiResponse({ status: 200, description: 'History retrieved' })
|
||||
@Get("history")
|
||||
@ApiOperation({ summary: "Get analysis history" })
|
||||
@ApiResponse({ status: 200, description: "History retrieved" })
|
||||
async getHistory(@CurrentUser() user: any) {
|
||||
const history = await this.analysisService.getAnalysisHistory(user.id);
|
||||
return { success: true, data: history };
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AnalysisController } from './analysis.controller';
|
||||
import { AnalysisService } from './analysis.service';
|
||||
import { DatabaseModule } from '../../database/database.module';
|
||||
import { ServicesModule } from '../../services/services.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AnalysisController } from "./analysis.controller";
|
||||
import { AnalysisService } from "./analysis.service";
|
||||
import { DatabaseModule } from "../../database/database.module";
|
||||
import { ServicesModule } from "../../services/services.module";
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, ServicesModule],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import {
|
||||
MatchAnalysisService,
|
||||
AnalysisResult,
|
||||
} from '../../services/match-analysis.service';
|
||||
} from "../../services/match-analysis.service";
|
||||
|
||||
@Injectable()
|
||||
export class AnalysisService {
|
||||
@@ -50,9 +50,9 @@ export class AnalysisService {
|
||||
}
|
||||
|
||||
// Build URL for analysis
|
||||
const sport = (targetMatch as any).sport || 'football';
|
||||
const sport = (targetMatch as any).sport || "football";
|
||||
const slug = (targetMatch as any).matchSlug || matchId;
|
||||
const url = `https://www.mackolik.com/${sport === 'basketball' ? 'basketbol/mac' : 'mac'}/${slug}/${matchId}`;
|
||||
const url = `https://www.mackolik.com/${sport === "basketball" ? "basketbol/mac" : "mac"}/${slug}/${matchId}`;
|
||||
|
||||
// Run analysis
|
||||
const result = await this.matchAnalysisService.analyzeMatch(
|
||||
@@ -110,7 +110,7 @@ export class AnalysisService {
|
||||
|
||||
// Check limits (default: 10 analyses, 3 coupons per day)
|
||||
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||
const isPremium = user?.subscriptionStatus === 'active';
|
||||
const isPremium = user?.subscriptionStatus === "active";
|
||||
|
||||
const maxAnalyses = isPremium ? 50 : 10;
|
||||
const maxCoupons = isPremium ? 10 : 3;
|
||||
@@ -145,7 +145,7 @@ export class AnalysisService {
|
||||
async getAnalysisHistory(userId: string, limit: number = 20) {
|
||||
return this.prisma.analysis.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { IsArray, IsString, ArrayMinSize, ArrayMaxSize } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsString, ArrayMinSize, ArrayMaxSize } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class AnalyzeMatchesDto {
|
||||
@ApiProperty({
|
||||
description: 'List of match IDs to analyze',
|
||||
example: ['match-1', 'match-2'],
|
||||
description: "List of match IDs to analyze",
|
||||
example: ["match-1", "match-2"],
|
||||
minItems: 1,
|
||||
maxItems: 20,
|
||||
})
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import { I18n, I18nContext } from 'nestjs-i18n';
|
||||
import { ApiTags, ApiOperation, ApiOkResponse } from '@nestjs/swagger';
|
||||
import { AuthService } from './auth.service';
|
||||
import { Controller, Post, Body, HttpCode } from "@nestjs/common";
|
||||
import { I18n, I18nContext } from "nestjs-i18n";
|
||||
import { ApiTags, ApiOperation, ApiOkResponse } from "@nestjs/swagger";
|
||||
import { AuthService } from "./auth.service";
|
||||
import {
|
||||
RegisterDto,
|
||||
LoginDto,
|
||||
RefreshTokenDto,
|
||||
TokenResponseDto,
|
||||
} from './dto/auth.dto';
|
||||
import { Public } from '../../common/decorators';
|
||||
} from "./dto/auth.dto";
|
||||
import { Public } from "../../common/decorators";
|
||||
import {
|
||||
ApiResponse,
|
||||
createSuccessResponse,
|
||||
} from '../../common/types/api-response.type';
|
||||
} from "../../common/types/api-response.type";
|
||||
|
||||
@ApiTags('Auth')
|
||||
@Controller('auth')
|
||||
@ApiTags("Auth")
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('register')
|
||||
@Post("register")
|
||||
@Public()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Register a new user' })
|
||||
@ApiOperation({ summary: "Register a new user" })
|
||||
@ApiOkResponse({
|
||||
description: 'User registered successfully',
|
||||
description: "User registered successfully",
|
||||
type: TokenResponseDto,
|
||||
})
|
||||
async register(
|
||||
@@ -32,28 +32,28 @@ export class AuthController {
|
||||
@I18n() i18n: I18nContext,
|
||||
): Promise<ApiResponse<TokenResponseDto>> {
|
||||
const result = await this.authService.register(dto);
|
||||
return createSuccessResponse(result, i18n.t('auth.registered'), 201);
|
||||
return createSuccessResponse(result, i18n.t("auth.registered"), 201);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@Post("login")
|
||||
@Public()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Login with email and password' })
|
||||
@ApiOkResponse({ description: 'Login successful', type: TokenResponseDto })
|
||||
@ApiOperation({ summary: "Login with email and password" })
|
||||
@ApiOkResponse({ description: "Login successful", type: TokenResponseDto })
|
||||
async login(
|
||||
@Body() dto: LoginDto,
|
||||
@I18n() i18n: I18nContext,
|
||||
): Promise<ApiResponse<TokenResponseDto>> {
|
||||
const result = await this.authService.login(dto);
|
||||
return createSuccessResponse(result, i18n.t('auth.login_success'));
|
||||
return createSuccessResponse(result, i18n.t("auth.login_success"));
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@Post("refresh")
|
||||
@Public()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Refresh access token' })
|
||||
@ApiOperation({ summary: "Refresh access token" })
|
||||
@ApiOkResponse({
|
||||
description: 'Token refreshed successfully',
|
||||
description: "Token refreshed successfully",
|
||||
type: TokenResponseDto,
|
||||
})
|
||||
async refreshToken(
|
||||
@@ -61,18 +61,18 @@ export class AuthController {
|
||||
@I18n() i18n: I18nContext,
|
||||
): Promise<ApiResponse<TokenResponseDto>> {
|
||||
const result = await this.authService.refreshToken(dto.refreshToken);
|
||||
return createSuccessResponse(result, i18n.t('auth.refresh_success'));
|
||||
return createSuccessResponse(result, i18n.t("auth.refresh_success"));
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@Post("logout")
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Logout and invalidate refresh token' })
|
||||
@ApiOkResponse({ description: 'Logout successful' })
|
||||
@ApiOperation({ summary: "Logout and invalidate refresh token" })
|
||||
@ApiOkResponse({ description: "Logout successful" })
|
||||
async logout(
|
||||
@Body() dto: RefreshTokenDto,
|
||||
@I18n() i18n: I18nContext,
|
||||
): Promise<ApiResponse<null>> {
|
||||
await this.authService.logout(dto.refreshToken);
|
||||
return createSuccessResponse(null, i18n.t('auth.logout_success'));
|
||||
return createSuccessResponse(null, i18n.t("auth.logout_success"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { JwtAuthGuard, RolesGuard, PermissionsGuard } from './guards';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { JwtModule, JwtModuleOptions } from "@nestjs/jwt";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtStrategy } from "./strategies/jwt.strategy";
|
||||
import { JwtAuthGuard, RolesGuard, PermissionsGuard } from "./guards";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
PassportModule.register({ defaultStrategy: "jwt" }),
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService): JwtModuleOptions => {
|
||||
const expiresIn =
|
||||
configService.get<string>('JWT_ACCESS_EXPIRATION') || '15m';
|
||||
configService.get<string>("JWT_ACCESS_EXPIRATION") || "15m";
|
||||
return {
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
secret: configService.get<string>("JWT_SECRET"),
|
||||
signOptions: {
|
||||
expiresIn: expiresIn as any,
|
||||
},
|
||||
|
||||
@@ -2,14 +2,14 @@ import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as crypto from 'crypto';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { RegisterDto, LoginDto, TokenResponseDto } from './dto/auth.dto';
|
||||
import { User, UserRole } from '@prisma/client';
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import * as crypto from "crypto";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import { RegisterDto, LoginDto, TokenResponseDto } from "./dto/auth.dto";
|
||||
import { User, UserRole } from "@prisma/client";
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
@@ -36,7 +36,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new ConflictException('EMAIL_ALREADY_EXISTS');
|
||||
throw new ConflictException("EMAIL_ALREADY_EXISTS");
|
||||
}
|
||||
|
||||
// Hash password
|
||||
@@ -76,7 +76,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('INVALID_CREDENTIALS');
|
||||
throw new UnauthorizedException("INVALID_CREDENTIALS");
|
||||
}
|
||||
|
||||
// Verify password
|
||||
@@ -86,11 +86,11 @@ export class AuthService {
|
||||
);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('INVALID_CREDENTIALS');
|
||||
throw new UnauthorizedException("INVALID_CREDENTIALS");
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
throw new UnauthorizedException('ACCOUNT_DISABLED');
|
||||
throw new UnauthorizedException("ACCOUNT_DISABLED");
|
||||
}
|
||||
|
||||
return this.generateTokens(user);
|
||||
@@ -109,7 +109,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!storedToken) {
|
||||
throw new UnauthorizedException('INVALID_REFRESH_TOKEN');
|
||||
throw new UnauthorizedException("INVALID_REFRESH_TOKEN");
|
||||
}
|
||||
|
||||
if (storedToken.expiresAt < new Date()) {
|
||||
@@ -117,7 +117,7 @@ export class AuthService {
|
||||
await this.prisma.refreshToken.delete({
|
||||
where: { id: storedToken.id },
|
||||
});
|
||||
throw new UnauthorizedException('INVALID_REFRESH_TOKEN');
|
||||
throw new UnauthorizedException("INVALID_REFRESH_TOKEN");
|
||||
}
|
||||
|
||||
// Delete old refresh token
|
||||
@@ -167,13 +167,13 @@ export class AuthService {
|
||||
|
||||
// Generate access token
|
||||
const accessToken = this.jwtService.sign(payload, {
|
||||
expiresIn: this.configService.get('JWT_ACCESS_EXPIRATION', '15m'),
|
||||
expiresIn: this.configService.get("JWT_ACCESS_EXPIRATION", "15m"),
|
||||
});
|
||||
|
||||
// Generate refresh token
|
||||
const refreshTokenValue = crypto.randomUUID();
|
||||
const refreshExpiration = this.parseExpiration(
|
||||
this.configService.get('JWT_REFRESH_EXPIRATION', '7d'),
|
||||
this.configService.get("JWT_REFRESH_EXPIRATION", "7d"),
|
||||
);
|
||||
|
||||
// Store refresh token
|
||||
@@ -190,7 +190,7 @@ export class AuthService {
|
||||
refreshToken: refreshTokenValue,
|
||||
expiresIn:
|
||||
this.parseExpiration(
|
||||
this.configService.get('JWT_ACCESS_EXPIRATION', '15m'),
|
||||
this.configService.get("JWT_ACCESS_EXPIRATION", "15m"),
|
||||
) / 1000, // Convert to seconds
|
||||
user: {
|
||||
id: user.id,
|
||||
@@ -233,13 +233,13 @@ export class AuthService {
|
||||
const unit = match[2];
|
||||
|
||||
switch (unit) {
|
||||
case 's':
|
||||
case "s":
|
||||
return value * 1000;
|
||||
case 'm':
|
||||
case "m":
|
||||
return value * 60 * 1000;
|
||||
case 'h':
|
||||
case "h":
|
||||
return value * 60 * 60 * 1000;
|
||||
case 'd':
|
||||
case "d":
|
||||
return value * 24 * 60 * 60 * 1000;
|
||||
default:
|
||||
return 15 * 60 * 1000;
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsEmail, IsString, MinLength, IsOptional } from "class-validator";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
|
||||
export class RegisterDto {
|
||||
@ApiProperty({ example: 'user@example.com' })
|
||||
@ApiProperty({ example: "user@example.com" })
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: 'password123', minLength: 8 })
|
||||
@ApiProperty({ example: "password123", minLength: 8 })
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'John' })
|
||||
@ApiPropertyOptional({ example: "John" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Doe' })
|
||||
@ApiPropertyOptional({ example: "Doe" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({ example: 'user@example.com' })
|
||||
@ApiProperty({ example: "user@example.com" })
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: 'password123' })
|
||||
@ApiProperty({ example: "password123" })
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -4,20 +4,22 @@ import {
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Request } from 'express';
|
||||
} from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { Request } from "express";
|
||||
import {
|
||||
IS_PUBLIC_KEY,
|
||||
ROLES_KEY,
|
||||
PERMISSIONS_KEY,
|
||||
} from '../../../common/decorators';
|
||||
} from "../../../common/decorators";
|
||||
import { normalizeRole } from "../../../common/constants/roles";
|
||||
|
||||
interface AuthenticatedUser {
|
||||
id: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
role?: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
@@ -25,14 +27,14 @@ interface AuthenticatedUser {
|
||||
* JWT Auth Guard - Validates JWT token
|
||||
*/
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
export class JwtAuthGuard extends AuthGuard("jwt") {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
if (request?.method === 'OPTIONS') {
|
||||
if (request?.method === "OPTIONS") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -55,10 +57,10 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
info: any,
|
||||
): TUser {
|
||||
if (err || !user) {
|
||||
if (info?.name === 'TokenExpiredError') {
|
||||
throw new UnauthorizedException('TOKEN_EXPIRED');
|
||||
if (info?.name === "TokenExpiredError") {
|
||||
throw new UnauthorizedException("TOKEN_EXPIRED");
|
||||
}
|
||||
throw err || new UnauthorizedException('AUTH_REQUIRED');
|
||||
throw err || new UnauthorizedException("AUTH_REQUIRED");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
@@ -73,7 +75,7 @@ export class RolesGuard implements CanActivate {
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const req = context.switchToHttp().getRequest<Request>();
|
||||
if (req?.method === 'OPTIONS') {
|
||||
if (req?.method === "OPTIONS") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -88,13 +90,30 @@ 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');
|
||||
throw new ForbiddenException("PERMISSION_DENIED");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -110,7 +129,7 @@ export class PermissionsGuard implements CanActivate {
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const req = context.switchToHttp().getRequest<Request>();
|
||||
if (req?.method === 'OPTIONS') {
|
||||
if (req?.method === "OPTIONS") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -134,7 +153,7 @@ export class PermissionsGuard implements CanActivate {
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new ForbiddenException('PERMISSION_DENIED');
|
||||
throw new ForbiddenException("PERMISSION_DENIED");
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './auth.guards';
|
||||
export * from "./auth.guards";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthService, JwtPayload } from '../auth.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
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) {
|
||||
@@ -10,9 +11,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
private readonly configService: ConfigService,
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
const secret = configService.get<string>('JWT_SECRET');
|
||||
const secret = configService.get<string>("JWT_SECRET");
|
||||
if (!secret) {
|
||||
throw new Error('JWT_SECRET is not defined');
|
||||
throw new Error("JWT_SECRET is not defined");
|
||||
}
|
||||
|
||||
super({
|
||||
@@ -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: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,31 +9,32 @@ import {
|
||||
UseGuards,
|
||||
Req,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiTags,
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { CouponsService } from './coupons.service';
|
||||
import { MatchesService } from '../matches/matches.service';
|
||||
import { SmartCouponService } from './services/smart-coupon.service';
|
||||
} from "@nestjs/swagger";
|
||||
import { CouponsService } from "./coupons.service";
|
||||
import { MatchesService } from "../matches/matches.service";
|
||||
import { SmartCouponService } from "./services/smart-coupon.service";
|
||||
import {
|
||||
UserCouponService,
|
||||
CreateCouponDto,
|
||||
} from './services/user-coupon.service';
|
||||
} from "./services/user-coupon.service";
|
||||
import {
|
||||
AnalyzeMatchDto,
|
||||
DailyBankoDto,
|
||||
SuggestCouponDto,
|
||||
} from './dto/coupons-request.dto';
|
||||
import { Public } from '../../common/decorators';
|
||||
import { JwtAuthGuard } from '../auth/guards/auth.guards'; // Assuming standard guard
|
||||
import { Sport } from '../matches/dto';
|
||||
FrequencyCouponDto,
|
||||
} from "./dto/coupons-request.dto";
|
||||
import { Public } from "../../common/decorators";
|
||||
import { JwtAuthGuard } from "../auth/guards/auth.guards"; // Assuming standard guard
|
||||
import { Sport } from "../matches/dto";
|
||||
|
||||
@ApiTags('Coupon')
|
||||
@Controller('coupon')
|
||||
@ApiTags("Coupon")
|
||||
@Controller("coupon")
|
||||
export class CouponsController {
|
||||
private readonly logger = new Logger(CouponsController.name);
|
||||
|
||||
@@ -48,15 +49,15 @@ export class CouponsController {
|
||||
* POST /coupon/analyze-match
|
||||
* Analyze a single match with V20+ single-match package
|
||||
*/
|
||||
@Post('analyze-match')
|
||||
@Post("analyze-match")
|
||||
@Public()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Analyze single match with V20 model' })
|
||||
@ApiResponse({ status: 200, description: 'Match analysis' })
|
||||
@ApiOperation({ summary: "Analyze single match with V20 model" })
|
||||
@ApiResponse({ status: 200, description: "Match analysis" })
|
||||
async analyzeMatch(@Body() dto: AnalyzeMatchDto) {
|
||||
const analysis = await this.smartCouponService.analyzeMatch(dto.matchId);
|
||||
if (!analysis) {
|
||||
return { success: false, message: 'Analiz yapılamadı.' };
|
||||
return { success: false, message: "Analiz yapılamadı." };
|
||||
}
|
||||
return { success: true, data: analysis };
|
||||
}
|
||||
@@ -64,11 +65,11 @@ export class CouponsController {
|
||||
/**
|
||||
* POST /coupon/analyze (alias for /analyze-match - frontend compatibility)
|
||||
*/
|
||||
@Post('analyze')
|
||||
@Post("analyze")
|
||||
@Public()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Analyze single match with V20 model (alias)',
|
||||
summary: "Analyze single match with V20 model (alias)",
|
||||
deprecated: true,
|
||||
})
|
||||
async analyzeMatchAlias(@Body() dto: AnalyzeMatchDto) {
|
||||
@@ -83,7 +84,7 @@ export class CouponsController {
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create and save a user coupon (alias)' })
|
||||
@ApiOperation({ summary: "Create and save a user coupon (alias)" })
|
||||
async createCouponAlias(@Body() dto: CreateCouponDto, @Req() req: any) {
|
||||
return this.createCoupon(dto, req);
|
||||
}
|
||||
@@ -92,11 +93,11 @@ export class CouponsController {
|
||||
* POST /coupon/daily-banko
|
||||
* Generate a high-confidence banko combo (2 matches)
|
||||
*/
|
||||
@Post('daily-banko')
|
||||
@Post("daily-banko")
|
||||
@Public()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Generate a high-confidence banko combo (2 matches)',
|
||||
summary: "Generate a high-confidence banko combo (2 matches)",
|
||||
})
|
||||
async getDailyBanko(@Body() dto: DailyBankoDto) {
|
||||
// If no match IDs provided, fetch from system (top 50 upcoming)
|
||||
@@ -122,7 +123,7 @@ export class CouponsController {
|
||||
if (candidateMatches.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Kupon için uygun, henüz başlamamış maç bulunamadı.',
|
||||
message: "Kupon için uygun, henüz başlamamış maç bulunamadı.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,7 +132,7 @@ export class CouponsController {
|
||||
if (!coupon) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Kriterlere uygun (80%+ güvenli) yeterli maç bulunamadı.',
|
||||
message: "Kriterlere uygun (80%+ güvenli) yeterli maç bulunamadı.",
|
||||
};
|
||||
}
|
||||
return { success: true, data: coupon };
|
||||
@@ -141,11 +142,11 @@ export class CouponsController {
|
||||
* POST /coupon/suggest
|
||||
* Generate Smart Coupon
|
||||
*/
|
||||
@Post('suggest')
|
||||
@Post("suggest")
|
||||
@Public()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Suggest Smart Coupon' })
|
||||
@ApiResponse({ status: 200, description: 'Smart Coupon generated' })
|
||||
@ApiOperation({ summary: "Suggest Smart Coupon" })
|
||||
@ApiResponse({ status: 200, description: "Smart Coupon generated" })
|
||||
async suggestCoupon(@Body() dto: SuggestCouponDto) {
|
||||
// If no match IDs provided, fetch from system (top 50 upcoming)
|
||||
let candidateMatches = dto.matchIds || [];
|
||||
@@ -170,7 +171,7 @@ export class CouponsController {
|
||||
if (candidateMatches.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Tahmin için uygun, henüz başlamamış maç bulunamadı.',
|
||||
message: "Tahmin için uygun, henüz başlamamış maç bulunamadı.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,24 +184,59 @@ export class CouponsController {
|
||||
},
|
||||
);
|
||||
if (!coupon) {
|
||||
return { success: false, message: 'Kupon oluşturulamadı.' };
|
||||
return { success: false, message: "Kupon oluşturulamadı." };
|
||||
}
|
||||
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
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /coupon/create
|
||||
* Save a user generated coupon
|
||||
*/
|
||||
@Post('create')
|
||||
@Post("create")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create and save a user coupon' })
|
||||
@ApiOperation({ summary: "Create and save a user coupon" })
|
||||
async createCoupon(@Body() dto: CreateCouponDto, @Req() req: any) {
|
||||
// req.user is populated by JwtAuthGuard
|
||||
const coupon = await this.userCouponService.createCoupon(req.user, dto);
|
||||
@@ -211,10 +247,10 @@ export class CouponsController {
|
||||
* GET /coupon/my-stats
|
||||
* Get user betting statistics (ROI, Win Rate)
|
||||
*/
|
||||
@Get('my-stats')
|
||||
@Get("my-stats")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get user betting statistics' })
|
||||
@ApiOperation({ summary: "Get user betting statistics" })
|
||||
async getUserStats(@Req() req: any) {
|
||||
const stats = await this.userCouponService.getUserStatistics(req.user.id);
|
||||
return { success: true, data: stats };
|
||||
@@ -224,11 +260,11 @@ export class CouponsController {
|
||||
* GET /coupon/history
|
||||
* Get coupon history (Public/System coupons)
|
||||
*/
|
||||
@Get('history')
|
||||
@Get("history")
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get coupon history' })
|
||||
@ApiResponse({ status: 200, description: 'History retrieved' })
|
||||
async getHistory(@Query('limit') limit?: string) {
|
||||
@ApiOperation({ summary: "Get coupon history" })
|
||||
@ApiResponse({ status: 200, description: "History retrieved" })
|
||||
async getHistory(@Query("limit") limit?: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
const results = await this.couponsService.getCouponHistory(
|
||||
Number(limit) || 10,
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
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 { CouponsService } from './coupons.service';
|
||||
import { DatabaseModule } from '../../database/database.module';
|
||||
import { ServicesModule } from '../../services/services.module';
|
||||
import { MatchesModule } from '../matches/matches.module';
|
||||
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";
|
||||
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 {}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { AiService } from '../../services/ai.service';
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import { AiService } from "../../services/ai.service";
|
||||
// [REMOVED V16 IMPORTS]
|
||||
|
||||
export type RiskLevel = 'banko' | 'safe' | 'value';
|
||||
export type RiskLevel = "banko" | "safe" | "value";
|
||||
|
||||
export interface CouponMatch {
|
||||
matchId: string;
|
||||
|
||||
@@ -8,19 +8,19 @@ import {
|
||||
ArrayMaxSize,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
} from "class-validator";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
|
||||
export enum CouponStrategyEnum {
|
||||
SAFE = 'SAFE',
|
||||
BALANCED = 'BALANCED',
|
||||
AGGRESSIVE = 'AGGRESSIVE',
|
||||
VALUE = 'VALUE',
|
||||
MIRACLE = 'MIRACLE',
|
||||
SAFE = "SAFE",
|
||||
BALANCED = "BALANCED",
|
||||
AGGRESSIVE = "AGGRESSIVE",
|
||||
VALUE = "VALUE",
|
||||
MIRACLE = "MIRACLE",
|
||||
}
|
||||
|
||||
export class AnalyzeMatchDto {
|
||||
@ApiProperty({ description: 'Match ID to analyze' })
|
||||
@ApiProperty({ description: "Match ID to analyze" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
matchId: string;
|
||||
@@ -28,8 +28,8 @@ export class AnalyzeMatchDto {
|
||||
|
||||
export class DailyBankoDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Optional match IDs — system fetches if empty',
|
||||
example: ['match-1', 'match-2'],
|
||||
description: "Optional match IDs — system fetches if empty",
|
||||
example: ["match-1", "match-2"],
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@@ -40,8 +40,8 @@ export class DailyBankoDto {
|
||||
|
||||
export class SuggestCouponDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Match IDs — system fetches if empty',
|
||||
example: ['match-1', 'match-2'],
|
||||
description: "Match IDs — system fetches if empty",
|
||||
example: ["match-1", "match-2"],
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@@ -57,7 +57,7 @@ export class SuggestCouponDto {
|
||||
@IsEnum(CouponStrategyEnum)
|
||||
strategy?: CouponStrategyEnum;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Maximum matches in coupon', example: 5 })
|
||||
@ApiPropertyOptional({ description: "Maximum matches in coupon", example: 5 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@@ -65,7 +65,7 @@ export class SuggestCouponDto {
|
||||
maxMatches?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Minimum confidence threshold (0-100)',
|
||||
description: "Minimum confidence threshold (0-100)",
|
||||
example: 60,
|
||||
})
|
||||
@IsOptional()
|
||||
@@ -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,10 +1,18 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import axios from 'axios';
|
||||
import { GeminiService } from '../../gemini/gemini.service';
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
||||
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';
|
||||
export type BetGrade = 'A' | 'B' | 'C' | 'PASS';
|
||||
export type PredictionRiskLevel = "LOW" | "MEDIUM" | "HIGH" | "EXTREME";
|
||||
export type PredictionDataQuality = "HIGH" | "MEDIUM" | "LOW";
|
||||
export type BetGrade = "A" | "B" | "C" | "PASS";
|
||||
|
||||
export interface PredictionPickRow {
|
||||
market: string;
|
||||
@@ -126,28 +134,41 @@ 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) {
|
||||
this.aiEngineUrl = process.env.AI_ENGINE_URL || 'http://ai-engine:8000';
|
||||
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(
|
||||
'AI analyze failed',
|
||||
"AI analyze failed",
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
@@ -168,7 +189,7 @@ export class SmartCouponService {
|
||||
const result = await this.geminiService.generateText(
|
||||
JSON.stringify(prediction, null, 2),
|
||||
{
|
||||
model: 'gemini-2.0-flash',
|
||||
model: "gemini-2.0-flash",
|
||||
temperature: 0.7,
|
||||
maxTokens: 600,
|
||||
systemPrompt: MATCH_COMMENTARY_SYSTEM_PROMPT,
|
||||
@@ -176,7 +197,7 @@ export class SmartCouponService {
|
||||
);
|
||||
return result.text || null;
|
||||
} catch (error) {
|
||||
this.logger.warn('AI commentary generation failed, skipping', error);
|
||||
this.logger.warn("AI commentary generation failed, skipping", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -188,7 +209,7 @@ export class SmartCouponService {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getSmartCoupon(matchIds, 'SAFE', {
|
||||
return this.getSmartCoupon(matchIds, "SAFE", {
|
||||
maxMatches: 2,
|
||||
minConfidence: 78,
|
||||
});
|
||||
@@ -197,16 +218,16 @@ export class SmartCouponService {
|
||||
async getSmartCoupon(
|
||||
matchIds: string[],
|
||||
strategy:
|
||||
| 'SAFE'
|
||||
| 'BALANCED'
|
||||
| 'AGGRESSIVE'
|
||||
| 'VALUE'
|
||||
| 'MIRACLE' = 'BALANCED',
|
||||
| "SAFE"
|
||||
| "BALANCED"
|
||||
| "AGGRESSIVE"
|
||||
| "VALUE"
|
||||
| "MIRACLE" = "BALANCED",
|
||||
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,21 +236,251 @@ export class SmartCouponService {
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate smart coupon', error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
const detail = error.response?.data?.detail || error.message;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error("Failed to generate smart coupon", error);
|
||||
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(
|
||||
'Coupon generation failed',
|
||||
"Coupon generation failed",
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../database/prisma.service';
|
||||
import { User, UserCoupon, Match } from '@prisma/client';
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../../database/prisma.service";
|
||||
import { User, UserCoupon, Match } from "@prisma/client";
|
||||
|
||||
export class CreateCouponDto {
|
||||
strategy: string; // 'SAFE', 'VALUE', 'CUSTOM'
|
||||
@@ -39,7 +39,7 @@ export class UserCouponService {
|
||||
strategy: dto.strategy,
|
||||
totalOdds: parseFloat(totalOdds.toFixed(2)),
|
||||
isPublic: dto.isPublic || false,
|
||||
status: 'PENDING',
|
||||
status: "PENDING",
|
||||
couponItems: {
|
||||
create: dto.items.map((item) => ({
|
||||
matchId: item.matchId,
|
||||
@@ -66,7 +66,7 @@ export class UserCouponService {
|
||||
async updatePendingCoupons(): Promise<void> {
|
||||
// Sadece bitmiş (FT) maçları içeren PENDING kuponları çek
|
||||
const pendingCoupons = await this.prisma.userCoupon.findMany({
|
||||
where: { status: 'PENDING' },
|
||||
where: { status: "PENDING" },
|
||||
include: {
|
||||
couponItems: {
|
||||
include: { match: true },
|
||||
@@ -80,7 +80,7 @@ export class UserCouponService {
|
||||
let allMatchesFinished = true;
|
||||
|
||||
for (const item of coupon.couponItems) {
|
||||
if (item.match.status !== 'FT') {
|
||||
if (item.match.status !== "FT") {
|
||||
allMatchesFinished = false;
|
||||
break; // Henüz bitmemiş maç var, kuponu güncelleme
|
||||
}
|
||||
@@ -104,12 +104,12 @@ export class UserCouponService {
|
||||
if (isCouponLost) {
|
||||
await this.prisma.userCoupon.update({
|
||||
where: { id: coupon.id },
|
||||
data: { status: 'LOST' },
|
||||
data: { status: "LOST" },
|
||||
});
|
||||
} else if (allMatchesFinished && isCouponWon) {
|
||||
await this.prisma.userCoupon.update({
|
||||
where: { id: coupon.id },
|
||||
data: { status: 'WON' },
|
||||
data: { status: "WON" },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -125,23 +125,23 @@ export class UserCouponService {
|
||||
const total = home + away;
|
||||
|
||||
switch (selection) {
|
||||
case 'MS 1':
|
||||
case "MS 1":
|
||||
return home > away;
|
||||
case 'MS X':
|
||||
case "MS X":
|
||||
return home === away;
|
||||
case 'MS 2':
|
||||
case "MS 2":
|
||||
return away > home;
|
||||
case '1.5 UST':
|
||||
case "1.5 UST":
|
||||
return total > 1.5;
|
||||
case '2.5 UST':
|
||||
case "2.5 UST":
|
||||
return total > 2.5;
|
||||
case '3.5 UST':
|
||||
case "3.5 UST":
|
||||
return total > 3.5;
|
||||
case '2.5 ALT':
|
||||
case "2.5 ALT":
|
||||
return total < 2.5;
|
||||
case 'KG VAR':
|
||||
case "KG VAR":
|
||||
return home > 0 && away > 0;
|
||||
case 'KG YOK':
|
||||
case "KG YOK":
|
||||
return home === 0 || away === 0;
|
||||
default:
|
||||
return false; // Bilinmeyen market
|
||||
@@ -155,7 +155,7 @@ export class UserCouponService {
|
||||
const coupons = await this.prisma.userCoupon.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ['WON', 'LOST'] },
|
||||
status: { in: ["WON", "LOST"] },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -171,7 +171,7 @@ export class UserCouponService {
|
||||
};
|
||||
}
|
||||
|
||||
const wonCoupons = coupons.filter((c) => c.status === 'WON');
|
||||
const wonCoupons = coupons.filter((c) => c.status === "WON");
|
||||
const totalInvested = totalCoupons; // Her kupona 1 birim yatırıldığını varsayıyoruz
|
||||
const totalReturn = wonCoupons.reduce((acc, c) => acc + c.totalOdds, 0);
|
||||
const winRate = (wonCoupons.length / totalCoupons) * 100;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* Database operations using Prisma
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import {
|
||||
Sport,
|
||||
MatchSummary,
|
||||
@@ -20,8 +20,9 @@ import {
|
||||
DbEventPayload,
|
||||
DbMarketPayload,
|
||||
BasketballTeamStats,
|
||||
} from './feeder.types';
|
||||
import { ImageUtils } from '../../common/utils/image.util';
|
||||
} from "./feeder.types";
|
||||
import { ImageUtils } from "../../common/utils/image.util";
|
||||
import { deriveStoredMatchStatus } from "../../common/utils/match-status.util";
|
||||
|
||||
@Injectable()
|
||||
export class FeederPersistenceService {
|
||||
@@ -33,7 +34,7 @@ export class FeederPersistenceService {
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================
|
||||
private safeString(value: any): string | null {
|
||||
return value === null || value === undefined || value === ''
|
||||
return value === null || value === undefined || value === ""
|
||||
? null
|
||||
: String(value);
|
||||
}
|
||||
@@ -51,12 +52,12 @@ export class FeederPersistenceService {
|
||||
private mapPositionToEnum(position: string | null): any {
|
||||
if (!position) return null;
|
||||
const pos = position.toLowerCase();
|
||||
if (pos.includes('kaleci') || pos.includes('goalkeeper'))
|
||||
return 'goalkeeper';
|
||||
if (pos.includes('defans') || pos.includes('defender')) return 'defender';
|
||||
if (pos.includes('orta saha') || pos.includes('midfielder'))
|
||||
return 'midfielder';
|
||||
if (pos.includes('forvet') || pos.includes('striker')) return 'striker';
|
||||
if (pos.includes("kaleci") || pos.includes("goalkeeper"))
|
||||
return "goalkeeper";
|
||||
if (pos.includes("defans") || pos.includes("defender")) return "defender";
|
||||
if (pos.includes("orta saha") || pos.includes("midfielder"))
|
||||
return "midfielder";
|
||||
if (pos.includes("forvet") || pos.includes("striker")) return "striker";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@ export class FeederPersistenceService {
|
||||
}
|
||||
|
||||
for (const s of market.selectionCollection) {
|
||||
if (!s || s.odd === '-' || s.odd === '') continue;
|
||||
if (!s || s.odd === "-" || s.odd === "") continue;
|
||||
|
||||
const sName = this.safeString(s.name);
|
||||
const sValue = this.safeString(s.odd);
|
||||
@@ -107,7 +108,7 @@ export class FeederPersistenceService {
|
||||
|
||||
if (existingSel) {
|
||||
if (existingSel.oddValue !== sValue) {
|
||||
const oldVal = parseFloat(existingSel.oddValue || '0');
|
||||
const oldVal = parseFloat(existingSel.oddValue || "0");
|
||||
const newVal = parseFloat(sValue);
|
||||
|
||||
if (!isNaN(oldVal) && !isNaN(newVal)) {
|
||||
@@ -182,13 +183,13 @@ export class FeederPersistenceService {
|
||||
const teamsToUpsert = [
|
||||
{
|
||||
id: homeTeamId,
|
||||
name: matchSummary.homeTeam?.name || 'Unknown',
|
||||
name: matchSummary.homeTeam?.name || "Unknown",
|
||||
slug: matchSummary.homeTeam?.slug || homeTeamId,
|
||||
sport: sport,
|
||||
},
|
||||
{
|
||||
id: awayTeamId,
|
||||
name: matchSummary.awayTeam?.name || 'Unknown',
|
||||
name: matchSummary.awayTeam?.name || "Unknown",
|
||||
slug: matchSummary.awayTeam?.slug || awayTeamId,
|
||||
sport: sport,
|
||||
},
|
||||
@@ -221,18 +222,18 @@ export class FeederPersistenceService {
|
||||
update: {},
|
||||
create: {
|
||||
id: countryId,
|
||||
name: league.country.name || 'Unknown',
|
||||
name: league.country.name || "Unknown",
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'P2002') throw error;
|
||||
if (error.code !== "P2002") throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Save League (Handle ID changes by checking unique constraint)
|
||||
let finalLeagueId = this.safeString(league.id);
|
||||
if (finalLeagueId && countryId) {
|
||||
const leagueName = league.name || 'Unknown';
|
||||
const leagueName = league.name || "Unknown";
|
||||
|
||||
// Check if league exists by unique constraint (name + country + sport)
|
||||
const existingLeague = await tx.league.findUnique({
|
||||
@@ -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 },
|
||||
@@ -455,7 +438,7 @@ export class FeederPersistenceService {
|
||||
}
|
||||
|
||||
// 8. Save Team Stats (Football)
|
||||
if (stats && sport === 'football') {
|
||||
if (stats && sport === "football") {
|
||||
const statsRows = [
|
||||
{
|
||||
matchId,
|
||||
@@ -499,7 +482,7 @@ export class FeederPersistenceService {
|
||||
}
|
||||
|
||||
// 8b. Save Team Stats (Basketball)
|
||||
if (basketballTeamStats && sport === 'basketball') {
|
||||
if (basketballTeamStats && sport === "basketball") {
|
||||
const teams = [
|
||||
{ id: homeTeamId, data: basketballTeamStats.home },
|
||||
{ id: awayTeamId, data: basketballTeamStats.away },
|
||||
@@ -558,7 +541,7 @@ export class FeederPersistenceService {
|
||||
}
|
||||
|
||||
// 8c. Save Player Stats (Basketball)
|
||||
if (basketballPlayerStats.length > 0 && sport === 'basketball') {
|
||||
if (basketballPlayerStats.length > 0 && sport === "basketball") {
|
||||
await tx.basketballPlayerStats.deleteMany({ where: { matchId } });
|
||||
|
||||
for (const p of basketballPlayerStats) {
|
||||
@@ -592,12 +575,12 @@ export class FeederPersistenceService {
|
||||
await this.saveOddsInTransaction(tx, matchId, oddsArray);
|
||||
|
||||
// 10. Save Officials
|
||||
if (sport === 'football' && officialsData.length > 0) {
|
||||
if (sport === "football" && officialsData.length > 0) {
|
||||
await tx.matchOfficial.deleteMany({ where: { matchId } });
|
||||
const processedOfficials = new Set<string>();
|
||||
|
||||
for (const o of officialsData) {
|
||||
const roleName = o.role || 'Referee';
|
||||
const roleName = o.role || "Referee";
|
||||
const uniqueKey = `${o.name}_${roleName}`;
|
||||
|
||||
if (processedOfficials.has(uniqueKey)) continue;
|
||||
@@ -798,10 +781,10 @@ export class FeederPersistenceService {
|
||||
const history = await this.prisma.match.findMany({
|
||||
where: {
|
||||
OR: [{ homeTeamId: teamId }, { awayTeamId: teamId }],
|
||||
status: 'FT',
|
||||
status: "FT",
|
||||
mstUtc: { lt: match.mstUtc },
|
||||
},
|
||||
orderBy: { mstUtc: 'desc' },
|
||||
orderBy: { mstUtc: "desc" },
|
||||
take: 5,
|
||||
});
|
||||
|
||||
@@ -840,8 +823,8 @@ export class FeederPersistenceService {
|
||||
|
||||
return {
|
||||
match_id: match.id,
|
||||
home_team: match.homeTeam?.name || 'Unknown',
|
||||
away_team: match.awayTeam?.name || 'Unknown',
|
||||
home_team: match.homeTeam?.name || "Unknown",
|
||||
away_team: match.awayTeam?.name || "Unknown",
|
||||
home_team_id: match.homeTeamId,
|
||||
away_team_id: match.awayTeamId,
|
||||
league_id: match.leagueId,
|
||||
@@ -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: {} } },
|
||||
@@ -934,7 +913,7 @@ export class FeederPersistenceService {
|
||||
scoreHome: liveMatch.scoreHome,
|
||||
scoreAway: liveMatch.scoreAway,
|
||||
mstUtc: liveMatch.mstUtc,
|
||||
sport: liveMatch.sport || 'football',
|
||||
sport: liveMatch.sport || "football",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* HTTP requests with exact headers from working curl commands
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
import {
|
||||
Sport,
|
||||
SPORTS_CONFIG,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
SidelinedResponse,
|
||||
SidelinedTeamData,
|
||||
SidelinedPlayer,
|
||||
} from './feeder.types';
|
||||
} from "./feeder.types";
|
||||
|
||||
@Injectable()
|
||||
export class FeederScraperService {
|
||||
@@ -43,13 +43,13 @@ export class FeederScraperService {
|
||||
this.axios.interceptors.response.use(
|
||||
(response) => {
|
||||
this.logger.debug(
|
||||
`✅ [${response.config.url?.split('?')[0]}] Status: ${response.status}`,
|
||||
`✅ [${response.config.url?.split("?")[0]}] Status: ${response.status}`,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
const status = error.response?.status || 'N/A';
|
||||
const url = error.config?.url?.split('?')[0] || 'Unknown';
|
||||
const status = error.response?.status || "N/A";
|
||||
const url = error.config?.url?.split("?")[0] || "Unknown";
|
||||
this.logger.error(`❌ [${url}] Status: ${status} - ${error.message}`);
|
||||
throw error;
|
||||
},
|
||||
@@ -72,7 +72,7 @@ export class FeederScraperService {
|
||||
|
||||
const response = await this.axios.get(url, {
|
||||
params: {
|
||||
'sports[]': sportParam,
|
||||
"sports[]": sportParam,
|
||||
matchDate: dateString,
|
||||
},
|
||||
});
|
||||
@@ -80,11 +80,11 @@ export class FeederScraperService {
|
||||
const payload = response.data as unknown;
|
||||
if (
|
||||
!payload ||
|
||||
typeof payload !== 'object' ||
|
||||
!('status' in payload) ||
|
||||
!('data' in payload)
|
||||
typeof payload !== "object" ||
|
||||
!("status" in payload) ||
|
||||
!("data" in payload)
|
||||
) {
|
||||
throw new Error('Historical source payload has invalid shape');
|
||||
throw new Error("Historical source payload has invalid shape");
|
||||
}
|
||||
|
||||
return payload as LivescoresApiResponse;
|
||||
@@ -101,14 +101,14 @@ export class FeederScraperService {
|
||||
const response = await this.axios.get(url, {
|
||||
params: {
|
||||
matchId,
|
||||
sdapiLanguageCode: 'tr-mk',
|
||||
ajaxViewName: 'match-details',
|
||||
ajaxPartialViewName: 'match-details-status',
|
||||
displayMode: 'all',
|
||||
sdapiLanguageCode: "tr-mk",
|
||||
ajaxViewName: "match-details",
|
||||
ajaxPartialViewName: "match-details-status",
|
||||
displayMode: "all",
|
||||
},
|
||||
});
|
||||
|
||||
return this.parseMatchHeader(response.data.data?.html || '');
|
||||
return this.parseMatchHeader(response.data.data?.html || "");
|
||||
}
|
||||
|
||||
private parseMatchHeader(html: string): ParsedMatchHeader {
|
||||
@@ -116,7 +116,7 @@ export class FeederScraperService {
|
||||
|
||||
// Extract match-status from data attribute
|
||||
const matchStatus =
|
||||
($('[data-match-status]').attr('data-match-status') as any) || 'postGame';
|
||||
($("[data-match-status]").attr("data-match-status") as any) || "postGame";
|
||||
|
||||
// Extract scores
|
||||
const scoreHome = this.safeInt($('[data-slot="score-home"]').text().trim());
|
||||
@@ -126,7 +126,7 @@ export class FeederScraperService {
|
||||
let htScoreHome: number | null = null;
|
||||
let htScoreAway: number | null = null;
|
||||
|
||||
const detailedScore = $('.p0c-soccer-match-details-header__detailed-score')
|
||||
const detailedScore = $(".p0c-soccer-match-details-header__detailed-score")
|
||||
.text()
|
||||
.trim();
|
||||
const htMatch = detailedScore.match(/\(İY\s*(\d+)\s*-\s*(\d+)\)/);
|
||||
@@ -143,7 +143,7 @@ export class FeederScraperService {
|
||||
// ============================================
|
||||
async fetchKeyEvents(
|
||||
matchId: string,
|
||||
): Promise<KeyEventsResponse['data'] | null> {
|
||||
): Promise<KeyEventsResponse["data"] | null> {
|
||||
const url = `https://www.mackolik.com/ajax/football/key-events`;
|
||||
|
||||
this.logger.debug(`📡 [${matchId}] Fetching key events`);
|
||||
@@ -151,7 +151,7 @@ export class FeederScraperService {
|
||||
try {
|
||||
const response = await this.axios.get<KeyEventsResponse>(url, {
|
||||
params: {
|
||||
ajaxViewName: 'events',
|
||||
ajaxViewName: "events",
|
||||
matchId,
|
||||
seasonId: matchId, // Same as matchId
|
||||
},
|
||||
@@ -172,7 +172,7 @@ export class FeederScraperService {
|
||||
// ============================================
|
||||
async fetchStartingFormation(
|
||||
matchId: string,
|
||||
): Promise<MatchStatsResponse['data'] | null> {
|
||||
): Promise<MatchStatsResponse["data"] | null> {
|
||||
const url = `https://www.mackolik.com/ajax/football/match-stats`;
|
||||
|
||||
this.logger.debug(`📡 [${matchId}] Fetching starting formation`);
|
||||
@@ -180,7 +180,7 @@ export class FeederScraperService {
|
||||
try {
|
||||
const response = await this.axios.get<MatchStatsResponse>(url, {
|
||||
params: {
|
||||
ajaxViewName: 'starting-formation',
|
||||
ajaxViewName: "starting-formation",
|
||||
matchId,
|
||||
seasonId: matchId,
|
||||
},
|
||||
@@ -201,7 +201,7 @@ export class FeederScraperService {
|
||||
// ============================================
|
||||
async fetchSubstitutions(
|
||||
matchId: string,
|
||||
): Promise<MatchStatsResponse['data'] | null> {
|
||||
): Promise<MatchStatsResponse["data"] | null> {
|
||||
const url = `https://www.mackolik.com/ajax/football/match-stats`;
|
||||
|
||||
this.logger.debug(`📡 [${matchId}] Fetching substitutions`);
|
||||
@@ -209,7 +209,7 @@ export class FeederScraperService {
|
||||
try {
|
||||
const response = await this.axios.get<MatchStatsResponse>(url, {
|
||||
params: {
|
||||
ajaxViewName: 'substitutions',
|
||||
ajaxViewName: "substitutions",
|
||||
matchId,
|
||||
seasonId: matchId,
|
||||
},
|
||||
@@ -230,7 +230,7 @@ export class FeederScraperService {
|
||||
// ============================================
|
||||
async fetchGameStats(
|
||||
matchId: string,
|
||||
): Promise<GameStatsResponse['data'] | null> {
|
||||
): Promise<GameStatsResponse["data"] | null> {
|
||||
const url = `https://www.mackolik.com/ajax/soccer/match/gameStats`;
|
||||
|
||||
this.logger.debug(`📡 [${matchId}] Fetching game stats`);
|
||||
@@ -253,7 +253,7 @@ export class FeederScraperService {
|
||||
// ============================================
|
||||
// MANAGER
|
||||
// ============================================
|
||||
async fetchManager(matchId: string): Promise<ManagerResponse['data'] | null> {
|
||||
async fetchManager(matchId: string): Promise<ManagerResponse["data"] | null> {
|
||||
const url = `https://www.mackolik.com/ajax/football/match-stats`;
|
||||
|
||||
this.logger.debug(`📡 [${matchId}] Fetching manager`);
|
||||
@@ -261,7 +261,7 @@ export class FeederScraperService {
|
||||
try {
|
||||
const response = await this.axios.get<ManagerResponse>(url, {
|
||||
params: {
|
||||
ajaxViewName: 'manager',
|
||||
ajaxViewName: "manager",
|
||||
matchId,
|
||||
seasonId: matchId,
|
||||
},
|
||||
@@ -287,10 +287,10 @@ export class FeederScraperService {
|
||||
|
||||
try {
|
||||
const response = await this.axios.get<IddaaMarketsHtmlResponse>(url, {
|
||||
params: { template: 'all' },
|
||||
params: { template: "all" },
|
||||
});
|
||||
|
||||
return this.parseIddaaMarketsHtml(response.data.data?.html || '');
|
||||
return this.parseIddaaMarketsHtml(response.data.data?.html || "");
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 404) {
|
||||
this.logger.warn(`[${matchId}] Iddaa markets not found (404)`);
|
||||
@@ -306,30 +306,30 @@ export class FeederScraperService {
|
||||
const $ = cheerio.load(html);
|
||||
const markets: ParsedMarket[] = [];
|
||||
|
||||
$('.widget-iddaa-markets__market-item').each((_, marketEl) => {
|
||||
$(".widget-iddaa-markets__market-item").each((_, marketEl) => {
|
||||
const $market = $(marketEl);
|
||||
|
||||
const marketId = $market.attr('data-market') || '';
|
||||
const marketId = $market.attr("data-market") || "";
|
||||
const marketName = $market
|
||||
.find('.widget-iddaa-markets__header-text')
|
||||
.find(".widget-iddaa-markets__header-text")
|
||||
.text()
|
||||
.trim();
|
||||
const iddaaCode = $market
|
||||
.find('.widget-iddaa-markets__iddaa-code')
|
||||
.find(".widget-iddaa-markets__iddaa-code")
|
||||
.text()
|
||||
.trim();
|
||||
const mbc = $market.find('.widget-iddaa-markets__mbc').text().trim();
|
||||
const mbc = $market.find(".widget-iddaa-markets__mbc").text().trim();
|
||||
|
||||
const selections: ParsedSelection[] = [];
|
||||
|
||||
$market.find('.widget-iddaa-markets__option').each((_, optionEl) => {
|
||||
$market.find(".widget-iddaa-markets__option").each((_, optionEl) => {
|
||||
const $option = $(optionEl);
|
||||
|
||||
selections.push({
|
||||
shortcode: $option.attr('data-shortcode') || '',
|
||||
outcomeNo: $option.attr('data-outcome-no') || '',
|
||||
label: $option.find('.widget-iddaa-markets__label').text().trim(),
|
||||
value: $option.find('.widget-iddaa-markets__value').text().trim(),
|
||||
shortcode: $option.attr("data-shortcode") || "",
|
||||
outcomeNo: $option.attr("data-outcome-no") || "",
|
||||
label: $option.find(".widget-iddaa-markets__label").text().trim(),
|
||||
value: $option.find(".widget-iddaa-markets__value").text().trim(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -347,7 +347,7 @@ export class FeederScraperService {
|
||||
// ============================================
|
||||
async fetchBasketballBoxScore(
|
||||
matchId: string,
|
||||
): Promise<BasketballBoxScoreResponse['data'] | null> {
|
||||
): Promise<BasketballBoxScoreResponse["data"] | null> {
|
||||
// Updated URL based on user request
|
||||
const url = `https://www.mackolik.com/ajax/basketball/match/box-score`;
|
||||
|
||||
@@ -357,8 +357,8 @@ export class FeederScraperService {
|
||||
const response = await this.axios.get<BasketballBoxScoreResponse>(url, {
|
||||
params: { matchId },
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'User-Agent': DEFAULT_HEADERS['User-Agent'],
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"User-Agent": DEFAULT_HEADERS["User-Agent"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -382,25 +382,25 @@ export class FeederScraperService {
|
||||
const players: Partial<BasketballPlayerStats>[] = [];
|
||||
|
||||
// Parse individual players from widget rows
|
||||
$('.widget-basketball-match-box-score__row').each((_, elem) => {
|
||||
$(".widget-basketball-match-box-score__row").each((_, elem) => {
|
||||
const row = $(elem);
|
||||
// Skip if no player name found
|
||||
const nameElem = row.find('.widget-basketball-match-box-score__player');
|
||||
const nameElem = row.find(".widget-basketball-match-box-score__player");
|
||||
if (!nameElem.length) return;
|
||||
|
||||
const name = nameElem.text().trim();
|
||||
// Indices based on User HTML:
|
||||
// 0: Name, 1: Min, 2: Pts, 3: Reb, 4: Ast, 5: 2FG, 6: 3FG, 7: FT, 8: Fouls, 9: Blk, 10: Stl, 11: TO
|
||||
const values = row.find('td');
|
||||
const values = row.find("td");
|
||||
|
||||
// Check if it's a valid player row (should have enough columns)
|
||||
if (values.length < 10) return;
|
||||
|
||||
// Extract ID from link if possible
|
||||
let playerId = '';
|
||||
const link = nameElem.find('a').attr('href');
|
||||
let playerId = "";
|
||||
const link = nameElem.find("a").attr("href");
|
||||
if (link) {
|
||||
playerId = this.extractPlayerIdFromUrl(link) || '';
|
||||
playerId = this.extractPlayerIdFromUrl(link) || "";
|
||||
}
|
||||
|
||||
players.push({
|
||||
@@ -410,16 +410,16 @@ export class FeederScraperService {
|
||||
points: this.safeInt(values.eq(2).text().trim()) || 0,
|
||||
rebounds: this.safeInt(values.eq(3).text().trim()) || 0,
|
||||
assists: this.safeInt(values.eq(4).text().trim()) || 0,
|
||||
fgMade: this.safeInt(values.eq(5).text().trim().split('/')[0]) || 0,
|
||||
fgMade: this.safeInt(values.eq(5).text().trim().split("/")[0]) || 0,
|
||||
fgAttempted:
|
||||
this.safeInt(values.eq(5).text().trim().split('/')[1]) || 0,
|
||||
this.safeInt(values.eq(5).text().trim().split("/")[1]) || 0,
|
||||
threePtMade:
|
||||
this.safeInt(values.eq(6).text().trim().split('/')[0]) || 0,
|
||||
this.safeInt(values.eq(6).text().trim().split("/")[0]) || 0,
|
||||
threePtAttempted:
|
||||
this.safeInt(values.eq(6).text().trim().split('/')[1]) || 0,
|
||||
ftMade: this.safeInt(values.eq(7).text().trim().split('/')[0]) || 0,
|
||||
this.safeInt(values.eq(6).text().trim().split("/")[1]) || 0,
|
||||
ftMade: this.safeInt(values.eq(7).text().trim().split("/")[0]) || 0,
|
||||
ftAttempted:
|
||||
this.safeInt(values.eq(7).text().trim().split('/')[1]) || 0,
|
||||
this.safeInt(values.eq(7).text().trim().split("/")[1]) || 0,
|
||||
fouls: this.safeInt(values.eq(8).text().trim()) || 0,
|
||||
blocks: this.safeInt(values.eq(9).text().trim()) || 0,
|
||||
steals: this.safeInt(values.eq(10).text().trim()) || 0,
|
||||
@@ -428,7 +428,7 @@ export class FeederScraperService {
|
||||
});
|
||||
|
||||
// Parse Team Totals from Footer
|
||||
const footerRow = $('.widget-basketball-match-box-score__footer td');
|
||||
const footerRow = $(".widget-basketball-match-box-score__footer td");
|
||||
let teamTotals: any = {};
|
||||
|
||||
if (footerRow.length > 5) {
|
||||
@@ -438,16 +438,16 @@ export class FeederScraperService {
|
||||
points: this.safeInt(footerRow.eq(2).text().trim()) || 0,
|
||||
rebounds: this.safeInt(footerRow.eq(3).text().trim()) || 0,
|
||||
assists: this.safeInt(footerRow.eq(4).text().trim()) || 0,
|
||||
fgMade: this.safeInt(footerRow.eq(5).text().trim().split('/')[0]) || 0,
|
||||
fgMade: this.safeInt(footerRow.eq(5).text().trim().split("/")[0]) || 0,
|
||||
fgAttempted:
|
||||
this.safeInt(footerRow.eq(5).text().trim().split('/')[1]) || 0,
|
||||
this.safeInt(footerRow.eq(5).text().trim().split("/")[1]) || 0,
|
||||
threePtMade:
|
||||
this.safeInt(footerRow.eq(6).text().trim().split('/')[0]) || 0,
|
||||
this.safeInt(footerRow.eq(6).text().trim().split("/")[0]) || 0,
|
||||
threePtAttempted:
|
||||
this.safeInt(footerRow.eq(6).text().trim().split('/')[1]) || 0,
|
||||
ftMade: this.safeInt(footerRow.eq(7).text().trim().split('/')[0]) || 0,
|
||||
this.safeInt(footerRow.eq(6).text().trim().split("/")[1]) || 0,
|
||||
ftMade: this.safeInt(footerRow.eq(7).text().trim().split("/")[0]) || 0,
|
||||
ftAttempted:
|
||||
this.safeInt(footerRow.eq(7).text().trim().split('/')[1]) || 0,
|
||||
this.safeInt(footerRow.eq(7).text().trim().split("/")[1]) || 0,
|
||||
fouls: this.safeInt(footerRow.eq(8).text().trim()) || 0,
|
||||
blocks: this.safeInt(footerRow.eq(9).text().trim()) || 0,
|
||||
steals: this.safeInt(footerRow.eq(10).text().trim()) || 0,
|
||||
@@ -474,11 +474,11 @@ export class FeederScraperService {
|
||||
// For HTML pages, we DON'T send X-Requested-With header
|
||||
const response = await this.axios.get(url, {
|
||||
headers: {
|
||||
'User-Agent': DEFAULT_HEADERS['User-Agent'],
|
||||
Referer: DEFAULT_HEADERS['Referer'],
|
||||
'Accept-Language': DEFAULT_HEADERS['Accept-Language'],
|
||||
"User-Agent": DEFAULT_HEADERS["User-Agent"],
|
||||
Referer: DEFAULT_HEADERS["Referer"],
|
||||
"Accept-Language": DEFAULT_HEADERS["Accept-Language"],
|
||||
Accept:
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
// NO X-Requested-With for HTML pages!
|
||||
},
|
||||
});
|
||||
@@ -507,8 +507,8 @@ export class FeederScraperService {
|
||||
const response = await this.axios.get(url, {
|
||||
params: { matchId },
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'User-Agent': DEFAULT_HEADERS['User-Agent'],
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"User-Agent": DEFAULT_HEADERS["User-Agent"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -532,12 +532,12 @@ export class FeederScraperService {
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const rows = $(
|
||||
'.widget-basketball-match-details-header__score-details tbody tr',
|
||||
".widget-basketball-match-details-header__score-details tbody tr",
|
||||
);
|
||||
if (rows.length < 2) return null;
|
||||
|
||||
const parseRow = (row: any) => {
|
||||
const cols = $(row).find('td');
|
||||
const cols = $(row).find("td");
|
||||
// Format: TeamName, Q1, Q2, Q3, Q4, Final
|
||||
// Values are inside .widget-basketball-match-details-header__score-part (just the quarter score)
|
||||
// or direct text if simple table.
|
||||
@@ -545,7 +545,7 @@ export class FeederScraperService {
|
||||
const getScore = (index: number) => {
|
||||
const cell = cols.eq(index);
|
||||
const part = cell.find(
|
||||
'.widget-basketball-match-details-header__score-part',
|
||||
".widget-basketball-match-details-header__score-part",
|
||||
);
|
||||
const val = part.length ? part.text() : cell.text();
|
||||
return this.safeInt(val.trim());
|
||||
@@ -580,10 +580,10 @@ export class FeederScraperService {
|
||||
|
||||
try {
|
||||
const response = await this.axios.get<IddaaMarketsHtmlResponse>(url, {
|
||||
params: { template: 'all' },
|
||||
params: { template: "all" },
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'User-Agent': DEFAULT_HEADERS['User-Agent'],
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"User-Agent": DEFAULT_HEADERS["User-Agent"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -602,7 +602,7 @@ export class FeederScraperService {
|
||||
|
||||
extractPlayerIdFromUrl(url: string | undefined): string | null {
|
||||
if (!url) return null;
|
||||
const parts = url.split('/');
|
||||
const parts = url.split("/");
|
||||
return parts[parts.length - 1] || null;
|
||||
}
|
||||
|
||||
@@ -620,12 +620,12 @@ export class FeederScraperService {
|
||||
try {
|
||||
const response = await this.axios.get(url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
Accept:
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
Referer: 'https://www.mackolik.com',
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
"Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
Referer: "https://www.mackolik.com",
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
@@ -652,24 +652,24 @@ export class FeederScraperService {
|
||||
$: cheerio.CheerioAPI,
|
||||
teamIndex: number,
|
||||
): SidelinedTeamData {
|
||||
const sidelinedWidgets = $('.widget-sidelined-players');
|
||||
const sidelinedWidgets = $(".widget-sidelined-players");
|
||||
|
||||
if (sidelinedWidgets.length <= teamIndex) {
|
||||
return { teamName: '', teamId: '', totalSidelined: 0, players: [] };
|
||||
return { teamName: "", teamId: "", totalSidelined: 0, players: [] };
|
||||
}
|
||||
|
||||
const widget = sidelinedWidgets.eq(teamIndex);
|
||||
|
||||
const teamCrest = widget.find('.widget-sidelined-players__header-crest');
|
||||
const teamCrestSrc = teamCrest.attr('src') || '';
|
||||
const teamId = teamCrestSrc.split('/').pop() || '';
|
||||
const teamCrest = widget.find(".widget-sidelined-players__header-crest");
|
||||
const teamCrestSrc = teamCrest.attr("src") || "";
|
||||
const teamId = teamCrestSrc.split("/").pop() || "";
|
||||
const teamName = widget
|
||||
.find('.widget-sidelined-players__header-text')
|
||||
.find(".widget-sidelined-players__header-text")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const players: SidelinedPlayer[] = [];
|
||||
widget.find('.widget-sidelined-players__item').each((_, element) => {
|
||||
widget.find(".widget-sidelined-players__item").each((_, element) => {
|
||||
const playerData = this._parsePlayerItem($, $(element));
|
||||
if (playerData) {
|
||||
players.push(playerData);
|
||||
@@ -689,44 +689,44 @@ export class FeederScraperService {
|
||||
$item: cheerio.Cheerio<any>,
|
||||
): SidelinedPlayer | null {
|
||||
try {
|
||||
const nameElem = $item.find('.widget-sidelined-players__name');
|
||||
const nameElem = $item.find(".widget-sidelined-players__name");
|
||||
const playerName = nameElem.text().trim();
|
||||
const playerUrl = nameElem.attr('href') || '';
|
||||
const playerId = playerUrl.split('/').pop() || '';
|
||||
const playerUrl = nameElem.attr("href") || "";
|
||||
const playerId = playerUrl.split("/").pop() || "";
|
||||
|
||||
const positionElem = $item.find('.widget-sidelined-players__position');
|
||||
const position = positionElem.attr('title') || '';
|
||||
const positionElem = $item.find(".widget-sidelined-players__position");
|
||||
const position = positionElem.attr("title") || "";
|
||||
const positionShort = positionElem.text().trim();
|
||||
|
||||
const reasonImg = $item.find('.widget-sidelined-players__reason img');
|
||||
const reasonIcon = reasonImg.attr('src') || '';
|
||||
const reasonImg = $item.find(".widget-sidelined-players__reason img");
|
||||
const reasonIcon = reasonImg.attr("src") || "";
|
||||
|
||||
const numbers = $item.find('.widget-sidelined-players__number');
|
||||
const numbers = $item.find(".widget-sidelined-players__number");
|
||||
// Use parseInt EXACTLY as in JS script (ignoring potential NaN for now, will handle via helper if needed but safer to stick to script logic first)
|
||||
const matchesMissedText =
|
||||
numbers.length > 0 ? numbers.eq(0).text().trim() : '';
|
||||
numbers.length > 0 ? numbers.eq(0).text().trim() : "";
|
||||
const matchesMissed = matchesMissedText
|
||||
? parseInt(matchesMissedText, 10)
|
||||
: null;
|
||||
|
||||
const averageText = numbers.length > 1 ? numbers.eq(1).text().trim() : '';
|
||||
const averageText = numbers.length > 1 ? numbers.eq(1).text().trim() : "";
|
||||
const average = averageText ? parseInt(averageText, 10) : null;
|
||||
|
||||
const description = $item
|
||||
.find('.widget-sidelined-players__value')
|
||||
.find(".widget-sidelined-players__value")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const type = reasonIcon.includes('shortage_1.png')
|
||||
? 'injury'
|
||||
: reasonIcon.includes('suspension')
|
||||
? 'suspension'
|
||||
: 'other';
|
||||
const type = reasonIcon.includes("shortage_1.png")
|
||||
? "injury"
|
||||
: reasonIcon.includes("suspension")
|
||||
? "suspension"
|
||||
: "other";
|
||||
|
||||
return {
|
||||
playerId,
|
||||
playerName,
|
||||
playerUrl: playerUrl.startsWith('http')
|
||||
playerUrl: playerUrl.startsWith("http")
|
||||
? playerUrl
|
||||
: `https://www.mackolik.com${playerUrl}`,
|
||||
position,
|
||||
@@ -735,7 +735,7 @@ export class FeederScraperService {
|
||||
description,
|
||||
matchesMissed: isNaN(matchesMissed as number) ? null : matchesMissed,
|
||||
average: isNaN(average as number) ? null : average,
|
||||
reasonIcon: reasonIcon.startsWith('http')
|
||||
reasonIcon: reasonIcon.startsWith("http")
|
||||
? reasonIcon
|
||||
: `https://www.mackolik.com${reasonIcon}`, // Keep safer URL construction but stick closer to logic
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Transforms raw API data into database-ready formats
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import * as cheerio from "cheerio";
|
||||
import {
|
||||
RawKeyEvent,
|
||||
TransformedEvent,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
GameStatsResponse,
|
||||
DbEventPayload,
|
||||
DbMarketPayload,
|
||||
} from './feeder.types';
|
||||
} from "./feeder.types";
|
||||
|
||||
@Injectable()
|
||||
export class FeederTransformerService {
|
||||
@@ -28,7 +28,7 @@ export class FeederTransformerService {
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================
|
||||
private safeString(value: any): string | null {
|
||||
return value === null || value === undefined || value === ''
|
||||
return value === null || value === undefined || value === ""
|
||||
? null
|
||||
: String(value);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export class FeederTransformerService {
|
||||
|
||||
private extractPlayerIdFromUrl(url: string | undefined): string | null {
|
||||
if (!url) return null;
|
||||
const parts = url.split('/');
|
||||
const parts = url.split("/");
|
||||
return parts[parts.length - 1] || null;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export class FeederTransformerService {
|
||||
matchId: string,
|
||||
): TransformedEvent[] {
|
||||
return rawEvents.map((e) => {
|
||||
const playerId = this.extractPlayerIdFromUrl(e.playerUrl) || '';
|
||||
const playerId = this.extractPlayerIdFromUrl(e.playerUrl) || "";
|
||||
const assistPlayerId = e.assistPlayerUrl
|
||||
? this.extractPlayerIdFromUrl(e.assistPlayerUrl)
|
||||
: null;
|
||||
@@ -68,16 +68,16 @@ export class FeederTransformerService {
|
||||
: null;
|
||||
|
||||
// Determine event type
|
||||
let eventType: 'goal' | 'card' | 'substitute' | 'other' = 'other';
|
||||
if (e.type === 'goal') eventType = 'goal';
|
||||
else if (e.type === 'card') eventType = 'card';
|
||||
else if (e.type === 'substitute') eventType = 'substitute';
|
||||
let eventType: "goal" | "card" | "substitute" | "other" = "other";
|
||||
if (e.type === "goal") eventType = "goal";
|
||||
else if (e.type === "card") eventType = "card";
|
||||
else if (e.type === "substitute") eventType = "substitute";
|
||||
|
||||
return {
|
||||
matchId,
|
||||
playerId,
|
||||
playerName: e.playerName,
|
||||
teamId: e.position === 'home' ? homeTeamId : awayTeamId,
|
||||
teamId: e.position === "home" ? homeTeamId : awayTeamId,
|
||||
eventType,
|
||||
eventSubtype: e.subType || null,
|
||||
timeMinute: e.timeMin,
|
||||
@@ -136,7 +136,7 @@ export class FeederTransformerService {
|
||||
// GAME STATS TRANSFORMER
|
||||
// ============================================
|
||||
transformGameStats(
|
||||
data: GameStatsResponse['data'] | null,
|
||||
data: GameStatsResponse["data"] | null,
|
||||
): TransformedMatchStats | null {
|
||||
if (!data || !data.home) return null;
|
||||
|
||||
@@ -173,20 +173,20 @@ export class FeederTransformerService {
|
||||
// MATCH STATE TO STATUS MAPPER
|
||||
// ============================================
|
||||
mapMatchStateToStatus(state: MatchState | undefined): string {
|
||||
if (!state) return 'NS';
|
||||
if (!state) return "NS";
|
||||
|
||||
switch (state) {
|
||||
case 'postGame':
|
||||
case 'post':
|
||||
return 'FT';
|
||||
case 'preGame':
|
||||
case 'pre':
|
||||
return 'NS';
|
||||
case 'live':
|
||||
case 'liveGame':
|
||||
return 'LIVE';
|
||||
case "postGame":
|
||||
case "post":
|
||||
return "FT";
|
||||
case "preGame":
|
||||
case "pre":
|
||||
return "NS";
|
||||
case "live":
|
||||
case "liveGame":
|
||||
return "LIVE";
|
||||
default:
|
||||
return 'NS';
|
||||
return "NS";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,28 +200,28 @@ export class FeederTransformerService {
|
||||
const officials: MatchOfficial[] = [];
|
||||
|
||||
// Try standard officials component
|
||||
$('.p0c-match-officials__official-list-item').each((_, elem) => {
|
||||
$(".p0c-match-officials__official-list-item").each((_, elem) => {
|
||||
const name = $(elem)
|
||||
.find('.p0c-match-officials__official-name')
|
||||
.find(".p0c-match-officials__official-name")
|
||||
.text()
|
||||
.trim();
|
||||
const role = $(elem)
|
||||
.find('.p0c-match-officials__official-group-title')
|
||||
.find(".p0c-match-officials__official-group-title")
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
if (name) {
|
||||
officials.push({ name, role: role || 'Referee' });
|
||||
officials.push({ name, role: role || "Referee" });
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback: look for referee info in match info section
|
||||
if (officials.length === 0) {
|
||||
// Try alternative selectors
|
||||
$('.widget-match-info__referee-name, .referee-name').each((_, elem) => {
|
||||
$(".widget-match-info__referee-name, .referee-name").each((_, elem) => {
|
||||
const name = $(elem).text().trim();
|
||||
if (name) {
|
||||
officials.push({ name, role: 'Referee' });
|
||||
officials.push({ name, role: "Referee" });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -331,8 +331,8 @@ export class FeederTransformerService {
|
||||
(
|
||||
e,
|
||||
): e is TransformedEvent & {
|
||||
eventType: 'goal' | 'card' | 'substitute';
|
||||
} => e.eventType !== 'other' && !!e.playerId,
|
||||
eventType: "goal" | "card" | "substitute";
|
||||
} => e.eventType !== "other" && !!e.playerId,
|
||||
)
|
||||
.map((e) => ({
|
||||
match_id: e.matchId,
|
||||
@@ -354,6 +354,6 @@ export class FeederTransformerService {
|
||||
// BASKETBALL PLAYER ID GENERATOR
|
||||
// ============================================
|
||||
generateBasketballPlayerId(teamId: string, playerName: string): string {
|
||||
return `${teamId}-${playerName.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`;
|
||||
return `${teamId}-${playerName.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase()}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* Feeder Module - Senior Level Implementation
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FeederService } from './feeder.service';
|
||||
import { FeederScraperService } from './feeder-scraper.service';
|
||||
import { FeederTransformerService } from './feeder-transformer.service';
|
||||
import { FeederPersistenceService } from './feeder-persistence.service';
|
||||
import { DatabaseModule } from '../../database/database.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { FeederService } from "./feeder.service";
|
||||
import { FeederScraperService } from "./feeder-scraper.service";
|
||||
import { FeederTransformerService } from "./feeder-transformer.service";
|
||||
import { FeederPersistenceService } from "./feeder-persistence.service";
|
||||
import { DatabaseModule } from "../../database/database.module";
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user