From 72c2aedc1ab62bfb2b1c1f9b32ffb031d1b1cc25 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Fri, 21 Aug 2026 15:55:36 +0200 Subject: [PATCH] Muster als gefuellte geometrische Formen im MT5-Chart (v1.47) User-Vorgabe: Dreiecke/Rechtecke/Ellipsen ueber OBJ_* an den berechneten Swings verankern, mit leichtem Grauton gefuellt. PT-Zeile traegt jetzt die Geometrie: PT;;;;;;
;;;;;... dreieck (3 Anker) -> OBJ_TRIANGLE Doppeltop/-boden, Dreiecke, Keile, Wimpel dreieck2 (5 Anker) -> 2x OBJ_TRIANGLE SKS, inv. SKS, Dreifach-Top/-Boden rechteck (2 Anker) -> OBJ_RECTANGLE Rechteck (Range), Flagge ellipse (3 Anker) -> OBJ_ELLIPSE Tasse+Henkel, inverse Tasse dreieck2 braucht ZWEI Objekte, weil OBJ_TRIANGLE nur drei Punkte kennt; die beiden Dreiecke (Schulter-Kopf, Kopf-Schulter) kacheln die Silhouette exakt. OBJPROP_FILL faerbt mit OBJPROP_COLOR - ein Objekt kann keine graue Flaeche UND einen farbigen Rand haben. Deshalb Flaeche neutral grau (InpMusterFill) und OBJPROP_BACK (hinter den Kerzen); die Richtungsaussage tragen Trigger- und Ziellinie darueber. Inhaltlich richtig: die Form ist eine Beobachtung, keine Richtungsprognose. Python (core/patterns.py): add() nimmt form + punkte, legt sie als form/anker in den Muster-dict. REIN ADDITIV - kein bestehender Schluessel aendert sich. Neuer Helfer _keil(): schneidet die beiden Linien und DECKELT den Apex auf last_idx+40 (ein fast paralleles Paar schneidet sich im Unendlichen); nach links begrenzt, damit der Apex nicht im Muster liegt. _anker() rechnet einen Index jenseits des letzten Bars ueber die Bar-Dauer hoch - sonst waere der Apex auf den letzten Bar geklemmt und jedes Dreieck haette einen senkrechten rechten Rand. Bar-Dauer aus den DATEN, nicht aus der TF-Konstante. Zeitstempel sind ROHE Broker-Zeit (rates["time"]) - genau das, was iTime() liefert. Keine Offset-Rechnung, gleiche Konvention wie EQ; und TR;. Vor dem Export wird geprueft, dass Form und Punktzahl zueinander passen - ein Dreieck mit zwei Punkten wuerde der Indikator still verwerfen. Abwaertsvertraeglich in beide Richtungen. Verifiziert: 6 synthetische Muster (Form, Punktzahl, aufsteigende Zeitstempel, Apex in der Zukunft) + live Snapshot patterns_tf.M5 form=dreieck anker=3 + CSV-Zeile mit Geometrie. Compile 0 errors, Pipeline gruen, deployt. --- CLAUDE.md | 60 ++++++++++++++++++++++++ core/engine.py | 28 ++++++++++- core/patterns.py | 114 ++++++++++++++++++++++++++++++++++++++------- mql5/SR_Levels.mq5 | 81 +++++++++++++++++++++++++++++++- 4 files changed, 264 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 990be67..d271e84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9504,3 +9504,63 @@ Zeitebene. `.ex5` frisch, `0 errors`. **Nur das Ergebnis fehlte** — dieselbe Klasse wie die vier dokumentierten Fälle „Erfolg melden, wo nichts geprüft wurde". Abschaltbar: `InpShowMuster=false`; Farben `InpMusterBullColor`/`…BearColor`. + +## ✅ MUSTER ALS GEFÜLLTE GEOMETRISCHE FORMEN IM MT5-CHART (2026-08-21, v1.47) + +User-Vorgabe: Dreiecke/Rechtecke/Ellipsen über `OBJ_*` an den berechneten +Swings verankern, **„mit leichtem Grauton gefüllt"**. + +**Die `PT;`-Zeile trägt jetzt die Geometrie mit:** +``` +PT;;;;;;;;;;;… +``` +| `form` | Anker | MT5-Objekt | Muster | +|---|---|---|---| +| `dreieck` | 3 | `OBJ_TRIANGLE` | Doppeltop/-boden, Dreiecke, Keile, Wimpel | +| `dreieck2` | 5 | **2×** `OBJ_TRIANGLE` | SKS, inverse SKS, Dreifach-Top/-Boden | +| `rechteck` | 2 | `OBJ_RECTANGLE` | Rechteck (Range), Flagge | +| `ellipse` | 3 | `OBJ_ELLIPSE` | Tasse+Henkel, inverse Tasse | + +⚠ **`dreieck2` braucht ZWEI Objekte**, weil `OBJ_TRIANGLE` nur drei Punkte +kennt. Die Dreiecke (Schulter–Kopf und Kopf–Schulter) kacheln die Silhouette +exakt — kein Annähern, keine Überdeckung. + +⚠⚠ **Die Füllung kann KEINEN andersfarbigen Rand haben:** `OBJPROP_FILL` +färbt mit `OBJPROP_COLOR`. Deshalb ist die Fläche neutral grau +(`InpMusterFill`, `C'205,205,205'`) **und `OBJPROP_BACK`** — sie liegt hinter +den Kerzen; die Richtungsaussage tragen die farbige Trigger- und Ziellinie +darüber. Das ist auch inhaltlich richtig: die **Form** ist eine Beobachtung, +keine Richtungsprognose. + +**Umsetzung Python** (`core/patterns.py`): `add()` nimmt `form` und `punkte` +(Bar-Index + Preis) und legt sie als `form`/`anker` in den Muster-dict — **rein +additiv**, kein bestehender Schlüssel ändert sich (das Verdict-Gewicht liest +ohnehin nur `type`/`dir`/`status`/`quality`). +⚠ Neuer Helfer **`_keil()`** für die konvergierenden Muster: er schneidet die +beiden Linien und **deckelt den Apex** auf `last_idx + 40` — ein fast +paralleles Linienpaar schneidet sich sonst im Unendlichen und spännte ein +Dreieck über den halben Chart. Nach links begrenzt auf einen Bar hinter dem +letzten Stützpunkt, sonst läge der Apex INNERHALB des Musters und die Form +würde sich selbst durchdringen. +⚠ `_anker()` rechnet einen Index **jenseits des letzten Bars** über die +Bar-Dauer hoch (`T` hat dafür keinen Eintrag). Ohne das wäre der Apex auf den +letzten Bar geklemmt und **jedes Dreieck hätte einen senkrechten rechten Rand**. +Die Bar-Dauer kommt aus den DATEN (`T[-1]-T[-2]`), nicht aus der TF-Konstante — +so stimmt sie auch, wenn der Broker Bars auslässt. +⚠ Die Zeitstempel sind **rohe Broker-Zeit** aus `rates["time"]` — genau das, +was `iTime()` liefert und was ein `OBJ_*`-Anker erwartet. **Keine +Offset-Rechnung**, gleiche Konvention wie `EQ;` und `TR;`. + +✅ **Vor dem Export geprüft, dass Form und Punktzahl zueinander passen** — ein +Dreieck mit zwei Punkten würde der Indikator still verwerfen, und der Fehler +wäre von außen unsichtbar (dieselbe Klasse wie der `PERIOD_CURRENT`-Fall). +✅ **Abwärtsverträglich in beide Richtungen:** eine alte CSV ohne Geometrie +fällt still auf „nur Linien" zurück, ein alter Indikator ignoriert die +Zusatzfelder. +✅ **Gegenprobe an sechs synthetischen Mustern** (Doppeltop, Doppelboden, SKS, +Tasse, auf-/absteigendes Dreieck): Form, Punktzahl und aufsteigende Zeitstempel +stimmen; der Dreiecks-Apex landet korrekt in der Zukunft. +✅ **Live verifiziert:** Snapshot `patterns_tf.M5 = {form: dreieck, anker: 3}`, +CSV-Zeile +`PT;M5;Doppeltop;bear;87.384;86.814;dreieck;1787327400;87.954;1787328000;87.384;1787330100;87.914`. +Abschaltbar wie bisher `InpShowMuster=false`; Füllfarbe `InpMusterFill`. diff --git a/core/engine.py b/core/engine.py index 819ccb8..40efc1f 100644 --- a/core/engine.py +++ b/core/engine.py @@ -2989,6 +2989,10 @@ class TradingEngine: "target": best.get("target"), "height_atr": best.get("height_atr"), "bild": _PAT_BILD.get(best.get("type")), + # Geometrie fuer den MT5-Export (User-Vorgabe 2026-08-21: + # die Muster als gefuellte Formen zeichnen). + "form": best.get("form"), + "anker": best.get("anker"), } except Exception: out[lbl] = None @@ -3602,7 +3606,14 @@ class TradingEngine: except Exception: pass # ── BESTAETIGTES CHARTMUSTER je Zeitebene (User-Wunsch 2026-08-21) - # PT;;;;; + # PT;;;;;;;;;;;… + # = dreieck (3 Anker) | dreieck2 (5) | rechteck (2) | + # ellipse (3). Der Indikator zeichnet daraus das + # passende native OBJ_* und fuellt es grau. + # ⚠ Die Zeitstempel sind ROHE BROKER-Zeit (aus `rates["time"]`) — + # genau das, was `iTime()` liefert und was ein OBJ_*-Anker + # erwartet. KEINE Offset-Rechnung: die waere hier falsch (wie bei + # `EQ;`/`TR;`, die dieselbe Konvention nutzen). # Der Indikator zeichnet NUR die Zeile, deren zu SEINEM Chart # passt — so bekommt jedes Fenster (M1/M5/M15/H1) sein eigenes # Muster, ohne dass fuenf Linien uebereinander liegen. @@ -3623,8 +3634,21 @@ class TradingEngine: if not _trg or not _zl: continue _nm = str(_m.get("name") or "").replace(";", ",")[:28] + _fm = str(_m.get("form") or "") + _ak = _m.get("anker") or [] + # ⚠ Nur exportieren, wenn Form UND Punktzahl zueinander + # passen — ein Dreieck mit zwei Punkten wuerde der + # Indikator still verwerfen, und der Fehler waere von + # aussen unsichtbar (die Klasse "Erfolg melden, wo nichts + # geprueft wurde"). + _soll = {"dreieck": 3, "dreieck2": 5, "rechteck": 2, + "ellipse": 3}.get(_fm, 0) + _geo = "" + if _soll and len(_ak) == _soll: + _geo = ";" + _fm + "".join( + f";{int(_pt[0])};{float(_pt[1]):.3f}" for _pt in _ak) lines.append(f"PT;{_tf};{_nm};{_m.get('dir') or ''};" - f"{float(_trg):.3f};{float(_zl):.3f}") + f"{float(_trg):.3f};{float(_zl):.3f}{_geo}") except Exception as _pe: log.debug(f"PT-Export: {_pe}") diff --git a/core/patterns.py b/core/patterns.py index cda3be3..154531b 100644 --- a/core/patterns.py +++ b/core/patterns.py @@ -96,7 +96,32 @@ def _status(dir_, trigger, cur, extreme): return "forming" -def _detect(piv, cur, atr, last_idx): +def _keil(a1, a2, b1, b2, last_idx, max_extra=40): + """Zwei konvergierende Linien -> DREIECK: linke Kante + Apex. + + `a`/`b` je zwei (bar_index, preis). Der Apex ist der Schnittpunkt. + ⚠ Er wird auf `last_idx + max_extra` GEDECKELT: ein fast paralleles + Linienpaar schneidet sich irgendwo im Unendlichen und wuerde sonst ein + Dreieck ueber den halben Chart aufspannen. Nach links begrenzt auf + mindestens einen Bar hinter dem letzten Stuetzpunkt, damit der Apex + nicht INNERHALB des Musters landet und die Form sich selbst durchdringt. + """ + (x1, y1), (x2, y2) = a1, a2 + (x3, y3), (x4, y4) = b1, b2 + ma = (y2 - y1) / (x2 - x1) if x2 != x1 else 0.0 + mb = (y4 - y3) / (x4 - x3) if x4 != x3 else 0.0 + x0 = min(x1, x3) + if abs(ma - mb) < 1e-12: + xa = last_idx + max_extra + else: + xa = (y3 - mb * x3 - y1 + ma * x1) / (ma - mb) + xa = max(min(xa, last_idx + max_extra), max(x2, x4) + 1) + return [(int(x0), y1 + ma * (x0 - x1)), + (int(x0), y3 + mb * (x0 - x3)), + (int(round(xa)), y1 + ma * (xa - x1))] + + +def _detect(piv, cur, atr, last_idx, T=None, bar_s=0): """Sucht Muster in den letzten Swings. → Liste Muster-dicts.""" out = [] P = piv[-_MAX_LOOKBACK:] @@ -104,7 +129,30 @@ def _detect(piv, cur, atr, last_idx): return out pr = [p[1] for p in P]; kd = [p[2] for p in P]; ix = [p[0] for p in P] - def add(typ, name, dir_, quality, trigger, extreme, note): + def _anker(punkte): + """[(bar_index, preis)] -> [[epoch, preis]] fuer den MT5-Export. + + ⚠ Ein Index JENSEITS des letzten Bars (der Dreiecks-Apex liegt in + der Zukunft) wird ueber die Bar-Dauer hochgerechnet — `T` hat dafuer + keinen Eintrag. Ohne das waere der Apex auf den letzten Bar geklemmt + und jedes Dreieck haette einen senkrechten rechten Rand. + """ + if not T: + return None + aus = [] + for i, pr_ in punkte: + i = int(i) + if 0 <= i < len(T): + t = int(T[i]) + elif i >= len(T): + t = int(T[-1] + (i - len(T) + 1) * max(bar_s, 60)) + else: + t = int(T[0]) + aus.append([t, round(float(pr_), 3)]) + return aus + + def add(typ, name, dir_, quality, trigger, extreme, note, + form=None, punkte=None): h = abs(extreme - trigger) if h < _MIN_HEIGHT_ATR * atr: # zu flach = Rauschen return @@ -122,6 +170,12 @@ def _detect(piv, cur, atr, last_idx): # Projekts). Gemessen wurden: Doppeltop/-boden, Tasse/inv. Tasse, # auf-/absteigendes Dreieck, SKS/inv. SKS. "measured": typ in _MEASURED, + # ⚠ Geometrie fuer den MT5-Export (User-Vorgabe 2026-08-21: die + # Muster als gefuellte Formen zeichnen). REIN ADDITIV — kein + # bestehender Schluessel aendert sich; das Verdict-Gewicht liest + # ohnehin nur type/dir/status/quality. + "form": form, + "anker": _anker(punkte) if punkte else None, }) # über alle 3er/5er-Fenster am aktuellen Rand @@ -133,14 +187,16 @@ def _detect(piv, cur, atr, last_idx): q = _sim(Ha, Hb, atr) if q > 0.35 and Lm < min(Ha, Hb): add("double_top", "Doppeltop", "bear", q, Lm, max(Ha, Hb), - f"zwei Hochs {Ha:.2f}/{Hb:.2f}, Nackenlinie {Lm:.2f}") + f"zwei Hochs {Ha:.2f}/{Hb:.2f}, Nackenlinie {Lm:.2f}", + "dreieck", [(w[0][2], Ha), (w[1][2], Lm), (w[2][2], Hb)]) # ── Doppelboden: L, H, L ── if len(w) >= 3 and w[0][0] == "L" and w[1][0] == "H" and w[2][0] == "L": La, Hm, Lb = w[0][1], w[1][1], w[2][1] q = _sim(La, Lb, atr) if q > 0.35 and Hm > max(La, Lb): add("double_bottom", "Doppelboden", "bull", q, Hm, min(La, Lb), - f"zwei Tiefs {La:.2f}/{Lb:.2f}, Nackenlinie {Hm:.2f}") + f"zwei Tiefs {La:.2f}/{Lb:.2f}, Nackenlinie {Hm:.2f}", + "dreieck", [(w[0][2], La), (w[1][2], Hm), (w[2][2], Lb)]) # ── Kopf-Schulter (SKS): H, L, H, L, H (Kopf = mittleres Hoch am höchsten) ── if len(w) >= 5 and [x[0] for x in w[:5]] == ["H", "L", "H", "L", "H"]: ls, t1, head, t2, rs = (w[i][1] for i in range(5)) @@ -149,7 +205,8 @@ def _detect(piv, cur, atr, last_idx): q = _sim(ls, rs, atr) * _sim(t1, t2, atr) if q > 0.25 and abs(t1 - t2) < _NECK_TOL_ATR * atr: add("hs", "Kopf-Schulter", "bear", q, neck, head, - f"Kopf {head:.2f}, Schultern {ls:.2f}/{rs:.2f}, Nacken {neck:.2f}") + f"Kopf {head:.2f}, Schultern {ls:.2f}/{rs:.2f}, Nacken {neck:.2f}", + "dreieck2", [(w[i][2], w[i][1]) for i in range(5)]) # ── Inverse SKS: L, H, L, H, L ── if len(w) >= 5 and [x[0] for x in w[:5]] == ["L", "H", "L", "H", "L"]: ls, t1, head, t2, rs = (w[i][1] for i in range(5)) @@ -158,7 +215,8 @@ def _detect(piv, cur, atr, last_idx): q = _sim(ls, rs, atr) * _sim(t1, t2, atr) if q > 0.25 and abs(t1 - t2) < _NECK_TOL_ATR * atr: add("inv_hs", "Inverse SKS", "bull", q, neck, head, - f"Kopf {head:.2f}, Schultern {ls:.2f}/{rs:.2f}, Nacken {neck:.2f}") + f"Kopf {head:.2f}, Schultern {ls:.2f}/{rs:.2f}, Nacken {neck:.2f}", + "dreieck2", [(w[i][2], w[i][1]) for i in range(5)]) # ── (Inverse) Tasse+Henkel (heuristisch): Rand-Extrema ~gleich, Boden/Top # dazwischen, breit; Henkel = letzter kleiner Gegen-Swing. ── if len(w) >= 3: @@ -168,13 +226,15 @@ def _detect(piv, cur, atr, last_idx): q = _sim(rim_l, rim_r, atr) * 0.9 if q > 0.4 and top > max(rim_l, rim_r): add("inv_cup", "Inverse Tasse+Henkel", "bear", q, min(rim_l, rim_r), top, - f"abgerundetes Top {top:.2f}, Rand ~{rim_r:.2f} (Henkel-Unterkante)") + f"abgerundetes Top {top:.2f}, Rand ~{rim_r:.2f} (Henkel-Unterkante)", + "ellipse", [(w[0][2], rim_l), (w[2][2], rim_r), (w[1][2], top)]) if w[0][0] == "H" and w[1][0] == "L" and w[2][0] == "H" and span >= 10: rim_l, bot, rim_r = w[0][1], w[1][1], w[2][1] # Tasse (abger. Boden) q = _sim(rim_l, rim_r, atr) * 0.9 if q > 0.4 and bot < min(rim_l, rim_r): add("cup", "Tasse+Henkel", "bull", q, max(rim_l, rim_r), bot, - f"abgerundeter Boden {bot:.2f}, Rand ~{rim_r:.2f} (Henkel-Oberkante)") + f"abgerundeter Boden {bot:.2f}, Rand ~{rim_r:.2f} (Henkel-Oberkante)", + "ellipse", [(w[0][2], rim_l), (w[2][2], rim_r), (w[1][2], bot)]) # ══ HANDBUCH-MUSTER (ergänzt 2026-07-31, User-Vorgabe „alle Muster aus dem # Handbuch") — ALLE REINE ANZEIGE (`measured=False`), sie tragen die @@ -190,14 +250,16 @@ def _detect(piv, cur, atr, last_idx): 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}") + f"drei Hochs {h1:.2f}/{h2:.2f}/{h3:.2f}, Nacken {neck:.2f}", + "dreieck2", [(w[i][2], w[i][1]) for i in range(5)]) 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}") + f"drei Tiefs {l1:.2f}/{l2:.2f}/{l3:.2f}, Nacken {neck:.2f}", + "dreieck2", [(w[i][2], w[i][1]) for i in range(5)]) # ── Flagge / Wimpel (Fortsetzung): impulsive STANGE, danach eine kleine # Konsolidierung GEGEN die Stangenrichtung. Flagge = paralleler Kanal, @@ -217,9 +279,16 @@ def _detect(piv, cur, atr, last_idx): 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)") + _ci = [x[2] for x in w[1:5]] + # Wimpel = konvergierend -> Dreieck; Flagge = paralleler + # Kanal -> Rechteck (zwei diagonale Ecken). + _pk = ([(_ci[0], max(cons)), (_ci[0], min(cons)), + (_ci[-1], cons[-1])] if pennant + else [(_ci[0], max(cons)), (_ci[-1], min(cons))]) 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") + f"{'konvergierende' if pennant else 'flache'} Konsolidierung", + "dreieck" if pennant else "rechteck", _pk) # ── Rechteck (Range): zwei ~flache Hochs UND zwei ~flache Tiefs. Das Handbuch # führt es als Fortsetzung — die Ausbruchsrichtung ist aber offen, deshalb @@ -240,6 +309,8 @@ def _detect(piv, cur, atr, last_idx): "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", + "form": "rechteck", + "anker": _anker([(hh[0][0], top), (ll[1][0], bot)]), }) # ── Keil: beide Linien in DIESELBE Richtung geneigt und konvergierend. # Steigender Keil = bärisch, fallender Keil = bullisch (Handbuch). @@ -249,7 +320,8 @@ def _detect(piv, cur, atr, last_idx): 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") + "beide Linien steigen, Tiefs schneller — Erschöpfung nach oben", + "dreieck", _keil(hh[0], hh[1], ll[0], ll[1], last_idx)) # ⚠ 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 @@ -258,7 +330,8 @@ def _detect(piv, cur, atr, last_idx): 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") + "beide Linien fallen, Hochs schneller — Erschöpfung nach unten", + "dreieck", _keil(hh[0], hh[1], ll[0], ll[1], last_idx)) # ── 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:] @@ -275,11 +348,13 @@ def _detect(piv, cur, atr, last_idx): if flat_h and rising_l: # aufsteigendes Dreieck (bullisch) res_ = max(h1p, h2p) add("tri_asc", "Aufsteigendes Dreieck", "bull", 0.6, res_, min(l1p, l2p), - f"flacher Widerstand {res_:.2f}, steigende Tiefs") + f"flacher Widerstand {res_:.2f}, steigende Tiefs", + "dreieck", _keil(highs[0], highs[1], lows[0], lows[1], last_idx)) elif flat_l and falling_h: # absteigendes Dreieck (bärisch) sup_ = min(l1p, l2p) add("tri_desc", "Absteigendes Dreieck", "bear", 0.6, sup_, max(h1p, h2p), - f"flacher Support {sup_:.2f}, fallende Hochs") + f"flacher Support {sup_:.2f}, fallende Hochs", + "dreieck", _keil(highs[0], highs[1], lows[0], lows[1], last_idx)) elif falling_h and rising_l: # symmetrisch → Ausbruch offen (nur Anzeige) out.append({ "type": "tri_sym", "name": "Symmetrisches Dreieck", "dir": "neutral", @@ -288,6 +363,9 @@ def _detect(piv, cur, atr, last_idx): "status": "forming", "bars_ago": last_idx - ix[-1], "note": f"konvergierend (Hochs fallen, Tiefs steigen) — Ausbruchsrichtung offen", "measured": False, # neutral, ohne Richtung → nie stimmberechtigt + "form": "dreieck", + "anker": _anker(_keil(highs[0], highs[1], lows[0], lows[1], + last_idx)), }) # Duplikate (gleicher Typ+Trigger) zusammenfassen, beste Qualität behalten @@ -376,8 +454,12 @@ class PatternDetector: atr = _atr(H, L, C) if not atr or atr <= 0: return + T = [int(b["time"]) for b in bars] + # ⚠ Bar-Dauer aus den DATEN, nicht aus der TF-Konstante — so + # stimmt sie auch, wenn der Broker Bars auslaesst. + _bs = (T[-1] - T[-2]) if len(T) > 1 else 0 piv = _pivots(H, L, _PIVOT_K) - patterns = _detect(piv, C[-1], atr, len(H) - 1) + patterns = _detect(piv, C[-1], atr, len(H) - 1, T, _bs) with self._lock: self._snap = {"patterns": patterns, "atr": round(atr, 4), "cur": round(C[-1], 3), "ts": now, diff --git a/mql5/SR_Levels.mq5 b/mql5/SR_Levels.mq5 index 2f9f268..6e6dce4 100644 --- a/mql5/SR_Levels.mq5 +++ b/mql5/SR_Levels.mq5 @@ -12,7 +12,7 @@ //| 4) Der Bot muss laufen (schreibt die CSV alle ~5 s). | //+------------------------------------------------------------------+ #property copyright "Oil Trading Bot" -#property version "1.46" +#property version "1.47" // v1.42 (2026-08-19): Y-Stapel oben rechts GEMESSEN statt geschaetzt. Der // Faktor 1,8 aus v1.41 war nur die naechste Schaetzung; jetzt liefert // TextGetSize die TATSAECHLICHE Pixelhoehe (LabelH). Ausserdem wird Y bei @@ -276,6 +276,9 @@ input color InpZoneSupColor = C'20,45,35'; // Support-Zone (dunkelgruen) input bool InpShowMuster = true; // bestaetigtes Chartmuster zeichnen input color InpMusterBullColor = C'70,180,100';// bullisch input color InpMusterBearColor = C'220,90,80'; // baerisch +// ⚠ "leichter Grauton" (User-Vorgabe 2026-08-21). Die Flaeche traegt KEINE +// Richtungsaussage — die steht in den farbigen Trigger-/Ziellinien darueber. +input color InpMusterFill = C'205,205,205';// Fuellung der Musterform const string PFX = "SRB_"; // Präfix S/R-Linien (aus Datei) const string RPFX = "SRR_"; // Präfix dynamische Range (aus Chart) @@ -342,6 +345,33 @@ string TFName(ENUM_TIMEFRAMES tf) return EnumToString(tf); } +//+------------------------------------------------------------------+ +//| Gefuellte Muster-Form (User-Vorgabe 2026-08-21). | +//| ⚠⚠ OBJPROP_FILL faerbt mit OBJPROP_COLOR — ein Objekt kann NICHT | +//| gleichzeitig eine graue Flaeche und einen farbigen Rand haben. | +//| Deshalb: Flaeche grau und OBJPROP_BACK (hinter den Kerzen), die | +//| Richtungsaussage tragen Trigger- und Ziellinie darueber. | +//| ⚠ Kein Aufraeumen noetig: `ObjectsDeleteAll(0, PFX)` loescht bei | +//| jedem Redraw alle PFX-Objekte, es kann also keine Form aus einem| +//| vorherigen Muster stehenbleiben. | +//+------------------------------------------------------------------+ +void DrawMusterForm(string name, ENUM_OBJECT otyp, + datetime &t[], double &pz[], int n) +{ + if(ObjectFind(0, name) >= 0) ObjectDelete(0, name); + bool ok = false; + if(n == 2) + ok = ObjectCreate(0, name, otyp, 0, t[0], pz[0], t[1], pz[1]); + else if(n == 3) + ok = ObjectCreate(0, name, otyp, 0, t[0], pz[0], t[1], pz[1], t[2], pz[2]); + if(!ok) return; + ObjectSetInteger(0, name, OBJPROP_COLOR, InpMusterFill); + ObjectSetInteger(0, name, OBJPROP_FILL, true); + ObjectSetInteger(0, name, OBJPROP_BACK, true); + ObjectSetInteger(0, name, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); +} + //| ⚠ MT5 kann bei OBJ_RECTANGLE KEIN echtes Alpha (nur Canvas- | //| Objekte könnten das). Da der Hintergrund bekannt und einfarbig | //| ist, sieht die Mischung exakt aus wie eine transparente Füllung. | @@ -1066,6 +1096,55 @@ void Redraw() + DoubleToString(zl, (int)_Digits) + " (trifft 13-38 %)"); ObjectSetInteger(0, plb, OBJPROP_COLOR, pc); ObjectMove(0, plb, 0, RightTime(), trg); + // ── GEFUELLTE MUSTER-FORM (User-Vorgabe 2026-08-21) ────────── + // Feldlage: p[6]=form, danach je Anker t;preis. + // dreieck (3 Punkte) → OBJ_TRIANGLE · rechteck (2) → + // OBJ_RECTANGLE · ellipse (3) → OBJ_ELLIPSE · dreieck2 (5) → + // ZWEI Dreiecke, weil OBJ_TRIANGLE nur drei Punkte kennt. + // ⚠ Aeltere CSV ohne Geometrie faellt still auf "nur Linien" + // zurueck — der Indikator bleibt damit zu beiden Staenden + // vertraeglich. + if(k >= 9) + { + string form = p[6]; + int need = 3; + if(form == "rechteck") need = 2; + else if(form == "dreieck2") need = 5; + if(k >= 7 + 2 * need) + { + datetime at[5]; double ap[5]; + bool gut = true; + for(int q = 0; q < need; q++) + { + at[q] = (datetime)StringToInteger(p[7 + 2 * q]); + ap[q] = StringToDouble(p[8 + 2 * q]); + if(at[q] <= 0 || ap[q] <= 0) gut = false; + } + if(gut) + { + if(form == "rechteck") + DrawMusterForm(PFX + "MUST_F0", OBJ_RECTANGLE, at, ap, 2); + else if(form == "ellipse") + DrawMusterForm(PFX + "MUST_F0", OBJ_ELLIPSE, at, ap, 3); + else if(form == "dreieck") + DrawMusterForm(PFX + "MUST_F0", OBJ_TRIANGLE, at, ap, 3); + else if(form == "dreieck2") + { + // SKS / Dreifach-Top: fuenf Swings. Die beiden Dreiecke + // (Schulter..Kopf und Kopf..Schulter) kacheln genau die + // Silhouette — ein Dreieck allein koennte sie nicht fassen. + datetime a1[3], a2[3]; double b1[3], b2[3]; + for(int q = 0; q < 3; q++) + { + a1[q] = at[q]; b1[q] = ap[q]; + a2[q] = at[q + 2]; b2[q] = ap[q + 2]; + } + DrawMusterForm(PFX + "MUST_F0", OBJ_TRIANGLE, a1, b1, 3); + DrawMusterForm(PFX + "MUST_F1", OBJ_TRIANGLE, a2, b2, 3); + } + } + } + } continue; } if(typ == "RZ" || typ == "SZ")