Ugrás a tartalomhoz
bence@portfolio:~$Bence Krisztián-Világos
← vissza a projektekhez

esettanulmány · AIbet / ML-validáció

Hogyan öltem meg a saját backtestemet

A foci-predikciós modellem backtestje +6,42%-os hozamot mutatott. Egy célzott mérés kimutatta, hogy ez az „él” teljes egészében adatszivárgásból jött: a modell a jövőből is látott adatot. Szivárgás nélkül +0,68% maradt, ami zajszint. Ez a történet arról szól, hogyan derült ki, és mit építettem, hogy ne fordulhasson elő újra.

backteszt-ROI, szivárgással → nélküle
+6,42% → +0,68%
értékelt meccs (walk-forward)
17 356
érintett feature
12
random seed, ugyanaz az eredmény
3×

$ cat lelet.md

A modell 60 feature-ből dolgozik. Ebből 12 (labdabirtoklás, lövések/90, védési arány, szabálytalanságok, hazai/vendég/különbség bontásban) egy fbref-táblából jön, ami csapatonként és szezononként egyetlen sort tartalmaz. Ez a sor viszont a teljes szezon összesítése — a szezon végén keletkezik.

eles_tipp_generalas.py:236
row = fbref_df[(fbref_df["Squad"] == team) & (fbref_df["season"] == season)]
if len(row):
    return row.iloc[0]

Vagyis egy októberi meccs előrejelzésekor a modell már „tudta”, hogyan teljesít a csapat májusig. A gördülő (rolling) statisztikák és a keret-változás feature-ök tiszták voltak — csak ez az egy illesztés szivárgott, és ez elég volt ahhoz, hogy az egész eredmény hamis legyen.

szezon-összesítés (a szezon végén)szivárgó út: a modell ezt is láttaaugszeptnovdecjanfebrmárcáprmájelőrejelzett meccs (okt)helyes: csak korábbi meccsek
A szivárgás mechanizmusa. Lila: amit a modell tanításkor látott. Cián: amit látnia szabad lett volna.

$ ./meres --szivargas=be|ki

A mérés négy változatot hasonlított össze ugyanazon a walk-forward kereten (2018/19–2022/23, szezononként újratanított modell, 17 356 meccs), ugyanazzal a döntési szabállyal, három különböző random seeddel. Kapcsold ki a szivárgást, és nézd, mi marad az élből.

a pontok fölé húzva: seedenkénti érték

+6,42%

V1 — azonos szezonos fbref (szivárgó)

átlag 3 seeden · ~2151 tét · logloss 0,96240

Ezt mutatta a backteszt: stabil, szép, mindhárom seeden megismételhető hozam. Pont ez volt a gyanús benne.

-2%0%+2%+4%+6%+8%V1 · szivárgó+6,42%V2 · éles állapot-0,86%V3 · fbref nélkül+0,68%V4 · as-of-date+0,64%
szivárgó tanításszivárgásmentes3 seed átlaga
táblázatként
változatseed 42seed 7seed 123átlagtétlogloss
V1 — azonos szezonos fbref (szivárgó)+6,09%+6,65%+6,52%+6,42%~21510,96240
V2 — előző szezonos fbref (amit az éles rendszer kapott)-0,75%-1,28%-0,54%-0,86%~13250,96668
V3 — a 12 fbref-feature nélkül+0,44%+0,23%+1,36%+0,68%~14460,96604
V4 — szezon-eddigi átlagok, csak múltbeli meccsekből-0,23%+1,36%+0,78%+0,64%~13920,96619

A lényeg nem a pontos számokban van — azok ±1%-on belül szórnak —, hanem az előjelváltásban: a szivárgó változat mindhárom seeden +6% fölött van, minden tiszta változat a nulla körül. Ez nem zaj, hanem mechanizmus.

$ cat train_serve_skew.md

A mérés egy másik rejtélyt is megoldott. Élesben a modell rosszabbul teljesített, mint a backtestben, és ezt eddig „modell-zsugorodásnak” gondoltam. Valójában az éles rendszer a folyó szezonra még nem kapott fbref-sort, ezért az előző szezon adatait használta — ez a V2 állapot, −0,86%-kal.

A modellt tehát a V1-ben tanítottam, és a V2-ben futtattam. Ez a klasszikus train/serve skew: nem a modell romlott el élesben, hanem a tanítás hazudott.

$ ls ./vedelem

A javítás nem egy sor törlése volt, hanem olyan szabályok, amik a következő hasonló hibát is elkapják:

  • xfail(strict=True) teszt

    Egy teszt kifejezetten a szivárgást mutatja ki. Amíg a hiba fennáll, „várt hibaként” fut; ha valaki megjavítja, a teszt piros lesz, amíg a jelölést tudatosan le nem veszik. A hiba nem tűnhet el csendben.

  • kötelező időablak

    A főkönyv csak egy kötelező ablak-paraméterrel olvasható, alapértelmezés nincs. Nem lehet „véletlenül” a teljes adatot betölteni.

  • fail-closed záró ár

    Záró árnak csak a kezdés előtti pillanatkép számít. Ha nincs ilyen, a sor kimarad, és nem egy később mentett (élő) árral pótlódik.

  • zárolt holdout + variáns-számláló

    Egy félretett, zárolt adathalmazhoz csak jóváhagyott körben lehet nyúlni, és minden kipróbált változat számít. 10 variáns fölött szigorúbb küszöb jön, 20 fölött a keresés leáll.

$ git log --agent

A mérést egy AI kódoló-agent (Claude Code) futtatta, előre rögzített szabályok között. Így dolgoztunk:

  1. 01Külön ágon mért, a keresési ablakon belül (2023-07-01 előtt); a zárolt holdouthoz nem nyúlt.
  2. 02A feladat mérés volt, nem javítás — a riportban külön „amit ez NEM állít” szakasz szerepel.
  3. 03Az állítást három seeddel ellenőrizte, mielőtt kimondta, hogy nem zaj.
  4. 04Az ágat én néztem át és mergeltem; a javítás külön döntés lett, dátummal rögzítve.
  5. 05Szabály maradt: agent kódot ír és tesztel, élesbe, pénzhez és a main ágra csak ember nyúl.

$ cat NEM_ALLITJA.md

  • Minden ROI Pinnacle nyers áron értendő, döntési szabállyal szűrt tétekre — nem általános modell-pontosság.
  • Egy hiperparaméter-készlet, három seed, öt szezon. Az előjelváltás robusztus, a ±1%-os különbségek nem.
  • Nem állítom, hogy a modell menthetetlen — csak azt, hogy a mostani feature-készlettel, szivárgás nélkül ezen a mintán nincs kimutatható él.
  • Ez kutatási projekt, nem tippszolgáltatás.

$ echo $TANULSAG

A szép backteszt gyanú, amíg a szivárgás-teszt nem zöld.

Ez azóta a projekt egyik alapszabálya. A legdrágább hiba nem az, amelyik elszáll, hanem az, amelyik csendben jó számokat ad.