diff --git a/CLAUDE.md b/CLAUDE.md index 97553c8..4f98f35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5617,6 +5617,64 @@ Berührungen schliesst. Live prüft den **laufenden** Kurs im Sekundentakt; das Analogon ist die **Bar-Spanne**. Danach feuert er in 21–23 % der Trades (live 4 von 11 = 36 %, bei n=11 kein Widerspruch). +### ⚠⚠ SIZING AUF DER ECHTEN HISTORIE GERECHNET (`analyze_sizing_live.py`, 2026-08-13) + +Folge der S/R-Widerlegung: der Hebel zeigt auf die GRÖSSE. Pfad-Simulation über +**1.278 geschlossene Trades (08.05.–13.08.)**, `pnl/lots` als größenunabhängiges +Ergebnis, Lots aus der jeweils simulierten Equity abgeleitet. +⚠ Zwei dokumentierte Zahlen stimmen nicht mehr und wurden **aus den Daten neu +geschätzt**: EUR je $ und Lot = **86,90** (notiert 87,6) und Margin je Lot +**~405 €** (notiert 670 — bei 1.279 € Balance und ~3 Lots). + +**(1) DER IST-PFAD IST DIE EIGENTLICHE BOTSCHAFT:** +| | | +|---|---| +| Start (rekonstruiert) | 1.115,70 € | +| **Hoch** | **2.724,16 €** (05.08.) | +| **Tief** | **261,36 €** (30.07.) | +| **Max-Drawdown** | **82,8 %** | +| schlechtester Einzeltrade | **−381,16 €** | +| Ende | 1.279,04 € | +⚠⚠ Das Konto hat **zweimal** den Großteil seines Werts verloren — −82,8 % bis +zum 30.07., danach vom Hoch am 05.08. nochmals **−53 %**. **42 von 71 Tagen sind +negativ**; getragen wird alles von wenigen Tagen (04.08. **+1.297 €**). Für +**unbeaufsichtigten** Betrieb ist das die entscheidende Zahl, nicht der Endstand. + +**(2) VARIANTEN** (Ende / MaxDD / schlechtester Trade): +| Regel | Ende | MaxDD | schlecht. Trade | +|---|---|---|---| +| **IST** | 1.279 € | **82,8 %** | **−381 €** | +| Lot-Deckel 1,5 | 1.362 € | 82,8 % | −345 € | +| Risiko 1,5 %/Trade | 680 € | **54,4 %** | **−77 €** | +| Risiko 0,5 %/Trade | 960 € | **22,2 %** | **−27 €** | +| **IST + Breaker 8 %/Tag** | **2.018 €** | **59,2 %** | −381 € | +| IST + Breaker 5 %/Tag | 1.976 € | 65,5 % | −381 € | +✅ **Lot-Deckel bringen fast nichts** — die realen Lots lagen meist darunter. +✅ **Risiko-Sizing drittelt den schlechtesten Trade und halbiert den Drawdown**, +kostet aber Endstand. ⚠ **Confounder: die Risiko-Varianten laufen auf 1.118 der +1.278 Trades** (nur die mit `sl_at_entry`) — nicht direkt vergleichbar. +⚠⚠ **Der Breaker verbessert BEIDES** (Ende +739 €, DD −23,6 Pp) — aber: + +**(3) ⚠⚠ EIN TAG TRÄGT 70 % DES BREAKER-VORTEILS.** Vermiedene Beträge je +Auslösetag: **07.08. +556,82** · 30.07. +200,48 · 24.07. +106,61 · 28.07. +81,24 +· 23.07. +63,23 · Rest klein — und an **drei Tagen kostet er**: 11.08. −106,85 · +29.07. −96,00 · 27.07. −50,77. Summe +800,19 € über 11 Auslösetage. +⚠ Ohne den 07.08. bleiben **+243 €** über 10 Tage. **Das ist kein Makel der +Messung, sondern das Wesen einer Versicherung** — ihr Wert sitzt im Tail. Aber +die +739 € sind eine **Realisierung**, keine Erwartung. +⚠⚠ **UND EINE GRENZE, die für HEUTE entscheidend ist:** die Sim sperrt nach dem +Auslösen **alle** Trades des Tages. Live sperrt `_check_circuit_breaker` nur die +**autonomen** (`_check_auto_squeeze`), manuelle Orders bleiben frei. Bei einem zu +94 % manuell gehandelten Konto misst die Sim damit eher eine **Disziplin-Regel** +als das heutige Code-Verhalten. **Für das Ziel Autotrading ist sie dagegen das +richtige Modell** — dort greift die Sperre wirklich. +⚠ Weitere Grenzen: EIN Pfad ist keine Verteilung (für die Ruin-Frage bleibt +`backtest_sizing.py` das Werkzeug: Margin-Sizing 36 % P(DD>80 %) — was der +gemessene IST-Pfad mit 82,8 % **bestätigt**); die Sim unterstellt dieselben +Trades bei anderer Größe, was bei **manuellen** Trades eine starke Annahme ist; +und der Breaker wird auf **realisierte** Tagesverluste geprüft, während er live +auch die offene Position kappt → die Sim **unterschätzt** ihn. + ### ✅ S/R-AUTO-CLOSE WIEDER AN (User-Entscheidung 2026-08-11) Frage war: „sollten wir Trades bei einem S/R-Bounce automatisch schliessen?" diff --git a/analyze_sizing_live.py b/analyze_sizing_live.py new file mode 100644 index 0000000..3b572bd --- /dev/null +++ b/analyze_sizing_live.py @@ -0,0 +1,207 @@ +#!/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())