Neues Modul: Visuelle Chartmuster-Erkennung (core/patterns.py) — reine Anzeige
Erkennt aus M30-Swing-Pivots: Doppeltop/-boden, Kopf-Schulter/inverse SKS, Tasse+Henkel/invers, auf-/absteigendes+symmetrisches Dreieck. Je Muster Richtung, Trigger/Nackenlinie, Measured-Move-Ziel, Status (bildet sich/bestätigt/ungültig), Güte. Dashboard-Karte #card-patterns + Disclaimer (v=116). KEIN Signal, kein Verdict-Gewicht — Signal-Einfluss erst nach Backtest (Muster-Klasse 2x verworfen, hohe Beweislast). Live getestet: findet bestätigten Doppeltop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cd8ee5b01b
commit
e5589da08b
@@ -1122,6 +1122,22 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
klein & kaum handelbar; NY Fed: SKS nicht profitabel; out-of-sample schwach) UND der
|
klein & kaum handelbar; NY Fed: SKS nicht profitabel; out-of-sample schwach) UND der
|
||||||
Struktur-Klasse (Muster = Swings+Trendlinien = die gerade verworfene Klasse). **11.
|
Struktur-Klasse (Muster = Swings+Trendlinien = die gerade verworfene Klasse). **11.
|
||||||
verworfener Signal-Eingriff — am Signal ist nichts zu holen.**
|
verworfener Signal-Eingriff — am Signal ist nichts zu holen.**
|
||||||
|
- **Chartmuster-Kachel `#card-patterns` (`core/patterns.py`, 2026-07-27, User-Idee
|
||||||
|
„Visuelle Muster-Erkennung"): REINE ANZEIGE, KEIN Signal.** Erkennt mechanisch aus
|
||||||
|
den M30-Swing-Pivots (k=3, wie `structure.py`): **Doppeltop/-boden, Kopf-Schulter/
|
||||||
|
inverse SKS, Tasse+Henkel/invers (heuristisch), auf-/absteigendes + symmetrisches
|
||||||
|
Dreieck.** Je Muster: Richtung (bull/bear/neutral), Nackenlinie/Trigger, Measured-
|
||||||
|
Move-Ziel, Status (bildet sich / bestätigt / ungültig→raus), Güte (Extrem-Symmetrie),
|
||||||
|
Bars-her. Refresh ~30 s im Trend-Loop unter `mt5_lock`; Snapshot `patterns
|
||||||
|
{patterns[], atr, cur}`; Frontend zeigt Karte + Disclaimer „kein Signal". ⚠ **Signal-
|
||||||
|
Einfluss ERST nach Backtest** (`backtest_patterns.py`, noch zu bauen) — die
|
||||||
|
Muster-KLASSE ist bereits 2× als Signal verworfen (`backtest_doubletop.py` oben +
|
||||||
|
`backtest_structure.py`), also **hohe Beweislast**; die Kachel macht das Muster nur
|
||||||
|
sichtbar (was ein Kommentator meint), steuert nichts. v2-Kandidaten (Flags/Wimpel,
|
||||||
|
Rechteck, Rundung) dokumentiert, noch nicht gebaut. Web-Recherche (strike.money,
|
||||||
|
IG, Axi): Umkehr (Doppel-, SKS, Rundung) vs. Fortsetzung (Dreieck, Flag, Tasse);
|
||||||
|
Kursziel stets = Muster-Höhe ab Ausbruch projiziert — überall mit dem Caveat
|
||||||
|
„keine Garantie, nur Indikation".
|
||||||
- **Kurslücken-Kachel** (`#card-gaps`): offene D1-Gaps des laufenden Jahres
|
- **Kurslücken-Kachel** (`#card-gaps`): offene D1-Gaps des laufenden Jahres
|
||||||
(`gaps.GapAnalyzer`, Refresh ~10 min im Trend-Loop unter `mt5_lock`). Je Gap:
|
(`gaps.GapAnalyzer`, Refresh ~10 min im Trend-Loop unter `mt5_lock`). Je Gap:
|
||||||
Richtung, Zone, Fill-Level, Abstand zum Kurs. Die **Fill-Level fließen als
|
Richtung, Zone, Fill-Level, Abstand zum Kurs. Die **Fill-Level fließen als
|
||||||
@@ -1214,7 +1230,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
|
Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
|
||||||
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
|
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
|
||||||
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap.
|
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap.
|
||||||
- Asset-Version aktuell **v=115** (in `web/index.html` hochzählen, siehe Workflows).
|
- Asset-Version aktuell **v=116** (in `web/index.html` hochzählen, siehe Workflows).
|
||||||
Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis
|
Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis
|
||||||
14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
|
14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
|
||||||
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` —
|
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` —
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from core.market_hours import session_state
|
|||||||
from core.daily_levels import DailyLevels
|
from core.daily_levels import DailyLevels
|
||||||
from core.gaps import GapAnalyzer
|
from core.gaps import GapAnalyzer
|
||||||
from core.structure import MarketStructure
|
from core.structure import MarketStructure
|
||||||
|
from core.patterns import PatternDetector
|
||||||
from core.candle_logger import CandleLogger
|
from core.candle_logger import CandleLogger
|
||||||
from core.wave_rec import (_atr as _wr_atr, _ema_last,
|
from core.wave_rec import (_atr as _wr_atr, _ema_last,
|
||||||
_EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _STRETCH_MAX)
|
_EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _STRETCH_MAX)
|
||||||
@@ -146,6 +147,7 @@ class TradingEngine:
|
|||||||
self.wave.set_dead_hours([h.strip() for h in _dh.split(",") if h.strip()])
|
self.wave.set_dead_hours([h.strip() for h in _dh.split(",") if h.strip()])
|
||||||
self.elliott = ElliottAnalyzer(mt5.TIMEFRAME_M5)
|
self.elliott = ElliottAnalyzer(mt5.TIMEFRAME_M5)
|
||||||
self.structure = MarketStructure() # Marktstruktur (M30) — reine Anzeige
|
self.structure = MarketStructure() # Marktstruktur (M30) — reine Anzeige
|
||||||
|
self.patterns = PatternDetector() # visuelle Chartmuster (M30) — reine Anzeige
|
||||||
self.candles = CandleLogger(HISTORY_DB_FILE) # M1-Candle-Log (Daten-Sammlung)
|
self.candles = CandleLogger(HISTORY_DB_FILE) # M1-Candle-Log (Daten-Sammlung)
|
||||||
# News: eigener Fetch-Loop (versorgt UI /api/news UND Agent-Kontext)
|
# News: eigener Fetch-Loop (versorgt UI /api/news UND Agent-Kontext)
|
||||||
tcfg = self.cfg["translation"]
|
tcfg = self.cfg["translation"]
|
||||||
@@ -475,6 +477,8 @@ class TradingEngine:
|
|||||||
except Exception as ee: log.warning(f"elliott.refresh_market: {ee}")
|
except Exception as ee: log.warning(f"elliott.refresh_market: {ee}")
|
||||||
try: self.structure.refresh_market(self.data.symbol)
|
try: self.structure.refresh_market(self.data.symbol)
|
||||||
except Exception as se2: log.warning(f"structure.refresh_market: {se2}")
|
except Exception as se2: log.warning(f"structure.refresh_market: {se2}")
|
||||||
|
try: self.patterns.refresh_market(self.data.symbol)
|
||||||
|
except Exception as pe2: log.warning(f"patterns.refresh_market: {pe2}")
|
||||||
try: self.candles.log(self.data.symbol) # M1-Candle-Log (~55 s)
|
try: self.candles.log(self.data.symbol) # M1-Candle-Log (~55 s)
|
||||||
except Exception as ce: log.warning(f"candles.log: {ce}")
|
except Exception as ce: log.warning(f"candles.log: {ce}")
|
||||||
try: self._evaluate_pbreak_predictions() # ~alle 5 min, self-throttled
|
try: self._evaluate_pbreak_predictions() # ~alle 5 min, self-throttled
|
||||||
@@ -2048,6 +2052,7 @@ class TradingEngine:
|
|||||||
"daily_levels": self.daily.snapshot(),
|
"daily_levels": self.daily.snapshot(),
|
||||||
"gaps": self.gaps.snapshot(),
|
"gaps": self.gaps.snapshot(),
|
||||||
"structure": self.structure.snapshot(),
|
"structure": self.structure.snapshot(),
|
||||||
|
"patterns": self.patterns.snapshot(),
|
||||||
"stats_compact": {"week": self._compact_stats("week"),
|
"stats_compact": {"week": self._compact_stats("week"),
|
||||||
"all": self._compact_stats("all")},
|
"all": self._compact_stats("all")},
|
||||||
"close_alert_count": self._close_alert_count,
|
"close_alert_count": self._close_alert_count,
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
"""
|
||||||
|
core/patterns.py — Visuelle Chartmuster-Erkennung (ANZEIGE, kein Signal)
|
||||||
|
========================================================================
|
||||||
|
Erkennt mechanisch definierte Chartmuster aus den M30-Swing-Pivots und liefert
|
||||||
|
sie fürs Dashboard: Typ, Richtung, Nackenlinie/Trigger, Measured-Move-Ziel,
|
||||||
|
Status (bildet sich / bestätigt / ungültig) und eine Qualitäts-Note.
|
||||||
|
|
||||||
|
Erkannte Muster (v1):
|
||||||
|
• Doppeltop / Doppelboden (2 gleiche Extrema + Nackenlinie)
|
||||||
|
• Kopf-Schulter / invers (SKS) (3 Extrema, Mitte am höchsten/tiefsten)
|
||||||
|
• Tasse+Henkel / invers (heuristisch: abgerundetes Extrem + Henkel)
|
||||||
|
|
||||||
|
⚠ REINE ANZEIGE — KEIN Trade-Trigger, KEIN Verdict-Gewicht. Signal-Einfluss ERST
|
||||||
|
nach Backtest (`backtest_patterns.py`, noch zu bauen): dieselbe Klasse (Doppeltop,
|
||||||
|
Struktur) ist im Projekt schon 2× als SIGNAL verworfen (`backtest_doubletop.py`,
|
||||||
|
`backtest_structure.py`) → hohe Beweislast. Das Modul zeigt nur den Ist-Zustand +
|
||||||
|
das (mechanische) Measured-Move-Ziel, damit man sieht, was ein Kommentator meint.
|
||||||
|
|
||||||
|
Thread-sicher wie structure.py: refresh_market(sym) holt M30-Bars unter mt5_lock
|
||||||
|
(~30 s gedrosselt), snapshot() liefert den letzten Stand ohne MT5-Call.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
|
from core.mt5_utils import mt5_lock
|
||||||
|
from core.logger import get_logger
|
||||||
|
|
||||||
|
log = get_logger("patterns")
|
||||||
|
|
||||||
|
_TF = mt5.TIMEFRAME_M30
|
||||||
|
_N_BARS = 240
|
||||||
|
_PIVOT_K = 3
|
||||||
|
_REFRESH_S = 30.0
|
||||||
|
_MIN_HEIGHT_ATR = 1.2 # Muster-Höhe muss ≥ so viel ATR sein (sonst Rauschen)
|
||||||
|
_TOL_ATR = 0.7 # zwei "gleiche" Extrema dürfen so weit auseinander (×ATR)
|
||||||
|
_NECK_TOL_ATR = 0.9 # Nackenlinie "flach": zwei Tröge/Hochs so weit (×ATR)
|
||||||
|
_MAX_LOOKBACK = 9 # nur die letzten N Swings betrachten (aktuelle Muster)
|
||||||
|
|
||||||
|
|
||||||
|
def _atr(highs, lows, closes, p=14):
|
||||||
|
trs = []
|
||||||
|
for i in range(1, len(closes)):
|
||||||
|
trs.append(max(highs[i]-lows[i], abs(highs[i]-closes[i-1]), abs(lows[i]-closes[i-1])))
|
||||||
|
return (sum(trs[-p:]) / min(len(trs), p)) if trs else None
|
||||||
|
|
||||||
|
|
||||||
|
def _pivots(highs, lows, k):
|
||||||
|
"""Alternierende Swing-Punkte → [(index, price, 'H'/'L')] (wie structure.py)."""
|
||||||
|
n = len(highs); raw = []
|
||||||
|
for i in range(k, n - k):
|
||||||
|
if highs[i] == max(highs[i-k:i+k+1]) and highs[i] > highs[i-1] and highs[i] >= highs[i+1]:
|
||||||
|
raw.append((i, highs[i], "H"))
|
||||||
|
elif lows[i] == min(lows[i-k:i+k+1]) and lows[i] < lows[i-1] and lows[i] <= lows[i+1]:
|
||||||
|
raw.append((i, lows[i], "L"))
|
||||||
|
out = []
|
||||||
|
for p in raw:
|
||||||
|
if out and out[-1][2] == p[2]:
|
||||||
|
if (p[2] == "H" and p[1] > out[-1][1]) or (p[2] == "L" and p[1] < out[-1][1]):
|
||||||
|
out[-1] = p
|
||||||
|
else:
|
||||||
|
out.append(p)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _sim(a, b, atr):
|
||||||
|
"""Ähnlichkeit zweier Preise (1 = identisch, 0 = ≥ _TOL_ATR entfernt)."""
|
||||||
|
return max(0.0, 1.0 - abs(a - b) / (_TOL_ATR * atr)) if atr else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _status(dir_, trigger, cur, extreme):
|
||||||
|
"""bestätigt (Trigger gebrochen) / ungültig (Extrem überschritten) / bildet sich."""
|
||||||
|
if dir_ == "bear":
|
||||||
|
if cur < trigger: return "confirmed"
|
||||||
|
if cur > extreme: return "invalidated"
|
||||||
|
else:
|
||||||
|
if cur > trigger: return "confirmed"
|
||||||
|
if cur < extreme: return "invalidated"
|
||||||
|
return "forming"
|
||||||
|
|
||||||
|
|
||||||
|
def _detect(piv, cur, atr, last_idx):
|
||||||
|
"""Sucht Muster in den letzten Swings. → Liste Muster-dicts."""
|
||||||
|
out = []
|
||||||
|
P = piv[-_MAX_LOOKBACK:]
|
||||||
|
if len(P) < 3:
|
||||||
|
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):
|
||||||
|
h = abs(extreme - trigger)
|
||||||
|
if h < _MIN_HEIGHT_ATR * atr: # zu flach = Rauschen
|
||||||
|
return
|
||||||
|
target = trigger - h if dir_ == "bear" else trigger + h
|
||||||
|
out.append({
|
||||||
|
"type": typ, "name": name, "dir": dir_,
|
||||||
|
"quality": round(quality, 2), "trigger": round(trigger, 3),
|
||||||
|
"target": round(target, 3), "height_atr": round(h / atr, 1),
|
||||||
|
"status": _status(dir_, trigger, cur, extreme),
|
||||||
|
"bars_ago": last_idx - ix[-1], "note": note,
|
||||||
|
})
|
||||||
|
|
||||||
|
# über alle 3er/5er-Fenster am aktuellen Rand
|
||||||
|
for s in range(len(P) - 2):
|
||||||
|
w = list(zip(kd[s:], pr[s:], ix[s:]))
|
||||||
|
# ── Doppeltop: H, L, H (zwei ~gleiche Hochs, Trog dazwischen) ──
|
||||||
|
if len(w) >= 3 and w[0][0] == "H" and w[1][0] == "L" and w[2][0] == "H":
|
||||||
|
Ha, Lm, Hb = w[0][1], w[1][1], w[2][1]
|
||||||
|
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}")
|
||||||
|
# ── 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}")
|
||||||
|
# ── 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))
|
||||||
|
if head > ls and head > rs:
|
||||||
|
neck = (t1 + t2) / 2.0
|
||||||
|
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}")
|
||||||
|
# ── 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))
|
||||||
|
if head < ls and head < rs:
|
||||||
|
neck = (t1 + t2) / 2.0
|
||||||
|
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}")
|
||||||
|
# ── (Inverse) Tasse+Henkel (heuristisch): Rand-Extrema ~gleich, Boden/Top
|
||||||
|
# dazwischen, breit; Henkel = letzter kleiner Gegen-Swing. ──
|
||||||
|
if len(w) >= 3:
|
||||||
|
span = w[2][2] - w[0][2] # Bars zwischen den Rand-Punkten
|
||||||
|
if w[0][0] == "L" and w[1][0] == "H" and w[2][0] == "L" and span >= 10:
|
||||||
|
rim_l, top, rim_r = w[0][1], w[1][1], w[2][1] # inverse Tasse (abger. Top)
|
||||||
|
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)")
|
||||||
|
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)")
|
||||||
|
|
||||||
|
# ── 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:]
|
||||||
|
lows = [(x[2], x[1]) for x in zip(kd, pr, ix) if x[0] == "L"][-2:]
|
||||||
|
if len(highs) == 2 and len(lows) == 2:
|
||||||
|
(ih1, h1p), (ih2, h2p) = highs
|
||||||
|
(il1, l1p), (il2, l2p) = lows
|
||||||
|
flat_h = abs(h2p - h1p) <= _NECK_TOL_ATR * atr
|
||||||
|
flat_l = abs(l2p - l1p) <= _NECK_TOL_ATR * atr
|
||||||
|
rising_l = l2p > l1p + 0.4 * atr
|
||||||
|
falling_h = h2p < h1p - 0.4 * atr
|
||||||
|
wide = abs(max(h1p, h2p) - min(l1p, l2p))
|
||||||
|
if wide >= _MIN_HEIGHT_ATR * atr:
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
elif falling_h and rising_l: # symmetrisch → Ausbruch offen (nur Anzeige)
|
||||||
|
out.append({
|
||||||
|
"type": "tri_sym", "name": "Symmetrisches Dreieck", "dir": "neutral",
|
||||||
|
"quality": 0.5, "trigger": round((max(h1p, h2p) + min(l1p, l2p)) / 2, 3),
|
||||||
|
"target": None, "height_atr": round(wide / atr, 1),
|
||||||
|
"status": "forming", "bars_ago": last_idx - ix[-1],
|
||||||
|
"note": f"konvergierend (Hochs fallen, Tiefs steigen) — Ausbruchsrichtung offen",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Duplikate (gleicher Typ+Trigger) zusammenfassen, beste Qualität behalten
|
||||||
|
best = {}
|
||||||
|
for m in out:
|
||||||
|
key = (m["type"], round(m["trigger"], 2))
|
||||||
|
if key not in best or m["quality"] > best[key]["quality"]:
|
||||||
|
best[key] = m
|
||||||
|
# ungültige raus, dann nach Status (confirmed/forming) + Qualität + Recency
|
||||||
|
res = [m for m in best.values() if m["status"] != "invalidated"]
|
||||||
|
rank = {"confirmed": 2, "forming": 1}
|
||||||
|
res.sort(key=lambda m: (rank.get(m["status"], 0), m["quality"], -m["bars_ago"]), reverse=True)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class PatternDetector:
|
||||||
|
def __init__(self):
|
||||||
|
self._snap = {"patterns": [], "atr": None, "ts": 0.0}
|
||||||
|
self._last_refresh = 0.0
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def refresh_market(self, sym: str):
|
||||||
|
now = time.time()
|
||||||
|
if now - self._last_refresh < _REFRESH_S:
|
||||||
|
return
|
||||||
|
self._last_refresh = now
|
||||||
|
try:
|
||||||
|
with mt5_lock(timeout=2) as got:
|
||||||
|
if not got:
|
||||||
|
return
|
||||||
|
bars = mt5.copy_rates_from_pos(sym, _TF, 0, _N_BARS)
|
||||||
|
if bars is None or len(bars) < 60:
|
||||||
|
return
|
||||||
|
H = [float(b["high"]) for b in bars]
|
||||||
|
L = [float(b["low"]) for b in bars]
|
||||||
|
C = [float(b["close"]) for b in bars]
|
||||||
|
atr = _atr(H, L, C)
|
||||||
|
if not atr or atr <= 0:
|
||||||
|
return
|
||||||
|
piv = _pivots(H, L, _PIVOT_K)
|
||||||
|
patterns = _detect(piv, C[-1], atr, len(H) - 1)
|
||||||
|
with self._lock:
|
||||||
|
self._snap = {"patterns": patterns, "atr": round(atr, 4),
|
||||||
|
"cur": round(C[-1], 3), "ts": now}
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"patterns.refresh_market: {e}")
|
||||||
|
|
||||||
|
def snapshot(self) -> dict:
|
||||||
|
with self._lock:
|
||||||
|
return dict(self._snap)
|
||||||
+32
@@ -224,6 +224,37 @@ function renderCircuitBreaker(cb) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chartmuster (visuell) — reine Anzeige. Status: bildet sich / bestätigt / (ungültig raus).
|
||||||
|
function renderPatterns(pt) {
|
||||||
|
const el = $("pt-list");
|
||||||
|
if (!el) return;
|
||||||
|
const items = (pt && pt.patterns) || [];
|
||||||
|
if (!items.length) {
|
||||||
|
el.innerHTML = `<span class="dim">Aktuell kein klares Muster erkannt.</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = items.map(m => {
|
||||||
|
const dirCls = m.dir === "bull" ? "up" : m.dir === "bear" ? "down" : "dim";
|
||||||
|
const arr = m.dir === "bull" ? "▲" : m.dir === "bear" ? "▼" : "◆";
|
||||||
|
const st = m.status === "confirmed"
|
||||||
|
? `<span class="pt-badge conf">bestätigt</span>`
|
||||||
|
: `<span class="pt-badge form">bildet sich</span>`;
|
||||||
|
const tgt = m.target != null
|
||||||
|
? `Ziel <b>${fmt(m.target, 2)}</b>` : `Ausbruch offen`;
|
||||||
|
const q = Math.round((m.quality || 0) * 100);
|
||||||
|
return `<div class="pt-item ${dirCls}">
|
||||||
|
<div class="pt-head"><span class="pt-name">${arr} ${m.name}</span>${st}
|
||||||
|
<span class="pt-q">Güte ${q}%</span></div>
|
||||||
|
<div class="pt-det">Trigger <b>${fmt(m.trigger, 2)}</b> · ${tgt}
|
||||||
|
· Höhe ${m.height_atr}×ATR · vor ${m.bars_ago} Bars</div>
|
||||||
|
<div class="pt-note">${m.note || ""}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join("") +
|
||||||
|
`<div class="pt-disclaimer">📊 Reine Anzeige (mechanisch aus M30-Pivots) — KEIN Signal,
|
||||||
|
kein Einfluss auf die Empfehlung. Muster-Klasse im Projekt als Signal noch unbelegt;
|
||||||
|
Einfluss erst nach Backtest.</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function render(d) {
|
function render(d) {
|
||||||
// ── Header ──────────────────────────────
|
// ── Header ──────────────────────────────
|
||||||
$("conn").className = "dot " + (d.connected ? "on" : "off");
|
$("conn").className = "dot " + (d.connected ? "on" : "off");
|
||||||
@@ -314,6 +345,7 @@ function render(d) {
|
|||||||
// ── Gesamtempfehlung (aggregiert) ───────
|
// ── Gesamtempfehlung (aggregiert) ───────
|
||||||
renderVerdict(d.verdict || {}, d);
|
renderVerdict(d.verdict || {}, d);
|
||||||
renderCircuitBreaker(d.circuit_breaker);
|
renderCircuitBreaker(d.circuit_breaker);
|
||||||
|
renderPatterns(d.patterns);
|
||||||
renderStructure(d.structure);
|
renderStructure(d.structure);
|
||||||
renderStatsCompact(d.stats_compact, d.market);
|
renderStatsCompact(d.stats_compact, d.market);
|
||||||
renderGaps(d.gaps, d.market);
|
renderGaps(d.gaps, d.market);
|
||||||
|
|||||||
+8
-2
@@ -6,7 +6,7 @@
|
|||||||
<meta name="theme-color" content="#0d1117">
|
<meta name="theme-color" content="#0d1117">
|
||||||
<title>Oil · MT5</title>
|
<title>Oil · MT5</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="stylesheet" href="style.css?v=115">
|
<link rel="stylesheet" href="style.css?v=116">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Modul-Menü (ganz oben) -->
|
<!-- Modul-Menü (ganz oben) -->
|
||||||
@@ -116,6 +116,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Chartmuster (visuell) — reine Anzeige, kein Signal (noch nicht backtestet) -->
|
||||||
|
<section class="card wide" id="card-patterns">
|
||||||
|
<h2>Chartmuster <span class="pt-tf">M30 · visuell</span></h2>
|
||||||
|
<div class="pt-list" id="pt-list"><span class="dim">—</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- KI-Agent -->
|
<!-- KI-Agent -->
|
||||||
<section class="card wide" id="card-agent">
|
<section class="card wide" id="card-agent">
|
||||||
<h2>KI-Copilot</h2>
|
<h2>KI-Copilot</h2>
|
||||||
@@ -220,6 +226,6 @@
|
|||||||
<div id="toast" class="toast hidden"></div>
|
<div id="toast" class="toast hidden"></div>
|
||||||
|
|
||||||
<script src="lightweight-charts.standalone.production.js?v=114"></script>
|
<script src="lightweight-charts.standalone.production.js?v=114"></script>
|
||||||
<script src="app.js?v=115"></script>
|
<script src="app.js?v=116"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -141,6 +141,21 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
|
|||||||
.circuit.tripped{background:#3d1a1a;color:#ff6b6b;border:1px solid var(--down);
|
.circuit.tripped{background:#3d1a1a;color:#ff6b6b;border:1px solid var(--down);
|
||||||
box-shadow:0 0 0 1px var(--down)}
|
box-shadow:0 0 0 1px var(--down)}
|
||||||
.circuit.near{background:#2a1f00;color:var(--accent);border:1px solid #4a3600}
|
.circuit.near{background:#2a1f00;color:var(--accent);border:1px solid #4a3600}
|
||||||
|
/* Chartmuster-Karte (visuell, reine Anzeige) */
|
||||||
|
#card-patterns .pt-tf{font-size:11px;color:var(--dim);font-weight:600;margin-left:6px}
|
||||||
|
.pt-item{background:var(--surface-2);border:1px solid var(--border);border-left:3px solid var(--line);
|
||||||
|
border-radius:var(--r-sm);padding:9px 12px;margin-bottom:8px}
|
||||||
|
.pt-item.up{border-left-color:var(--up)} .pt-item.down{border-left-color:var(--down)}
|
||||||
|
.pt-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||||
|
.pt-name{font-weight:700;font-size:15px}
|
||||||
|
.pt-item.up .pt-name{color:var(--up)} .pt-item.down .pt-name{color:var(--down)}
|
||||||
|
.pt-badge{font-size:11px;font-weight:700;border-radius:4px;padding:1px 7px}
|
||||||
|
.pt-badge.conf{background:#0d2a16;color:var(--up);border:1px solid #1a4731}
|
||||||
|
.pt-badge.form{background:#2a1f00;color:var(--accent);border:1px solid #4a3600}
|
||||||
|
.pt-q{margin-left:auto;font-size:12px;color:var(--dim);font-family:var(--mono)}
|
||||||
|
.pt-det{font-size:13px;margin-top:4px;font-family:var(--mono);color:var(--bright)}
|
||||||
|
.pt-note{font-size:12px;color:var(--dim);margin-top:3px}
|
||||||
|
.pt-disclaimer{font-size:12px;color:var(--dim);margin-top:6px;line-height:1.4}
|
||||||
#card-verdict{border-color:var(--accent)}
|
#card-verdict{border-color:var(--accent)}
|
||||||
.vd-head{display:flex;align-items:center;gap:14px;margin-bottom:10px}
|
.vd-head{display:flex;align-items:center;gap:14px;margin-bottom:10px}
|
||||||
.vd-ring{width:60px;height:60px;border-radius:50%;flex:0 0 auto;
|
.vd-ring{width:60px;height:60px;border-radius:50%;flex:0 0 auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user