Skip to content

Latest commit

 

History

History
315 lines (234 loc) · 13.7 KB

File metadata and controls

315 lines (234 loc) · 13.7 KB

SARSA BOT

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.

Sharpe po ziarnach


Wynik

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

Kapitał out-of-sample

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.

Wniosek

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ć.


Droga do tego wyniku

Każdy etap został obalony przez następny:

Etapy badania

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).

Turnover a zwrot


Struktura

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.

Uruchomienie

1. Dane

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 uniwersum

Konfiguracja 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.sh

Wymaga 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.sh

Format 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.

2. Trening i walidacja

./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.npz

Nigdy 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.

3. Wykresy

docker run --rm -v "$PWD/reports:/app/reports" -e PYTHONPATH=/app/src \
  -e MPLCONFIGDIR=/tmp aibot python src/plots.py

Jak to działa

Algorytm

Czysty 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).

Portfel

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.

Stan

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.

Fundamenty

--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 VALUE7JPM 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 8

Ostrzeż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.

Nagroda

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.

Higiena backtestu

  • cecha w barze t używa danych do t włącznie; nagroda z t → 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_YEAR ustawiane z --bar-tag — inaczej Sharpe dla dziennych byłby zawyżony 2.5×

Ograniczenia

  • 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.

Wnioski

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.