--- knowledge_schema: 1 id: object/47b8029c-5874-e73f-0279-d335785b359a type: arch role: module representation: canonical state: current derived_from: object/302e9929-e186-c930-2406-ad4a8f17c6fd _organized: true --- # Modul-12: Backtesting **Status: FREIGEGEBEN** · Container `Modul-12-Backtesting` · Image `backtesting:0.1.2` (V1 + V1.1 parallel) ## Zweck Deterministische, reproduzierbare **historische Strategietests** auf echten OHLCV-Daten aus **Modul-03** (PostgreSQL). Gleiche Strategie-Logik wie **Modul-05** (`trend_pullback_v1`) und identische Regime-Engine wie **Modul-04** — der Backtest entscheidet exakt so wie Live/Paper. **V1 ohne KI/ML, ohne Live-Orders, ohne Broker-Ausführung.** Modular erweiterbar für weitere Strategien (Breakout, ORB, Mean-Reversion) über gemeinsame `Strategy`-Schnittstelle + Registry. ## Architektur / Datenfluss ``` OHLCV (Modul-01 PostgreSQL, Tabelle ohlcv) │ synchron, direkt nach Timestamp/Zeitraum (KEIN /history?limit) ▼ Modul-12-Backtesting ── POST /backtest │ ├── core/engine.py deterministische Backtest-Loop (kein Lookahead) ├── strategies/ trend_pullback_v1 (1:1 zu Modul-05) ├── regime/engine.py Regime-Engine (1:1 zu Modul-04) ├── storage/storage.py Persistenz backtest_run/trade/equity └── api/main.py REST-API (intern, Port 55012) API (Docker-intern, Port 55012): /health, /health/ready POST /backtest Backtest starten (synchron, returns Ergebnis) GET /backtest/{run_id} Run-Metadaten GET /backtest/{run_id}/status Status + run_hash/data_hash GET /backtest/{run_id}/trades gespeicherte Trades GET /backtest/{run_id}/equity Equity-Kurve GET /backtest alle Runs POST /backtest/{run_id}/rebuild (idempotenter Rebuild aus DB) GET /strategies registrierte Strategien + Versionen ``` Kein RabbitMQ: Backtest ist ein synchroner, request/response-Aufruf. Port `55012` ist **nur intern** (`expose:`), kein öffentliches Port-Mapping. ## Datenquelle - OHLCV direkt aus PostgreSQL (`ohlcv`), Spalten `provider, symbol, timeframe, ts, open, high, low, close, volume`. - Abfrage **nach Timestamp/Zeitraum** (`start_date`..`end_date`), nicht via `/history?limit=N` — vollständige, deterministische Datenbasis. - Mindestanzahl Candles: `min_candles_required` (default 220), sonst Fehler. ## Bias-Schutz / Realismus (kein Lookahead) - **Entry am OPEN der Folgewandle** nach dem Signal (nie zum bekannten Signal-Close). - **Regime & Strategie** werden nur auf **abgeschlossenen** Candles (bis Candle i) ausgewertet — keine zukünftige Information. - **Intrabar Stop/Target konservativ**: Stop wird VOR Target geprüft (kein Lookahead durch Intrabar-Reihenfolge). Falls beide in einer Candle getroffen, gewinnt Stop (Worst-Case). - **Gap-Handling**: Überspringt der Open den Stop/Target, Fill zum Gap-Open (realistisch schlechter). - **Gebühren/Spread/Slippage** werden auf Entry UND Exit angewendet (verschlechtern den Fill). - **R-Multiple** bezieht sich auf das **Geldrisiko** `qty * |entry − stop|` (nicht Preisdifferenz) — `expectancy_r` daher korrekt (vorheriger Bug: 31431 → korrekt 2.0). ## Eingaben (POST /backtest) - Symbol, Asset-Klasse, Provider, Timeframe, Start-/Enddatum - Strategy + Version (`trend_pullback_v1`, default `1.0.0`) - Initiales Kapital, Risk % (Risk-basierte Positionsgröße) - Gebühren (fix + %), Spread (Preispunkte), Slippage (Preispunkte) - Optionale Limits: `max_positions`, `max_qty` ## Eigene Tabellen (Migration `migrations/001_backtest.sql`) - `backtest_run` — Run-Metadaten: run_id, strategy+version, symbol, timeframe, status (COMPLETED/…), data_hash, run_hash, Parameter (JSON), Datenzeitraum. - `backtest_trade` — einzelne Trades: direction (LONG/SHORT), qty, entry/exit-Preis, entry/exit_ts, signal_ts, reason (target/stop/force_close), regime, gross_pnl, fee, pnl, r (R-Multiple), strategy_version. - `backtest_equity` — Equity-Kurve (Zeitstempel, Equity, offene Positionen). Jede Tabelle FK auf `backtest_run(run_id) ON DELETE CASCADE`. ## Determinismus / Reproduzierbarkeit - Eindeutige `run_id` + deterministischer `run_hash` + `data_hash`. - `run_hash`: Hash über Strategy+Version+Parameter+Datenzeitraum+data_hash. - `data_hash`: Hash über die geladenen OHLCV-Daten. - **Gleiche Daten + gleiche Version + gleiche Parameter → identische Trades, Equity, Kennzahlen und identischer run_hash** (E2E-verifiziert). ## Tests - `tests/test_backtest.py`: 12/12 grün (deterministisch, LONG/SHORT, fees, slippage, lookahead, stop/target, gaps, unzureichende Daten, Strategy-Version/Parameter, Rebuild-Identität). - `tests/fixtures.py`: deterministische Fixture-Factory. - `tests/m12_fixtures.py`: erzeugt E2E-Fixtures (BT_PULL LONG, BT_PULL_S SHORT) für die VPS-ohlcv-Tabelle. - `tests/determinism_check.py` / `determinism_diff.py`: E2E-Determinismus-Nachweis (identische Trades/Equity/Kennzahlen/Hashes). ## E2E-Verifikation (VPS, 20.08.2026) - **LONG** (BT_PULL, TREND_UP): 1 Trade, winrate 1.0, expectancy_r 1.94, net_pnl 196.59, max_drawdown 0.0001, r=1.94, reason=target. - **SHORT** (BT_PULL_S, TREND_DOWN): 1 Trade, short_count 1, net_pnl 194.96. - `signal_ts 15:00 → entry_ts 16:00` (Einstieg am Open der Folgewandle — Lookahead praktisch verifiziert). - run_hash + data_hash gesetzt (Beispiel: `bc6e2553…` / `d76a7549…`). - **Determinismus**: zwei identische Backtests → identischer run_hash, data_hash, Metrics, Trades (normalisiert), Equity-Curve. - **Stop/Target/Gap**: Engine-`_evaluate_exit` geprüft — Gap-Down unter Stop → Fill zum Gap-Open; Target bei high≥target; Stop vor Target (konservativ); SHORT Gap-Up → Fill zum Gap-Open. - Health `/health` 200, `/health/ready` 200; keine öffentlichen Ports. ## Compose / Betrieb - Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Port 55012 nur intern (`expose`), `restart: unless-stopped`. Kein öffentliches Port-Mapping. ## Nächste Module - **Modul-13: Optimization** — Image `optimization:0.1.0`, E2E-verifiziert (12/12 Punkte), **FREIGEGEBEN** (20.08.2026). Details: `modul-13-optimization.md`. - **V1.1-Strategie**: `trend_pullback_v1.1` (Image `backtesting:0.1.2`) — Spezifikation und Parameter: `trend-pullback-v1.1-spec.md`. --- ## Phase 10e — Intrabar Execution + Gap Execution Realism (23.08.2026) Versionierung der Gap-/Intrabar-Semantik (KEIN separates `gap_execution_version`): - Träger: `intrabar_policy` + `intrabar_policy_version = "intrabar_policy_v1"` im `ExecutionContext` (`shared/historical/execution_context.py`). Dadurch ist die neue Semantik eindeutig versioniert und ändert den V2-`run_hash` bei Policy-Wechsel (PESSIMISTIC ≠ OPTIMISTIC → anderer run_hash). - Legacy-Pfad bleibt unangetastet (PESSIMISTIC-Default, identisches Verhalten). Intrabar-Policy (nur V2/BID_ASK): `PESSIMISTIC` | `OPTIMISTIC` | `STOP_FIRST` | `TARGET_FIRST` | `TICK_RESOLUTION` | `UNKNOWN`. **Fail-closed** §18: `UNKNOWN`/ `TICK_RESOLUTION`/`BOGUS` → `ValueError`/`NotImplementedError`, kein Auto-Default. Gap-Semantik (deterministisch, kein Lookahead): - LONG: Stop/Target/Exit auf **BID**; SHORT auf **ASK**. - Gap-Stop → Fill zum Gap-Open (schlechter, NICHT auf Stop-Level geclampt). - Target-Gap → Fill zum besseren Gap-Open (nicht geclampt auf Target). - Intrabar-Target füllt auf `target` (nicht low/high); Intrabar-Ambiguity (Stop UND Target in einer Bar) per Policy (PESSIMISTIC→Stop, OPTIMISTIC→Target). - Slippage wird adversial auf den tatsächlichen Fill (Trigger ≠ Market ≠ Final Fill bei Gap + Slippage); Commission auf finalem Fill (rate × qty × fill pro Seite). - **Kein Doppelspread/Slippage/Commission** (Vier-Ebenen-Trennung 10b/10c/10d/10e). Audit (18 Felder, forensisch rekonstruierbar): `reason`, `intrabar_policy`, `trigger_price`, `market_execution_price`, `exit_fill_price`, `gap_execution`, `gap_open_price`, `execution_model`, `price_basis`, `spread_model`, `slippage_model`, `slippage_value`, `cost_model`, `entry_fee`, `exit_fee`, `gross_pnl`, `net_pnl` (verwendete vorhandene Feldnamen, keine künstlichen). Verifikation 23.08.2026: - Unit-Suite `test_phase10e_intrabar_gap.py`: 40/40 grün (Intrabar, Gap, Slippage nach Gap, Commission auf Final Fill, BID/ASK-Seiten, Doppelzählung, Audit-18, pathologische Fälle, 4 numerische PnL-E2E-Beispiele). - Fixture-E2E (`test_phase10e_fixture_e2e.py`): 7/7 grün (A LONG Stop-Gap, B SHORT Stop-Gap, C Intrabar PESSIMISTIC, D Intrabar OPTIMISTIC run_hash-Differenz, E LONG Target-Gap, F SHORT Target-Gap, ENV-RESET); numerische Kette Trigger → Gap-Open → Slippage → Final Fill → Commission → Gross → Net nachgewiesen. - Regression Lauf 1 + Lauf 2: vollständig grün (M12 169 inkl. 10e, Phase5 20, Phase6 14, Phase7 A–J Legacy-Hash `8a5760ae…`, M13 Shared A–J, M13 Compat 5/5). - Legacy Production Smoke 2×: A==B==VORHER (run_hash `bc6e2553…`, data_hash `d76a7549…`, net_pnl 196.586062, 1× LONG target). DB-Wahrheit verifiziert (backtest_run/backtest_trade konsistent). Legacy unverändert durch Phase 10e. - Production Gates M12+M13: `HISTORICAL_DATA_SOURCE`/`ALLOW_FIXTURE_DATA` UNSET. - Deployt: `core/engine.py`, `core/service.py`, `api/schemas.py` (Container-Hashes aktualisiert); `shared/historical/execution_context.py` + `pricing.py` unverändert. --- ## Geändert ``` Geändert von: Rain Ocampo Datum: 23.08.2026 Grund: Modul-12-Doku um Phase 10e (Intrabar Execution + Gap Realism) ergänzt — Versionierung, Intrabar-Policy, Gap-Semantik, Audit-18, Verifikation, Deploy, Smoke, Production Gates. ``` ``` Geändert von: Rain Ocampo Datum: 20.08.2026 Grund: Modul-12-Backtesting als FREIGEGEBEN markiert (deployt + E2E verifiziert: deterministisch, kein Lookahead, Stop/Target/Gap, LONG/SHORT, run_hash+data_hash). ``` Geändert von: Rain Ocampo Datum: 20.08.2026 Grund: Modul-12-Doku aktualisiert — Image auf backtesting:0.1.2 (V1.1), V1-Kompatmodus bitgenau, Referenz auf V1.1-Spec und Modul-13.