D+A: Klimax-Fade verworfen, Kerzen-Anatomie-Kachel gebaut (reine Anzeige, v=123)

D - backtest_candle_fade.py: der Lead aus backtest_candles.py (Fade der
volumenstarken Klimax-Kerze) faellt im echten Trade-Sim durch. Live-Exit
(SL 2,0/Trail 1,5/BE 1,3/Time-Stop 120') + Echtkosten, mit KONTROLLGRUPPE
"Fade ohne Volumen-Filter" (die Lehre aus backtest_patterns.py).

  Kontrolle   -0,080 / -0,046
  Vol >= 2,0  -0,059 / +0,103
  Vol >= 2,5  +0,144 / -0,080
  Vol >= 3,0  +0,113 / -0,064

Benachbarte Schwellen exakt gegenlaeufig = Rauschmuster. Die einzige
beidseitig positive Zelle (Vol>=2,0 + Marubozu, +0,036/+0,043) hat n=91/118
bei 7 Varianten = Mehrfachvergleich-Artefakt. 18. verworfener Eingriff.

Wichtigste Lehre: Informations-Ueberschuss != handelbarer Edge. Dieselbe
2,0-Schwelle liefert im Forward-Fenster +0,12/+0,20 beim Fade, im echten
Trade -0,059/+0,103 - die Gegenbewegung ist diffus, der 2xATR-Stop wird
unterwegs getroffen (WR nur 35-40 %). Der Weg zum Ziel zaehlt.

A - core/candles.py + Kachel #card-candles (M5, ~20 s, unter mt5_lock):
Marubozu, Langer Koerper, Langer Docht oben/unten, Spinning Top, Doji,
Engulfing. Schwellen identisch zum Backtest.

Bewusste UI-Entscheidung: die Kachel ist NEUTRAL gefaerbt (keine gruen/rot-
Ampel) und nennt die GEMESSENE Lesart ("Klimax-Kerze - der Kurs laeuft
danach eher GEGEN die Kerze"), nicht die Lehrbuch-Lesart. Eine
Richtungsampel wuerde hier systematisch falsch stupsen - gleiche Korrektur
wie bei der Bounce-Anzeige. Kein Verdict-Gewicht, kein Trigger.

Beim Bau gefunden: mt5_lock ist ein CONTEXT-MANAGER, kein Lock-Objekt -
.acquire()/.release() wirft. Jetzt "with mt5_lock(3.0) as got".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-31 08:15:46 +02:00
co-authored by Claude Opus 5
parent 7873bf2fb7
commit 03be3b8f5f
7 changed files with 467 additions and 3 deletions
+33 -1
View File
@@ -1426,6 +1426,38 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
Continuation = breakeven-negativ (0,011/0,005, Momentum-Klasse). Damit ist auch Continuation = breakeven-negativ (0,011/0,005, Momentum-Klasse). Damit ist auch
die Struktur-Idee empirisch durch — die Kachel bleibt Ist-Zustand-Anzeige (wie die Struktur-Idee empirisch durch — die Kachel bleibt Ist-Zustand-Anzeige (wie
TF-Ampel/Squeeze), steuert nichts. TF-Ampel/Squeeze), steuert nichts.
- **Kerzen-Anatomie (`core/candles.py` + Kachel `#card-candles`, 2026-07-31, User-Vorgabe
„Kerzengröße und Dochtlänge … im Kontext"): REINE ANZEIGE — und die Kachel widerspricht
bewusst der Lehrbuch-Deutung.** Erkennt mechanisch aus M5-OHLCV: Marubozu · Langer
Körper · Langer Docht oben/unten · Spinning Top · Doji · Engulfing (Schwellen identisch
zum Backtest, damit Anzeige und Messung dieselbe Sprache sprechen); Refresh ~20 s im
Trend-Loop unter `mt5_lock`, Snapshot `candle_anat {tf, items[], vol_hot_x}`.
**GEMESSEN (`backtest_candles.py`, 80k M5, 2 Halbjahre, Forward-Return 60 min): die
gängige Deutung ist in BEIDEN Hälften INVERTIERT.** ⚠ Methodik-Kern: gerichtete Klassen
werden gegen die **DRIFT-NULL** (bm×E[d]) gelesen, nicht gegen die Baseline — sonst
liest man reine Regime-Drift als Edge (H1 war Abwärts-, H2 Aufwärts-Regime).
Überschuss-Befunde: **„Langer Körper = Fortsetzung" → 0,059/0,076** (Kurs läuft
GEGEN die Kerze); **„im Trend = Fortsetzung" → 0,132/0,042** (es ist die KLIMAX-,
nicht die Fortsetzungskerze); **„2× Volumen = echt" → 0,117/0,204** vs 0,065/0,035
bei normalem Volumen (**monoton in die falsche Richtung** — mehr Volumen = stärkere
Gegenbewegung); **„Docht am Level zuverlässig" → in BEIDEN Hälften SCHLECHTER als in
der Range-Mitte**; „mit/gegen Trend" beim Docht kippt (0,102/+0,089) = Rauschen.
**Folgetest als Handelssignal = ERNEUT VERWORFEN (`backtest_candle_fade.py`, Live-Exit
SL 2,0/Trail 1,5/BE 1,3/Time-Stop 120', Echtkosten):** der Fade der Klimax-Kerze
fällt durch — Kontrolle (ohne Volumen-Filter) 0,080/0,046; Vol≥2,0 0,059/**+0,103**;
Vol≥2,5 **+0,144**/0,080; Vol≥3,0 +0,113/0,064 → **benachbarte Schwellen exakt
gegenläufig** = Rauschmuster. Einzige beidseitig positive Zelle (Vol≥2,0+Marubozu
+0,036/+0,043) hat n=91/118 bei 7 getesteten Varianten = Mehrfachvergleich-Artefakt.
⚠ **Methodisch die wichtigste Lehre des Laufs: Informations-Überschuss ≠ handelbarer
Edge.** Dieselbe 2,0-Schwelle liefert im Forward-Fenster 0,117/0,204 (also +0,12/+0,20
beim Fade), im echten Trade aber 0,059/+0,103 — die Gegenbewegung ist diffus, der
2×ATR-Stop wird unterwegs getroffen (WR nur 3540 %). **Der Weg zum Ziel zählt, nicht
nur das Ziel.** **18. verworfener Signal-Eingriff.****Konsequenz für die UI:** die
Kachel ist bewusst **NEUTRAL** gefärbt (keine grün/rot-Ampel) und der Hinweistext nennt
die **gemessene** Lesart („⚠ Klimax-Kerze — der Kurs läuft danach eher GEGEN die
Kerze"), nicht die Lehrbuch-Lesart — eine Richtungsampel würde hier systematisch
falsch stupsen. Gleiche Korrektur wie bei der Bounce-Anzeige (heißt deshalb
„Überdehnung", bernstein statt grün/rot). Kein Verdict-Gewicht, kein Trade-Trigger.
- **Chart-Muster (Doppeltop/-boden) als Signal = VERWORFEN (gemessen `backtest_doubletop.py`, - **Chart-Muster (Doppeltop/-boden) als Signal = VERWORFEN (gemessen `backtest_doubletop.py`,
2026-07-20, User-Idee „Mustererkennung SKS/Doppeltop"):** das am saubersten mechanisch 2026-07-20, User-Idee „Mustererkennung SKS/Doppeltop"):** das am saubersten mechanisch
definierbare Umkehrmuster (2 ~gleiche Swing-Pivots + Nackenlinien-Bruch, aus derselben definierbare Umkehrmuster (2 ~gleiche Swing-Pivots + Nackenlinien-Bruch, aus derselben
@@ -1647,7 +1679,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=120** (in `web/index.html` hochzählen, siehe Workflows). - Asset-Version aktuell **v=123** (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`
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Fade der Klimax-Kerze: langer Körper + hohes Volumen GEGEN die Körperrichtung.
Folgetest zu `backtest_candles.py` (2026-07-31). Dort ergab die reine INFORMATIONS-
Messung (Forward-Return 60 min) den einzigen robusten Lead des Laufs:
Langer Körper Überschuss -0,059 / -0,076 (beide Hälften)
... + Volumen >= 2x Ø Überschuss -0,117 / -0,204 (monoton verstärkt)
d. h. nach einer volumenstarken großen Kerze läuft der Kurs GEGEN sie — das Gegenteil
der Lehrbuch-Lesart („2× Volumen = echt"). Brutto +0,117/+0,204 beim Fade, real
kosten aber ~0,265×ATR → unter der Schwelle. OFFEN blieb, ob ein **Live-Exit** mehr
holt als das starre 60-min-Fenster (bei den Chartmustern machte genau das den
Unterschied zwischen negativ und +0,43).
DESHALB HIER: echter Trade-Sim mit dem LIVE-Exit (Initial-SL 2,0×ATR · Trail 1,5 ab
0,3 · Breakeven 1,3 · Lock ab 3,5 · Time-Stop 120 min) und ECHTKOSTEN (Bar-Spread/ATR).
⚠ ENTSCHEIDEND — die KONTROLLGRUPPE: „Fade OHNE Volumen-Bedingung". Ohne sie wäre
nicht unterscheidbar, ob der Ertrag vom Volumen kommt oder ob schlicht JEDER Fade
einer großen Kerze + Trailing funktioniert (der Kontrollfehler, der in
`backtest_patterns.py` unentdeckt blieb und den Muster-„Edge" als Exit-Artefakt
entlarvte, als er nachgeholt wurde).
Urteil „trägt" nur, wenn (a) in BEIDEN Hälften ØR netto > 0, (b) die Volumen-Variante
die Kontrolle in BEIDEN Hälften schlägt, (c) benachbarte Volumen-Schwellen nicht das
Vorzeichen kippen.
"""
import sys
from statistics import median
import MetaTrader5 as mt5
_MAXH = 200 # max. Haltedauer in Bars
_ATRMIN = 0.06 # Live-Floor
_VOL_N = 20 # Fenster für Ø-Volumen / Median-Body
_COOL = 12 # Cooldown nach einem Trade (keine Überlappung)
# Live-Exit
_SL = 2.0; _TRAIL = 1.5; _TRAIL_START = 0.3; _BE = 1.3
_LOCK_START = 3.5; _LOCK_SCALE = 0.6; _LOCK_MIN = 1.2
_TIMESTOP = 24 # 120 min = 24 M5-Bars, greift nur in Phase „Init"
def _atr_series(H, L, C, p=14):
t = [0.0]
for i in range(1, len(C)):
t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
out = []
for i in range(len(C)):
if not i:
out.append(None); continue
w = t[max(1, i - p + 1):i + 1]
out.append(sum(w) / max(1, len(w)))
return out
def _ema(v, p):
k = 2.0 / (p + 1); o = []; e = v[0]
for i, x in enumerate(v):
e = x if i == 0 else x * k + e * (1 - k)
o.append(e)
return o
def sim(entry, d, atr, H, L, C, j0):
"""LIVE-Exit-Modell (identisch zu backtest_trailing_sr.py)."""
sl = entry - d * _SL * atr
hw = entry; rank = 0
end = min(j0 + _MAXH, len(C) - 1)
for j in range(j0, end + 1):
hi, lo = H[j], L[j]
if (lo <= sl) if d > 0 else (hi >= sl):
return (sl - entry) * d / atr
hw = max(hw, hi) if d > 0 else min(hw, lo)
profit = (hw - entry) * d
ph = 0 if profit < _TRAIL_START * atr else (1 if profit < _LOCK_START * atr else 2)
if ph < rank:
ph = rank
rank = ph
# Time-Stop: nach 120 min noch in Phase „Init" (nie 0,3×ATR im Plus)
if rank == 0 and (j - j0) >= _TIMESTOP:
return (C[j] - entry) * d / atr
if ph == 0:
continue
m = _TRAIL if ph == 1 else max(_LOCK_MIN, _TRAIL * _LOCK_SCALE)
cand = hw - d * m * atr
if profit >= _BE * atr or ph == 2:
cand = max(cand, entry) if d > 0 else min(cand, entry)
sl = max(sl, cand) if d > 0 else min(sl, cand)
return (C[end] - entry) * d / atr
def st(Rs):
if not Rs:
return "Trades= 0"
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
pf = up / dn if dn > 0 else 9.99
return (f"Trades={n:>5} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} PF={pf:>4.2f} "
f"Worst={min(Rs):+.2f} ΣR={s:+.0f}")
def scan(O, H, L, C, V, A, SP, EF, ES, lo_i, hi_i, vol_x, body_x=1.5,
br_min=0.60, only_with_trend=False):
"""vol_x = 0 → KONTROLLE (kein Volumen-Filter)."""
Rs = []
i = max(lo_i, _VOL_N + 2)
while i < min(hi_i, len(C) - _MAXH - 2):
atr = A[i]
if not atr or atr < _ATRMIN:
i += 1; continue
rng = H[i] - L[i]
body = abs(C[i] - O[i])
if rng <= 0 or body / rng < br_min:
i += 1; continue
bref = median([abs(C[k] - O[k]) for k in range(i - _VOL_N, i)])
if bref <= 0 or body < body_x * bref:
i += 1; continue
if vol_x > 0:
vref = sum(V[i - _VOL_N:i]) / _VOL_N
if vref <= 0 or V[i] < vol_x * vref:
i += 1; continue
d_body = 1 if C[i] > O[i] else -1
if only_with_trend:
trend = 1 if EF[i] > ES[i] else -1
if d_body != trend: # nur Klimax IM Trend
i += 1; continue
d = -d_body # FADE: gegen die Körperrichtung
entry = C[i] # Einstieg am Close der Signal-Kerze
cost = (SP[i] if SP[i] > 0 else 0.0225) / atr
Rs.append(sim(entry, d, atr, H, L, C, i + 1) - cost)
i += _COOL
return Rs
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
mt5.initialize(); sym = None
for cand in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
if mt5.symbol_info(cand):
sym = cand; break
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
si = mt5.symbol_info(sym); point = si.point
mt5.shutdown()
O = [float(b["open"]) for b in bars]; H = [float(b["high"]) for b in bars]
L = [float(b["low"]) for b in bars]; C = [float(b["close"]) for b in bars]
V = [float(b["tick_volume"]) for b in bars]
SP = [float(b["spread"]) * point for b in bars]
A = _atr_series(H, L, C); EF = _ema(C, 12); ES = _ema(C, 50)
N = len(C); mid = N // 2
halves = [("H1", 100, mid), ("H2", mid, N)]
print("=" * 104)
print(f" KLIMAX-FADE — {sym} M5 ({N} Bars) · Live-Exit (SL 2,0 · Trail 1,5 ab 0,3 · "
f"BE 1,3 · Time-Stop 120') · Echtkosten")
print(" Einstieg GEGEN einen langen Körper (Body/Range>=0,60, Body>=1,5x Median)")
print("=" * 104)
variants = [
("KONTROLLE: Fade ohne Volumen-Filter", dict(vol_x=0)),
("Volumen >= 1,5x Ø", dict(vol_x=1.5)),
("Volumen >= 2,0x Ø", dict(vol_x=2.0)),
("Volumen >= 2,5x Ø", dict(vol_x=2.5)),
("Volumen >= 3,0x Ø", dict(vol_x=3.0)),
("Vol >= 2,0x + nur IM Trend", dict(vol_x=2.0, only_with_trend=True)),
("Vol >= 2,0x + Marubozu (BR>=0,90)", dict(vol_x=2.0, br_min=0.90)),
]
for name, kw in variants:
print(f"\n{name}")
for lbl, a, b in halves:
Rs = scan(O, H, L, C, V, A, SP, EF, ES, a, b, **kw)
print(f" {lbl} {st(Rs)}")
if __name__ == "__main__":
main()
+187
View File
@@ -0,0 +1,187 @@
"""core/candles.py — Kerzen-Anatomie (Body/Docht-Klassifikation). REINE ANZEIGE.
User-Vorgabe 2026-07-31 („Kerzengröße und Dochtlänge … im Kontext mit Trendrichtung,
Volumen und Liquiditätszonen"). Erkennt mechanisch aus OHLCV die sechs Klassen
Marubozu · Langer Körper · Langer Docht (oben/unten) · Spinning Top · Doji · Engulfing.
⚠⚠ WICHTIG — DIE BESCHRIFTUNG FOLGT DER MESSUNG, NICHT DEM LEHRBUCH.
`backtest_candles.py` (80k M5-Bars, 2 Halbjahre, Forward-Return gegen die
DRIFT-NULL gelesen) hat die gängige Deutung in BEIDEN Hälften **invertiert**
vorgefunden:
* „Langer Körper = Momentum, im Trend = Fortsetzung" → Überschuss 0,059/0,076;
IM Trend sogar 0,132/0,042. Es ist die **Klimax-/Erschöpfungskerze**, nicht
die Fortsetzung.
* „Langer Körper mit 2× Volumen = echt"0,117/0,204, monoton
schlechter mit MEHR Volumen. Volumen bestätigt hier nicht, es kapituliert.
* „Docht am Level = zuverlässig, Range-Mitte = Noise" → am Level in BEIDEN
Hälften SCHLECHTER als in der Range-Mitte.
* „mit/gegen Trend" beim Docht kippt zwischen den Hälften = Rauschen.
Der Folgetest als Handelssignal (`backtest_candle_fade.py`, Live-Exit + Echtkosten)
fiel ebenfalls durch (benachbarte Volumen-Schwellen exakt gegenläufig). Deshalb:
**KEIN Verdict-Gewicht, kein Trade-Trigger** — die Kachel zeigt nur, WAS gerade
gedruckt wurde, mit der gemessenen Lesart als Hinweis. Wer sie als Signal liest,
handelt gegen die eigene Datenlage.
Warum trotzdem gebaut: dieselbe Rolle wie `structure.py`/`patterns.py` — sichtbar
machen, was ein Chart-Leser sieht, ohne es zu bewerten. Der Hinweistext ist bewusst
so formuliert, dass er NICHT in die Lehrbuch-Richtung stupst (dieselbe Korrektur wie
bei der Bounce-Anzeige, die deshalb heute „Überdehnung" heißt und bernstein ist).
"""
from __future__ import annotations
import threading
import time
from statistics import median
import MetaTrader5 as mt5
from core.logger import get_logger
from core.mt5_utils import mt5_lock
log = get_logger("candles")
_REFRESH_S = 20.0 # Kachel muss nicht schneller sein als eine M5-Kerze
_N_BARS = 60 # Referenzfenster (Median-Body, Ø-Volumen)
_REF_N = 20 # Fenster für „lang" und „viel Volumen"
_LOOK = 6 # so viele letzte Kerzen werden klassifiziert
# Schwellen — identisch zu `backtest_candles.py`, damit Anzeige und Messung
# dieselbe Sprache sprechen.
_MARU_BR = 0.90 # Body/Range ab hier = Marubozu
_LONG_BR = 0.60 # Body/Range für „langer Körper"
_LONG_X = 1.5 # Body >= X × Median-Body
_DOJI_BR = 0.10
_SPIN_BR = 0.30
_SPIN_W = 0.30 # beide Dochte > X × Range
_WICK = 0.50 # Docht/Range ab hier = „langer Docht"
_VOL_HOT = 2.0 # Volumen >= X × Ø
class CandleAnatomy:
"""Klassifiziert die letzten Kerzen einer TF. Thread-safe, self-throttled."""
def __init__(self, tf: str = "M5"):
self.tf = tf
self._lock = threading.Lock()
self._snap: dict = {}
self._ts = 0.0
# ── Klassifikation (rein rechnerisch, ohne MT5) ──────────────────────────
@staticmethod
def classify(o, h, l, c, body_ref, prev=None):
"""→ (klasse, richtung, kennzahlen). richtung 0 = keine implizierte Richtung.
`prev` = (o,c) der Vorkerze, nur für Engulfing.
"""
rng = h - l
if rng <= 0:
return None, 0, {}
body = abs(c - o)
up_w = h - max(o, c)
lo_w = min(o, c) - l
br = body / rng
d = 1 if c > o else -1 if c < o else 0
m = {"body_pct": round(100 * br), "wick_up_pct": round(100 * up_w / rng),
"wick_lo_pct": round(100 * lo_w / rng)}
long_body = body_ref > 0 and body >= _LONG_X * body_ref
# Engulfing zuerst prüfen (zwei-Kerzen-Muster schlägt Ein-Kerzen-Form)
if prev and long_body:
po, pc = prev
if d > 0 and pc < po and c >= po and o <= pc:
return "Engulfing", 1, m
if d < 0 and pc > po and c <= po and o >= pc:
return "Engulfing", -1, m
if br >= _MARU_BR:
return "Marubozu", d, m
if br < _DOJI_BR:
return "Doji", 0, m
if br < _SPIN_BR and up_w / rng > _SPIN_W and lo_w / rng > _SPIN_W:
return "SpinningTop", 0, m
if up_w / rng >= _WICK:
return "LangerDochtOben", -1, m
if lo_w / rng >= _WICK:
return "LangerDochtUnten", 1, m
if long_body and br >= _LONG_BR:
return "LangerKoerper", d, m
return None, 0, m
# ── Anzeige-Text: GEMESSENE Lesart, nicht die Lehrbuch-Lesart ────────────
@staticmethod
def reading(cls: str, d: int, hot: bool) -> str:
if cls in ("LangerKoerper", "Marubozu", "Engulfing"):
base = ("⚠ Klimax-Kerze — gemessen läuft der Kurs danach eher GEGEN "
"die Kerze (nicht Fortsetzung)")
if hot:
base += " · hohes Volumen verstärkt das (gemessen 0,12…−0,20×ATR)"
return base
if cls in ("LangerDochtOben", "LangerDochtUnten"):
seite = "oben" if cls.endswith("Oben") else "unten"
return (f"Ablehnung nach {seite} — ⚠ am Level gemessen NICHT zuverlässiger "
f"als in der Range-Mitte")
if cls == "SpinningTop":
return "Unentschlossenheit (kleiner Körper, beide Seiten gedrückt)"
if cls == "Doji":
return "Gleichstand — keine Seite dominiert"
return ""
LABELS = {"Marubozu": "Marubozu", "LangerKoerper": "Langer Körper",
"LangerDochtOben": "Langer Docht oben",
"LangerDochtUnten": "Langer Docht unten",
"SpinningTop": "Spinning Top", "Doji": "Doji",
"Engulfing": "Engulfing"}
# ── Datenbeschaffung ─────────────────────────────────────────────────────
def refresh_market(self, symbol: str) -> None:
"""Im Trend-Loop aufrufen; self-throttled. Nimmt den `mt5_lock` selbst."""
if not symbol or time.time() - self._ts < _REFRESH_S:
return
self._ts = time.time()
tf_map = {"M1": mt5.TIMEFRAME_M1, "M5": mt5.TIMEFRAME_M5,
"M15": mt5.TIMEFRAME_M15, "M30": mt5.TIMEFRAME_M30,
"H1": mt5.TIMEFRAME_H1}
try:
# ⚠ `mt5_lock` ist ein CONTEXT-MANAGER (kein Lock-Objekt) und liefert
# `got` — nicht .acquire()/.release() verwenden.
with mt5_lock(3.0) as got:
if not got:
return
bars = mt5.copy_rates_from_pos(
symbol, tf_map.get(self.tf, mt5.TIMEFRAME_M5), 0, _N_BARS + 2)
if bars is None or len(bars) < _REF_N + 3:
return
self._build(bars)
except Exception as e:
log.warning(f"candles.refresh_market: {e}")
def _build(self, bars) -> None:
O = [float(b["open"]) for b in bars]; H = [float(b["high"]) for b in bars]
L = [float(b["low"]) for b in bars]; C = [float(b["close"]) for b in bars]
V = [float(b["tick_volume"]) for b in bars]
# letzte Kerze ist noch NICHT abgeschlossen → nur bis -1 klassifizieren
last = len(C) - 1
out = []
for i in range(max(_REF_N, last - _LOOK), last):
bref = median([abs(C[k] - O[k]) for k in range(i - _REF_N, i)])
vref = sum(V[i - _REF_N:i]) / _REF_N
cls, d, m = self.classify(O[i], H[i], L[i], C[i], bref,
prev=(O[i - 1], C[i - 1]))
if not cls:
continue
vol_x = round(V[i] / vref, 1) if vref > 0 else 0.0
hot = vol_x >= _VOL_HOT
out.append({
"cls": cls, "label": self.LABELS.get(cls, cls), "dir": d,
"bars_ago": last - i, "vol_x": vol_x, "hot": hot,
"reading": self.reading(cls, d, hot), **m,
})
out.reverse() # neueste zuerst
with self._lock:
self._snap = {"tf": self.tf, "items": out,
"vol_hot_x": _VOL_HOT, "ts": time.time()}
def snapshot(self) -> dict:
with self._lock:
return dict(self._snap)
+10
View File
@@ -42,6 +42,7 @@ 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.patterns import PatternDetector
from core.candles import CandleAnatomy
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)
@@ -165,6 +166,10 @@ class TradingEngine:
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.patterns = PatternDetector() # visuelle Chartmuster (M30) — reine Anzeige
# Kerzen-Anatomie (M5) — REINE ANZEIGE. Die Lehrbuch-Deutung ist gemessen
# invertiert (`backtest_candles.py`), das Handelssignal fiel durch
# (`backtest_candle_fade.py`) → kein Verdict-Gewicht, kein Trigger.
self.candle_anat = CandleAnatomy("M5")
# Echte Liquiditäts-Wände vom HL-Orderbuch (Anzeige im MT5-Chart, kein Signal). # Echte Liquiditäts-Wände vom HL-Orderbuch (Anzeige im MT5-Chart, kein Signal).
# Braucht das HL-Dashboard auf 8001; ist es aus, bleibt der Chart unverändert. # Braucht das HL-Dashboard auf 8001; ist es aus, bleibt der Chart unverändert.
from core.hl_walls import HLWalls from core.hl_walls import HLWalls
@@ -539,6 +544,8 @@ class TradingEngine:
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) try: self.patterns.refresh_market(self.data.symbol)
except Exception as pe2: log.warning(f"patterns.refresh_market: {pe2}") except Exception as pe2: log.warning(f"patterns.refresh_market: {pe2}")
try: self.candle_anat.refresh_market(self.data.symbol)
except Exception as ka: log.warning(f"candles.refresh_market: {ka}")
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
@@ -2704,6 +2711,9 @@ class TradingEngine:
"gaps": self.gaps.snapshot(), "gaps": self.gaps.snapshot(),
"structure": self.structure.snapshot(), "structure": self.structure.snapshot(),
"patterns": _pat_snap, "patterns": _pat_snap,
# Kerzen-Anatomie (M5) — reine Anzeige, KEIN Verdict-Gewicht (gemessen:
# Lehrbuch-Deutung invertiert, Handelssignal durchgefallen).
"candle_anat": self.candle_anat.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,
+34
View File
@@ -275,6 +275,39 @@ function _patternSvg(type, col) {
stroke-linejoin="round" stroke-linecap="round"/></svg>`; stroke-linejoin="round" stroke-linecap="round"/></svg>`;
} }
// Kerzen-Anatomie (M5). ⚠ Bewusst OHNE grün/rot-Richtungsfarbe: die Lehrbuch-
// Deutung ist gemessen invertiert (`backtest_candles.py` — langer Körper läuft eher
// GEGEN sich, Volumen verstärkt das statt zu bestätigen), eine Ampel würde also
// systematisch in die falsche Richtung stupsen. Dieselbe Korrektur wie bei der
// Bounce-Anzeige, die deshalb heute „Überdehnung" heißt.
function renderCandles(ca) {
const el = $("ca-list");
if (!el) return;
const items = (ca && ca.items) || [];
if (!items.length) {
el.innerHTML = `<span class="dim">Keine markante Kerzenform in den letzten Bars.</span>`;
return;
}
el.innerHTML = items.map(m => {
const arr = m.dir > 0 ? "▲" : m.dir < 0 ? "▼" : "◆";
const ago = m.bars_ago === 1 ? "letzte Kerze" : `vor ${m.bars_ago} Kerzen`;
const vol = m.vol_x
? `<span class="ca-vol${m.hot ? " hot" : ""}">Vol ${fmt(m.vol_x, 1)}×</span>` : "";
return `<div class="ca-item${m.hot ? " hot" : ""}">
<div class="ca-head"><span class="ca-name">${arr} ${m.label}</span>
${vol}<span class="ca-ago">${ago}</span></div>
<div class="ca-bars">Körper ${m.body_pct}% · Docht oben ${m.wick_up_pct}%
· unten ${m.wick_lo_pct}%</div>
<div class="ca-note">${m.reading || ""}</div>
</div>`;
}).join("") +
`<div class="pt-disclaimer">🕯 Reine Anzeige — KEIN Signal. Gemessen
(<code>backtest_candles.py</code>, 80k Bars, 2 Halbjahre): die gängige Deutung ist
<b>invertiert</b> — der lange Körper läuft eher GEGEN sich (0,06/0,08×ATR), mit
2× Volumen noch stärker (0,12/0,20). Als Handelssignal durchgefallen
(<code>backtest_candle_fade.py</code>).</div>`;
}
function renderPatterns(pt) { function renderPatterns(pt) {
const el = $("pt-list"); const el = $("pt-list");
if (!el) return; if (!el) return;
@@ -398,6 +431,7 @@ function render(d) {
renderVerdict(d.verdict || {}, d); renderVerdict(d.verdict || {}, d);
renderCircuitBreaker(d.circuit_breaker); renderCircuitBreaker(d.circuit_breaker);
renderPatterns(d.patterns); renderPatterns(d.patterns);
renderCandles(d.candle_anat);
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);
+9 -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=122"> <link rel="stylesheet" href="style.css?v=123">
</head> </head>
<body> <body>
<!-- Modul-Menü (ganz oben) --> <!-- Modul-Menü (ganz oben) -->
@@ -125,6 +125,13 @@
<div class="pt-list" id="pt-list"><span class="dim"></span></div> <div class="pt-list" id="pt-list"><span class="dim"></span></div>
</section> </section>
<!-- Kerzen-Anatomie (M5) — reine Anzeige; die Lehrbuch-Deutung ist gemessen
invertiert, deshalb steht die GEMESSENE Lesart im Hinweistext -->
<section class="card wide" id="card-candles">
<h2>Kerzen-Anatomie <span class="ca-tf">M5 · nur Anzeige</span></h2>
<div class="ca-list" id="ca-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>
@@ -230,6 +237,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=122"></script> <script src="app.js?v=123"></script>
</body> </body>
</html> </html>
+18
View File
@@ -162,6 +162,24 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
.pt-det{font-size:13px;margin-top:4px;font-family:var(--mono);color:var(--bright)} .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-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} .pt-disclaimer{font-size:12px;color:var(--dim);margin-top:6px;line-height:1.4}
.pt-disclaimer code{font-family:var(--mono);font-size:11px}
/* Kerzen-Anatomie — bewusst NEUTRAL gefärbt (keine grün/rot-Ampel): die
Lehrbuch-Deutung ist gemessen invertiert, eine Richtungsfarbe würde
systematisch falsch stupsen. Nur „hot" (Volumen>=2x) wird hervorgehoben. */
#card-candles .ca-tf{font-size:11px;color:var(--dim);font-weight:600;margin-left:6px}
.ca-item{background:var(--surface-2);border:1px solid var(--border);
border-left:3px solid var(--line);border-radius:var(--r-sm);
padding:8px 12px;margin-bottom:7px}
.ca-item.hot{border-left-color:var(--accent)}
.ca-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.ca-name{font-weight:700;font-size:15px;color:var(--bright)}
.ca-vol{font-size:11px;font-weight:700;border-radius:4px;padding:1px 7px;
font-family:var(--mono);background:var(--bg);color:var(--dim);
border:1px solid var(--border)}
.ca-vol.hot{background:#2a1f00;color:var(--accent);border-color:#4a3600}
.ca-ago{margin-left:auto;font-size:12px;color:var(--dim);font-family:var(--mono)}
.ca-bars{font-size:12px;margin-top:3px;font-family:var(--mono);color:var(--dim)}
.ca-note{font-size:12px;color:var(--dim);margin-top:3px;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;