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;<TF>;<name>;<dir>;<trigger>;<ziel>;<form>;<t1>;<p1>;<t2>;<p2>;...

  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.
This commit is contained in:
Axel Hocks
2026-08-21 15:55:36 +02:00
parent c512863e5f
commit 72c2aedc1a
4 changed files with 264 additions and 19 deletions
+98 -16
View File
@@ -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,