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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-04 16:34:26 +02:00
co-authored by Claude Opus 5
parent 7432be0e8c
commit da7d8f5b90
7 changed files with 284 additions and 18 deletions
+55 -1
View File
@@ -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:<weight> 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 (~23 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
+74 -1
View File
@@ -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,320,50 =
# Kostenfalle, 1517 Uhr ~0,13 = günstig. Reine Anzeige (Chip) — die
+80 -13
View File
@@ -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
+6 -1
View File
@@ -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}")
+34
View File
@@ -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 = '<span class="dim">—</span>'; return; }
const cls = v => v > 0 ? "up" : v < 0 ? "down" : "";
const box = (lbl, b) => !b || !b.n ? "" :
`<div class="mt-box"><div class="mt-lbl">${lbl}</div>`
+ `<div class="mt-val ${cls(b.sum)}">${b.sum >= 0 ? "+" : ""}${fmt(b.sum, 2)}</div>`
+ `<div class="mt-sub2">${b.n} Trades · ${fmt(b.wr, 1)} % · Ø ${b.avg >= 0 ? "+" : ""}${fmt(b.avg, 2)}</div></div>`;
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 ? "" :
`<span class="mt-chip"><b>${lbl}</b> ${b.n}× · ${fmt(b.wr, 1)} % · `
+ `<span class="${cls(b.sum)}">${b.sum >= 0 ? "+" : ""}${fmt(b.sum, 2)}</span></span>`;
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);
+12 -2
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0d1117">
<title>Oil · MT5</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css?v=133">
<link rel="stylesheet" href="style.css?v=134">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -124,6 +124,16 @@
</div>
</section>
<!-- Manuelle Trades — deskriptiv (2026-08-04). Zeigt die EIGENE Historie,
rechnet nichts hoch. Der volle Entscheidungskontext (ctx_*) wird erst
seit dem 04.08. mitgeschrieben; die Reifegrad-Zeile macht das sichtbar. -->
<section class="card wide" id="card-manual">
<h2>Manuelle Trades <span class="mt-sub">deine Historie · 30 Tage</span></h2>
<div class="mt-grid" id="mt-grid"></div>
<div class="mt-split" id="mt-split"></div>
<div class="mt-note dim" id="mt-note"></div>
</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>
@@ -249,6 +259,6 @@
<div id="toast" class="toast hidden"></div>
<script src="lightweight-charts.standalone.production.js?v=114"></script>
<script src="app.js?v=133"></script>
<script src="app.js?v=134"></script>
</body>
</html>
+23
View File
@@ -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; }