← hub
# AUDYT: logika wyjścia + funkcja optymalizująca (QFL) — dokument do review
**Autor audytu:** Satoshi ⚡ (Opus) · **Data:** 2026-07-20 · **Kod audytowany:** napisany wcześniej (Fable)
**Powód:** paper uparcie ma **2× SL-rate** (36 % vs backtest 17 %), wszystkie warianty ujemne
**mimo** naprawy over-entry. Hipoteza Bartosza: „coś źle policzone — funkcja optymalizująca i
wyjście źle napisane". Ten dokument opisuje DOKŁADNIE, jak zakodowana jest każda część, i wskazuje
gdzie realnie jest rozjazd. Zasada: źródłem prawdy jest KOD, nie opis (fakt #897).
---
## ⚠️ KOREKTA (2026-07-20, PO ZŁOTYM STANDARDZIE) — CZYTAJ NAJPIERW
Zbudowałem złoty standard (`sq_replay_check.py`) — ten sam silnik, te same 610 tys. trade'ów,
jedyna różnica = model fillu. Wynik **OBALIŁ** moją hipotezę §3/§6 (fill marketable jako główną
przyczynę):
```
market (poprawny): SL 3.0% | trail 91.9% | WIN 83.7% | +0.594%/trade
level (bug): SL 4.2% | trail 90.6% | WIN 82.5% | +0.472%/trade
```
→ Fill marketable to realny, ale **MAŁY** bug (SL 3→4.2 %). **NIE tłumaczy** żywych 36 % SL / 57 % win.
**PRAWDZIWA PRZYCZYNA = KADENCJA EGZEKUCJI WYJŚĆ (nie logika, nie fill).**
Edge strategii to **trailing = 92 % wyjść** w backteście. Na żywo (qfl_paper_v2.db):
`trailing 38 % / SL 37 % / TP 25 %` vs backtest `trailing 92 % / SL 3 % / TP 4 %`.
Demon sprawdza wyjścia **co ~80–90 min** (loop spowolniony fetchowaniem 250 symboli + rate-limit),
a nie co 5 min — i patrzy tylko na 1 świecę (`k[-2]`), więc **GUBI ~17 z 18 świec 5m**, w tym te,
na których trailing wyszedłby na +1 %. Pominięty trailing → pozycja jedzie dalej → **SL**.
**Wniosek:** funkcja optymalizująca OK, formuły wyjścia OK, fill = drobiazg (naprawiony). **Realny
błąd: żywy egzekutor NIE URUCHAMIA sprawdzenia wyjścia z wymaganą częstotliwością (5 min).** Backtest
zakłada idealny trailing na KAŻDEJ świecy 5m; żywy demon tego nie dowozi. Fix = rozdzielić lekką
pętlę wyjść (co 5 min, tylko otwarte pozycje) od ciężkiego skanu 250 symboli; docelowo egzekucja
zleceniami stop po stronie giełdy / wspólny silnik VenueSim (zad. #5/#8). Sekcje §3/§6/§8 poniżej
zostają jako zapis toku audytu, ale ich ranking jest SKORYGOWANY tą sekcją.
---
## ✅ ROZWIĄZANIE DOCELOWE — WDROŻONE (2026-07-20, decyzja Bartosza: „docelowy, nie szybki")
Nie łatka (szybka pętla wyjść), tylko **wspólny silnik + replay pełnego strumienia**:
1. **`paper/exec_engine.py`** — JEDYNY autorytatywny silnik cyklu życia pozycji (fille marketable po
c0 / limity na przecięciu + SL→TP→trailing→time-stop, ~newbar, net liczony per-szczebel). Backtest
i live używają TEGO SAMEGO kodu porządku. Weryfikacja na 610 tys. zdarzeń (`verify_exec_engine.py`):
```
exec_engine : SL 3.6 | TP 3.6 | TRAIL 91.4 | WIN 83.9 | mean +0.640
s3 backtest : SL 3.0 | TP 3.6 | TRAIL 91.9 | WIN 83.7 | mean +0.594
```
→ zgodność wzorowa = **zero dryfu backtest↔live**.
2. **`qfl_paper.py::five_min` PRZEPISANY**: dla każdej otwartej pozycji **REPLAY pełnego strumienia
świec 5m OD OTWARCIA** przez `exec_engine` (klines `startTime = open+1h`, limit dobierany do wieku
pozycji). **Kadencja pollingu przestaje mieć znaczenie** — replay łapie KAŻDĄ świecę (w tym
trailing), choćby demon chodził co 80 min; `exit_ms` rekonstruowany do faktycznej świecy wyjścia.
To bezpośrednio usuwa przyczynę z sekcji KOREKTA (trailing 38 %→~92 % na żywo).
3. **Deploy:** restart demona z czystą bazą (stara zarchiwizowana) → re-pomiar zbieżności.
**Oczekiwanie:** live wróci do ~83 % win / SL ~3–4 % / trailing ~92 % (jak backtest). Dopiero to
domknie werdykt „QFL dochodowy".
4. **Etap docelowy-docelowy (następny):** ten sam `exec_engine` za zleceniami STOP po stronie giełdy
/ replay na realnym L2 (VenueSim, zad. #5/#8) — pełna realność egzekucji + poślizgu.
---
---
## 1. Łańcuch plików — kto co liczy
| plik | rola |
|---|---|
| `s2q_bases.py` | detektor baz QFL (kauzalny) → `q_events.parquet` (sym, t, dir, w, crack, base, touches) |
| `s3_trades_full.py` | **silnik backtestu zwycięskiej rodziny** (kotwica BAZA) → tabela trade'ów 2020–26 |
| `s4_walkforward_portfolio.py` | walk-forward 22 foldy + portfel → **źródło liczby +328 %/rok** |
| `s2_exit_optimizer.py`, `s2q_engine.py` | wcześniejsze optymalizatory wyjścia (jak dobrano config) |
| `s7_percoin_optimizer.py` | reoptymalizacja żywa + histereza → `s7_config_store.parquet` |
| `paper/qfl_paper.py` | **ŻYWY egzekutor**: `detect()` + `try_open()` + `five_min()` |
**Kluczowa obserwacja architektoniczna:** backtest (`s3`) i egzekutor live (`paper`) to **DWIE
NIEZALEŻNE implementacje** tego samego pomysłu. Optymalizacja dobiera config pod fille `s3`,
a paper wykonuje je INNYM kodem fillów. Jeśli te dwa kody się rozjeżdżają — optymalny config
przestaje być optymalny na żywo. To jest korzeń problemu (szczegóły §6).
---
## 2. Detekcja zdarzeń (crack bazy)
### 2.1 Backtest — `s2q_bases.py` (dokładnie)
- **Baza LONG** = ostatni POTWIERDZONY pivot low: `rolling(2w+1, center).min()`, potwierdzenie
`conf = pivot + w` barów później (**kauzalne, bez repaintu**). Short = lustro na pivot high.
- `seg = searchsorted(conf, bars) − 1` → który pivot jest „ostatnią potwierdzoną bazą" w danym barze.
- **Crack** = pierwsze `close < base×(1−crack%)` W SEGMENCIE życia bazy; **jeden crack na segment**,
po cracku czeka na NOWĄ bazę (bez zombie-poziomów).
- Osie: `w∈{12,24} × crack∈{1,2,3%}`; `touches` = ile razy poziom testowany ±0,5 % przed crackiem.
### 2.2 Paper — `qfl_paper.py::detect()`
- Liczone na **ostatnich 100 barach 1h**, `w∈{12,24}`, bazy + crack + ruch-%.
- **⚠ DO ZWERYFIKOWANIA (rozjazd #2):** czy `detect()` replikuje 1:1 „potwierdzony pivot +
jeden-crack-na-segment" z `s2q_bases`? Mój wcześniejszy fix (`base_last` po `conf_ms`) emuluje
„jeden crack na bazę", ale sama definicja pivota/potwierdzenia w oknie 100 barów może wybierać
INNE bazy niż pełny detektor na całej historii. Różne bazy → różne (gorsze) wejścia.
---
## 3. Wejście (drabinka) — TU JEST GŁÓWNY BUG
### 3.1 Kotwica i poziomy — ZGODNE
Oba: `levels = base × (1 − pdir·(f + k·3%)/100)`, 5 szczebli, s=3 %. ✔ identyczne.
### 3.2 Model FILLI — ROZJAZD #1 (główny podejrzany)
**Backtest `s3_trades_full.py` (linie 49, 59–68):**
```
c0 = close pierwszej świecy 5m po cracku
marketable = (levels >= c0) # poziom już „w pieniądzu" względem ceny
→ fill NATYCHMIAST po cenie c0 (TAKER) # bo limit-buy powyżej rynku = egzekucja po rynku
reszta (poziomy poniżej c0) → limit na przecięciu Lm<level (MAKER)
```
**Paper `qfl_paper.py::five_min()` (linie 202–204):**
```
for i, lvp in enumerate(levels):
if filled[i] is None and lo < lvp: # (long)
filled[i] = lvp # ← FILL PO CENIE POZIOMU, zawsze
avg = mean(filled) # zawsze MAKER na wejściu
```
**Brak logiki `marketable`/`c0`.** Paper zapisuje fill po cenie POZIOMU nawet dla szczebli, które
w chwili postawienia drabinki są JUŻ w pieniądzu (poziom powyżej bieżącej ceny dla longa).
### 3.3 Dlaczego to zawyża SL-rate (liczbowo, na przykładzie)
Long, baza=1.0, cena po cracku `c0=0.90`. Szczeble: 0.94, 0.91, 0.88, 0.85, 0.82.
Szczeble 0.94 i 0.91 są POWYŻEJ c0 (już ITM).
- **Backtest:** te dwa = marketable → fill po `c0=0.90` → **avg=0.90**.
- **Paper:** `lo(0.90) < 0.94` i `<0.91` → fill po 0.94 i 0.91 → **avg=0.925**.
Aggregat-SL = `avg×(1−8%)`:
- Backtest SL @ 0.90×0.92 = **0.828**
- Paper SL @ 0.925×0.92 = **0.851**
Cena spada (to nóż). **Paper wypada na SL przy 0.851; backtest przeżywa** (nie dosięga 0.828) i
później realizuje TP. Na każdym nożu, który zanurkuje ~do 0.84 i odbije: **paper = SL, backtest =
wygrana.** To DOKŁADNIE tłumaczy kierunek: 2× więcej SL i niższy win-rate na żywo.
> Realizm: limit-buy postawiony POWYŻEJ bieżącej ceny wypełnia się natychmiast po rynku (~c0),
> a nie po swojej cenie. Więc **backtest ma rację, paper ma buga** — zawyża cenę wejścia.
---
## 4. Wyjście — SL / TP / trailing / time-stop
### 4.1 Formuły — ZGODNE (backtest `s3` vs paper `five_min`)
| element | wzór (oba) |
|---|---|
| SL agregat | `lossa = (adverse/avg − pdir)·100`; jeśli `≤ −8` → exit `avg×(1−pdir·8%)`, TAKER |
| TP (baza) | `tppx = base×(1−pdir·tpd%)`; jeśli `fav` przebija tppx i tppx po stronie zysku → exit, MAKER |
| trailing | `(best − close·pdir) ≥ (1%)·(best·pdir)` i w zysku → exit close, TAKER |
| time-stop | po 72 h → exit close, TAKER |
### 4.2 Kolejność intra-bar — ZGODNA
Oba: **SL → TP → trailing → time-stop**, TP/trailing NIE w barze świeżego fillu (`~newbar`),
SL po cenie ADVERSE (low dla longa) = konserwatywnie.
**Wniosek §4:** formuły i kolejność WYJŚCIA są zgodne backtest↔paper. **Problem NIE jest w tym,
JAK liczymy wyjście — tylko w cenie WEJŚCIA (avg), która wchodzi do wzoru SL/TP.** Zawyżona avg
z §3 psuje wszystkie progi wyjścia naraz.
---
## 5. Funkcja optymalizująca — czy „źle napisana"?
### 5.1 Funkcja celu (s2 / s2q / s4)
```
score = mean_net(train) − 0.5·|p5_net(train)| # zysk minus pół lewego ogona
próg n_train ≥ 300 (lub 200 w s4)
selekcja: argmax(score) na TRAIN
werdykt: WYŁĄCZNIE kolumny OOS (test od 2025-07-01 / fold testowy)
```
### 5.2 Walk-forward (s4)
train 365 d → test 90 d, krok 90 d; `pick()` osobno long/short per fold; portfel: stały kapitał,
`ret = LEV·net/N_SLOTS`, 40 slotów, breaker dzienny −6 %. Bez składania (anty-miraż).
### 5.3 Reoptymalizacja żywa (s7): histereza 5 % (nowy config musi bić stary o 5 %).
### 5.4 OCENA funkcji optymalizującej
**Metodologicznie POPRAWNA:** selekcja na train / werdykt OOS, kara za lewy ogon (`−0.5·|p5|`),
próg liczności, brak składania, konserwatywne fille w silniku na którym optymalizuje. **To NIE
jest źródło rozjazdu.** Optymalizator uczciwie dobrał config pod fille `s3`. Problem w tym, że
**paper wykonuje inne fille niż `s3`** (§3), więc dobrany config nie przenosi się na żywo.
> Czyli: nie „funkcja optymalizująca policzyła źle", tylko „optymalizowała na modelu egzekucji A,
> a żywy bot egzekwuje modelem B (z bugiem)". Klasyczny rozjazd backtest↔live.
---
## 6. Rozjazdy — ranking (audyt)
| # | rozjazd | efekt | pewność |
|---|---|---|---|
| **1** | **paper fill po cenie POZIOMU zamiast po rynku (c0) dla szczebli ITM** (§3) | zawyża avg → SL-rate ↑, win ↓ | **wysoka** (kierunek policzony) |
| 2 | detektor baz `detect()` (100 barów) vs `s2q_bases` (pełny, potwierdzony pivot) | inne/słabsze bazy → gorsze wejścia | do zweryfikowania |
| 3 | timing: paper bierze 1 świecę/cykl (`k[-2]`); gdy cykl >5 min lub demon dławi się na fapi → GUBI świece → gubi fille i wyjścia | dryf vs backtest (który widzi każdą świecę) | średnia |
| 4 | paper: wejście zawsze MAKER; backtest: marketable = TAKER | drobny, przeciwny kierunek (paper zaniża koszt) | niska waga |
**Czy backtest +328 % to miraż?** W samym backteście NIE widać oczywistego look-ahead: SL przed TP,
fille konserwatywne (marketable po gorszym c0), werdykt OOS. Bug jest po stronie PAPERA (§3), i jest
w kierunku „paper gorszy niż backtest" — więc +328 % nie jest obalone. ALE **pewny werdykt dopiero
po naprawie #1 i re-pomiarze** — dopóki paper nie zbiegnie się z backtestem, liczbie nie ufamy.
---
## 7. Rekomendacja napraw (kolejność)
1. **Napraw #1 (fill marketable) w paperze** — przy otwarciu drabinki: szczeble już ITM względem
ceny bieżącej fillują NATYCHMIAST po rynku (≈c0), taker; reszta zostaje limitem. 1:1 z `s3`.
2. **Zweryfikuj #2** — puść `detect()` i `s2q_bases` na tych samych klinach, porównaj wybrane bazy.
3. **Twardy dowód (złoty standard):** offline replay TYCH SAMYCH `q_events` przez `s3` i przez
silnik papera — net per trade powinien być ~równy. Rozjazd = miara buga. (To jest kontrola,
której brakowało: jeden wspólny silnik egzekucji dla backtestu i live — patrz VenueSim/SimExecutor,
zadania #5/#8, już zbudowane dla Darwina.)
4. Po naprawie: restart turnieju, obserwacja zbieżności (win-rate ma wrócić ~83 %).
## 8. Jedno zdanie werdyktu
**Funkcja optymalizująca jest napisana dobrze; formuły wyjścia są zgodne; realny błąd to model
FILLI wejścia w żywym paperze (szczeble ITM fillowane po cenie poziomu zamiast po rynku), co
zawyża średnią cenę wejścia i wywala pozycje na SL tam, gdzie backtest przeżywa.** To jest
naprawialne i wyjaśnia cały kierunek rozjazdu — ale werdykt „QFL dochodowy" zawieszony do
re-pomiaru po naprawie.