From da7d8f5b909745742dcb24c8fdaa271c583c3750 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Tue, 4 Aug 2026 16:34:26 +0200 Subject: [PATCH] Stufe 1: voller Entscheidungskontext beim Einstieg + Karte "Manuelle Trades" Frage: kann der Bot aus den manuellen Trades lernen? Vorpruefung: noch NICHT. 1) Die entscheidenden Merkmale wurden nie gespeichert - beim Einstieg nur 5 Felder, davon ai_sentiment in 1,4 % und news_score in 34 % befuellt. 2) Das naive Lernziel ("wann steigt er ein") braucht unbeschriftete Negative; brauchbar ist nur "welche Einstiege liefen gut" (Labels ueber P&L). 3) Die scheinbaren Muster sind Einzeltage: Stunde 13:00 zeigt +478 EUR ueber n=69, davon +382 EUR (80 %) aus den fuenf Trades des 04.08. Gebaut wurde Stufe 1 - reine Telemetrie, kein Verhalten: - trades um 16 ctx_*-Spalten erweitert (DB-Backup .bak-2026-08-04, 1.148 Zeilen unveraendert): p_break_target/-stop, dist_res_atr/dist_sup_atr, structure, squeeze, htf_trend/h1_trend, spread_atr, session, block_reason, conf_pct, atr_m5, bias, cone_pos, hour - log_trade_open haengt die Spalten dynamisch an und ignoriert unbekannte Schluessel -> aeltere DB ohne Migration laeuft weiter (fail-open) - Kette: engine._run_analysis -> trader.set_open_context(ctx=) -> log_trade_open Fallstrick beim Bau: wave_snap existiert in _run_analysis NICHT (nur in snapshot()). Der erste Entwurf haette einen NameError erzeugt, den das umgebende except still geschluckt haette - der Kontext waere dauerhaft leer geblieben ohne dass es auffaellt. Jetzt self.wave.snapshot(); vd zusaetzlich per locals()-Guard. Verifiziert auf einer DB-KOPIE: alle 16 Spalten korrekt geschrieben, unbekannter Schluessel ignoriert, Basisfelder unberuehrt. Snapshot liefert manual_stats. Dashboard-Karte "Manuelle Trades" (#card-manual, v=134): heute / 30 Tage / gesamt / Bot-Vergleich plus Split mit vs ohne Signal-Deckung. Backend history.manual_stats(30) mit ~120-s-Cache wie _alignment_cached. Bewusst neutral gefaerbt und deskriptiv; eine Reifegrad-Zeile nennt die ctx-Abdeckung (heute 0 %), damit die Karte nicht ueberschaetzt wird. Stufe 2 NICHT gebaut: erst bei ausreichender ctx-Abdeckung messen, was Gewinner von Verlierern unterscheidet (Fit H1, validiert H2, AUC + Kalibrierung). Ergebnis waere ein Hinweis im Order-Dialog, ausdruecklich KEIN Auto-Entry. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 56 ++++++++++++++++++++++++++++- core/engine.py | 75 ++++++++++++++++++++++++++++++++++++++- core/history.py | 93 ++++++++++++++++++++++++++++++++++++++++++------- core/trader.py | 7 +++- web/app.js | 34 ++++++++++++++++++ web/index.html | 14 ++++++-- web/style.css | 23 ++++++++++++ 7 files changed, 284 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8d05774..bc2966e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2503,7 +2503,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=133** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=134** (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` — @@ -3027,6 +3027,60 @@ PF>1 in BEIDEN Hälften, Nachbarparameter müssen mithalten). Begründung gegen wöchentliche Läufe (Mehrfachvergleich, ~47 Fehlalarme/Jahr) steht unverändert. Anlass war hier die *einmalige* Signaländerung. +## Modul „Manuelle Trades" + voller Entscheidungskontext (Stufe 1, 2026-08-04) + +**User-Frage:** „Kann der Bot aus meinen manuellen Trades lernen — evtl. ein neues +Modul?" Auslöser: am 04.08. machten 18 manuelle Trades **+728 €**, der Bot −16,53 €, +bei 93 % WARTEN. + +⚠ **Die exploratorische Vorprüfung ergab: noch NICHT lernbar** — und zwar aus drei +Gründen, die alle benannt gehören: +1. **Die entscheidenden Merkmale wurden nie gespeichert.** Beim Einstieg schrieb der + Bot 5 Felder; davon war `ai_sentiment` in **1,4 %** und `news_score` in **34 %** der + Trades befüllt. Die Marktlage eines manuellen Einstiegs war damit nicht + rekonstruierbar. +2. **Das naive Lernziel ist untauglich.** „Wann steigt er ein" bräuchte Gegenbeispiele + (Bars ohne Einstieg) — Millionen unbeschriftete Negative, und „nicht eingestiegen" + heißt nicht „hätte man nicht sollen". Die brauchbare Frage lautet: **welche seiner + Einstiege liefen gut** (saubere Labels über P&L). +3. **Die scheinbaren Muster sind Einzeltage.** Stunde 13:00 zeigt +478 € über n=69 — + davon stammen **+382 € (80 %) aus den fünf Trades des 04.08.** Genau die Falle, an + der ORB und die Dead-Hours gescheitert sind. + +**Gebaut wurde deshalb Stufe 1 — Telemetrie, kein Verhalten:** +`trades` um **16 `ctx_*`-Spalten** erweitert (DB-Backup `…bak-2026-08-04`, 1.148 +Zeilen unverändert): `p_break_target/-stop` · `dist_res_atr`/`dist_sup_atr` · +`structure` · `squeeze` · `htf_trend`/`h1_trend` · `spread_atr` · `session` · +`block_reason` · `conf_pct` · `atr_m5` · `bias` · `cone_pos` · `hour`. Alle Werte +werden im Snapshot ohnehin berechnet und nur eingesammelt +(`engine._run_analysis` → `trader.set_open_context(ctx=…)` → `history.log_trade_open`). +`log_trade_open` hängt die Spalten **dynamisch** an und ignoriert unbekannte Schlüssel +— eine ältere DB ohne Migration läuft weiter (fail-open). +⚠ **Fallstrick beim Bau:** `wave_snap` existiert in `_run_analysis` NICHT (nur in +`snapshot()`). Der erste Entwurf hätte einen `NameError` erzeugt, den das umgebende +`except` still geschluckt hätte — der Kontext wäre dauerhaft leer geblieben, ohne dass +es auffällt. Jetzt `self.wave.snapshot()`; `vd` zusätzlich per `locals()`-Guard +abgesichert (es entsteht in einem eigenen `try`). +Verifiziert auf einer DB-**Kopie**: alle 16 Spalten korrekt geschrieben, unbekannter +Schlüssel ignoriert, Basisfelder unberührt. + +**Dashboard-Karte „Manuelle Trades"** (`#card-manual`, v=134): heute / 30 Tage / +gesamt / Bot-Vergleich, dazu der Split **mit vs. ohne Signal-Deckung**. Backend +`history.manual_stats(30)` → `engine._manual_stats_cached()` (~120 s Cache, wie +`_alignment_cached` — die Aggregation läuft über die ganze Tabelle und hat im +1-s-Snapshot nichts zu suchen). Bewusst **neutral gefärbt** (keine Ampel) und +ausdrücklich **deskriptiv**: die Karte zeigt eine Reifegrad-Zeile mit der +`ctx`-Abdeckung (heute 0 %), damit niemand sie für aussagekräftiger hält, als sie ist. + +**Stufe 2 (später, NICHT gebaut):** wenn genug `ctx`-Daten vorliegen (~2–3 Monate), +messen was Gewinner von Verlierern unterscheidet — Fit auf H1, validiert auf H2, AUC +und Kalibrierung berichtet, wie beim P(break)-Modell. Ergebnis wäre ein **Hinweis im +Order-Dialog** („deine Trades in dieser Konstellation: X % WR, n=Y"), analog zum +bestehenden Ausrichtungs-Split. ⚠ **Ausdrücklich KEIN Auto-Entry daraus** — 22 +Signal-Eingriffe sind gescheitert, und die gemessene Stärke des Users liegt in der +AUSWAHL, nicht in einer Regel. Ein Modul, das ihm seine eigene Historie zur aktuellen +Lage zeigt, spielt auf diese Stärke; eines das für ihn einsteigt, ersetzt sie. + ## Backtest-Audit 2026-08-04 — rechnen die Skripte noch korrekt? **Anlass:** User-Frage nach dem `angle=`-Fund in `backtest_auto_signal.py`. Prüfung diff --git a/core/engine.py b/core/engine.py index 641e657..e927f7d 100644 --- a/core/engine.py +++ b/core/engine.py @@ -1474,6 +1474,59 @@ class TradingEngine: log.debug(f"log_verdict_votes: {e}") # Kontext für Trade-Logging (set_open_context beim Öffnen) + # ── Voller Entscheidungszustand beim Einstieg (Stufe 1 „Manuelle Trades", + # 2026-08-04). Bis dahin wurden 5 Felder gespeichert, davon zwei fast nie + # befüllt — die Marktlage eines manuellen Einstiegs war damit NICHT + # rekonstruierbar. Alles hier ist bereits berechnet (Snapshot-Teile), es + # wird nur eingesammelt. Reine Telemetrie, kein Verhalten. + _ctx = {} + try: + # ⚠ `wave_snap` existiert in dieser Methode NICHT (anders als in + # `snapshot()`) — hier den Snapshot selbst holen. Ein NameError wäre vom + # `except` unten still geschluckt worden und hätte den Kontext dauerhaft + # leer gelassen, ohne dass es jemand merkt. + _ws = self.wave.snapshot() or {} + _bid = s.get("bid") + _atr5 = (_ws.get("pb_feats") or {}).get("atr") + _sh = self._last_sr_close_hint or {} + _sa = self._last_stop_approach or {} + _dl = self._draw_levels() or {} + _res = (_dl.get("res") or [None])[0] + _sup = (_dl.get("sup") or [None])[0] + _st = (self.structure.snapshot() or {}).get("trend") + _sq = (_ws.get("squeeze") or {}).get("state") + _ses = (session_state() or {}).get("phase") + _cone = self._last_cone or {} + _c120 = next((l for l in (_cone.get("levels") or []) + if l.get("band") == 80 and l.get("minutes") == 120), None) + _ctx = { + "p_break_target": _sh.get("p_break"), + "p_break_stop": _sa.get("p_break"), + "dist_res_atr": (round((_res - _bid) / _atr5, 2) + if (_res and _bid and _atr5) else None), + "dist_sup_atr": (round((_bid - _sup) / _atr5, 2) + if (_sup and _bid and _atr5) else None), + "structure": _st, + "squeeze": _sq, + "htf_trend": _ws.get("htf_trend"), + "h1_trend": _ws.get("h1_trend"), + "spread_atr": (round(float(s.get("spread") or 0) / _atr5, 3) + if _atr5 else None), + "session": _ses, + "block_reason": rec.get("block"), + "conf_pct": rec.get("conf_pct"), + "atr_m5": (round(_atr5, 4) if _atr5 else None), + # `vd` entsteht oben in einem try — bei einem Fehler dort wäre es + # gar nicht definiert. `locals()`-Guard statt NameError. + "bias": (locals().get("vd") or {}).get("bias"), + # Position im 120-min-Kegel: 0 = unteres Band, 1 = oberes + "cone_pos": (round((_bid - _c120["lo"]) + / max(_c120["hi"] - _c120["lo"], 1e-9), 3) + if (_c120 and _bid) else None), + "hour": datetime.now(_BERLIN).hour, + } + except Exception as e: + log.debug(f"open-context: {e}") self._last_rec = { "rec_signal": rec.get("signal"), "rec_score": rec.get("score"), @@ -1482,7 +1535,8 @@ class TradingEngine: "setup": rec.get("setup"), "regime": None, "rsi": s.get("rsi_m15"), - "news_score": None, + "news_score": _news, + "ctx": _ctx, } def _check_flip_close(self): @@ -2682,6 +2736,21 @@ class TradingEngine: # ══════════════════════════════════════════════════════════════════════ _SNAP_TTL_S = 0.8 # geteilter Snapshot gilt so lange als frisch + def _manual_stats_cached(self) -> dict | None: + """Kennzahlen der manuellen Trades für die Karte „Manuelle Trades" + (2026-08-04). Gleiches Cache-Muster wie `_alignment_cached` — die + Aggregation läuft über die ganze `trades`-Tabelle und hat im 1-s-Snapshot + nichts zu suchen.""" + now = time.time() + if now - getattr(self, "_manual_ts", 0.0) > 120: + try: + self._manual_cache = self.history.manual_stats(30) + except Exception as e: + log.debug(f"manual_stats: {e}") + self._manual_cache = None + self._manual_ts = now + return getattr(self, "_manual_cache", None) + def _alignment_cached(self) -> dict | None: """Ausrichtungs-Split (mit/gegen/ohne Empfehlung, letzte 40 Trades) für den Order-Bestätigungsdialog — ehrliche eigene Zahlen statt generischem Hinweis @@ -3220,6 +3289,10 @@ class TradingEngine: "news_sentiment": (lambda sc: round(float(sc), 2) if sc is not None else None)( (self.news.sentiment or {}).get("score")), "alignment": self._alignment_cached(), + # Kennzahlen der MANUELLEN Trades für die gleichnamige Karte + # (2026-08-04). Rein deskriptiv; ~120 s gecacht, damit der heiße + # Snapshot-Pfad nicht je Abruf die DB aggregiert. + "manual_stats": self._manual_stats_cached(), # Live-Handelskosten relativ zur Vola (2026-07-24): Spread/ATR(M5). # Gemessen (`backtest_realcosts.py`): Ø 0,265×ATR, Nacht 0,32–0,50 = # Kostenfalle, 15–17 Uhr ~0,13 = günstig. Reine Anzeige (Chip) — die diff --git a/core/history.py b/core/history.py index c353553..ef345e2 100644 --- a/core/history.py +++ b/core/history.py @@ -270,26 +270,45 @@ class HistoryLogger: setup: str | None = None, regime: str | None = None, rsi_at_entry: float | None = None, - news_score: float | None = None): - """Loggt das Öffnen eines Trades. Idempotent über UNIQUE(ticket).""" + news_score: float | None = None, + ctx: dict | None = None): + """Loggt das Öffnen eines Trades. Idempotent über UNIQUE(ticket). + + `ctx` = vollständiger Entscheidungszustand beim Einstieg (Stufe 1 des + „Manuelle Trades"-Moduls, 2026-08-04). Bis dahin wurden nur 5 Felder + gespeichert — davon `ai_sentiment` in 1,4 % und `news_score` in 34 % der + Fälle befüllt. Damit war NICHT rekonstruierbar, in welcher Marktlage der + User eingestiegen ist, und jede Auswertung seiner Diskretion blieb + Spekulation. Alle `ctx_*`-Werte werden im Snapshot ohnehin berechnet; + hier werden sie nur mitgeschrieben (reine Telemetrie, kein Verhalten). + Unbekannte Schlüssel werden ignoriert — so bricht ein späterer Ausbau + nichts.""" # Guard (Fix 2026-07-19): 0-Lot-/Preis-lose Einträge sind Reconcile- # Artefakte (real: Ticket 47966457, 0.0L, closed_by=unknown) — sie # verfälschen WR/Statistik. Nicht loggen. if not lots or lots <= 0 or not entry_price or entry_price <= 0: return now_ts = int(time.time()) + # Kontext-Spalten dynamisch anhängen — nur solche, die es in der Tabelle + # wirklich gibt. So läuft eine ältere DB ohne Migration weiter (fail-open), + # statt beim INSERT zu werfen. + ctx = ctx or {} with self._lock, self._connect() as conn: - conn.execute(""" - INSERT OR IGNORE INTO trades - (ticket, symbol, direction, lots, - entry_time, entry_price, sl_at_entry, tp_at_entry, - ai_sentiment, ai_confidence, rec_signal, rec_score, - setup, regime, rsi_at_entry, news_score) - VALUES (?,?,?,?, ?,?,?,?, ?,?,?,?, ?,?,?,?) - """, (ticket, symbol, direction, lots, - now_ts, entry_price, sl_at_entry, tp_at_entry, - ai_sentiment, ai_confidence, rec_signal, rec_score, - setup, regime, rsi_at_entry, news_score)) + vorhanden = {r[1] for r in conn.execute("PRAGMA table_info(trades)")} + extra = [(f"ctx_{k}", v) for k, v in ctx.items() + if f"ctx_{k}" in vorhanden and v is not None] + spalten = ("ticket, symbol, direction, lots, entry_time, entry_price, " + "sl_at_entry, tp_at_entry, ai_sentiment, ai_confidence, " + "rec_signal, rec_score, setup, regime, rsi_at_entry, news_score") + werte = [ticket, symbol, direction, lots, now_ts, entry_price, + sl_at_entry, tp_at_entry, ai_sentiment, ai_confidence, + rec_signal, rec_score, setup, regime, rsi_at_entry, news_score] + for name, v in extra: + spalten += f", {name}" + werte.append(v) + platz = ",".join("?" * len(werte)) + conn.execute(f"INSERT OR IGNORE INTO trades ({spalten}) VALUES ({platz})", + werte) conn.commit() # Kein Telegram bei Einstieg — nur Ergebnis (Close) wird gesendet @@ -454,6 +473,54 @@ class HistoryLogger: pattern, pattern_w, orderbook, orderbook_w, liqtrend, liqtrend_w)) conn.commit() + def manual_stats(self, tage: int = 30) -> dict: + """Kennzahlen NUR der manuellen Trades — Datengrundlage der Dashboard-Karte + „Manuelle Trades" (2026-08-04). + + ⚠ Bewusst DESKRIPTIV, nicht prädiktiv. Der volle Entscheidungszustand wird + erst seit dem 04.08. mitgeschrieben (`ctx_*`); bis genug davon vorliegt, + wäre jede Vorhersage aus diesen Zahlen Rauschen. Die Karte zeigt deshalb, + was die eigene Historie SAGT, und rechnet nichts hoch. + ⚠ `ctx_abdeckung` macht sichtbar, wie weit der neue Kontext schon reicht — + ohne diese Zahl könnte man die Karte für aussagekräftiger halten, als sie ist. + """ + MAN = ("(setup IS NULL OR (setup NOT LIKE 'AUTOSIG%' " + "AND setup NOT LIKE 'SQUEEZE%'))") + + def agg(row): + n = row["n"] or 0 + return {"n": n, "wr": round(100.0 * (row["w"] or 0) / n, 1) if n else 0.0, + "sum": round(row["s"] or 0.0, 2), + "avg": round((row["s"] or 0.0) / n, 2) if n else 0.0} + + out = {"tage": tage} + with self._connect() as conn: + conn.row_factory = sqlite3.Row + q = (f"SELECT COUNT(*) n, SUM(CASE WHEN pnl>0 THEN 1 ELSE 0 END) w, " + f"SUM(pnl) s FROM trades WHERE pnl IS NOT NULL AND {MAN}") + seit = f"AND exit_time >= strftime('%s','now','-{int(tage)} days')" + out["fenster"] = agg(conn.execute(f"{q} {seit}").fetchone()) + out["gesamt"] = agg(conn.execute(q).fetchone()) + out["heute"] = agg(conn.execute( + f"{q} AND exit_time >= strftime('%s','now','start of day')").fetchone()) + # Mit / ohne Signal-Deckung — die dokumentierte Kern-Frage + for lbl, cond in (("mit_signal", "rec_signal IN ('LONG','SHORT')"), + ("ohne_signal", "rec_signal='WARTEN'")): + out[lbl] = agg(conn.execute(f"{q} {seit} AND {cond}").fetchone()) + # Bot zum Vergleich (dieselbe Fensterlogik) + qb = ("SELECT COUNT(*) n, SUM(CASE WHEN pnl>0 THEN 1 ELSE 0 END) w, " + "SUM(pnl) s FROM trades WHERE pnl IS NOT NULL AND " + "(setup LIKE 'AUTOSIG%' OR setup LIKE 'SQUEEZE%')") + out["bot"] = agg(conn.execute(f"{qb} {seit}").fetchone()) + # Abdeckung des NEUEN Kontexts — ehrlicher Reifegrad-Indikator + r = conn.execute( + f"SELECT COUNT(*) n, SUM(CASE WHEN ctx_atr_m5 IS NOT NULL THEN 1 ELSE 0 END) c " + f"FROM trades WHERE {MAN}").fetchone() + out["ctx_abdeckung"] = { + "n": r["n"] or 0, "mit_ctx": r["c"] or 0, + "pct": round(100.0 * (r["c"] or 0) / (r["n"] or 1), 1)} + return out + def alignment_stats(self, last_n: int = 40) -> dict: """Ausrichtungs-Split der letzten N geschlossenen Trades: liefen sie MIT, GEGEN oder OHNE die Empfehlung (rec_signal beim Entry)? Für die ehrliche diff --git a/core/trader.py b/core/trader.py index 55554bb..234b525 100644 --- a/core/trader.py +++ b/core/trader.py @@ -315,11 +315,14 @@ class TradeManager: def set_open_context(self, *, ai_sentiment=None, ai_confidence=None, rec_signal=None, rec_score=None, - setup=None, regime=None, rsi=None, news_score=None): + setup=None, regime=None, rsi=None, news_score=None, + ctx=None): + # `ctx` = voller Entscheidungszustand (2026-08-04, s. `history.log_trade_open`) self._open_context = { "ai_sentiment": ai_sentiment, "ai_confidence": ai_confidence, "rec_signal": rec_signal, "rec_score": rec_score, "setup": setup, "regime": regime, "rsi": rsi, "news_score": news_score, + "ctx": ctx or {}, } def _log_open(self, ticket: int, sym: str, direction: str, entry_price: float): @@ -351,6 +354,8 @@ class TradeManager: rec_score=ctx.get("rec_score"), setup=ctx.get("setup"), regime=ctx.get("regime"), rsi_at_entry=ctx.get("rsi"), news_score=ctx.get("news_score"), + # Voller Entscheidungszustand (2026-08-04, „Manuelle Trades") + ctx=ctx.get("ctx"), ) except Exception as e: log_hist.error(f"log_trade_open: {e}") diff --git a/web/app.js b/web/app.js index 0e002b1..aa9da17 100644 --- a/web/app.js +++ b/web/app.js @@ -226,6 +226,39 @@ function renderGaps(g, m) { }).join(""); } +// Manuelle Trades (2026-08-04) — DESKRIPTIV. Zeigt die eigene Historie, macht +// keine Vorhersage: der volle Entscheidungskontext wird erst seit dem 04.08. +// mitgeschrieben, vorher wären Aussagen daraus Rauschen. Die Reifegrad-Zeile +// nennt die Abdeckung ehrlich, damit die Karte nicht überschätzt wird. +function renderManual(m) { + const grid = $("mt-grid"); if (!grid) return; // null-sicher (Cache-Regel) + if (!m || !m.gesamt) { grid.innerHTML = ''; return; } + const cls = v => v > 0 ? "up" : v < 0 ? "down" : ""; + const box = (lbl, b) => !b || !b.n ? "" : + `
${lbl}
` + + `
${b.sum >= 0 ? "+" : ""}${fmt(b.sum, 2)}
` + + `
${b.n} Trades · ${fmt(b.wr, 1)} % · Ø ${b.avg >= 0 ? "+" : ""}${fmt(b.avg, 2)}
`; + grid.innerHTML = box("heute", m.heute) + box(`${m.tage} Tage`, m.fenster) + + box("gesamt", m.gesamt) + box("Bot (Vergleich)", m.bot); + + // Signal-Deckung: die dokumentierte Kern-Frage — trägt Handeln OHNE Empfehlung? + const sp = $("mt-split"); + if (sp) { + const z = (lbl, b) => !b || !b.n ? "" : + `${lbl} ${b.n}× · ${fmt(b.wr, 1)} % · ` + + `${b.sum >= 0 ? "+" : ""}${fmt(b.sum, 2)}`; + sp.innerHTML = z("mit Signal", m.mit_signal) + z("ohne Signal", m.ohne_signal); + } + const nt = $("mt-note"); + if (nt) { + const c = m.ctx_abdeckung || {}; + nt.textContent = `Entscheidungs-Kontext gespeichert für ${c.mit_ctx || 0} von ` + + `${c.n || 0} Trades (${fmt(c.pct || 0, 1)} %). Erst ab ausreichender ` + + `Abdeckung lässt sich messen, WORAN gute von schlechten Einstiegen zu ` + + `unterscheiden sind — bis dahin ist diese Karte reine Beschreibung.`; + } +} + // Marktstruktur (M30) — reine Anzeige (HH/HL/LH/LL, BOS, Regressionskanal). function renderStructure(s) { s = s || {}; @@ -571,6 +604,7 @@ function render(d) { renderCandles(d.candle_anat); renderCone(d.cone); renderStructure(d.structure); + renderManual(d.manual_stats); renderStatsCompact(d.stats_compact, d.market); renderGaps(d.gaps, d.market); diff --git a/web/index.html b/web/index.html index 11f172d..4fc6c68 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -124,6 +124,16 @@ + +
+

Manuelle Trades deine Historie · 30 Tage

+
+
+
+
+

Chartmuster M30 · visuell

@@ -249,6 +259,6 @@ - + diff --git a/web/style.css b/web/style.css index bc9a92e..472b6bf 100644 --- a/web/style.css +++ b/web/style.css @@ -489,3 +489,26 @@ main{padding-bottom:74px} /* Platz für die fixe Leiste */ gefaerbt als der Broker-Kurs, damit man ihn nicht verwechselt. */ #hdr-price.hl-price{color:var(--accent)} #hdr-price-lbl{letter-spacing:.3px} + +/* ── Manuelle Trades (2026-08-04) ───────────────────────────────────────── + Bewusst NEUTRAL gehalten: die Karte beschreibt die eigene Historie und + sagt nichts voraus. Farbe nur am Vorzeichen der Summe, keine Ampel. */ +#card-manual .mt-sub { font-size: 13px; color: var(--dim); font-weight: 400; } +.mt-grid { + display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 10px; margin-bottom: 10px; +} +.mt-box { + background: var(--surface-2); border: 1px solid var(--border); + border-radius: var(--r-sm); padding: 9px 11px; +} +.mt-lbl { font-size: 13px; color: var(--dim); margin-bottom: 3px; } +.mt-val { font-family: var(--mono); font-size: 21px; font-weight: 600; } +.mt-sub2 { font-size: 12px; color: var(--dim); font-family: var(--mono); margin-top: 2px; } +.mt-split { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; } +.mt-chip { + background: var(--surface-2); border: 1px solid var(--line); + border-radius: var(--r-sm); padding: 5px 10px; + font-size: 13px; font-family: var(--mono); +} +.mt-note { font-size: 12px; line-height: 1.45; }