Chartmuster grafisch: SVG-Skizze in Karte + Trigger/Ziel-Linien im PWA-Chart

- patterns.py: nur AKTIVE, kursnahe Muster (abgearbeitete/Ziel erreicht raus,
  _MAX_DIST_ATR=5.0 Distanz-Filter)
- get_bars liefert bei M30 die Muster (patterns.snapshot) fürs Chart-Overlay
- app.js: _patternSvg-Skizze je Muster-Karte + 2 stärkste Muster als
  Trigger-/Ziel-Preislinien im M30-Chart; Gitternetz dezenter (#10151c)
- style.css: .pt-svg; index.html v=116->117; CLAUDE.md aktualisiert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-27 19:13:47 +02:00
co-authored by Claude Opus 4.8
parent 21d855b163
commit ae6bbb2f35
6 changed files with 78 additions and 10 deletions
+15 -3
View File
@@ -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=` → KEIN CDN wegen WireGuard/Offline). Daten via `GET /api/bars?tf=&n=` →
`engine.get_bars` (OHLC+EMA+Trend unter `mt5_lock`). `app.js`: `loadCharts`/ `engine.get_bars` (OHLC+EMA+Trend unter `mt5_lock`). `app.js`: `loadCharts`/
`renderChart`, Charts nur bei sichtbarem Tab geladen + Auto-Refresh 20 s `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 **Regressionskanal-Overlay (nur M30, 2026-07-20):** `get_bars` liefert bei M30
`channel {dir, mid[], upper[], lower[]}` (Arrays deckungsgleich mit den Bars, `channel {dir, mid[], upper[], lower[]}` (Arrays deckungsgleich mit den Bars,
`structure.channel_series` — Regression über 60 abgeschl. Bars, linear über die `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- Dreieck.** Je Muster: Richtung (bull/bear/neutral), Nackenlinie/Trigger, Measured-
Move-Ziel, Status (bildet sich / bestätigt / ungültig→raus), Güte (Extrem-Symmetrie), 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 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 Einfluss ERST nach Backtest** (`backtest_patterns.py`, noch zu bauen) — die
Muster-KLASSE ist bereits 2× als Signal verworfen (`backtest_doubletop.py` oben + Muster-KLASSE ist bereits 2× als Signal verworfen (`backtest_doubletop.py` oben +
`backtest_structure.py`), also **hohe Beweislast**; die Kachel macht das Muster nur `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 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=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 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`
+7 -1
View File
@@ -586,16 +586,22 @@ class TradingEngine:
# Regressionskanal — nur M30 (dort ist die Marktstruktur berechnet), als # Regressionskanal — nur M30 (dort ist die Marktstruktur berechnet), als
# Linien-Arrays deckungsgleich mit den Bars (Overlay im Charts-Tab). # Linien-Arrays deckungsgleich mit den Bars (Overlay im Charts-Tab).
channel = None channel = None
patterns = []
if tf_label == "M30": if tf_label == "M30":
try: try:
from core.structure import channel_series from core.structure import channel_series
channel = channel_series(closes, atr, k) channel = channel_series(closes, atr, k)
except Exception: except Exception:
channel = None 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, return {"tf": tf_label, "symbol": sym, "trend": trend, "bars": bars,
"ema12": [round(x, 3) for x in e12[-k:]], "ema12": [round(x, 3) for x in e12[-k:]],
"ema50": [round(x, 3) for x in e50[-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} "broker_utc_offset": 3 * 3600}
def _in_startup_grace(self) -> bool: def _in_startup_grace(self) -> bool:
+15 -2
View File
@@ -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) _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) _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_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): 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)) key = (m["type"], round(m["trigger"], 2))
if key not in best or m["quality"] > best[key]["quality"]: if key not in best or m["quality"] > best[key]["quality"]:
best[key] = m best[key] = m
# ungültige raus, dann nach Status (confirmed/forming) + Qualität + Recency def played_out(m):
res = [m for m in best.values() if m["status"] != "invalidated"] """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} rank = {"confirmed": 2, "forming": 1}
res.sort(key=lambda m: (rank.get(m["status"], 0), m["quality"], -m["bars_ago"]), reverse=True) res.sort(key=lambda m: (rank.get(m["status"], 0), m["quality"], -m["bars_ago"]), reverse=True)
return res return res
+37 -2
View File
@@ -225,6 +225,26 @@ function renderCircuitBreaker(cb) {
} }
// Chartmuster (visuell) — reine Anzeige. Status: bildet sich / bestätigt / (ungültig raus). // 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 `<svg class="pt-svg" viewBox="0 0 56 32" width="56" height="32" aria-hidden="true">
<path d="${d}" fill="none" stroke="${col}" stroke-width="2"
stroke-linejoin="round" stroke-linecap="round"/></svg>`;
}
function renderPatterns(pt) { function renderPatterns(pt) {
const el = $("pt-list"); const el = $("pt-list");
if (!el) return; if (!el) return;
@@ -235,6 +255,7 @@ function renderPatterns(pt) {
} }
el.innerHTML = items.map(m => { el.innerHTML = items.map(m => {
const dirCls = m.dir === "bull" ? "up" : m.dir === "bear" ? "down" : "dim"; 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 arr = m.dir === "bull" ? "▲" : m.dir === "bear" ? "▼" : "◆";
const st = m.status === "confirmed" const st = m.status === "confirmed"
? `<span class="pt-badge conf">bestätigt</span>` ? `<span class="pt-badge conf">bestätigt</span>`
@@ -243,7 +264,8 @@ function renderPatterns(pt) {
? `Ziel <b>${fmt(m.target, 2)}</b>` : `Ausbruch offen`; ? `Ziel <b>${fmt(m.target, 2)}</b>` : `Ausbruch offen`;
const q = Math.round((m.quality || 0) * 100); const q = Math.round((m.quality || 0) * 100);
return `<div class="pt-item ${dirCls}"> return `<div class="pt-item ${dirCls}">
<div class="pt-head"><span class="pt-name">${arr} ${m.name}</span>${st} <div class="pt-head">${_patternSvg(m.type, col)}
<span class="pt-name">${arr} ${m.name}</span>${st}
<span class="pt-q">Güte ${q}%</span></div> <span class="pt-q">Güte ${q}%</span></div>
<div class="pt-det">Trigger <b>${fmt(m.trigger, 2)}</b> · ${tgt} <div class="pt-det">Trigger <b>${fmt(m.trigger, 2)}</b> · ${tgt}
· Höhe ${m.height_atr}×ATR · vor ${m.bars_ago} Bars</div> · Höhe ${m.height_atr}×ATR · vor ${m.bars_ago} Bars</div>
@@ -1161,7 +1183,7 @@ function _makeChart(tf) {
const chart = LightweightCharts.createChart(el, { const chart = LightweightCharts.createChart(el, {
width: el.clientWidth || 320, height: 200, width: el.clientWidth || 320, height: 200,
layout: { background: { color: "#0d1117" }, textColor: "#8b949e", fontSize: 15 }, 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 }, timeScale: { borderColor: "#30363d", timeVisible: true, secondsVisible: false },
rightPriceScale: { borderColor: "#30363d" }, rightPriceScale: { borderColor: "#30363d" },
crosshair: { mode: LightweightCharts.CrosshairMode.Normal }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
@@ -1223,6 +1245,19 @@ function renderChart(tf, d) {
(LV.zones || []).forEach(z => { (LV.zones || []).forEach(z => {
addLine(z.hi, "#1f6feb", 1, "Zone"); addLine(z.lo, "#1f6feb", 1, ""); 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(); c.chart.timeScale().fitContent();
const tr = document.getElementById("ctr-" + tf); const tr = document.getElementById("ctr-" + tf);
const [txt, cls] = _TREND_TXT[d.trend] || ["—", "flat"]; const [txt, cls] = _TREND_TXT[d.trend] || ["—", "flat"];
+2 -2
View File
@@ -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=116"> <link rel="stylesheet" href="style.css?v=117">
</head> </head>
<body> <body>
<!-- Modul-Menü (ganz oben) --> <!-- Modul-Menü (ganz oben) -->
@@ -226,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=116"></script> <script src="app.js?v=117"></script>
</body> </body>
</html> </html>
+2
View File
@@ -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} 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-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-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-name{font-weight:700;font-size:15px}
.pt-item.up .pt-name{color:var(--up)} .pt-item.down .pt-name{color:var(--down)} .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{font-size:11px;font-weight:700;border-radius:4px;padding:1px 7px}