Richtungs-Pfeile im MT5 statt des Kegels (v1.32) + Handbuch-Muster ergaenzt
PFEILE (User: "anstatt des Trichters einen Pfeil, der die wahrscheinlichste
Kursentwicklung anzeigt"). Randbedingung: die FREIE Richtungsfrage ist
gemessen ein Muenzwurf (analyze_reversal.py, AUC 0,499-0,509 oos, in-sample
nur 0,52). Ein frei schwebender Prognose-Pfeil waere unbelegt - und
gefaehrlicher als der Kegel, weil er ueberzeugender aussieht. Deshalb DREI
Pfeile mit jeweils eigener, benannter Beleglage:
L blau naechstes Level, aus dem kalibrierten P(break)
(AUC 0,65 oos, live 27 % vorhergesagt vs 28 % real)
S neongruen Squeeze-Ausbruch - das EINZIGE validierte Richtungssignal
K grau Konsens der Module - NICHT kalibriert, heisst deshalb
"Konsens" und nicht "Prognose"
L hat ein TOTBAND 45-55 %: dort waagerechter Pfeil "unentschieden", statt
aus einem Muenzwurf eine Richtung zu zeichnen.
MQL5 v1.32: OBJ_ARROWED_LINE + Label an der Spitze. Label heisst ALBL_* und
NICHT T* - sonst zieht RepositionLabels() es an den rechten Rand und loest
es von der Pfeilspitze.
Kegel bleibt im Code und in der Dashboard-Kachel; der Bot exportiert ihn nur
nicht mehr ins Chart ([trading] export_cone=false).
CHARTMUSTER: alle Handbuch-Muster ergaenzt - Dreifach-Top/-Boden, Flagge,
Wimpel, Rechteck, Steigender/Fallender Keil. ALLE mit measured=False, sie
tragen die Verdict-Stimme NICHT: das Gewicht 1,0 stammt aus einem
Kontrolltest, der nur die alten Typen abdeckte. engine._verdict waehlt jetzt
nur Muster mit measured=True.
Bug beim Bau gefunden: der fallende Keil feuerte in 20.000 Bars KEIN
EINZIGES MAL - die Bedingung stand auf dh > dl, bei einem fallenden Keil
faellt aber die OBERE Linie schneller (sonst konvergiert nichts). Beim
Spiegelbild (steigender Keil) stimmte es.
backtest_patterns_v2.py (NEU) misst die neuen Typen - mit dem ECHTEN
Detektor (core.patterns._detect statt einer Nachbildung) und dem
kanonischen Exit, plus Kontrollgruppe "generischer Swing-Bruch".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
365a0584e5
commit
81614a3128
@@ -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()
|
||||||
+82
-6
@@ -368,6 +368,8 @@ class TradingEngine:
|
|||||||
self._mql5_files_dir = None # <terminal>\MQL5\Files (lazy, für den Indikator)
|
self._mql5_files_dir = None # <terminal>\MQL5\Files (lazy, für den Indikator)
|
||||||
self._export_trade_marks = self.cfg["trading"].get("export_trade_marks", "false").lower() == "true"
|
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"
|
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)
|
self._draw_last = {"r": None, "s": None} # letzte gezeichnete S/R (Hysterese)
|
||||||
try: # WHT-Quellensteuer (Broker behält % je GEWINN ein) → Netto-Anzeige
|
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))
|
self._wht_pct = max(0.0, float(self.cfg["trading"].get("wht_pct", "0") or 0))
|
||||||
@@ -2271,19 +2273,82 @@ class TradingEngine:
|
|||||||
lines.append(f"PX;{_bid}")
|
lines.append(f"PX;{_bid}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# WAHRSCHEINLICHKEITS-KEGEL: CN;<minuten>;<lo>;<hi> je Horizont (30/60/120),
|
_atr5 = 0.0
|
||||||
# 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 %.
|
|
||||||
try:
|
try:
|
||||||
_atr5 = float(((self.wave.snapshot() or {}).get("pb_feats") or {})
|
_atr5 = float(((self.wave.snapshot() or {}).get("pb_feats") or {})
|
||||||
.get("atr") or 0.0)
|
.get("atr") or 0.0)
|
||||||
|
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,))
|
_cn = _cone_build(_bid, _atr5, bands=(80,))
|
||||||
for lv in (_cn or {}).get("levels", []):
|
for lv in (_cn or {}).get("levels", []):
|
||||||
lines.append(f"CN;{lv['minutes']};{lv['lo']};{lv['hi']}")
|
lines.append(f"CN;{lv['minutes']};{lv['lo']};{lv['hi']}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# ── PFEILE: AR;<art>;<von>;<nach>;<text> (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):
|
# G/V der offenen Position, direkt unter dem Kurs (User-Wunsch 2026-07-30):
|
||||||
# PL;wert;waehrung. **NETTO** wie im Dashboard — bei Gewinn abzüglich der
|
# PL;wert;waehrung. **NETTO** wie im Dashboard — bei Gewinn abzüglich der
|
||||||
# WHT-Quellensteuer, die der Broker einbehält (`[trading] wht_pct`); bei
|
# WHT-Quellensteuer, die der Broker einbehält (`[trading] wht_pct`); bei
|
||||||
@@ -2441,7 +2506,18 @@ class TradingEngine:
|
|||||||
pv, pdet, pw = 0, "—", 0.0
|
pv, pdet, pw = 0, "—", 0.0
|
||||||
try:
|
try:
|
||||||
_pats = (snap_parts_patterns or {}).get("patterns") or []
|
_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:
|
if _p0:
|
||||||
pv = 1 if _p0["dir"] == "bull" else -1
|
pv = 1 if _p0["dir"] == "bull" else -1
|
||||||
_conf = _p0.get("status") == "confirmed"
|
_conf = _p0.get("status") == "confirmed"
|
||||||
|
|||||||
@@ -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
|
_MAX_DIST_ATR = 5.0 # Trigger max. so weit vom AKTUELLEN Kurs (sonst nicht mehr
|
||||||
# aktionabel — alte, weit weg gelaufene Muster ausblenden)
|
# 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):
|
def _atr(highs, lows, closes, p=14):
|
||||||
trs = []
|
trs = []
|
||||||
@@ -102,6 +115,13 @@ def _detect(piv, cur, atr, last_idx):
|
|||||||
"target": round(target, 3), "height_atr": round(h / atr, 1),
|
"target": round(target, 3), "height_atr": round(h / atr, 1),
|
||||||
"status": _status(dir_, trigger, cur, extreme),
|
"status": _status(dir_, trigger, cur, extreme),
|
||||||
"bars_ago": last_idx - ix[-1], "note": note,
|
"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
|
# ü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,
|
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)")
|
||||||
|
|
||||||
|
# ══ 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 ──
|
# ── 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:]
|
||||||
lows = [(x[2], x[1]) for x in zip(kd, pr, ix) if x[0] == "L"][-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),
|
"target": None, "height_atr": round(wide / atr, 1),
|
||||||
"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
|
||||||
})
|
})
|
||||||
|
|
||||||
# Duplikate (gleicher Typ+Trigger) zusammenfassen, beste Qualität behalten
|
# Duplikate (gleicher Typ+Trigger) zusammenfassen, beste Qualität behalten
|
||||||
|
|||||||
+67
-1
@@ -12,7 +12,19 @@
|
|||||||
//| 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.31"
|
#property version "1.32"
|
||||||
|
// v1.32 (2026-07-31): RICHTUNGS-PFEILE (AR;<art>;<von>;<nach>;<text>) 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;<minuten>;<lo>;<hi>) — zwei sich
|
// v1.31 (2026-07-31): WAHRSCHEINLICHKEITS-KEGEL (CN;<minuten>;<lo>;<hi>) — zwei sich
|
||||||
// oeffnende, gepunktete Pfade vom aktuellen Bar ueber 30/60/120 min.
|
// oeffnende, gepunktete Pfade vom aktuellen Bar ueber 30/60/120 min.
|
||||||
// ⚠ KEINE Richtungsprognose, sondern STREUUNG: wie weit der Kurs plausibel
|
// ⚠ 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 InpConsolAlpha = 8; // Deckkraft in % (komprimiert: x2)
|
||||||
input int InpConsolBars = 10; // Box-Laenge in Bars (= _SQ_N im Bot)
|
input int InpConsolBars = 10; // Box-Laenge in Bars (= _SQ_N im Bot)
|
||||||
input bool InpShowCone = true; // Wahrscheinlichkeits-Kegel (Streuung, NICHT Richtung)
|
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 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 bool InpShowPnL = true; // G/V der offenen Position unter dem Kurs
|
||||||
input color InpPnLUpColor = C'0,255,127'; // G/V im Plus (neongruen)
|
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;
|
g_coneT = t1; g_coneLo = clo; g_coneHi = chi;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// RICHTUNGS-PFEILE: AR;<art>;<von>;<nach>;<text> 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
|
// AKTUELLER KURS oben rechts, fett (User-Wunsch): PX;bid
|
||||||
if(typ == "PX")
|
if(typ == "PX")
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user