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
+60
View File
@@ -9504,3 +9504,63 @@ Zeitebene.
`.ex5` frisch, `0 errors`. **Nur das Ergebnis fehlte** — dieselbe Klasse wie die `.ex5` frisch, `0 errors`. **Nur das Ergebnis fehlte** — dieselbe Klasse wie die
vier dokumentierten Fälle „Erfolg melden, wo nichts geprüft wurde". vier dokumentierten Fälle „Erfolg melden, wo nichts geprüft wurde".
Abschaltbar: `InpShowMuster=false`; Farben `InpMusterBullColor`/`…BearColor`. 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;<TF>;<name>;<dir>;<trigger>;<ziel>;<form>;<t1>;<p1>;<t2>;<p2>;…
```
| `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 (SchulterKopf und KopfSchulter) 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`.
+26 -2
View File
@@ -2989,6 +2989,10 @@ class TradingEngine:
"target": best.get("target"), "target": best.get("target"),
"height_atr": best.get("height_atr"), "height_atr": best.get("height_atr"),
"bild": _PAT_BILD.get(best.get("type")), "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: except Exception:
out[lbl] = None out[lbl] = None
@@ -3602,7 +3606,14 @@ class TradingEngine:
except Exception: except Exception:
pass pass
# ── BESTAETIGTES CHARTMUSTER je Zeitebene (User-Wunsch 2026-08-21) # ── BESTAETIGTES CHARTMUSTER je Zeitebene (User-Wunsch 2026-08-21)
# PT;<TF>;<name>;<dir>;<trigger>;<ziel> # PT;<TF>;<name>;<dir>;<trigger>;<ziel>;<form>;<t1>;<p1>;<t2>;<p2>;…
# <form> = 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 <TF> zu SEINEM Chart # Der Indikator zeichnet NUR die Zeile, deren <TF> zu SEINEM Chart
# passt — so bekommt jedes Fenster (M1/M5/M15/H1) sein eigenes # passt — so bekommt jedes Fenster (M1/M5/M15/H1) sein eigenes
# Muster, ohne dass fuenf Linien uebereinander liegen. # Muster, ohne dass fuenf Linien uebereinander liegen.
@@ -3623,8 +3634,21 @@ class TradingEngine:
if not _trg or not _zl: if not _trg or not _zl:
continue continue
_nm = str(_m.get("name") or "").replace(";", ",")[:28] _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 ''};" 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: except Exception as _pe:
log.debug(f"PT-Export: {_pe}") log.debug(f"PT-Export: {_pe}")
+98 -16
View File
@@ -96,7 +96,32 @@ def _status(dir_, trigger, cur, extreme):
return "forming" 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.""" """Sucht Muster in den letzten Swings. → Liste Muster-dicts."""
out = [] out = []
P = piv[-_MAX_LOOKBACK:] P = piv[-_MAX_LOOKBACK:]
@@ -104,7 +129,30 @@ def _detect(piv, cur, atr, last_idx):
return out return out
pr = [p[1] for p in P]; kd = [p[2] for p in P]; ix = [p[0] for p in P] 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) h = abs(extreme - trigger)
if h < _MIN_HEIGHT_ATR * atr: # zu flach = Rauschen if h < _MIN_HEIGHT_ATR * atr: # zu flach = Rauschen
return return
@@ -122,6 +170,12 @@ def _detect(piv, cur, atr, last_idx):
# Projekts). Gemessen wurden: Doppeltop/-boden, Tasse/inv. Tasse, # Projekts). Gemessen wurden: Doppeltop/-boden, Tasse/inv. Tasse,
# auf-/absteigendes Dreieck, SKS/inv. SKS. # auf-/absteigendes Dreieck, SKS/inv. SKS.
"measured": typ in _MEASURED, "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 # über alle 3er/5er-Fenster am aktuellen Rand
@@ -133,14 +187,16 @@ def _detect(piv, cur, atr, last_idx):
q = _sim(Ha, Hb, atr) q = _sim(Ha, Hb, atr)
if q > 0.35 and Lm < min(Ha, Hb): if q > 0.35 and Lm < min(Ha, Hb):
add("double_top", "Doppeltop", "bear", q, Lm, max(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 ── # ── Doppelboden: L, H, L ──
if len(w) >= 3 and w[0][0] == "L" and w[1][0] == "H" and w[2][0] == "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] La, Hm, Lb = w[0][1], w[1][1], w[2][1]
q = _sim(La, Lb, atr) q = _sim(La, Lb, atr)
if q > 0.35 and Hm > max(La, Lb): if q > 0.35 and Hm > max(La, Lb):
add("double_bottom", "Doppelboden", "bull", q, Hm, min(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) ── # ── 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"]: 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)) 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) q = _sim(ls, rs, atr) * _sim(t1, t2, atr)
if q > 0.25 and abs(t1 - t2) < _NECK_TOL_ATR * atr: if q > 0.25 and abs(t1 - t2) < _NECK_TOL_ATR * atr:
add("hs", "Kopf-Schulter", "bear", q, neck, head, 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 ── # ── Inverse SKS: L, H, L, H, L ──
if len(w) >= 5 and [x[0] for x in w[:5]] == ["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)) 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) q = _sim(ls, rs, atr) * _sim(t1, t2, atr)
if q > 0.25 and abs(t1 - t2) < _NECK_TOL_ATR * atr: if q > 0.25 and abs(t1 - t2) < _NECK_TOL_ATR * atr:
add("inv_hs", "Inverse SKS", "bull", q, neck, head, 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 # ── (Inverse) Tasse+Henkel (heuristisch): Rand-Extrema ~gleich, Boden/Top
# dazwischen, breit; Henkel = letzter kleiner Gegen-Swing. ── # dazwischen, breit; Henkel = letzter kleiner Gegen-Swing. ──
if len(w) >= 3: if len(w) >= 3:
@@ -168,13 +226,15 @@ def _detect(piv, cur, atr, last_idx):
q = _sim(rim_l, rim_r, atr) * 0.9 q = _sim(rim_l, rim_r, atr) * 0.9
if q > 0.4 and top > max(rim_l, rim_r): 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, 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: 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) 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 q = _sim(rim_l, rim_r, atr) * 0.9
if q > 0.4 and bot < min(rim_l, rim_r): if q > 0.4 and bot < min(rim_l, rim_r):
add("cup", "Tasse+Henkel", "bull", q, max(rim_l, rim_r), bot, 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-MUSTER (ergänzt 2026-07-31, User-Vorgabe „alle Muster aus dem
# Handbuch") — ALLE REINE ANZEIGE (`measured=False`), sie tragen die # 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 if q > 0.30 and h2 <= max(h1, h3) + 0.3 * atr: # kein Kopf → kein SKS
neck = min(t1, t2) neck = min(t1, t2)
add("triple_top", "Dreifach-Top", "bear", q, neck, max(h1, h2, h3), 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"]: 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)) l1, t1, l2, t2, l3 = (w[i][1] for i in range(5))
q = _sim(l1, l2, atr) * _sim(l2, l3, atr) q = _sim(l1, l2, atr) * _sim(l2, l3, atr)
if q > 0.30 and l2 >= min(l1, l3) - 0.3 * atr: if q > 0.30 and l2 >= min(l1, l3) - 0.3 * atr:
neck = max(t1, t2) neck = max(t1, t2)
add("triple_bottom", "Dreifach-Boden", "bull", q, neck, min(l1, l2, l3), 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 # ── Flagge / Wimpel (Fortsetzung): impulsive STANGE, danach eine kleine
# Konsolidierung GEGEN die Stangenrichtung. Flagge = paralleler Kanal, # 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) ext = min(cons) if up else max(cons)
typ = "pennant" if pennant else "flag" typ = "pennant" if pennant else "flag"
nm = ("Wimpel" if pennant else "Flagge") + (" (bullisch)" if up else " (bärisch)") 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, add(typ, nm, "bull" if up else "bear", 0.55, trig, ext,
f"Stange {pole_h/atr:.1f}×ATR in {pole_bars} Bars, " 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 # ── Rechteck (Range): zwei ~flache Hochs UND zwei ~flache Tiefs. Das Handbuch
# führt es als Fortsetzung — die Ausbruchsrichtung ist aber offen, deshalb # 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", "height_atr": round((top - bot) / atr, 1), "status": "forming",
"bars_ago": last_idx - ix[-1], "measured": False, "bars_ago": last_idx - ix[-1], "measured": False,
"note": f"Range {bot:.2f}{top:.2f} — Ausbruchsrichtung offen", "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. # ── Keil: beide Linien in DIESELBE Richtung geneigt und konvergierend.
# Steigender Keil = bärisch, fallender Keil = bullisch (Handbuch). # 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: if conv and dh > 0.3 * atr and dl > 0.3 * atr and dl > dh:
add("wedge_rise", "Steigender Keil", "bear", 0.5, add("wedge_rise", "Steigender Keil", "bear", 0.5,
min(ll[0][1], ll[1][1]), max(hh[0][1], hh[1][1]), 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 # ⚠ Bedingung korrigiert (2026-07-31): stand `dh > dl` — bei einem FALLENDEN
# Keil fällt aber die OBERE Linie schneller (sonst konvergiert nichts), also # 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 # `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: if conv and dh < -0.3 * atr and dl < -0.3 * atr and dh < dl:
add("wedge_fall", "Fallender Keil", "bull", 0.5, add("wedge_fall", "Fallender Keil", "bull", 0.5,
max(hh[0][1], hh[1][1]), min(ll[0][1], ll[1][1]), 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 ── # ── 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:] 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) if flat_h and rising_l: # aufsteigendes Dreieck (bullisch)
res_ = max(h1p, h2p) res_ = max(h1p, h2p)
add("tri_asc", "Aufsteigendes Dreieck", "bull", 0.6, res_, min(l1p, l2p), 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) elif flat_l and falling_h: # absteigendes Dreieck (bärisch)
sup_ = min(l1p, l2p) sup_ = min(l1p, l2p)
add("tri_desc", "Absteigendes Dreieck", "bear", 0.6, sup_, max(h1p, h2p), 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) elif falling_h and rising_l: # symmetrisch → Ausbruch offen (nur Anzeige)
out.append({ out.append({
"type": "tri_sym", "name": "Symmetrisches Dreieck", "dir": "neutral", "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], "status": "forming", "bars_ago": last_idx - ix[-1],
"note": f"konvergierend (Hochs fallen, Tiefs steigen) — Ausbruchsrichtung offen", "note": f"konvergierend (Hochs fallen, Tiefs steigen) — Ausbruchsrichtung offen",
"measured": False, # neutral, ohne Richtung → nie stimmberechtigt "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 # Duplikate (gleicher Typ+Trigger) zusammenfassen, beste Qualität behalten
@@ -376,8 +454,12 @@ class PatternDetector:
atr = _atr(H, L, C) atr = _atr(H, L, C)
if not atr or atr <= 0: if not atr or atr <= 0:
return 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) 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: with self._lock:
self._snap = {"patterns": patterns, "atr": round(atr, 4), self._snap = {"patterns": patterns, "atr": round(atr, 4),
"cur": round(C[-1], 3), "ts": now, "cur": round(C[-1], 3), "ts": now,
+80 -1
View File
@@ -12,7 +12,7 @@
//| 4) Der Bot muss laufen (schreibt die CSV alle ~5 s). | //| 4) Der Bot muss laufen (schreibt die CSV alle ~5 s). |
//+------------------------------------------------------------------+ //+------------------------------------------------------------------+
#property copyright "Oil Trading Bot" #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 // 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 // Faktor 1,8 aus v1.41 war nur die naechste Schaetzung; jetzt liefert
// TextGetSize die TATSAECHLICHE Pixelhoehe (LabelH). Ausserdem wird Y bei // 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 bool InpShowMuster = true; // bestaetigtes Chartmuster zeichnen
input color InpMusterBullColor = C'70,180,100';// bullisch input color InpMusterBullColor = C'70,180,100';// bullisch
input color InpMusterBearColor = C'220,90,80'; // baerisch 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 PFX = "SRB_"; // Präfix S/R-Linien (aus Datei)
const string RPFX = "SRR_"; // Präfix dynamische Range (aus Chart) const string RPFX = "SRR_"; // Präfix dynamische Range (aus Chart)
@@ -342,6 +345,33 @@ string TFName(ENUM_TIMEFRAMES tf)
return EnumToString(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- | //| ⚠ MT5 kann bei OBJ_RECTANGLE KEIN echtes Alpha (nur Canvas- |
//| Objekte könnten das). Da der Hintergrund bekannt und einfarbig | //| Objekte könnten das). Da der Hintergrund bekannt und einfarbig |
//| ist, sieht die Mischung exakt aus wie eine transparente Füllung. | //| ist, sieht die Mischung exakt aus wie eine transparente Füllung. |
@@ -1066,6 +1096,55 @@ void Redraw()
+ DoubleToString(zl, (int)_Digits) + " (trifft 13-38 %)"); + DoubleToString(zl, (int)_Digits) + " (trifft 13-38 %)");
ObjectSetInteger(0, plb, OBJPROP_COLOR, pc); ObjectSetInteger(0, plb, OBJPROP_COLOR, pc);
ObjectMove(0, plb, 0, RightTime(), trg); 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; continue;
} }
if(typ == "RZ" || typ == "SZ") if(typ == "RZ" || typ == "SZ")