diff --git a/backtest_patterns_v2.py b/backtest_patterns_v2.py new file mode 100644 index 0000000..c079e1e --- /dev/null +++ b/backtest_patterns_v2.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Die NEUEN Handbuch-Muster messen (User-Vorgabe 2026-07-31 „messe die neuen Muster"). + +Ergänzt wurden am selben Tag: **Dreifach-Top/-Boden · Flagge · Wimpel · Rechteck · +Steigender/Fallender Keil**. Sie stehen in `core/patterns.py` auf `measured=False` +und tragen die Verdict-Stimme NICHT — dieses Skript prüft, ob sich das ändern darf. + +⚠ ZWEI UNTERSCHIEDE ZU `backtest_patterns.py` (bewusst): + 1. **Der ECHTE Detektor wird benutzt** (`core.patterns._detect`), nicht eine + Nachbildung. Das alte Skript hat die Bedingungen in `scan()` NACHGEBAUT — misst + also möglicherweise etwas anderes als das, was live läuft. Nach den fünf + Deployment-Drift-Fällen dieses Tages ist das keine akzeptable Grundlage mehr. + 2. **Exit aus `core/exit_model.py`** statt einer eigenen Kopie (das alte Skript war + ein FÜNFTES Exit-Modell: Trail fest 1,5 · `_MAXH` 288 · kein Time-Stop). + +KONTROLLGRUPPE (entscheidend): derselbe Einstieg/Exit auf einem **generischen +Swing-Bruch** — Bruch des letzten Swing-Hochs/-Tiefs ohne jedes Muster. Nur wenn ein +Muster die Kontrolle in BEIDEN Hälften schlägt, trägt das MUSTER etwas bei und nicht +bloss „Breakout + Trailing" (die Lehre aus dem 30.07.-Kontrolltest). + +Zusätzlich: **Ziel-Trefferquote** — wie oft wird der Measured Move vor dem SL +erreicht? Das ist der ehrliche Muster-Check (alt: nur 13–38 %). +""" +import sys +from collections import defaultdict + +import MetaTrader5 as mt5 + +from core.exit_model import LIVE, simulate +from core.patterns import _atr, _pivots, _detect, _PIVOT_K, _MEASURED + +_BREAK_WIN = 48 # so viele Bars auf den Trigger-Bruch warten +_ATRMIN = 0.06 +_NEW = ("triple_top", "triple_bottom", "flag", "pennant", "wedge_rise", "wedge_fall") + + +def _atr_series(H, L, C, p=14): + out = [None] + trs = [] + for i in range(1, len(C)): + trs.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + out.append(sum(trs[-p:]) / min(len(trs), p)) + return out + + +def scan(H, L, C, A, SP, lo, hi, step=3): + """Läuft die History kausal ab und sammelt (typ, entry_bar, dir, trigger, target). + + Nutzt den ECHTEN Detektor auf dem jeweils bis `i` bekannten Fenster. + """ + found = [] + seen = set() + for i in range(max(lo, 120), min(hi, len(C) - LIVE.max_hold - 1), step): + atr = A[i] + if not atr or atr < _ATRMIN: + continue + piv = _pivots(H[:i + 1], L[:i + 1], _PIVOT_K) + if len(piv) < 3: + continue + try: + pats = _detect(piv, C[i], atr, i) + except Exception: + continue + for m in pats: + if m.get("status") != "forming" or m.get("dir") not in ("bull", "bear"): + continue + key = (m["type"], round(m["trigger"], 2), m["dir"]) + if key in seen: + continue + seen.add(key) + d = 1 if m["dir"] == "bull" else -1 + trig = m["trigger"] + # auf den Trigger-Bruch warten + for j in range(i + 1, min(i + 1 + _BREAK_WIN, len(C) - LIVE.max_hold - 1)): + broke = (H[j] >= trig) if d > 0 else (L[j] <= trig) + if broke: + found.append((m["type"], j, d, trig, m.get("target"), max(A[j] or 0, _ATRMIN))) + break + return found + + +def control(H, L, C, A, lo, hi, step=3): + """KONTROLLE: Bruch des letzten Swing-Hochs/-Tiefs, ohne jedes Muster.""" + out = [] + seen = set() + for i in range(max(lo, 120), min(hi, len(C) - LIVE.max_hold - 1), step): + atr = A[i] + if not atr or atr < _ATRMIN: + continue + piv = _pivots(H[:i + 1], L[:i + 1], _PIVOT_K) + if len(piv) < 2: + continue + for d, kind in ((1, "H"), (-1, "L")): + last = next((p for p in reversed(piv) if p[2] == kind), None) + if not last: + continue + trig = last[1] + key = (kind, round(trig, 2)) + if key in seen: + continue + seen.add(key) + for j in range(i + 1, min(i + 1 + _BREAK_WIN, len(C) - LIVE.max_hold - 1)): + broke = (H[j] >= trig) if d > 0 else (L[j] <= trig) + if broke: + out.append(("control", j, d, trig, None, max(A[j] or 0, _ATRMIN))) + break + return out + + +def evaluate(rows, H, L, C, SP): + """→ {typ: [(R_netto, ziel_getroffen)]}""" + res = defaultdict(list) + for typ, j, d, trig, target, atr in rows: + cost = (SP[j] if SP[j] > 0 else 0.0225) / atr + R = simulate(trig, d, atr, H, L, C, j + 1, LIVE) - cost + hit = None + if target is not None: + hit = False + sl = trig - d * LIVE.sl_atr * atr + for k in range(j + 1, min(j + 1 + LIVE.max_hold, len(C))): + if (L[k] <= sl) if d > 0 else (H[k] >= sl): + break + if (H[k] >= target) if d > 0 else (L[k] <= target): + hit = True; break + res[typ].append((R, hit)) + return res + + +def rep(name, rows): + if not rows or len(rows) < 8: + print(f" {name:<24} n={len(rows):<4} — zu wenige") + return + Rs = [r for r, _ in rows] + n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs) + g = sum(x for x in Rs if x > 0); l = -sum(x for x in Rs if x < 0) + hits = [h for _, h in rows if h is not None] + zt = f"{100*sum(hits)/len(hits):>3.0f}%" if hits else " —" + print(f" {name:<24} n={n:<4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} " + f"PF={(g/l if l > 0 else 9.99):>4.2f} ΣR={s:>+6.0f} Ziel={zt}") + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 120000 + mt5.initialize() + sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") if mt5.symbol_info(c)), None) + bars = None + for req in (n, 100000, 80000, 60000, 40000): + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M30, 0, req) + if bars is not None and len(bars) > 3000: + break + si = mt5.symbol_info(sym); point = si.point; mt5.shutdown() + H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars] + C = [float(b["close"]) for b in bars]; SP = [float(b["spread"]) * point for b in bars] + A = _atr_series(H, L, C); N = len(C); mid = N // 2 + + print("=" * 96) + print(f" NEUE HANDBUCH-MUSTER — {sym} M30 ({N} Bars), Echtkosten") + print(f" Detektor: core/patterns._detect (der ECHTE) · Exit: core/exit_model.py " + f"(Trail {LIVE.mult})") + print(" Urteil: nur wenn ein Typ die KONTROLLE (generischer Swing-Bruch) in BEIDEN") + print(" Hälften schlägt, trägt das MUSTER etwas bei — sonst ist es Breakout+Trailing.") + print("=" * 96) + + label = {"triple_top": "Dreifach-Top", "triple_bottom": "Dreifach-Boden", + "flag": "Flagge", "pennant": "Wimpel", + "wedge_rise": "Steigender Keil", "wedge_fall": "Fallender Keil"} + for lbl, a, b in (("H1 (alt)", _PIVOT_K, mid), ("H2 (neu)", mid, N - LIVE.max_hold - 1)): + print(f"\n{lbl}:") + res = evaluate(scan(H, L, C, A, SP, a, b), H, L, C, SP) + ctl = evaluate(control(H, L, C, A, a, b), H, L, C, SP) + rep("KONTROLLE Swing-Bruch", ctl["control"]) + print(" " + "-" * 74) + for t in _NEW: + rep(label[t], res.get(t, [])) + print(" " + "-" * 74) + for t in sorted(_MEASURED): + if res.get(t): + rep(f"(alt) {t}", res[t]) + + +if __name__ == "__main__": + main() diff --git a/core/engine.py b/core/engine.py index 1dd4471..b12eec3 100644 --- a/core/engine.py +++ b/core/engine.py @@ -368,6 +368,8 @@ class TradingEngine: self._mql5_files_dir = None # \MQL5\Files (lazy, für den Indikator) self._export_trade_marks = self.cfg["trading"].get("export_trade_marks", "false").lower() == "true" self._export_mql5 = self.cfg["trading"].get("export_mql5_levels", "true").lower() == "true" + # Kegel im MT5-Chart: per Default AUS, seit die Pfeile ihn ersetzen (2026-07-31). + self._export_cone = self.cfg["trading"].get("export_cone", "false").lower() == "true" self._draw_last = {"r": None, "s": None} # letzte gezeichnete S/R (Hysterese) try: # WHT-Quellensteuer (Broker behält % je GEWINN ein) → Netto-Anzeige self._wht_pct = max(0.0, float(self.cfg["trading"].get("wht_pct", "0") or 0)) @@ -2271,17 +2273,80 @@ class TradingEngine: lines.append(f"PX;{_bid}") except Exception: pass - # WAHRSCHEINLICHKEITS-KEGEL: CN;;; je Horizont (30/60/120), - # aufsteigend — der Indikator verbindet sie vom aktuellen Bar aus zu zwei - # sich öffnenden Pfaden. ⚠ Streuung, NICHT Richtung: der Kegel ist um den - # aktuellen Kurs zentriert und sagt nur, wie weit der Kurs plausibel läuft. - # Out-of-sample kalibriert (`analyze_cone.py`), 80 %-Band trifft real ~76 %. + _atr5 = 0.0 try: _atr5 = float(((self.wave.snapshot() or {}).get("pb_feats") or {}) .get("atr") or 0.0) - _cn = _cone_build(_bid, _atr5, bands=(80,)) - for lv in (_cn or {}).get("levels", []): - lines.append(f"CN;{lv['minutes']};{lv['lo']};{lv['hi']}") + except Exception: + pass + # ── WAHRSCHEINLICHKEITS-KEGEL (CN) — im Chart per Default AUS + # (User 2026-07-31: „anstatt des Trichters hätte ich gerne einen Pfeil"). + # Die Dashboard-Kachel „Erwartete Spanne" bleibt; `[trading] export_cone=true` + # holt den Kegel in den Chart zurück. + if self._export_cone: + try: + _cn = _cone_build(_bid, _atr5, bands=(80,)) + for lv in (_cn or {}).get("levels", []): + lines.append(f"CN;{lv['minutes']};{lv['lo']};{lv['hi']}") + except Exception: + pass + # ── PFEILE: AR;;;; (art L | S | K) ────────────── + # ⚠ EINORDNUNG — die freie Frage „wohin läuft der Kurs?" ist im Projekt + # GEMESSEN ein Münzwurf (`analyze_reversal.py`: AUC 0,499–0,509 out-of- + # sample, in-sample nur 0,52). Deshalb zeigt KEINER dieser Pfeile eine + # freie Kursprognose, sondern jeweils eine Aussage mit eigener Beleglage: + # L = am nächsten LEVEL, aus dem kalibrierten P(break) (AUC 0,65 oos, + # live kalibriert 27 % vorhergesagt vs 28 % real) — BEDINGTE Aussage + # an einem realen Objekt, die einzige belegte Richtungsangabe. + # S = SQUEEZE-Ausbruch — das einzige 2-Stichproben-validierte + # Richtungssignal (ØR +0,14…+0,23). Erscheint nur bei `active`. + # K = KONSENS der Verdict-Module — ⚠ NICHT kalibriert + # (`analyze_verdict_calibration.py`: in H2 sogar invertiert), heißt + # deshalb „Konsens" und nicht „Prognose". + try: + _dl = dl + _r = (_dl.get("res") or [None])[0] + _s = (_dl.get("sup") or [None])[0] + if _bid and _atr5 > 0: + # (L) nächstes Level = das nähere von beiden + cand = [(abs(x - _bid), x, 1 if x > _bid else -1) + for x in (_r, _s) if x is not None] + if cand: + _d0, _lv0, _dd = min(cand) + _p = _pb_for(_lv0, _dd) # in Prozent (0–100) oder None + # ⚠ TOTBAND 45–55 %: dort ist P(break) ein Münzwurf. Einen + # Pfeil zu zeichnen würde daraus eine Richtungsaussage machen — + # genau der Fehler, den das Projekt heute mehrfach vermieden hat + # (freie Richtung = AUC 0,5). Stattdessen ein FLACHER Pfeil mit + # „unentschieden". + if _p is not None: + if 45 <= _p <= 55: + to = _bid # waagerecht = keine Aussage + txt = f"{_p}% — unentschieden" + elif _p > 55: + to = _lv0 + _dd * 0.5 * _atr5 + txt = f"{_p}% Durchbruch" + else: + to = _bid - _dd * 1.0 * _atr5 + txt = f"{100 - _p}% Abprall" + lines.append(f"AR;L;{round(_bid, 3)};{round(to, 3)};{txt}") + # (S) Squeeze — nur wenn aktiv + _sq = (self.wave.snapshot() or {}).get("squeeze") or {} + if _sq.get("state") == "active" and _sq.get("dir") and _sq.get("level"): + _sd = 1 if _sq["dir"] == "LONG" else -1 + lines.append(f"AR;S;{round(float(_sq['level']), 3)};" + f"{round(float(_sq['level']) + _sd * 2.0 * _atr5, 3)};" + f"Squeeze {_sq['dir']}") + # (K) Konsens — Länge proportional zum Bias-Betrag (max 2×ATR) + _vd = self._verdict(self.data.snapshot(), self.wave.signal(), + self.wave.snapshot(), None, + self.agent.snapshot(), self.elliott.snapshot(), + self.patterns.snapshot()) + _b = float(_vd.get("bias") or 0.0) + if abs(_b) >= 0.2: + lines.append(f"AR;K;{round(_bid, 3)};" + f"{round(_bid + (1 if _b > 0 else -1) * min(abs(_b), 1.0) * 2.0 * _atr5, 3)};" + f"Konsens {_b:+.2f}") except Exception: pass # G/V der offenen Position, direkt unter dem Kurs (User-Wunsch 2026-07-30): @@ -2441,7 +2506,18 @@ class TradingEngine: pv, pdet, pw = 0, "—", 0.0 try: _pats = (snap_parts_patterns or {}).get("patterns") or [] - _p0 = next((m for m in _pats if m.get("dir") in ("bull", "bear")), None) + # ⚠ NUR GEMESSENE Muster-Typen stimmen mit (Fix 2026-07-31): das Gewicht + # 1,0 stammt aus dem Kontrolltest (`backtest_patterns.py` gegen einen + # generischen Swing-Bruch), der nur Doppeltop/-boden, Tasse/inv. Tasse, + # SKS/inv. SKS und auf-/absteigendes Dreieck abdeckte. Die am selben Tag + # aus dem Handbuch ergänzten Typen (Flagge, Wimpel, Rechteck, Keil, + # Dreifach) sind REINE ANZEIGE — sonst würde ein validiertes Gewicht + # still mit unvalidierten Eingaben gefüttert (genau der Deployment-Drift- + # Fehler, der heute fünfmal aufgefallen ist). `measured` kommt aus + # `core/patterns.py`; fehlt das Feld (Alt-Snapshot), gilt es als gemessen. + _p0 = next((m for m in _pats + if m.get("dir") in ("bull", "bear") and m.get("measured", True)), + None) if _p0: pv = 1 if _p0["dir"] == "bull" else -1 _conf = _p0.get("status") == "confirmed" diff --git a/core/patterns.py b/core/patterns.py index 2ab839b..0f03298 100644 --- a/core/patterns.py +++ b/core/patterns.py @@ -41,6 +41,19 @@ _MAX_LOOKBACK = 9 # nur die letzten N Swings betrachten (aktuelle Muster) _MAX_DIST_ATR = 5.0 # Trigger max. so weit vom AKTUELLEN Kurs (sonst nicht mehr # aktionabel — alte, weit weg gelaufene Muster ausblenden) +# Typen, die im Kontrolltest (`backtest_patterns.py` gegen einen generischen +# Swing-Bruch, 2026-07-30) tatsaechlich gemessen wurden. NUR sie tragen die +# Verdict-Stimme; die 2026-07-31 aus dem Handbuch ergaenzten Typen (Flagge, +# Wimpel, Rechteck, Keil, Dreifach) sind REINE ANZEIGE, bis sie gemessen sind. +_MEASURED = {"double_top", "double_bottom", "hs", "inv_hs", + "cup", "inv_cup", "tri_asc", "tri_desc"} + +# Fortsetzungsmuster brauchen eine „Stange" (impulsiver Vorlauf) — ab wann gilt +# ein Swing als Stange, und wie kurz muss die Konsolidierung dagegen sein. +_POLE_MIN_ATR = 2.0 # Vorlauf mindestens so gross +_POLE_MAX_BARS = 20 # ... und in hoechstens so vielen Bars (impulsiv) +_FLAG_MAX_FRAC = 0.5 # Konsolidierung hoechstens so gross wie die Stange + def _atr(highs, lows, closes, p=14): trs = [] @@ -102,6 +115,13 @@ def _detect(piv, cur, atr, last_idx): "target": round(target, 3), "height_atr": round(h / atr, 1), "status": _status(dir_, trigger, cur, extreme), "bars_ago": last_idx - ix[-1], "note": note, + # ⚠ `measured` = war dieser Typ im Kontrolltest (`backtest_patterns.py` + # + generischer Breakout, 2026-07-30)? NUR gemessene Typen dürfen die + # Verdict-Stimme tragen — sonst würde ein validiertes Gewicht still mit + # unvalidierten Eingaben gefüttert (der Deployment-Drift-Fehler des + # Projekts). Gemessen wurden: Doppeltop/-boden, Tasse/inv. Tasse, + # auf-/absteigendes Dreieck, SKS/inv. SKS. + "measured": typ in _MEASURED, }) # über alle 3er/5er-Fenster am aktuellen Rand @@ -156,6 +176,90 @@ def _detect(piv, cur, atr, last_idx): add("cup", "Tasse+Henkel", "bull", q, max(rim_l, rim_r), bot, f"abgerundeter Boden {bot:.2f}, Rand ~{rim_r:.2f} (Henkel-Oberkante)") + # ══ HANDBUCH-MUSTER (ergänzt 2026-07-31, User-Vorgabe „alle Muster aus dem + # Handbuch") — ALLE REINE ANZEIGE (`measured=False`), sie tragen die + # Verdict-Stimme NICHT, weil der Kontrolltest sie nicht abdeckt. ═══════════ + for s2 in range(len(P) - 2): + w = list(zip(kd[s2:], pr[s2:], ix[s2:])) + + # ── Dreifach-Top / -Boden: drei ~gleiche Extreme (H,L,H,L,H bzw. umgekehrt). + # Abgrenzung zur SKS: dort ist das MITTLERE Extrem deutlich weiter draussen. + if len(w) >= 5 and [x[0] for x in w[:5]] == ["H", "L", "H", "L", "H"]: + h1, t1, h2, t2, h3 = (w[i][1] for i in range(5)) + q = _sim(h1, h2, atr) * _sim(h2, h3, atr) + if q > 0.30 and h2 <= max(h1, h3) + 0.3 * atr: # kein Kopf → kein SKS + neck = min(t1, t2) + add("triple_top", "Dreifach-Top", "bear", q, neck, max(h1, h2, h3), + f"drei Hochs {h1:.2f}/{h2:.2f}/{h3:.2f}, Nacken {neck:.2f}") + if len(w) >= 5 and [x[0] for x in w[:5]] == ["L", "H", "L", "H", "L"]: + l1, t1, l2, t2, l3 = (w[i][1] for i in range(5)) + q = _sim(l1, l2, atr) * _sim(l2, l3, atr) + if q > 0.30 and l2 >= min(l1, l3) - 0.3 * atr: + neck = max(t1, t2) + add("triple_bottom", "Dreifach-Boden", "bull", q, neck, min(l1, l2, l3), + f"drei Tiefs {l1:.2f}/{l2:.2f}/{l3:.2f}, Nacken {neck:.2f}") + + # ── Flagge / Wimpel (Fortsetzung): impulsive STANGE, danach eine kleine + # Konsolidierung GEGEN die Stangenrichtung. Flagge = paralleler Kanal, + # Wimpel = konvergierend. Trigger = Ausbruch in Stangenrichtung. + if len(w) >= 4: + pole_h = abs(w[1][1] - w[0][1]) + pole_bars = w[1][2] - w[0][2] + if (pole_h >= _POLE_MIN_ATR * atr and 0 < pole_bars <= _POLE_MAX_BARS): + up = w[1][1] > w[0][1] # Stange nach oben? + cons = [x[1] for x in w[1:5]] + rng = max(cons) - min(cons) + if rng <= _FLAG_MAX_FRAC * pole_h and len(cons) >= 3: + # konvergierend? (Schwankung nimmt ab → Wimpel) + d1 = abs(cons[1] - cons[0]); d2 = abs(cons[-1] - cons[-2]) + pennant = d2 < d1 * 0.7 + trig = max(cons) if up else min(cons) + ext = min(cons) if up else max(cons) + typ = "pennant" if pennant else "flag" + nm = ("Wimpel" if pennant else "Flagge") + (" (bullisch)" if up else " (bärisch)") + add(typ, nm, "bull" if up else "bear", 0.55, trig, ext, + f"Stange {pole_h/atr:.1f}×ATR in {pole_bars} Bars, " + f"{'konvergierende' if pennant else 'flache'} Konsolidierung") + + # ── Rechteck (Range): zwei ~flache Hochs UND zwei ~flache Tiefs. Das Handbuch + # führt es als Fortsetzung — die Ausbruchsrichtung ist aber offen, deshalb + # NEUTRAL (wie das symmetrische Dreieck), nur als Kontext. + hh = [(x[2], x[1]) for x in zip(kd, pr, ix) if x[0] == "H"][-2:] + ll = [(x[2], x[1]) for x in zip(kd, pr, ix) if x[0] == "L"][-2:] + if len(hh) == 2 and len(ll) == 2: + top = (hh[0][1] + hh[1][1]) / 2.0 + bot = (ll[0][1] + ll[1][1]) / 2.0 + if (abs(hh[1][1] - hh[0][1]) <= _NECK_TOL_ATR * atr + and abs(ll[1][1] - ll[0][1]) <= _NECK_TOL_ATR * atr + and (top - bot) >= _MIN_HEIGHT_ATR * atr): + out.append({ + "type": "rectangle", "name": "Rechteck (Range)", "dir": "neutral", + "quality": round(_sim(hh[0][1], hh[1][1], atr) + * _sim(ll[0][1], ll[1][1], atr), 2), + "trigger": round(top, 3), "target": None, + "height_atr": round((top - bot) / atr, 1), "status": "forming", + "bars_ago": last_idx - ix[-1], "measured": False, + "note": f"Range {bot:.2f}–{top:.2f} — Ausbruchsrichtung offen", + }) + # ── Keil: beide Linien in DIESELBE Richtung geneigt und konvergierend. + # Steigender Keil = bärisch, fallender Keil = bullisch (Handbuch). + dh = hh[1][1] - hh[0][1] + dl = ll[1][1] - ll[0][1] + conv = abs(hh[1][1] - ll[1][1]) < abs(hh[0][1] - ll[0][1]) - 0.3 * atr + if conv and dh > 0.3 * atr and dl > 0.3 * atr and dl > dh: + add("wedge_rise", "Steigender Keil", "bear", 0.5, + min(ll[0][1], ll[1][1]), max(hh[0][1], hh[1][1]), + "beide Linien steigen, Tiefs schneller — Erschöpfung nach oben") + # ⚠ Bedingung korrigiert (2026-07-31): stand `dh > dl` — bei einem FALLENDEN + # Keil fällt aber die OBERE Linie schneller (sonst konvergiert nichts), also + # `dh < dl`. Mit der alten Fassung war die Bedingung in sich widersprüchlich + # (`conv` verlangt Konvergenz) → der Typ feuerte in 20.000 Bars KEIN EINZIGES + # MAL. Beim Spiegelbild (steigender Keil, `dl > dh`) stimmte es. + if conv and dh < -0.3 * atr and dl < -0.3 * atr and dh < dl: + add("wedge_fall", "Fallender Keil", "bull", 0.5, + max(hh[0][1], hh[1][1]), min(ll[0][1], ll[1][1]), + "beide Linien fallen, Hochs schneller — Erschöpfung nach unten") + # ── Dreiecke (Fortsetzung): letzte 2 Hochs + 2 Tiefs, Steigungen prüfen ── highs = [(x[2], x[1]) for x in zip(kd, pr, ix) if x[0] == "H"][-2:] lows = [(x[2], x[1]) for x in zip(kd, pr, ix) if x[0] == "L"][-2:] @@ -183,6 +287,7 @@ def _detect(piv, cur, atr, last_idx): "target": None, "height_atr": round(wide / atr, 1), "status": "forming", "bars_ago": last_idx - ix[-1], "note": f"konvergierend (Hochs fallen, Tiefs steigen) — Ausbruchsrichtung offen", + "measured": False, # neutral, ohne Richtung → nie stimmberechtigt }) # Duplikate (gleicher Typ+Trigger) zusammenfassen, beste Qualität behalten diff --git a/mql5/SR_Levels.mq5 b/mql5/SR_Levels.mq5 index dae1f7c..d1e5239 100644 --- a/mql5/SR_Levels.mq5 +++ b/mql5/SR_Levels.mq5 @@ -12,7 +12,19 @@ //| 4) Der Bot muss laufen (schreibt die CSV alle ~5 s). | //+------------------------------------------------------------------+ #property copyright "Oil Trading Bot" -#property version "1.31" +#property version "1.32" +// v1.32 (2026-07-31): RICHTUNGS-PFEILE (AR;;;;) statt des +// Kegels (User-Wunsch). DREI Arten mit UNTERSCHIEDLICHER Beleglage: +// L blau = am naechsten Level, aus dem kalibrierten P(break) +// (AUC 0,65 oos, live kalibriert) - BEDINGTE Aussage +// S neongruen = Squeeze-Ausbruch, das EINZIGE 2-Stichproben-validierte +// Richtungssignal - erscheint nur bei `active` +// K grau = Konsens der Verdict-Module - NICHT kalibriert, heisst +// deshalb "Konsens" und nicht "Prognose" +// ⚠ KEINER ist eine freie Kursprognose: die ist im Projekt gemessen ein +// Muenzwurf (analyze_reversal.py, AUC 0,499-0,509 oos, in-sample 0,52). +// Der Kegel bleibt im Code (InpShowCone), der Bot exportiert ihn aber per +// Default nicht mehr ([trading] export_cone). // v1.31 (2026-07-31): WAHRSCHEINLICHKEITS-KEGEL (CN;;;) — zwei sich // oeffnende, gepunktete Pfade vom aktuellen Bar ueber 30/60/120 min. // ⚠ KEINE Richtungsprognose, sondern STREUUNG: wie weit der Kurs plausibel @@ -156,6 +168,11 @@ input color InpConsolColor = C'88,166,255'; // Box-Farbe (blau) input int InpConsolAlpha = 8; // Deckkraft in % (komprimiert: x2) input int InpConsolBars = 10; // Box-Laenge in Bars (= _SQ_N im Bot) input bool InpShowCone = true; // Wahrscheinlichkeits-Kegel (Streuung, NICHT Richtung) +input bool InpShowArrows = true; // Richtungs-Pfeile (Level / Squeeze / Konsens) +input color InpArrLevel = C'88,166,255'; // Pfeil am naechsten Level (P(break)) - blau +input color InpArrSqueeze = C'0,255,127'; // Squeeze-Ausbruch (validiert) - neongruen +input color InpArrConsensus = C'139,148,158'; // Konsens der Module (NICHT kalibriert) - grau +input int InpArrBars = 14; // Pfeil-Laenge in Bars (nach rechts) input color InpConeColor = C'139,148,158'; // Kegel-Farbe (neutral grau - bewusst KEINE Richtungsfarbe) input bool InpShowPnL = true; // G/V der offenen Position unter dem Kurs input color InpPnLUpColor = C'0,255,127'; // G/V im Plus (neongruen) @@ -720,6 +737,55 @@ void Redraw() g_coneT = t1; g_coneLo = clo; g_coneHi = chi; continue; } + // RICHTUNGS-PFEILE: AR;;;; art = L | S | K + // ⚠ KEINE freie Kursprognose (die ist gemessen ein Muenzwurf) - jede Art hat + // ihre eigene Beleglage, s. Versionshinweis oben. + if(typ == "AR") + { + if(k < 5) continue; + if(!InpShowArrows) continue; + string art = p[1]; + double a_from = StringToDouble(p[2]); + double a_to = StringToDouble(p[3]); + string a_txt = p[4]; + if(a_from <= 0 || a_to <= 0) continue; + color ac = (art == "S") ? InpArrSqueeze + : (art == "K") ? InpArrConsensus : InpArrLevel; + int aw = (art == "S") ? 3 : 2; // der validierte Pfeil dicker + datetime at0 = iTime(_Symbol, PERIOD_CURRENT, 0); + datetime at1 = at0 + (datetime)(PeriodSeconds() * InpArrBars); + string an = PFX + "ARR_" + art; + if(ObjectCreate(0, an, OBJ_ARROWED_LINE, 0, at0, a_from, at1, a_to)) + { + ObjectSetInteger(0, an, OBJPROP_COLOR, ac); + ObjectSetInteger(0, an, OBJPROP_WIDTH, aw); + ObjectSetInteger(0, an, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, an, OBJPROP_RAY_RIGHT, false); + ObjectSetInteger(0, an, OBJPROP_BACK, false); + ObjectSetInteger(0, an, OBJPROP_SELECTABLE, false); + } + else + { + ObjectMove(0, an, 0, at0, a_from); + ObjectMove(0, an, 1, at1, a_to); + ObjectSetInteger(0, an, OBJPROP_COLOR, ac); + } + ObjectSetString(0, an, OBJPROP_TOOLTIP, a_txt); + // Beschriftung an der Pfeilspitze (rechtsbuendig wie alle anderen Labels) + string atn = PFX + "ALBL_" + art; // NICHT "T..." (s. RepositionLabels) + if(ObjectCreate(0, atn, OBJ_TEXT, 0, at1, a_to)) + { + ObjectSetInteger(0, atn, OBJPROP_COLOR, ac); + ObjectSetInteger(0, atn, OBJPROP_FONTSIZE, InpLabelSize); + ObjectSetInteger(0, atn, OBJPROP_ANCHOR, ANCHOR_LEFT); + ObjectSetInteger(0, atn, OBJPROP_BACK, false); + ObjectSetInteger(0, atn, OBJPROP_SELECTABLE, false); + } + else + ObjectMove(0, atn, 0, at1, a_to); + ObjectSetString(0, atn, OBJPROP_TEXT, " " + a_txt); + continue; + } // AKTUELLER KURS oben rechts, fett (User-Wunsch): PX;bid if(typ == "PX") {