Initial commit: Oil Trading Bot (MT5, WTI)

Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/),
Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind
via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-24 08:29:23 +02:00
co-authored by Claude Opus 4.8
commit 75d28827e8
104 changed files with 21059 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
# Exit-Simulation (Befund B: Initial-SL-Breite)
`backtest_exit.py` — misst, ob ein **weiterer Initial-SL** netto mehr bringt,
unter originalgetreuer Nachbildung des Exit-Ablaufs aus `core/trailing.py`.
## Mechanik (nachgebildet)
- Initial-SL = **X×ATR** (getestete Variable).
- **Teil-Exit 50 %** bei +1,5×ATR (einmalig).
- Phasen: **Init** (<0,3×ATR → Initial-SL halten) · **Trail** (SL = HW∓1,5×ATR,
Breakeven-Floor ab +0,6×ATR) · **Lock** (≥3,5×ATR → enger). Phasen-Ratsche.
- Pessimistische Intrabar-Annahme: **Gegenlauf vor Mitlauf** → überschätzt den
Nutzen eines weiten SL NICHT.
- PnL in **R** (ATR-Vielfache). Fester Init-TP (+3,5×ATR) weggelassen — der
Runner-Exit läuft praktisch über den Trailing-SL (konservativ, dominant).
> Schlüssel: Der weite Init-SL wirkt **nur in der Init-Phase**. Sobald Trailing
> greift, kappt HW∓1,5×ATR ihn ohnehin. Er schützt also genau die Trades, die
> **sofort** gegen den Einstieg laufen (die „Frühstopps").
## Ergebnis (M5+M30, ~28 Tage, 3585 Signale)
| Init-SL | Treffer | Ø-R | Summe-R | PF | Ø-Verlierer | Worst-R | Init-Stops |
|--------:|--------:|----:|--------:|---:|-----------:|--------:|-----------:|
| 1,2 | 37 % | 0,169 | 607 | 1,33 | 1,09 | 1,20 | 37 % |
| 1,5 | 39 % | 0,182 | 654 | 1,33 | 1,29 | 1,50 | 31 % |
| **2,0** | **43 %** | **0,223** | **798** | **1,39** | 1,56 | 2,00 | 25 % |
| 2,5 | 46 % | 0,255 | 914 | 1,43 | 1,78 | 2,50 | 21 % |
| 4,0 | 51 % | 0,308 | 1104 | 1,49 | 2,28 | 4,00 | 14 % |
**Befund:** Ø-R, PF, Treffer steigen monoton mit der SL-Breite, Frühstopps sinken.
**Aber:** der größte Einzelverlust wächst 1:1 (Worst-R = X) → Tail-/Drawdown-Risiko.
## Bewertung
- Das aktuelle Band **1,21,5×ATR ist zu eng** (deckt sich mit MAE ~1,7×ATR und
37 %/31 % Frühstopps). Wechsel auf **~2,0×ATR** ist der ausgewogene Punkt:
+32 % Ø-R, PF 1,33→1,39, Frühstopps 37→25 %, Worst-Case auf 2,0×ATR begrenzt.
- **Nicht** auf 34×ATR: Durchschnitte steigen weiter, aber die großen Einzel-
verluste treiben Drawdown/Ruin-Risiko; Stichprobe ist zudem ein Trend-Regime
(„länger halten" bevorzugt).
-**Money-Management:** ein weiterer SL = mehr Risiko/Trade in EUR bei gleicher
Lotgröße. Für konstantes EUR-Risiko die Lots ~proportional senken (1,5→2,0 ≈ 25 %).
## Aufruf
`python backtest_exit.py [n_bars]` (Default 8000 M5-Bars)
+68
View File
@@ -0,0 +1,68 @@
# Gesamtempfehlung (Verdict-Panel)
Grafischer Empfehlungsbereich ganz oben im Mobile-Dashboard (`web/`), der die
Module zu **einer** Aussage zusammenfasst.
## Wer berechnet was
- **Backend (`core/engine.py``_verdict`)** baut die Aggregation und legt sie
als `verdict` in den Snapshot (`/api/snapshot`, `/ws`). Eine Quelle der
Wahrheit — das Frontend rendert nur.
- **Frontend (`web/app.js``renderVerdict`)** zeichnet Ring, Bias-Balken und
Modul-Chips. Markup in `web/index.html` (`#card-verdict`), Stil in
`web/style.css` (`.vd-*`).
## Logik
**Headline = die validierte Welle** (`wave_signal`) — das einzige Modul mit per
Backtest belegtem Edge. Sie bestimmt LONG/SHORT/WARTEN und die Order-Freigabe.
Die übrigen Module bilden nur einen **gewichteten Konsens** (Anzeige, kein
Eingriff in die Order-Logik).
Votes je Modul: `+1` LONG · `1` SHORT · `0` neutral. Gewichte:
| Modul | Gewicht | Quelle |
|-------|---------|--------|
| Welle | **3,0** | `wave_signal.signal` (validierter Edge) |
| M30 | 1,5 | `wave.htf_trend` (EMA12/50 der M30 — exakt das Signal, das die Welle nutzt) |
| H1 | 1,5 | `wave.h1_trend` (EMA12/50 der H1) |
| KI-Copilot | 1,0 *(nur mit Bias)* | `agent.advisory.bias` — ohne advisory/Bias **Gewicht 0** |
| Elliott/FVG | 1,0 *(nur mit Ziel)* | Richtung zum `elliott.target` — ohne Ziel **Gewicht 0** |
| Squeeze | 2,0 *(nur `active`)* | `wave.squeeze` — armed/keine Kompression **Gewicht 0** |
(TradersUnion wurde 2026-07-06 aus dem Konsens entfernt — nur noch Anzeige.)
```
bias = Σ(gewicht · vote) / Σ(gewicht) → 1 … +1
```
**Stimmberechtigt sind nur Module mit Aussage** (Fix 2026-07-15): Module ohne
anwendbares Signal (Squeeze ohne Ausbruch, Elliott ohne Ziel, KI aus) bekommen
Gewicht 0 und stehen NICHT im Nenner — vorher dämpfte z. B. das fast immer
neutrale Squeeze-Gewicht (2,0) die Nadel dauerhaft um ~×0,8 Richtung Mitte.
„Flach" bei Welle/M30/H1 bleibt dagegen eine ECHTE Neutral-Stimme (mit Gewicht).
`bias` steuert die **Nadel** im Balken (1 = ganz SHORT/links, +1 = ganz
LONG/rechts: `left% = (bias+1)/2·100`). `agree/total` = wie viele der **anderen**
stimmberechtigten Module (ohne die Welle selbst — die zählte sich bis 2026-07-15
mit) die Headline-Richtung teilen. Der **Konfidenz-Ring** zeigt `wave.conf_pct`
als conic-gradient in Richtungsfarbe.
> ⚠ Wichtig: Für M30/H1 werden die **EMA12/50-Signale der Welle** genutzt
> (`htf_trend`/`h1_trend`), NICHT das Regressions-Winkelmaß `market.angles` —
> die beiden können sich widersprechen, das Panel muss zur Empfehlung passen.
## Snapshot-Feld `verdict`
```json
{
"headline": "SHORT", "conf": 90, "bias": -1.0, "agree": 3, "total": 3,
"votes": [{"name": "Welle", "vote": -1, "weight": 3.0, "detail": "SHORT · 90%"}, ]
}
```
## Pflege
Neues Modul ins Panel → Vote/Gewicht in `_verdict` ergänzen; das Frontend rendert
`votes` generisch. Bei JS/CSS-Änderung Asset-Version `v=N` in `index.html`
hochzählen. Siehe `CLAUDE.md`.
+54
View File
@@ -0,0 +1,54 @@
# Risiko-Management (Positionsgröße & Stops)
Stand: 2026-07-08.
## Positionsgröße
> **Aktiv (User-Vorgabe): margin-basiert 95 %.** `calc_lots` setzt die Lots auf
> ~95 % der freien Margin (`[trading] margin_buffer_pct=95`, `risk_pct=0`). ⚠ Das
> ist faktisch All-in: **~7 %+ Konto-Risiko pro Trade** (gemessen
> `backtest_sizing.py`: 36 % Wahrscheinlichkeit eines >80 %-Drawdowns). Bewusste
> Entscheidung — 2026-07-06 kurz auf risiko-basiert 1,5 % gestellt, dann auf
> 95 % Margin zurück. Der harte Broker-SL wird trotzdem immer gesetzt.
Das **risiko-basierte** Sizing (`calc_lots_risk`) ist gebaut, gemessen und
jederzeit aktivierbar (`[trading] risk_pct = 1.5` + Restart). Es wählt die Lots
so, dass der **Verlust beim Initial-SL ≈ `risk_pct` % der Equity** ist:
```
val_per_price = tick_value / tick_size # € je 1.0 Preis je 1 Lot
lots = (Equity × risk_pct/100) / (SL_Distanz × val_per_price)
```
- **SL zuerst:** `_send_locked` berechnet erst den SL (`_calc_sl_tp`, Band
1,82,2×ATR), dann die Lots aus der **echten** SL-Distanz.
- **Margin bleibt Deckel:** nie mehr Lots als die freie Margin (× Buffer) erlaubt.
- **Fallback:** liefert `calc_lots_risk` 0 (fehlende Daten), greift `calc_lots`.
- Vergleich (102-€-Konto, SL 2×ATR≈0,53): 95 % Margin ≈ 0,15 L → 7 € (6,8 %)
am SL · 1,5 % Risiko ≈ 0,03 L → 1,40 € (1,4 %).
## Stops & Auto-Close
- **Harter Broker-SL auf jeder Bot-Order** (`req["sl"]`) — server-seitig, wirkt
auch bei Bot-/Verbindungsausfall. Band 1,82,2×ATR (Ziel 2,0; gemessen
`backtest_exit.py`); Breakeven ab +1,3×ATR; Trailing Init/Trail/Lock
(Parameter gemessen optimal, `backtest_trailing.py`; ATR-Floor 0,06,
`backtest_atrfloor.py`).
- **Adoptierte Fremd-Trades** (in MT5 manuell eröffnet) bekommen beim Erkennen
einen **Schutz-SL** nachgerüstet (`_refresh_locked`), falls keiner gesetzt ist.
- **Notfall-Stop (P&L ≤ Wert) + Gewinn-Ziel (P&L ≥ +Wert):** server-seitiger
Auto-Close (`engine._check_auto_close`, 1-s-Loop, wirkt bei gesperrtem Handy).
Notfall-Stop wird beim Öffnen automatisch auf den letzten Wert armiert
(Start `[trading] auto_emergency_loss`, Default 10 — ⚠ bei ~100-€-Konto sind
10 € ≈ 10 % Equity, das ist der faktische Risiko-Deckel). „aus"-Button
deaktiviert auch den Auto-Arm.
- **SL/TP sind im Snapshot + Web sichtbar** (Position-Kachel) — fehlender SL wird
rot „⚠ kein SL gesetzt!" markiert.
## Bewusst NICHT umgesetzt
- **Tagesverlust-Schalter / Circuit Breaker:** vom User **abgelehnt** — nicht
einbauen (s. CLAUDE.md).
- **Signal-Filter als Risiko-Schutz:** 5× gemessen wirkungslos/schädlich
(Winkel, HTF-Winkel, Konfluenz-Strafe, Gegen-H1-Reversal, ER-Chop-Gate).
Schutz läuft über Stops + Sizing, nicht über das Signal.