trading-system-docs/notes/trading/system-docs/phase10d_commission_fees.md

5.5 KiB
Raw Permalink Blame History

type _organized
Note 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 510d + 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 AJ): 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: AJ 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.pycommission_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 <echter-pfad>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.