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:
co-authored by
Claude Opus 5
parent
7873bf2fb7
commit
03be3b8f5f
@@ -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 35–40 %). **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` —
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user