Agent tradingowy uczony SARSA(λ) — TD(λ) na Q(s,a) z eligibility traces i liniową aproksymacją — testowany na dziennych danych z Interactive Brokers.
Wynik badania jest negatywny i to jest jego główna teza. Najlepsza wersja agenta osiągnęła Sharpe +0.58 w walidacji walk-forward na 6 latach out-of-sample, po czym ta sama polityka, puszczona bez żadnych zmian na inne uniwersum, dała −0.32. Szczegóły niżej.
Ta sama polityka, te same hiperparametry, ta sama procedura walk-forward — dwa uniwersa:
| MAG7 | ETF'Y | |
|---|---|---|
| Sharpe (6 lat OOS) | +0.58 | −0.32 |
| zwrot | +81.5% | −14.7% |
| ziarna dodatnie | 8/8 | 1/8 |
| zyskowne okna | 79% | 40% |
| beta | +0.08 | +0.01 |
Bieg kontrolny na ETF z --cost-bps 0 dał Sharpe −0.36, czyli sygnału brak
także brutto — koszty transakcyjne nie były przyczyną porażki.
Agent nie nauczył się ogólnej reguły o zachowaniu cen względnych. Nauczył się czegoś specyficznego dla siedmiu spółek technologicznych z lat 2016–2026 — mimo poprawnie przeprowadzonej walidacji kroczącej.
Walk-forward chroni przed dopasowaniem do OKRESU, ale nie do UNIWERSUM. To najważniejsza lekcja. Sharpe 0.58 przetrwał sześć rozłącznych okien czasowych i i tak okazał się artefaktem.
Jedyne, co przeniosło się bezbłędnie, to neutralność rynkowa (beta +0.01) — bo jest własnością strukturalną konstrukcji portfela, a nie czymś, czego agent musiał się nauczyć.
Każdy etap został obalony przez następny:
| etap | wynik | co go obaliło |
|---|---|---|
| market timing (gotówka/pozycja) | agent nie handluje wcale | koszty > sygnał |
| rotacja, norm. czasowa | Sharpe 0.44, zwrot jak baseline | rozrzut 62 pp po ziarnach |
| rotacja, norm. przekrojowa | 0/9 ziaren bije baseline | usuwa czynnik rynkowy |
| long/short, rebalans dzienny | −10.3%, 2/9 dodatnich | turnover (korelacja −0.86) |
| long/short, rebalans miesięczny | +12.8%, 8/9 dodatnich | tylko jeden okres testowy |
| walk-forward, MAG7 | Sharpe +0.58 | tylko jedno uniwersum |
| walk-forward, ETF | Sharpe −0.32 | — |
Najbardziej diagnostyczny moment: przy rebalansie dziennym korelacja między turnoverem a zwrotem wyniosła −0.86. To wskazało kosztom rolę głównego sprawcy i doprowadziło do rebalansu miesięcznego — jedynej zmiany w całym projekcie, która dała istotną poprawę (+23 pp, t = 3.28).
src/download_historical.py pobieranie cen z IBKR -> data/*_1day.parquet
src/download_fundamentals.py pobieranie sprawozdań z IBKR -> data/*_fundamentals.parquet
src/download_edgar.py to samo z SEC EDGAR (darmowe, bez subskrypcji)
src/fundamentals.py parser XML + cechy fundamentalne (point-in-time)
src/train_td_lambda.py agent SARSA(λ), środowisko, metryki
src/walk_forward.py walidacja krocząca
src/plots.py wykresy -> reports/*.png
scripts/run.sh pobranie cen (wymaga TWS)
scripts/fundamentals.sh pobranie sprawozdań (wymaga TWS + subskrypcji)
scripts/edgar.sh pobranie sprawozdań z EDGAR (wymaga tylko SEC_UA)
scripts/train.sh pojedynczy trening
scripts/seed_sweep.sh ten sam trening na wielu ziarnach
docker/Dockerfile środowisko
reports/ wyniki i wykresy
data/, models/ dane i wagi (poza gitem)
Wszystko działa w kontenerze — na hoście potrzebny tylko Docker.
Wymaga uruchomionego TWS albo IB Gateway z włączonym API
(Global Configuration → API → Settings → Enable ActiveX and Socket Clients,
127.0.0.1 na liście Trusted IPs).
./scripts/run.sh # MAG7, dzienne, 5 lat
TICKERS="XLK XLF XLE" YEARS_BACK=10 ./scripts/run.sh # inne uniwersumKonfiguracja przez env: TICKERS, BAR_SIZE, YEARS_BACK, IB_HOST,
IB_PORT, IB_CLIENT_ID. Kontener startuje z --network host, więc widzi
TWS na 127.0.0.1.
Przy barach dziennych pobieranie trwa ~1 min na instrument (10 s pauzy między requestami chroni przed błędem 162 — pacing violation).
Sprawozdania finansowe (opcjonalne, patrz Fundamenty) — jeden request na spółkę, więc szybko:
./scripts/fundamentals.sh # MAG7
TICKERS="JPM XOM JNJ PG KO WMT CAT" ./scripts/fundamentals.shWymaga subskrypcji Reuters Worldwide Fundamentals na koncie IBKR — bez niej request wraca pusty. ETF-y sprawozdań nie mają i mieć nie będą.
Bez subskrypcji (i bez TWS) to samo daje SEC EDGAR — darmowe API XBRL, historia od ~2009, wymaga tylko kontaktu w nagłówku User-Agent:
SEC_UA="imie nazwisko mail@example.com" ./scripts/edgar.sh
TICKERS="JPM XOM JNJ PG KO WMT CAT" SEC_UA="..." ./scripts/edgar.shFormat wyjściowy jest identyczny, więc reszta pipeline'u nie odróżnia źródeł.
EDGAR ma dodatkowo dwie przewagi na higienę backtestu: filed to faktyczna
data złożenia do SEC (nie przybliżenie „koniec okresu + 45 dni"), a z wersji
faktu bierzemy najwcześniej złożoną — tę, którą rynek widział wtedy, bez
korekt naniesionych po latach.
./scripts/train.sh # pojedynczy trening + raport
./scripts/seed_sweep.sh # 8 ziaren, rozkład wyników
docker run --rm -v "$PWD/data:/app/data:ro" -v "$PWD/reports:/app/reports" \
-e PYTHONPATH=/app/src aibot \
python src/walk_forward.py --seeds 1 2 3 4 5 6 7 8 --save reports/wf.npzNigdy nie oceniaj strategii po jednym biegu. Pojedyncze ziarno na pojedynczym podziale dało w tym projekcie wyniki od −15% do +47% przy identycznych danych i parametrach.
docker run --rm -v "$PWD/reports:/app/reports" -e PYTHONPATH=/app/src \
-e MPLCONFIGDIR=/tmp aibot python src/plots.pyCzysty TD(λ) uczy V(s) — to metoda predykcji. Do wyboru akcji potrzeba
Q(s,a), więc używamy wersji sterującej: on-policy SARSA(λ) z replacing
traces. Off-policy Q(λ) z aproksymacją liniową bywa rozbieżne.
δ = r + γ·Q(s',a') − Q(s,a)
e ← γλ·e ; e ← φ(s,a)
w ← w + α·δ·e
Q(s,a) = w · φ(s,a) — jeden wspólny wektor wag i cechy zależne od akcji.
Dzięki temu polityka ocenia spółki po ich cechach, a nie po pozycji w liście,
i da się ją przenieść na inne uniwersum (co właśnie zrobiliśmy).
k spółek długo, k krótko, wagi ±0.5/k. Dollar-neutral, ekspozycja brutto
1.0 (bez lewara, porównywalna z koszykiem long-only). Dla 7 spółek i k=2
daje to 210 dopuszczalnych portfeli, enumerowanych wprost.
Cechy portfela to ważona różnica cech strony długiej i krótkiej — to na niej zarabia strategia market-neutral — plus turnover (kara za obrót) i bias.
Dziewięć cech technicznych, z-score przekrojowy w obrębie bara (--norm cross),
przycięty do ±5σ: zwroty 1/5/20, dwa stosunki do SMA, RSI Wildera, zmienność
zrealizowana, zakres H-L, wolumen względny.
Z włączonymi fundamentami dochodzi dziewięć cech ze sprawozdań, normalizowanych tak samo — patrz niżej.
--fundamentals {auto,on,off} (domyślnie auto: włącz, gdy komplet plików
data/*_fundamentals.parquet jest na dysku). Dziewięć cech, liczonych z sum
kroczących po czterech kwartałach (TTM):
| cecha | sens |
|---|---|
earnings_yield, sales_yield, book_yield, fcf_yield |
wycena (odwrotności C/Z, C/S, C/WK) |
roe, gross_margin |
jakość |
rev_growth_yoy, earnings_growth_yoy |
dynamika r/r |
debt_to_equity |
dźwignia |
Dwie decyzje projektowe, które są tu istotniejsze niż dobór wskaźników:
Point-in-time. Sprawozdanie wiążemy z barami po dacie publikacji
(<Source Date> z XML-a IBKR), nie po dacie końca okresu — między nimi jest
30–45 dni, w których rynek wyniku jeszcze nie znał. Sklejanie po końcu okresu
to najprostszy sposób na wyprodukowanie fałszywego Sharpe'a z fundamentów.
Gdy IBKR nie poda daty publikacji, przyjmujemy koniec okresu + 45 dni (termin
ustawowy 10-Q), czyli celowo za późno, nigdy za wcześnie.
Tylko ilorazy. Żadnych poziomów w dolarach. Przychód 100 mld nic nie znaczy w rankingu przekrojowym, przychód do kapitalizacji — znaczy. Poziomy nie przeniosłyby się na inne uniwersum, a przenośność jest tu badaną własnością.
Cechy wygasają po 200 dniach bez nowego raportu (MAX_REPORT_AGE_DAYS), a bary
bez kompletu cech wypadają z osi czasu — dynamiki r/r wymagają ośmiu kwartałów,
więc początek historii się skraca. Lepiej krótsza próbka niż cecha policzona
z niczego.
Uniwersum kontrolne. ETF-y przestały nadawać się do testu przenośności
(nie mają sprawozdań), więc ich rolę przejmuje VALUE7 — JPM XOM JNJ PG KO WMT CAT, stara gospodarka o zupełnie innym profilu fundamentów niż MAG7:
python src/walk_forward.py --universe value7 --seeds 1 2 3 4 5 6 7 8Ostrzeżenie z głównego wyniku obowiązuje dalej: nowe cechy nie unieważniają
lekcji o dopasowaniu do uniwersum. Sharpe na MAG7 z fundamentami znaczy tyle
samo co bez nich, dopóki nie przeżyje value7.
Wynik: fundamenty pogarszają. Test parowany — te same okna walk-forward, te same ziarna, identyczna oś czasu (zweryfikowana co do dnia), jedyna różnica to obecność cech fundamentalnych w stanie:
| MAG7 | VALUE7 | |
|---|---|---|
| Sharpe, same ceny | +0.58 (8/8) | +0.08 (5/8) |
| Sharpe, ceny + fundamenty | +0.16 (5/8) | −0.71 (1/8) |
| Δ zwrotu na okno | −6.7% (t=−0.72) | −7.8% (t=−2.57) |
Na MAG7 pogorszenie jest nieistotne, na VALUE7 — istotne statystycznie. Cechy fundamentalne nie tylko nie dały przewagi przenośnej między uniwersami, ale aktywnie szkodzą: przy 36 decyzjach na epokę i kilkunastu wagach więcej liniowa aproksymacja dostała dodatkową powierzchnię do dopasowania się do szumu. To spójne z wnioskiem głównym — pojemniejszy model dopasowuje się dokładniej do szumu — tyle że tym razem pojemność dołożyły dane, nie sieć.
Zastrzeżenie do VALUE7: XOM nie ma w XBRL terminowych kwartalnych przychodów sprzed 2018 (istnieją tylko sumy roczne; kwartały pojawiły się rok później jako kolumny porównawcze), więc próg pokrycia wyciął z tego uniwersum cechy sprzedażowe — test biegł na 4 cechach zamiast 6.
r = Σ log(1 + w·zwrot_prosty) − koszt·turnover
Składanie na zwrotach prostych, nie logarytmicznych: przy pozycjach krótkich średnia ważona log-zwrotów nie jest zwrotem portfela.
- cecha w barze
tużywa danych dotwłącznie; nagroda zt → t+1 - statystyki normalizacji liczone wyłącznie na części treningowej każdego okna z osobna
- każde okno walk-forward dostaje świeżego agenta (wagi od zera)
- ewaluacja zawsze greedy (ε = 0), zawsze obok baselinu
BARS_PER_YEARustawiane z--bar-tag— inaczej Sharpe dla dziennych byłby zawyżony 2.5×
- Egzekucja idealna: ceny zamknięcia, bez poślizgu, spreadu i wpływu na rynek.
- Brak kosztu pożyczki akcji przy krótkiej sprzedaży — realnie 0.5–2% rocznie, co zjadłoby sporą część wyniku.
- Restatements: sprawozdania z IBKR to wersja bieżąca. Jeśli spółka skorygowała dane po latach, backtest widzi korektę, a rynek w tamtym momencie widział wersję pierwotną. Data publikacji chroni przed jednym rodzajem wyprzedzania informacji, przed tym — nie.
- Survivorship bias w MAG7: to spółki wybrane po fakcie jako zwycięzcy dekady.
- Krótka próbka: 6 lat OOS daje błąd standardowy Sharpe'a ±0.41. Nawet wynik +0.58 miał t = 1.42, czyli był nieistotny statystycznie.
- Ziarna losowe nie są niezależnymi obserwacjami rynku — mówią o stabilności uczenia, nie o istotności wyniku.
Nie ma sensu próbować innych cech technicznych, innego k ani głębszej sieci
zamiast aproksymacji liniowej — wszystkie te warianty czerpią z tego samego
źródła (historia cen i wolumenu), a właśnie pokazaliśmy, że nie ma w nim
przewagi możliwej do wydobycia przy realistycznych kosztach. Pojemniejszy model dopasuje się dokładniej do szumu.
Sensowna kontynuacja wymaga nowego źródła informacji: danych fundamentalnych, rewizji prognoz analityków, przepływów zleceń albo mikrostruktury rynku.
Wartością, która zostaje, jest infrastruktura do rzetelnego obalania hipotez tradingowych: sweep po ziarnach, walk-forward i test przenośności na obcym uniwersum.



