--- id: object/dd4f96c5-6cc8-28e4-c8d4-7069c949fa3f type: arch role: history representation: canonical state: historical knowledge_schema: 1 _organized: true --- # Phase 10d — Commission / Fees (cost_model_v1_simple) **Geändert von:** Rain Ocampo (Hermes) **Datum:** 23.08.2026 **Grund:** Deterministisches, reproduzierbares und auditierbares Commission-/Fee-Modell für Historical-V2-Backtests (M12), strikt nach Safe-Change-Management. ## Zusammenfassung Phase 10d führt ein deterministisches Cost-Modell `cost_model_v1_simple` in den Historical-V2-Backtest ein. Es unterstützt zwei kombinierbare Gebührenarten **pro Fill/Seite**: - **FIXED PER SIDE** (`commission_fixed_per_side`) — pauschal je Entry- und Exit-Fill. - **NOTIONAL RATE PER SIDE** (`commission_rate`) — prozentual auf den **tatsächlichen Fill-Notional** (`abs(fill_price * quantity) * rate`). `CostModel.NONE` lässt Legacy-Fee-Felder (`fee_fixed`/`fee_pct`) exakt unverändert. `CostModel.SIMPLE_V1` ersetzt im V2-Pfad die betreffende Fee-Berechnung und wird **nicht** zusätzlich zur Legacy-Fee berechnet (keine Doppelzählung). Spread (10b) und Slippage (10c) bleiben getrennte Ebenen und werden nicht als Commission erneut berechnet. ## Fee-Reihenfolge (verbindlich) ``` MARKET PRICE → BID/ASK SIDE (LONG Entry=ASK / Exit=BID; SHORT Entry=BID / Exit=ASK) → SLIPPAGE (adversarial, DETERMINISTIC_FIXED) → FILL PRICE → COMMISSION/FEE (auf Fill, pro Seite) → NET PNL ``` Die Rate-Commission verwendet den tatsächlichen **Fill NACH Slippage** — nicht den Pre-Slippage-Basispreis. ## Fee-Formel ``` fee_per_side = fixed_per_side + abs(fill_price * quantity) * rate ``` - **LONG/SHORT-symmetrisch** — Fees sind immer Kosten, keine Vorzeichenlogik. - **Keine FX-/Währungs-Konvertierung** — Fees in derselben PnL-/Quote-Währung. - **Entry- und Exit-Fee exakt einmal** pro Trade; **kein Exit → keine Exit-Fee**. - Target, Stop und Force-Close verwenden jeweils den tatsächlichen Exit-Fill. ## Gross / Net ``` gross_pnl − (entry_fee + exit_fee) = net_pnl ``` Trade-Audit trägt: `gross_pnl`, `entry_fee`, `exit_fee`, `total_fees`, `net_pnl`. ## NONE vs SIMPLE(0) `CostModel.NONE` und `cost_model_v1_simple(fixed=0, rate=0)` erzeugen **gleiche Fillpreise, gleiches gross_pnl, gleiches net_pnl**, aber **unterschiedlichen ExecutionContext und V2-run_hash** — gewollt. ## Fail-Closed Ungültige Werte blockieren: `fixed < 0`, `rate < 0`, `NaN`, `Inf`, nicht-numerisch, unbekanntes `cost_model`. Kein stilles Clamp, kein Fallback auf NONE. ## run_hash Für historical_v2 fließen in den reproduzierbaren Run-Kontext ein: `cost_model`, `cost_model_version`, `commission_fixed_per_side`, `commission_rate`. Legacy-Hash (`fee_fixed`/`fee_pct`-Pfad) bleibt exakt unverändert. ## Proof: deterministische Beispiele | Test | Erwartung | Ergebnis | |---|---|---| | A: FIXED LONG (Fill 100→110, qty 1, fixed 1) | gross +10, fees 2, net +8 | grün | | B: FIXED SHORT (Fill 110→100, qty 1, fixed 1) | gross +10, fees 2, net +8 | grün | | C: RATE LONG (Fill 100→110, qty 2, 1%) | entry 2.00, exit 2.20, gross 20, fee 4.20, net 15.80 | grün | | D: RATE SHORT (Fill 110→100, qty 2, 1%) | entry 2.20, exit 2.00, gross 20, fee 4.20, net 15.80 | grün | | E: FIXED + RATE | exakt additiv | grün | ## Testabdeckung - `test_phase10d_cost_model.py`: **38 Tests** — NONE, SIMPLE zero, fixed/rate LONG+SHORT, fixed+rate, quantity scaling, fill-price basis, slippage-before-fee, kein double spread/slippage, target/stop/force-close fee, gross/net, audit, run_hash-deterministic/-changes, fail-closed (negativ/NaN/Inf/unbekannt), fixture blocked/allowed, Legacy unchanged. ## Regression (Phase 5–10d + M12/M13, 2× grün) - pytest M12-Gruppe: **129 passed** × 2 - Phase 5 (Eligibility): 20/20 ×2 · Phase 6 (DatasetContext): 14/14 ×2 - Phase 7 (M13 run_hash A–J): grün ×2 (Legacy-Hash unverändert) - M13 compat: 5/5 ×2 (inkl. `optimizer_no_exec_params` = M13 optimiert KEINE Cost-Parameter) - M13 shared_repository: A–J grün ×2 ## Deploy - READ-ONLY Importkette geprüft; Backup + sha256 VORHER verifiziert. - Minimaler Dateisatz (5 Dateien): `execution_context.py`, `pricing.py`, `engine.py`, `service.py`, `api/schemas.py`. - `docker cp` (echter Pfad) → `chown 1001:1001` (als root) → `py_compile` OK → Import-Smoke OK → Restart → Health 200. - Production Gates: `HISTORICAL_DATA_SOURCE=UNSET`, `ALLOW_FIXTURE_DATA=UNSET`. ## Legacy-Production-Smoke (2×, byte-identisch) - run_hash `bc6e25533d22ee102b6921eeeccd8f7b7dff4ec74a3316e381007ec56b0e1b37` - data_hash `d76a75496453d9c05d5690237f4621534f954f9f3e87b67eee4ae9327c5c6bb9` - net_pnl **196.586062**, total_trades **1** — 2× identisch → Legacy unverändert. ## Geänderte Dateien (Phase 10d) - `shared/historical/execution_context.py` — Cost-Felder + Fail-Closed + `build_execution_context`-Parameter. - `m12_app/core/pricing.py` — `commission_for_fill` (FIXED + NOTIONAL, pro Seite). - `m12_app/core/engine.py` — `_position_fee`, Entry/Exit-Fee, Trade-Audit-Felder. - `m12_app/core/service.py` — run_hash-Felder, `_dataset_gate` explizite Cost-Parameter. - `m12_app/api/schemas.py` — Request-Felder. ## Technische Schulden / Hinweise - **Deploy-Vorfall (dokumentiert):** `docker cp /dev/stdin` erzeugt defekte Symlinks (`/proc/self/fd/0`). Sauberer Weg: scp → Staging-Pfad → `docker cp ` → `docker exec -u root chown`. Der Vorfall ist vollständig behoben, kein Daten-/PnL-Befund, Produktion valid. - Keine FX-Konvertierung; Overnight/Swap/Finanzierung/Borrow sind NICHT Teil von 10d. ## Nächster Schritt STOPP nach Phase 10d. Keine automatische Fortsetzung (Finanzierung/Overnight/Swap/Borrow/Phase 10e/Jahresbackfill/Live-Broker-Costs). Auf neue Freigabe warten.