trading-system-docs/notes/trading/system-docs/phase9_readiness_audit.md
Red Queen 7ae7249c00 feat(tolaria): migrate auto-safe knowledge batch 2 to schema v1
15 knowledge objects (log, note, index, module, history) migrated to
C3 knowledge schema v1. Bodies unchanged, metadata preserved.
2026-08-25 18:48:25 +00:00

20 KiB
Raw Permalink Blame History

id type role representation state knowledge_schema tags created
object/ca9b3551-53f8-408b-8697-9a21fa3a24d8 arch history canonical historical 1
trading
mt5
architecture
historical-v2
m12
m13
phase9
readiness-audit
phase2
2026-08-22

Phase 9 — M12/M13 Phase-2-Readiness-Audit + Implementierungsmatrix AN

Autor: Rain Ocampo | Datum: 22.08.2026 | Status: ANALYSE + DESIGN + VERIFIKATION (KEINE Implementierung) Geändert von: Rain Ocampo, Datum: 22.08.2026, Grund: Phase-9-Audit (read-only, Code-Evidenz)

Executive Summary

Phase 2 (realistischer Backtest: Bid/Ask-Fill, Spread, Slippage, Kosten, Intrabar, Gap) ist NICHT implementiert. Das heutige M12 nutzt ein reines Single-Price-Bid-Modell (nur Legacy-ohlcv-close, oder V2-bid_close als close). Es gibt keine Bid/Ask-Fill-Logik, kein separates Spread-Modell außer einem pauschalen Preispunkte-Abschlag auf Entry+Exit, keine Slippage außer derselben Punkte-Verschlechterung, und keinen Kosten-Breakdown. Der wichtigste Verzerrungs-Punkt: Der Intrabar-Exit ist symmetrie-verzerrt — Stop gewinnt immer bei Stop+Target-in-einer-Bar (konservativ, aber pessimistisch für Target-Trades).

Die Erweiterung muss einen ExecutionContext einführen, der execution_model, spread_model, slippage_model, cost_model, intrabar_policy, price_basis, feed_type transportiert und versioniert — inklusive Run-Hash-Integration, aber strikt getrennt von Dataset-Identität und Strategie-Parameter.


A — PRICE MODEL / SINGLE-PRICE AUDIT

Candle-Datenmodell (M12)

  • Legacy (repository.py:_load_candles_legacy, Zeile 305-327): ohlcv-Tabelle, Keys timestamp, open, high, low, close, volume. Nur Einzelpreis pro Bar (Mid/Einzelpreis). Kein bid/ask.
  • V2 (repository.py:_load_candles_v2, Zeile 332-371): liest aus historical_bar und mappt bid_open→open, bid_high→high, bid_low→low, bid_close→close. Das historical_bar-Schema HAT bid_open..ask_close (verifiziert: 37.480/37.480 bid+ask, mid=0), aber load_candles transportiert sie NICHT — wirft die ask-Seite weg und liefert nur bid-OHLC.
  • Die Engine bekommt also immer OHLC (nur bid/Einzel) — niemals ask.

Entry / Exit / Stop / Target (engine.py)

Feld Quelle Zeile
Entry (Signal) setup.entry = letzter Close der Signal-Candle (Strategie: entry = last_close) strategy 134/175
Entry-Fill (Open Pos) next_candle["open"] + Spread + Slippage (verschlechtert) _open_position 192-214
Stop setup.stop_loss = Swing-Low/High ± buffer 135, 176
Target setup.target = Entry ± 2R 139, 180
Exit (normales Ende) candle["close"] (OHNE Slippage/Spread auf Stop/Target) _close_position 283-336
Market-Order-Fill pauschaler Preispunkte-Abschlag LONG+ / SHORT- 198-200
Gap Open-Fill zum Gap-Open _evaluate_exit 268-280
Run-Ende Force-Close zum letzten close 141-153

Aktuelle Annahmen (Single-Price)

Annahme Konsequenz
open/close sind handelbarer Preis (Einzel) Kein Bid/Ask-Spread
LONG Entry = OPEN + spread + slippage Wird besser (künstlicher Overhead)
LONG Exit = CLOSE (bei normalem Exit) Kein Exit-Spread/Slippage — nur Entry hat Kosten

RISIKO: V2-Daten sind real bid_ask, aber load_candles reduziert auf bid — die ask-Seite wird VOR der Engine verworfen. Realisierung als Bid/Ask-Fill ist daher nicht möglich ohne Datenmodell-Änderung.

SOLL: Engine muss price_basis + bid_*/ask_*/mid_* transportieren; Entry/Exit-Getrennt je nach Richtung; Stop/Target auf entsprechende Seite anwenden.


B — BID/ASK READINESS

Was liefert Historical V2 tatsächlich?

  • historical_bar hat bid_open/high/low/close UND ask_open/high/low/close (verifiziert: 37.480/37.480/37.480, mid=0) — bid_ask vorhanden, mid NICHT.
  • load_bars_with_quality (Zeile 163-247) transportiert bid+ask+mid+quality-Keys (vollständig).
  • ABER: load_candles (der Pfad, den die Engine nutzt) liefert nur bid-OHLC — ask wird verworfen.

Kann M12 es heute transportieren?

Nein, nicht über den Engine-Pfad. Die Engine bekommt open/high/low/close als Einzelwerte. build_dataset_context bringt price_basis="bid_ask" (aus Bar-Metadaten), aber diese Info erreicht die Engine-Fills nicht (nur Run-Audit).

Zielmodell

LONG:  Entry grundsätzlich Ask (Käufer zahlt ask)
       Exit  grundsätzlich Bid (Verkäufer erhält bid)
SHORT: Entry grundsätzlich Bid (Verkäufer verkauft bid)
       Exit  grundsätzlich Ask (Käufer erhält ask)

Sonderfälle (zu dokumentieren, NICHT implementieren):

  • Stop LONG: Löse aus, wenn bid_low ≤ stop → fill = stop (oder gap-open bid, wenn gap unter)
  • Stop SHORT: bid_high ≥ stop → fill = stop (bid/ask?)
  • Target LONG: ask_high ≥ target → fill = target
  • Market: Entry LONG = ask_open; Entry SHORT = bid_open
  • Limit: (Phase 2 nicht eingeführt — kein Limit-Fill)
  • Gap: bid/ask-gap-opening überschlägt Stop/Target
  • Bar Open: Entry zum nächsten Bar-Open auf der korrekten Seite
  • End-of-data: force-close zum letzten bid/ask-close

C — SPREAD AUDIT

Heute (engine.py _open_position Zeile 198-201, _close_position Zeile 297-303):

  • Spread = ein fester Punktwert (params["spread"], Default 0.0, M13-Default 0.0).
  • Einheit: Preispunkte (absolute Preisdifferenz, nicht %).
  • Anwendung: sowohl Entry als auch Exit, je Richtung versetzt — d.h. ein doppelter Spread pro Trade (Entry+Exit) statt einmal.
  • Preisbasis: unklar — es wird einfach von open/close abgezogen, ohne bid/ask-Bezug. Da Daten heute bid-single sind, ist es ein "Abschlag" auf den einzigen Preis.

Doppelzählungsrisiko: Wenn Bid/Ask-Fill eingeführt wird, ist der aktuelle pauschale Spread-Abzug bereits implizit im echten bid/ask-Spread enthalten. Zusätzlich ein künstlicher spread-Abzug ⇒ Doppelzählung.

Zielregel:

  • price_basis=bid_ask (echte bid/ask) → Spread NICHT künstlich aufschlagen; nutze den intrinsischen bid/ask.
  • price_basis=mid/single → synthetisches Spread-Modell möglich (aufzuschlagen).
  • Beide Modi strikt unterscheidbar durch spread_model (z.B. BID_ASK_INTRINSIC vs SYNTHETIC_FIXED vs SYNTHETIC_POINTS).

D — SLIPPAGE AUDIT

Heute (engine.py):

  • Wann: nur Entry + normaler Exit (nicht Stop/Target, siehe unten).
  • Einheit: Preispunkte (params["slippage"], Default 0.0).
  • Long/Short: symmetrisch — Long schlägt auf (Entry+Exit), Short zieht ab. Adversarial in beiden Fällen.
  • Deterministisch: JA — ein fester Punkte-Wert. Kein Zufall, kein Seed.
  • Bestandteil run_hash: JA (steht in params_snapshot_compute_run_hash inkludiert params). Slippage-Änderung ändert run_hash.
  • Bestandteil Audit: indirekt über parameters (in metrics.audit nicht explizit).

Zielmodell: slippage_model (DETERMINISTIC_FIXED | SYNTHETIC_POINTS | BROKER_REPLAY...). Kein Zufall im deterministischen Kern; falls stochastisch, Seed fix + in Hash. Jeder Trade trennbar slippage_cost.


E — COST MODEL

Kosten Status Quelle Heutige Modellierung
Commission TEILWEISE CONFIG (fee_fixed+fee_pct) _fee() auf Entry+Exit
Spread TEILWEISE CONFIG (spread, Punkte) pauschaler Preispunkte-Abschlag auf Entry+Exit
Slippage TEILWEISE CONFIG (slippage, Punkte) pauschaler Preispunkte-Abschlag auf Entry+Exit
Financing/Overnight/Swap NICHT nicht modelliert
FX conversion NICHT nicht modelliert
Guaranteed Stop Premium NICHT nicht modelliert
sonstige Gebühren NICHT nicht modelliert

Kosten-Quellen heute: ausschließlich CONFIG (keine Broker-, keine Referenz-Kosten). Keine IG-Gebühren erfunden ✓.

Doppelzählungsrisiko (Kosten): fee UND spread UND slippage werden alle separat abgezogen — kein Broker/Referenz-Double-count, aber spread als pauschaler Abzug + künftiges bid/ask würde doppelt zählen (siehe C).


F — INTRABAR AMBIGUITY (engine.py _evaluate_exit, Zeile 251-281)

Aktuelles Verhalten — Code-Evidenz:

  • LONG: Stop wird VOR Target geprüft. Wenn open<=stop→Gap-Stop. Dann if low<=stop→Stop. Dann if high>=target→Target. ⇒ Wenn Stop UND Target in derselben BarStop gewinnt IMMER (Worst-Case konservativ).
  • SHORT: spiegelbildlich (Stop zuerst).

Verzerrung: M1-OHLC kann reale Intrabar-Reihenfolge nicht bestimmen (bekannt aus POC). Aktuelle Wahl ist deterministisch-pessimistisch (Stop-first), aber kann Target-Gewinner in realistischen Sequenzen unterschätzen (Stop-first bei bar mit beiden → Trades als Verluste klassifiziert, die real oft Target-Trades wären).

Zielmodi (Design, NICHT implementiert):

  • PESSIMISTIC (= Stop-first, heutiges Verhalten)
  • OPTIMISTIC (= Target-first)
  • STOP_FIRST / TARGET_FIRST (explizite Varianten)
  • TICK_RESOLUTION (benötigt Tick-Daten — nur V2 nicht lieferbar)
  • UNKNOWN/BLOCK (fail-closed: keine Intrabar-Reihenfolge festgelegt → Run blocken)
  • Optional DEFAULT=STOP_FIRST (behält heutiges Verhalten als Baseline; künftig wählbar)

G — GAP EXECUTION (engine.py, Zeile 268-280)

  • LONG Stop-Gap: wenn open<=stop → Fill zum min(open, stop) = Gap-Open (schlechter). Bewiesen: ein Gap-Down unter Stop füllt nicht perfekt bei stop, sondern Gap-Open.
  • LONG Target: high>=target→ fill=target (kein Gap-Fall bei Target gesondert). wenn open>=target → wird als high>=target abgefangen und zum target gefüllt (das wäre Optimistisch — Gap UP über Target müsste evtl. Gap-Open, nicht target).
  • SHORT analog.
  • RISIKO: target-Fill bei Gap wird zum target (nicht gap-open) — unrealistisch optimistisch für Gap-UP-LONG (sollte bid/ask-open nach gap). Umgekehrt ist der Stop-Gap korrekt pessimistisch.
  • Zielverhalten: Gap-Fill-Regel konsistent: bei Gap-Öffnung jenseits Stop/Target → zum Gap-Open der realen ausführbaren Seite, nie zum anvisierten Level.

H — REFERENCE VS BROKER FEED

  • Heute: M12 kennt keinen Feed-Unterschied. Engine ignoriert feed_type/price_basis. Ergebnis wird als generischer Backtest-Output geliefert (einzig Run-Audit enthält feed_type/price_basis, aber nur bei V2 + Context).
  • Zielregel: REFERENCE-Daten nie als exakte Broker-Ausführung ausgeben. Run muss execution_model + cost_model + spread_model + feed_type + price_basis sichtbar kennzeichnen.
  • Execution-Klassen (Phase 2):
    • MARKET_SIMULATION — nur OHLC, synthetische Modelle (Spread/Slippage/Cost als config)
    • BROKER_APPROXIMATION — echte bid/ask OHLC (REFERENCE), realistische Spread/Fill
    • BROKER_REPLAY — echte Broker-Feed + Broker-Fill (IG later), fehler bei REFERENCE
  • BROKER_REPLAY auf REFERENCE-Daten → fail-closed (inconsist).

I — M13 OPTIMIZATION IMPACT

M13 (optimizer.py) ruft M12 mit spread/slippage/fee_fixed/fee_pct als feste Baustein-Argumente (_bt_kwargs, Zeile 326-335, alle Default 0.0). NICHT Teil des Optimizer-Suchraumsparam_space enthält nur Strategie-Parameter.

Gefahr: Wenn spread/slippage/Fees in param_space aufgenommen würden, könnte der Optimizer sie MINIMIEREN (um Score zu maximieren) — realistische Kosten würden "schönoptimiert". Das ist verboten.

Klassifikation (Phase-2-Design):

Klasse Beispiele M13 optimiert?
STRATEGY_PARAMETER ema_period, rr_multiplier, stop_buffer JA
EXECUTION_PARAMETER intrabar_policy, gap_rule NEIN
BROKER_PARAMETER spread, slippage, commission, financing NEIN
DATA_PARAMETER data_source, feed, price_basis, timeframe NEIN
POLICY_PARAMETER eligibility_policy, quality_rule NEIN

M13 darf standardmäßig nur STRATEGY_PARAMETER optimieren; alle anderen Klassen bleiben fix aus dem Backtest-Request (nicht optimierbar).


J — RUN HASH / REPRODUCIBILITY

Phase-2-Parameter, die die Run-Identität beeinflussen müssen (Kandidaten):

  • execution_model_version
  • spread_model_version
  • slippage_model_version
  • cost_model_version
  • intrabar_policy_version

Entscheidung (Grundregel: Daten ≠ Ausführungsmodell ≠ Strategieparameter):

  • Dataset-Hash (data_hash): rein die OHLC/Bar-Daten (bid/ask). Unverändert durch Execution-Modelle.
  • Run-Hash: Dataset-Hash + Strategie+Param + Execution/Broker/Cost-Modell-Versionen (weil anderer Execution-Modus → anderes Ergebnis → anderes run_hash, sonst falscher Cache-Hit).
  • Audit: alle expliziten Modelle/Versions + breakdown, aber NICHT in den Hash (nur repräsentativ).
  • M13-Optimizer-Hash analog: Execution-Modell-Versionen zusätzlich.

K — RESULT METRICS

Heute (_compute_metrics): total_trades, winrate, profit_factor, expectancy_r, avg_r, net_pnl, gross_profit, gross_loss, avg_holding_h, long_count, short_count, max_drawdown, by_regime. Kein Kosten-Breakdown.

Zielmetriken (Phase-2-Design):

  • gross_pnl (ohne Kosten)
  • net_pnl (nach allen Kosten)
  • spread_cost / slippage_cost / commission_cost / financing_cost / total_cost
  • ambiguous_bars (Bars mit Intrabar-Ambiguität)
  • gap_fills
  • conditional_bars_used (V2-Conditional)
  • reference_feed_warning

L — FAIL-CLOSED CONDITIONS (Phase-2)

Design-Liste, Situationen in denen Phase-2-M12 NICHT rechnen darf:

Fall Error-Code
Bid/Ask erforderlich aber fehlt BID_ASK_REQUIRED_MISSING
unbekannte price_basis UNKNOWN_PRICE_BASIS
unbekanntes execution_model UNKNOWN_EXECUTION_MODEL
Fixture blockiert FIXTURE_DATA_BLOCKED
Dataset nicht eligible DATASET_NOT_ELIGIBLE / UNKNOWN_CRITICAL_METADATA
DatasetContext fehlt DATASET_CONTEXT_MISSING
Kostenmodell verlangt Brokerdaten fehlen COST_MODEL_BROKER_DATA_MISSING
BROKER_REPLAY + REFERENCE-Feed BROKER_REPLAY_REFERENCE_INCOMPATIBLE
Tick-Modus verlangt Tickdaten, hat nur M1 TICK_DATA_REQUIRED_MISSING

M — MIGRATION / COMPATIBILITY

Modus Daten Execution Spread Slippage Costs Intrabar Audit Reproducibility Zulässig
LEGACY Single-Price ohlcv single open/close, Stop-first Config-punkte Config-punkte Config-fix/% Stop-first Legacy-Hash run_hash
V2 REFERENCE BID/ASK historical bid/ask bid/ask-Fill intrinsic (bid/ask) Config/Determ. Config-fix/% wählbar full-Audit run_hash + execution_version (nach Implementierung)
BROKER BID/ASK Broker bid/ask broker-Fill real broker real broker real broker broker full Broker-Feed-Audit (künftig)
TICK tick-data tick-seq real real real tick-resolved full Tick-Audit (künftig)

Legacy bleibt unverändert (durch price_basis/execution_model-Gates). Kein stiller Fallback.


N — IMPLEMENTIERUNGSREIHENFOLGE (begründet)

Code-Audit belegt, dass die Engine rein Single-Price ist. Empfohlene Reihenfolge (jede Phase testbar + rückbaubar):

  1. ExecutionContext (Datenmodell/Versionierung) — Voraussetzung, trennt alles. LOW
  2. Bid/Ask-Datenmodell (load_candles/bars trägt bid/ask; Engine liest nach price_basis) — MEDIUM
  3. Bid/Ask Fill (LONG buy ask/sell bid, SHORT bid/ask) — HIGH (Fill-Preis)
  4. Spread-Modell (bid_ask intrinsic vs synthetic, keine Doppelzählung) — HIGH
  5. Slippage (slippage_model, Entry/Exit-Einheit, Determinismus) — MEDIUM
  6. Cost-Breakdown (Commission/Financing getrennt, nicht nur pauschal) — MEDIUM
  7. Gap-Fills (konsistentes Gap-Open-Verhalten, Stop+Target) — HIGH
  8. Intrabar-Policy (PESSIMISTIC/OPTIMISTIC/STOP_FIRST/TARGET_FIRST/TICK/UNKNOWN-BLOCK) — MEDIUM
  9. Result-Cost-Breakdown (gross/net/cost-Metriken) — LOW
  10. M13-Param-Grenzen (nur STRATEGY_PARAMETER optimizierbar, Execution-Kosten fix) — MEDIUM
  11. Reproducibility (execution_model_version in run_hash, Audit-Vollständigkeit) — MEDIUM
  12. Regression/Legacy (unverändert; Gate-Zusammenheit) — LOW
  13. Acceptance-Tests (Plan unten)

Die Reihenfolge weicht von der (User)-Suggestions ab: ExecutionContext-Versionierung zuerst ist zwingend, sonst werden Modelle ohne Versionierung eingebaut und Reproducibility bricht nachträglich. Bid/Ask-Datenmodell vor Fill zwingend.


RISIKO-MATRIX

Änderung Modul Datei/Fn Risiko Regression Test Rollback
ExecutionContext M12 service.py/engine.py LOW Hash-Vergleich revert service+engine
Bid/Ask-Datenmodell M12 marketdata/client.py, repo MEDIUM Daten-Format load-candles-Pfad revert client/repo
Bid/Ask-Fill M12 engine.py HIGH Fill-Preis LONG/SHORT Fill-Test revert engine
Spread-Modell M12 engine.py HIGH Spread, PnL No-Doppelzählung revert
Slippage M12 engine.py MEDIUM slippage-Einheit Entry/Exit-Slipp revert
Commission/Costs M12 engine.py MEDIUM Cost-Breakdown Fee-Test revert
Gap-Fills M12 engine.py HIGH Stop/Target, PnL Gap-Tests revert
Intrabar-Policy M12 engine.py MEDIUM Stop/Target Intrabar-Tests revert
Result-Cost-Breakdown M12 engine.py HIGH PnL Metrics revert
M13-Param-Grenzen M13 optimizer.py MEDIUM Overfit Optimizer-Test revert
Reproducibility M12/M13 service.py/optimizer.py MEDIUM Run-Hash Hash-Repro revert

ACCEPTANCE PLAN (vor Implementierung — Katalog)

Fill/Spread:

  • LONG Entry Ask, LONG Exit Bid (BID-ASK)
  • SHORT Entry Bid, SHORT Exit Ask
  • echte bid/ask-Spread NICHT doppelt (Bid/Ask-Modus)
  • synthetischer Spread korrekt (Single-Price-Modus)

Slippage/Cost:

  • Slippage Long/Short adversarial
  • Commission korrekt (fix+%)

Intrabar/Gap:

  • Stop-only, Target-only, Stop+Target gleiche Bar
  • Gap über Stop, Gap über Target
  • PESSIMISTIC/OPTIMISTIC unterscheidbar

Feed/Klassifizierung:

  • REFERENCE korrekt markiert
  • BROKER_REPLAY + REFERENCE → fail-closed

Reproduzierbarkeit:

  • gleicher Input → identisches Ergebnis
  • andere Execution-Version → anderer run_hash

Regression:

  • Legacy unverändert

OFFENE ENTSCHEIDUNGEN (für Christian)

STAND 22.08.2026: ALLE 7 ENTSCHEIDUNGEN FESTGELEGT (Christian, 22.08.2026).

  1. Spread → pro Fill/Seite modellieren, KEIN pauschaler Roundtrip-Aufschlag. Bei echten Bid/Ask-Daten entsteht der Spread über Entry-/Exit-Seite. Keine Doppelzählung.
  2. Intrabar-Default = PESSIMISTIC. Wenn Stop und Target in derselben Bar liegen und keine Tick-Reihenfolge vorhanden ist: konservativ ungünstigeren Ausgang verwenden. Später optional TICK_RESOLUTION.
  3. Slippage = deterministisch. Kein Zufall in V1. Später optional stochastisch nur mit festem Seed + eigener Modellversion.
  4. Cost Model = cost_model_v1_simple. Abdeckung: Spread, Slippage, Commission. NOCH KEINE Overnight-/Swap-/Financing-Komplexität in V1 (Financing später eigene Modellversion).
  5. Bid/Ask = durch den vollständigen M12-Engine-/Execution-Pfad propagieren. Nicht nur im Repository/load_bars. Keine Reduktion zurück auf Single Price im V2-Pfad.
  6. M13: Execution-/Broker-/Cost-Parameter sind FIX und NICHT optimierbar. M13 optimiert standardmäßig nur echte STRATEGY_PARAMETER. Spread/Slippage/Commission/Brokerparameter dürfen nicht schönoptimiert werden.
  7. Execution Models (verwenden):
    • LEGACY_SINGLE_PRICE
    • REFERENCE_BID_ASK
    • BROKER_APPROXIMATION
    • später BROKER_REPLAY
    • optional später TICK_REPLAY Dukascopy Historical V2 = REFERENCE_BID_ASK. Nicht als IG-Brokerfeed behandeln.

EXAKTER NÄCHSTER IMPLEMENTIERUNGSSCHRITT

Sobald Christian die offenen Entscheidungen freigibt, ist der 1. Schritt von Phase 10:

  1. ExecutionContext-Dataclass (execution_model, spread_model, slippage_model, cost_model, intrabar_policy, price_basis, feed_type + je *_version) — ausser DatasetContext, reine Ausführungs-Konfiguration.
  2. Engine lädt als Einzelz: execution_model in BacktestRequest.
  3. Tests: deterministisch, Hash-Integration, Legacy unverändert.

NOCH NICHT implementiert (verboten bis Freigabe): Bid/Ask-Fill, Spread-Änderung, Slippage-Änderung, Kostenmodell-Änderung, Intrabar-Ausführungslogik, Jahresbackfill, IG-Calls, Orders, Docker-Netzwerkänderung.