#!/usr/bin/env python3 """Was haetten Lot-Deckel, Risiko-Sizing und Circuit-Breaker WIRKLICH veraendert? ANLASS (2026-08-13). Die Squeeze-Zerlegung hat gezeigt: der Verlust steckt nicht im Exit (der S/R-Close trug +15,98 EUR bei), sondern in der GROESSE — -175,21 EUR bei 2,91 Lots aus einer 0,695-$-Bewegung, also dem planmaessigen 2xATR-Stop. Ø-Verlust 94 EUR auf 1.279 EUR Konto = 7,4 % je Verlusttrade. Diese Rechnung prueft das auf der ECHTEN Handelshistorie statt an einem Modell. METHODE — Pfad-Simulation auf realisierten Ergebnissen: · Je Trade ist `pnl / lots` das GROESSENUNABHAENGIGE Ergebnis (der Kursverlauf). Bei einem Retail-CFD ist das sauber: die eigene Order bewegt den Markt nicht, und der SL-Abstand kommt aus dem ATR, nicht aus der Lotzahl. · Unter Margin-Sizing skaliert die Lotzahl mit dem Konto. Die Sim fuehrt deshalb ein eigenes Equity-Konto und leitet die Groesse daraus ab — `lots_sim = lots_ist x (equity_sim / equity_ist)`. Damit braucht es KEINE Annahme ueber die Margin je Lot (die dokumentierten 670 EUR/Lot stimmen ohnehin nicht mehr: bei 1.279 EUR und ~3 Lots sind es ~405). · Risiko-Sizing nutzt `sl_at_entry`: Risiko je Lot = |entry - sl| x EUR/$/Lot. Der EUR-Faktor wird aus den Daten geschaetzt, nicht geglaubt. ⚠⚠ WAS DIESE RECHNUNG NICHT KANN: (1) Sie unterstellt, dass DIESELBEN Trades mit denselben Kursverlaeufen zustande kommen. Bei den MANUELLEN Trades ist das eine starke Annahme — ein Mensch handelt bei anderer Groesse womoeglich anders. (2) Der Circuit-Breaker wird auf REALISIERTE Tagesverluste geprueft. Live greift er auch auf die offene Position; er wuerde den ausloesenden Trade also FRUEHER kappen. Die Sim laesst ihn auslaufen -> sie UNTERSCHAETZT den Nutzen des Breakers. Das faellt zu Gunsten der Nullhypothese aus. (3) Vergangene Pfade sind keine Verteilung. Max-Drawdown auf EINEM Pfad ist eine Beobachtung, keine Wahrscheinlichkeit — `backtest_sizing.py` ist fuer die Ruin-Frage das richtige Werkzeug (dort: Margin-Sizing 36 % P(DD>80 %), 1-2 % Risiko 0 %). Aufruf: python analyze_sizing_live.py [--tage N] """ from __future__ import annotations import argparse import datetime as dt import sqlite3 import sys from zoneinfo import ZoneInfo DB = "oil_widget_history.db" _BERLIN = ZoneInfo("Europe/Berlin") def eur_pro_dollar_lot(rows) -> float: """EUR je 1,00 $ Kursbewegung und Lot — robust aus den Daten geschaetzt.""" f = [] for r in rows: if not (r["entry_price"] and r["exit_price"] and r["lots"]): continue d = 1 if r["direction"] == "BUY" else -1 mv = (r["exit_price"] - r["entry_price"]) * d if abs(mv) > 0.05: # kleine Bewegungen: Swap dominiert f.append(r["pnl"] / (mv * r["lots"])) f.sort() return f[len(f) // 2] if f else 87.0 def lade(tage: int | None): db = sqlite3.connect(DB); db.row_factory = sqlite3.Row w = "" if tage: w = f" AND exit_time >= strftime('%s','now') - {tage*86400}" rows = [dict(r) for r in db.execute( "SELECT entry_time,exit_time,direction,lots,entry_price,exit_price,pnl," "closed_by,setup,sl_at_entry FROM trades WHERE exit_time IS NOT NULL " f"AND pnl IS NOT NULL AND lots > 0{w} ORDER BY exit_time")] return rows def simuliere(rows, start, regel, deckel=None, breaker=None, eurfak=87.0, equity_ist=None): """Ein Equity-Pfad unter EINER Sizing-Regel. regel: 'ist' | 'margin' | ('risiko', pct) deckel: max. Lots oder None · breaker: Tagesverlust-Grenze in % oder None """ eq = start minimum = start peak = start maxdd = 0.0 worst = 0.0 tag_akt, tag_pnl, tag_start, gesperrt = None, 0.0, start, False n_block, n_trades = 0, 0 tage_getroffen = set() for idx, r in enumerate(rows): tag = dt.datetime.fromtimestamp(r["exit_time"], _BERLIN).date() if tag != tag_akt: tag_akt, tag_pnl, tag_start, gesperrt = tag, 0.0, eq, False if gesperrt: n_block += 1 continue je_lot = r["pnl"] / r["lots"] # groessenunabhaengig if regel == "ist": lots = r["lots"] elif regel == "margin": # Margin-Sizing skaliert linear mit dem Konto lots = r["lots"] * (eq / equity_ist[idx]) if equity_ist[idx] > 0 else 0.0 else: pct = regel[1] if not r["sl_at_entry"] or not r["entry_price"]: continue # ohne SL kein Risiko-Sizing dist = abs(r["entry_price"] - r["sl_at_entry"]) risiko_lot = dist * eurfak if risiko_lot <= 0: continue lots = (eq * pct / 100.0) / risiko_lot if deckel is not None: lots = min(lots, deckel) if lots <= 0: continue pnl = je_lot * lots eq += pnl tag_pnl += pnl n_trades += 1 worst = min(worst, pnl) peak = max(peak, eq) minimum = min(minimum, eq) if peak > 0: maxdd = max(maxdd, 100 * (peak - eq) / peak) if breaker is not None and tag_start > 0 and tag_pnl <= -breaker / 100 * tag_start: gesperrt = True tage_getroffen.add(tag) if eq <= 0: return {"eq": 0.0, "maxdd": 100.0, "worst": worst, "n": n_trades, "block": n_block, "bt": len(tage_getroffen), "ruin": True} return {"eq": eq, "maxdd": maxdd, "worst": worst, "n": n_trades, "block": n_block, "bt": len(tage_getroffen), "ruin": False} def main(): ap = argparse.ArgumentParser() ap.add_argument("--tage", type=int, default=None) a = ap.parse_args() rows = lade(a.tage) if not rows: print(" keine Trades"); return 1 eurfak = eur_pro_dollar_lot(rows) ende = 1279.04 # aktuelle Balance (Snapshot) start = ende - sum(r["pnl"] for r in rows) # Equity-Pfad, wie er TATSAECHLICH lief — Bezug fuer die Margin-Skalierung eq_ist, e = [], start for r in rows: eq_ist.append(e); e += r["pnl"] mit_sl = sum(1 for r in rows if r["sl_at_entry"]) f = lambda t: dt.datetime.fromtimestamp(t).strftime("%d.%m.%Y") print(f"\n {len(rows)} geschlossene Trades · {f(rows[0]['exit_time'])} bis " f"{f(rows[-1]['exit_time'])}") print(f" Startkapital rekonstruiert {start:.2f} EUR -> IST-Ende {ende:.2f} EUR") print(f" EUR je $ und Lot (aus den Daten): {eurfak:.2f} · mit SL: {mit_sl}/{len(rows)}") print(f" ⚠ Pfad-Simulation auf realisierten Ergebnissen — Grenzen s. Docstring\n") kopf = f" {'Regel':<34}{'Ende EUR':>11}{'MaxDD':>8}{'schlecht. Trade':>17}{'Trades':>8}{'geblockt':>10}" def zeig(name, r): ruin = " ⚠ RUIN" if r["ruin"] else "" bt = f" ({r['bt']} Tage)" if r["bt"] else "" print(f" {name:<34}{r['eq']:>11.2f}{r['maxdd']:>7.1f}%{r['worst']:>17.2f}" f"{r['n']:>8}{r['block']:>10}{bt}{ruin}") print(" ── BASIS ──────────────────────────────────────────────────────") print(kopf) ist = simuliere(rows, start, "ist", eurfak=eurfak, equity_ist=eq_ist) zeig("IST (so gehandelt)", ist) print("\n ── LOT-DECKEL (Margin-Sizing, aber gedeckelt) ─────────────────") print(kopf) zeig("Margin ohne Deckel (Kontrolle)", simuliere(rows, start, "margin", eurfak=eurfak, equity_ist=eq_ist)) for c in (0.5, 1.0, 1.5, 2.0, 3.0): zeig(f"Margin + Deckel {c:.1f} Lots", simuliere(rows, start, "margin", deckel=c, eurfak=eurfak, equity_ist=eq_ist)) print("\n ── RISIKO-SIZING (calc_lots_risk, braucht sl_at_entry) ────────") print(kopf) for p in (0.5, 1.0, 1.5, 2.0, 3.0): zeig(f"Risiko {p:.1f} % je Trade", simuliere(rows, start, ("risiko", p), eurfak=eurfak, equity_ist=eq_ist)) print("\n ── CIRCUIT-BREAKER (Tagesverlust-Stopp, auf IST-Groesse) ──────") print(kopf) for b in (5, 8, 10, 15): zeig(f"IST + Breaker {b} %/Tag", simuliere(rows, start, "ist", breaker=b, eurfak=eurfak, equity_ist=eq_ist)) print("\n ── KOMBINATION ───────────────────────────────────────────────") print(kopf) for p, b in ((1.5, 8), (2.0, 8), (1.5, None), (2.0, None)): nm = f"Risiko {p:.1f} %" + (f" + Breaker {b} %" if b else "") zeig(nm, simuliere(rows, start, ("risiko", p), breaker=b, eurfak=eurfak, equity_ist=eq_ist)) print("\n ⚠ Lesehilfe: 'Ende EUR' ist EIN Pfad, keine Erwartung. Entscheidend " "sind\n MaxDD und der schlechteste Einzeltrade — die sagen, was das " "Konto AUSHAELT.") return 0 if __name__ == "__main__": raise SystemExit(main())