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:
+98
-16
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user