diff --git a/CLAUDE.md b/CLAUDE.md index 64b571d..0b0d504 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -932,7 +932,10 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: KEIN CDN wegen WireGuard/Offline). Daten via `GET /api/bars?tf=&n=` → `engine.get_bars` (OHLC+EMA+Trend unter `mt5_lock`). `app.js`: `loadCharts`/ `renderChart`, Charts nur bei sichtbarem Tab geladen + Auto-Refresh 20 s - (schont den Lock). Broker-Zeit (UTC+3) → UTC via `broker_utc_offset`. + (schont den Lock). Broker-Zeit (UTC+3) → UTC via `broker_utc_offset`. **Gitternetz + dezent** (2026-07-27, User „dunkler/unauffälliger": `grid`-Farbe `#161b22`→`#10151c`, + näher am Hintergrund `#0d1117`). **Chartmuster-Overlay (nur M30)** — s. Chartmuster- + Kachel: die 2 stärksten M30-Muster als Trigger-/Ziel-Preislinien. **Regressionskanal-Overlay (nur M30, 2026-07-20):** `get_bars` liefert bei M30 `channel {dir, mid[], upper[], lower[]}` (Arrays deckungsgleich mit den Bars, `structure.channel_series` — Regression über 60 abgeschl. Bars, linear über die @@ -1134,7 +1137,16 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: 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- + {patterns[], atr, cur}`. **Nur AKTIVE, kursnahe Muster** (2026-07-27, `_detect`-Filter): + abgearbeitete Muster (Ziel bereits erreicht) UND zu weit entfernte (`_MAX_DIST_ATR=5,0`, + Trigger >5×ATR vom Kurs) werden ausgeblendet — sonst zeigte die Kachel alte, nicht mehr + handelbare Muster. **Grafische Darstellung (2026-07-27, User „grafisch darstellen"):** + (a) Muster-Karte zeigt je Item eine **kompakte SVG-Skizze** der Form (`_patternSvg`, + viewBox 56×32, farbcodiert grün/rot/grau) — Form auf einen Blick; (b) im **Charts-Tab + (M30)** werden die **2 stärksten Muster als Preislinien** gezeichnet: Trigger = kräftige + Linie (grün bull/rot bear, Label „▶ bestätigt"/„◌ bildet sich"), Ziel = gepunktete Linie + gleicher Farbe. Dazu liefert `get_bars` bei M30 jetzt `patterns` (aus `patterns.snapshot`). + 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 @@ -1249,7 +1261,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei **negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap. -- Asset-Version aktuell **v=116** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=117** (in `web/index.html` hochzählen, siehe Workflows). 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 `font: Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` — diff --git a/core/engine.py b/core/engine.py index 19629ed..50ed2f1 100644 --- a/core/engine.py +++ b/core/engine.py @@ -586,16 +586,22 @@ class TradingEngine: # Regressionskanal — nur M30 (dort ist die Marktstruktur berechnet), als # Linien-Arrays deckungsgleich mit den Bars (Overlay im Charts-Tab). channel = None + patterns = [] if tf_label == "M30": try: from core.structure import channel_series channel = channel_series(closes, atr, k) except Exception: channel = None + # Visuelle Chartmuster (M30) fürs Overlay — Trigger-/Ziel-Preislinien. + try: + patterns = (self.patterns.snapshot() or {}).get("patterns") or [] + except Exception: + patterns = [] return {"tf": tf_label, "symbol": sym, "trend": trend, "bars": bars, "ema12": [round(x, 3) for x in e12[-k:]], "ema50": [round(x, 3) for x in e50[-k:]], - "levels": levels, "channel": channel, + "levels": levels, "channel": channel, "patterns": patterns, "broker_utc_offset": 3 * 3600} def _in_startup_grace(self) -> bool: diff --git a/core/patterns.py b/core/patterns.py index 9051022..2ab839b 100644 --- a/core/patterns.py +++ b/core/patterns.py @@ -38,6 +38,8 @@ _MIN_HEIGHT_ATR = 1.2 # Muster-Höhe muss ≥ so viel ATR sein (sonst Rausche _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) +_MAX_DIST_ATR = 5.0 # Trigger max. so weit vom AKTUELLEN Kurs (sonst nicht mehr + # aktionabel — alte, weit weg gelaufene Muster ausblenden) def _atr(highs, lows, closes, p=14): @@ -189,8 +191,19 @@ def _detect(piv, cur, atr, last_idx): 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"] + def played_out(m): + """Ziel bereits erreicht → Muster abgearbeitet, nicht mehr zeigen.""" + t = m.get("target") + if t is None: + return False + return (cur <= t) if m["dir"] == "bear" else (cur >= t) + + # raus: ungültig · abgearbeitet (Ziel erreicht) · zu weit vom Kurs (nicht mehr + # aktionabel — nur AKTIVE, kursnahe Muster zeigen). + res = [m for m in best.values() + if m["status"] != "invalidated" + and not played_out(m) + and abs(m["trigger"] - cur) <= _MAX_DIST_ATR * atr] rank = {"confirmed": 2, "forming": 1} res.sort(key=lambda m: (rank.get(m["status"], 0), m["quality"], -m["bars_ago"]), reverse=True) return res diff --git a/web/app.js b/web/app.js index cdf465e..439fc32 100644 --- a/web/app.js +++ b/web/app.js @@ -225,6 +225,26 @@ function renderCircuitBreaker(cb) { } // Chartmuster (visuell) — reine Anzeige. Status: bildet sich / bestätigt / (ungültig raus). +// Kompakte SVG-Skizze der Muster-Form (viewBox 60×32). Rein schematisch, damit die +// Form auf einen Blick erkennbar ist; Farbe = Richtung (grün bull / rot bear / grau). +function _patternSvg(type, col) { + const P = { + double_top: "M2,26 L14,8 L22,16 L34,8 L46,26", + double_bottom: "M2,6 L14,24 L22,16 L34,24 L46,6", + hs: "M2,24 L12,14 L18,20 L28,4 L38,20 L44,14 L54,24", + inv_hs: "M2,8 L12,18 L18,12 L28,28 L38,12 L44,18 L54,8", + cup: "M2,6 C2,30 44,30 44,6 M44,6 L54,12", + inv_cup: "M2,26 C2,2 44,2 44,26 M44,26 L54,20", + tri_asc: "M2,26 L52,26 M2,4 L52,22", + tri_desc: "M2,6 L52,6 M2,28 L52,10", + tri_sym: "M2,4 L52,16 M2,28 L52,16", + }; + const d = P[type] || "M2,16 L54,16"; + return ``; +} + function renderPatterns(pt) { const el = $("pt-list"); if (!el) return; @@ -235,6 +255,7 @@ function renderPatterns(pt) { } el.innerHTML = items.map(m => { const dirCls = m.dir === "bull" ? "up" : m.dir === "bear" ? "down" : "dim"; + const col = m.dir === "bull" ? "#3fb950" : m.dir === "bear" ? "#f85149" : "#8b949e"; const arr = m.dir === "bull" ? "▲" : m.dir === "bear" ? "▼" : "◆"; const st = m.status === "confirmed" ? `bestätigt` @@ -243,7 +264,8 @@ function renderPatterns(pt) { ? `Ziel ${fmt(m.target, 2)}` : `Ausbruch offen`; const q = Math.round((m.quality || 0) * 100); return `
-
${arr} ${m.name}${st} +
${_patternSvg(m.type, col)} + ${arr} ${m.name}${st} Güte ${q}%
Trigger ${fmt(m.trigger, 2)} · ${tgt} · Höhe ${m.height_atr}×ATR · vor ${m.bars_ago} Bars
@@ -1161,7 +1183,7 @@ function _makeChart(tf) { const chart = LightweightCharts.createChart(el, { width: el.clientWidth || 320, height: 200, layout: { background: { color: "#0d1117" }, textColor: "#8b949e", fontSize: 15 }, - grid: { vertLines: { color: "#161b22" }, horzLines: { color: "#161b22" } }, + grid: { vertLines: { color: "#10151c" }, horzLines: { color: "#10151c" } }, timeScale: { borderColor: "#30363d", timeVisible: true, secondsVisible: false }, rightPriceScale: { borderColor: "#30363d" }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal }, @@ -1223,6 +1245,19 @@ function renderChart(tf, d) { (LV.zones || []).forEach(z => { addLine(z.hi, "#1f6feb", 1, "Zone"); addLine(z.lo, "#1f6feb", 1, ""); }); + // Chartmuster-Overlay (nur M30): Trigger = kräftige Linie (grün bull / rot bear), + // Ziel = gepunktete Linie gleicher Farbe. Nur die 2 stärksten, sonst wird's voll. + (d.patterns || []).slice(0, 2).forEach(p => { + const bull = p.dir === "bull"; + const col = bull ? "#3fb950" : (p.dir === "bear" ? "#f85149" : "#8b949e"); + c.lines.push(c.candle.createPriceLine({ + price: p.trigger, color: col, lineWidth: 2, lineStyle: 0, + axisLabelVisible: true, title: (p.status === "confirmed" ? "▶ " : "◌ ") + p.name })); + if (p.target != null) + c.lines.push(c.candle.createPriceLine({ + price: p.target, color: col, lineWidth: 1, lineStyle: 1, + axisLabelVisible: true, title: "Ziel" })); + }); c.chart.timeScale().fitContent(); const tr = document.getElementById("ctr-" + tf); const [txt, cls] = _TREND_TXT[d.trend] || ["—", "flat"]; diff --git a/web/index.html b/web/index.html index e25627d..b476ba3 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -226,6 +226,6 @@ - + diff --git a/web/style.css b/web/style.css index 1471a17..f738f23 100644 --- a/web/style.css +++ b/web/style.css @@ -147,6 +147,8 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px} 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-svg{flex:0 0 auto;background:var(--bg);border:1px solid var(--border); + border-radius:4px;padding:1px 2px} .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}