Slippage vs Vola-Regime gemessen + Ziel-Erreichbarkeit gebaut (v=154)
MESSUNG (analyze_slippage_vola.py, 337 SL-Closes): die Exit-Slippage steigt MONOTON ueber alle vier Delta-ATR-Quartile: 0,39-0,91 Schaetzer 0,0200 $ 0,91-1,08 0,0390 $ 1,08-1,26 0,0645 $ 1,26-2,39 0,0839 $ Faktor 4,19x Mechanismus stimmig: die Slip-RATE bleibt flach (20-23 %), die GROESSE waechst (0,094 -> 0,396 $). Es rutscht nicht oefter, sondern weiter. ABER: der Bootstrap auf die DIFFERENZ oben-unten ergibt [-0,012 ... +0,183] - enthaelt die Null. Bei n=85 je Quartil NICHT statistisch belegt. Meine vorab fixierte Regel (Faktor >=2 UND monoton) war damit unterspezifiziert - sie haette ein Konfidenzintervall verlangen muessen. Konsequenz: gebaut als reiner HINWEIS im Kosten-Gate, kein Gate, und die Beschriftung sagt "historisch ~4x hoeher", nicht "belegt". ZIEL-ERREICHBARKEIT (Vorschlag 3, Decay-Clock): reine Geometrie auf dem kalibrierten Kegel - passt das Gegenlevel noch in das 80-%-Band des jeweiligen Horizonts? Live: Ziel 77,784 (0,401 $) liegt im 120'-Kegel, nicht im 30'/60'. Die im Vorschlag mitgedachte Annahme "je laenger, desto eher bricht es" ist BEWUSST NICHT eingebaut - sie ist nicht belegt (Touch-Zahl: umgekehrtes U auf 4 Live-Tagen, im Backtest +0,010 AUC, nach dem Nachtrainieren gar nichts mehr). Umsetzung ohne zweiten MT5-Fetch: _vola_regime puffert den ATR_M15 aus dem ohnehin laufenden Turn-Fetch (deque, ~5 h). Kaltstart liefert datr=null statt zu raten - live verifiziert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
44955bf70a
commit
e5adfc9757
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Steigt die Exit-Slippage mit dem Vola-REGIME? — die offene Ausführungs-Frage.
|
||||
|
||||
ANLASS (2026-08-08, User-Vorschlag 4/2). Das Vola-Regime ist als
|
||||
RICHTUNGS-Signal gemessen tot (`backtest_pbreak_regime.py`: Bruchrate über 5
|
||||
ATR-Quintile flach, 37,0–39,2 %; das Regime-Merkmal fiel durch die vorab
|
||||
fixierte Regel). Als **Ausführungskosten-Warnung** ist es aber nie gemessen
|
||||
worden — und das ist eine ganz andere Behauptung: nicht „wohin läuft der Kurs",
|
||||
sondern „wie teuer wird das Verlassen der Position".
|
||||
|
||||
METHODE — bewusst die aus `analyze_slippage_holdtime.py`, damit die Zahlen
|
||||
vergleichbar bleiben:
|
||||
gap = (sl_at_entry − exit_price) × Richtung, nur `closed_by='sl'`
|
||||
gap > 0 heisst „schlechter als der Initial-SL"
|
||||
⚠ Warum ausgerechnet dieser gap unkontaminiert ist: das **Trailing kann ihn nur
|
||||
NEGATIVER machen** (es zieht den SL in Gewinnrichtung). Ein positiver gap kann
|
||||
also nicht vom Trailing stammen — er ist zwingend Slippage.
|
||||
Schätzer (Untergrenze): E[Slippage] ≥ P(gap>0) × Ø(gap | gap>0)
|
||||
|
||||
NEUE ACHSE: ΔATR = ATR_M15(Exit-Zeitpunkt) / Median(ATR_M15 der letzten 20 Bars).
|
||||
> 1 heisst „Vola zieht gerade an". ⚠ KAUSAL: der Median endet VOR dem Exit-Bar.
|
||||
|
||||
⚠⚠ ENTSCHEIDUNGSREGEL, VORAB: ein Warn-Flag im Kosten-Gate ist nur dann
|
||||
gerechtfertigt, wenn die Slippage im obersten ΔATR-Bucket **mindestens doppelt
|
||||
so hoch** ist wie im untersten UND der Zusammenhang **monoton** verläuft. Ein
|
||||
einzelner Ausreisser-Bucket reicht nicht — dieselbe Nachbar-Regel, an der ORB
|
||||
und die VWAP-Bänder gescheitert sind.
|
||||
⚠ Stichprobe: es gibt insgesamt ~317 SL-Closes. Über 4–5 Buckets sind das
|
||||
~60–80 je Zelle — dünn. Ein ❌ ist belastbar, ein ✅ nur ein Hinweis.
|
||||
|
||||
Aufruf: python analyze_slippage_vola.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import bisect
|
||||
import sqlite3
|
||||
import statistics as st
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
DB = "oil_widget_history.db"
|
||||
_BROKER_OFF = 10800 # Brokerzeit = UTC+3
|
||||
_MA = 20 # Fenster fuer den ATR-Median
|
||||
_M15_SEK = 900
|
||||
|
||||
|
||||
def atr_series(H, L, C, p=14):
|
||||
tr = [0.0]
|
||||
for i in range(1, len(C)):
|
||||
tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
|
||||
out = [None]
|
||||
for i in range(1, len(C)):
|
||||
seg = tr[max(1, i - p + 1):i + 1]
|
||||
out.append(sum(seg) / len(seg))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
db = sqlite3.connect(DB); db.row_factory = sqlite3.Row
|
||||
rows = []
|
||||
for r in db.execute(
|
||||
"SELECT exit_time, exit_price, sl_at_entry, direction, entry_time "
|
||||
"FROM trades WHERE closed_by='sl' AND sl_at_entry IS NOT NULL "
|
||||
"AND exit_price IS NOT NULL AND exit_time IS NOT NULL"):
|
||||
d = 1 if (r["direction"] or "").upper() in ("BUY", "LONG") else -1
|
||||
rows.append({"t": int(r["exit_time"]),
|
||||
"gap": (r["sl_at_entry"] - r["exit_price"]) * d,
|
||||
"hold": (int(r["exit_time"]) - int(r["entry_time"] or 0)) / 60.0})
|
||||
db.close()
|
||||
if len(rows) < 60:
|
||||
print(f"nur {len(rows)} SL-Closes — zu wenig"); return 1
|
||||
|
||||
mt5.initialize()
|
||||
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
|
||||
if mt5.symbol_info(c)), None)
|
||||
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M15, 0, 26000)
|
||||
mt5.shutdown()
|
||||
T = [int(b["time"]) - _BROKER_OFF 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]
|
||||
A = atr_series(H, L, C)
|
||||
|
||||
# ΔATR je Trade — kausal: der Median endet VOR dem Exit-Bar
|
||||
dat = []
|
||||
for r in rows:
|
||||
i = bisect.bisect_right(T, r["t"]) - 1
|
||||
if i < _MA + 15 or i >= len(A) or not A[i]:
|
||||
continue
|
||||
fenster = [a for a in A[i - _MA:i] if a]
|
||||
if len(fenster) < _MA // 2:
|
||||
continue
|
||||
med = st.median(fenster)
|
||||
if med <= 0:
|
||||
continue
|
||||
dat.append({**r, "datr": A[i] / med, "atr": A[i]})
|
||||
|
||||
print("=" * 84)
|
||||
print(" EXIT-SLIPPAGE gegen das VOLA-REGIME (ΔATR = ATR / Median der letzten 20)")
|
||||
print("=" * 84)
|
||||
print(f" {len(rows)} SL-Closes, davon {len(dat)} mit M15-Abdeckung")
|
||||
if len(dat) < 60:
|
||||
print(" zu wenige mit Abdeckung — keine Aussage"); return 1
|
||||
|
||||
ds = sorted(x["datr"] for x in dat)
|
||||
print(f" ΔATR: Median {ds[len(ds)//2]:.2f} 10-Perz. {ds[len(ds)//10]:.2f} "
|
||||
f"90-Perz. {ds[int(0.9*len(ds))]:.2f}")
|
||||
|
||||
# ── Quartile, damit die Zellen nicht zu duenn werden ──────────────────
|
||||
kanten = [ds[0], ds[len(ds)//4], ds[len(ds)//2], ds[3*len(ds)//4], ds[-1] + 1e-9]
|
||||
print(f"\n {'ΔATR-Bereich':<18}{'n':>5}{'slip>0':>8}{'Rate':>7}"
|
||||
f"{'Ø gap|>0':>11}{'Schaetzer':>12}")
|
||||
werte = []
|
||||
for a, b in zip(kanten, kanten[1:]):
|
||||
grp = [x for x in dat if a <= x["datr"] < b]
|
||||
if len(grp) < 15:
|
||||
print(f" {f'{a:.2f}–{b:.2f}':<18}{len(grp):>5}{'':>8}{'zu wenig':>7}")
|
||||
werte.append(None); continue
|
||||
pos = [x["gap"] for x in grp if x["gap"] > 0]
|
||||
rate = len(pos) / len(grp)
|
||||
m = sum(pos) / len(pos) if pos else 0.0
|
||||
sch = rate * m
|
||||
werte.append(sch)
|
||||
print(f" {f'{a:.2f}–{b:.2f}':<18}{len(grp):>5}{len(pos):>8}{100*rate:>6.0f}%"
|
||||
f"{m:>11.4f}{sch:>12.4f}")
|
||||
|
||||
gut = [w for w in werte if w is not None]
|
||||
print("\n " + "-" * 78)
|
||||
if len(gut) < 3:
|
||||
print(" zu wenige belegte Buckets fuer ein Urteil"); return 0
|
||||
unten, oben = gut[0], gut[-1]
|
||||
monoton = all(gut[i] <= gut[i + 1] for i in range(len(gut) - 1))
|
||||
faktor = (oben / unten) if unten > 0 else float("inf")
|
||||
print(f" unterstes Quartil {unten:.4f} $ → oberstes {oben:.4f} $ "
|
||||
f"Faktor {faktor:.2f}×")
|
||||
print(f" monoton steigend: {'JA' if monoton else 'NEIN'}")
|
||||
print(f" Regel (Faktor ≥2 UND monoton): "
|
||||
f"{'ERFUELLT — Warn-Flag gerechtfertigt' if (faktor >= 2 and monoton) else 'VERFEHLT — kein Flag'}")
|
||||
print("\n ⚠ Zum Vergleich (analyze_slippage_holdtime): Median-Spread 0,0280 $;")
|
||||
print(" <30-min-Trades 0,0370 $ ueber alle Stunden, 0,0124 $ im Fenster 10–17.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -3974,6 +3974,43 @@ class TradingEngine:
|
||||
log.debug(f"sr_close_hint: {e}")
|
||||
return None
|
||||
|
||||
def _vola_regime(self, atr15) -> dict | None:
|
||||
"""ΔATR = aktueller ATR_M15 / Median der letzten ~5 h.
|
||||
|
||||
⚠ KEIN zweiter MT5-Fetch: der ATR wird aus dem ohnehin laufenden
|
||||
Turn-Fetch gesammelt (`wave.tf_atr`), hier nur gepuffert. Ein extra
|
||||
Abruf fuer Zahlen, die schon da sind, war beim Konsens-Pfeil der Fehler.
|
||||
|
||||
⚠⚠ BELEGLAGE, ehrlich: gemessen (`analyze_slippage_vola.py`, 337
|
||||
SL-Closes) steigt die Exit-Slippage MONOTON ueber alle vier
|
||||
ΔATR-Quartile — 0,0200 → 0,0390 → 0,0645 → 0,0839 $, Faktor 4,19×.
|
||||
Die Slip-RATE bleibt dabei flach (20–23 %), die GROESSE waechst
|
||||
(0,094 → 0,396 $): es rutscht nicht oefter, sondern weiter.
|
||||
ABER: der Bootstrap auf die DIFFERENZ oben−unten ergibt
|
||||
[−0,012 … +0,183] — **enthaelt die Null**. Bei n≈85 je Quartil ist der
|
||||
Unterschied NICHT statistisch belegt. Deshalb reine ANZEIGE, kein Gate,
|
||||
und die Beschriftung sagt das auch.
|
||||
"""
|
||||
try:
|
||||
if not atr15 or atr15 <= 0:
|
||||
return None
|
||||
buf = getattr(self, "_atr15_buf", None)
|
||||
if buf is None:
|
||||
from collections import deque
|
||||
buf = self._atr15_buf = deque(maxlen=1200) # ~5 h bei 15-s-Takt
|
||||
buf.append(float(atr15))
|
||||
if len(buf) < 200:
|
||||
return {"datr": None, "stufe": None}
|
||||
import statistics as _st
|
||||
med = _st.median(buf)
|
||||
if med <= 0:
|
||||
return {"datr": None, "stufe": None}
|
||||
d = atr15 / med
|
||||
return {"datr": round(d, 2),
|
||||
"stufe": "hoch" if d >= 1.26 else "normal" if d >= 0.91 else "ruhig"}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _m15_setup(self, market, wave_snap, cone, alignment) -> dict | None:
|
||||
"""SETUP-BEREITSCHAFT auf M15 — der Ersatz für die entfernte Gesamtempfehlung.
|
||||
|
||||
@@ -4044,6 +4081,32 @@ class TradingEngine:
|
||||
"lo": x.get("lo"), "hi": x.get("hi")}
|
||||
for x in b80] or None
|
||||
|
||||
# ── 4b) ZIEL-ERREICHBARKEIT — reine GEOMETRIE auf dem Kegel.
|
||||
# Ziel eines Trades am nahen Level ist das GEGENÜBERLIEGENDE.
|
||||
# Gefragt wird nur: passt dieser Abstand noch in das 80-%-Band
|
||||
# des jeweiligen Horizonts? ⚠ KEINE Bruch-Annahme — die im
|
||||
# Vorschlag mitgedachte „je länger, desto eher bricht es"-Regel
|
||||
# ist gemessen NICHT robust (Touch-Zahl: umgekehrtes U auf 4
|
||||
# Live-Tagen, im Backtest +0,010 AUC und nach dem Nachtrainieren
|
||||
# gar nichts mehr). Hier steckt deshalb nur Arithmetik.
|
||||
if len(zonen) == 2 and bid and b80:
|
||||
nah = min(zonen, key=lambda x: x["gap_atr15"])
|
||||
fern = max(zonen, key=lambda x: x["gap_atr15"])
|
||||
strecke = abs(fern["level"] - bid)
|
||||
out["ziel"] = {
|
||||
"level": fern["level"], "strecke": round(strecke, 3),
|
||||
"horizonte": [{"minutes": x.get("minutes"),
|
||||
"drin": bool(x.get("lo") is not None
|
||||
and x.get("hi") is not None
|
||||
and x["lo"] <= fern["level"] <= x["hi"])}
|
||||
for x in b80],
|
||||
"von": nah["level"]}
|
||||
else:
|
||||
out["ziel"] = None
|
||||
|
||||
# ── 4c) VOLA-REGIME als Slippage-HINWEIS (kein Gate, s. Docstring)
|
||||
out["vola"] = self._vola_regime(atr15)
|
||||
|
||||
# ── 5) VETO — die eigene Bilanz, je Lot (absolute Euro sind bei
|
||||
# wechselnder Positionsgröße ungültig, Lehre vom 04.08.)
|
||||
out["veto"] = alignment or None
|
||||
|
||||
+28
-5
@@ -170,10 +170,19 @@ function renderM15(s) {
|
||||
const k = s.kosten || {};
|
||||
const kOk = k.stufe === "guenstig" || k.stufe === "normal";
|
||||
$("m15-kosten").className = "m15-item " + (k.ratio == null ? "" : kOk ? "ok" : "fail");
|
||||
// ⚠ Slippage-Hinweis: gemessen steigt die Exit-Slippage monoton mit ΔATR
|
||||
// (0,020 → 0,084 $, Faktor 4,2). ABER der Bootstrap auf die Differenz enthält
|
||||
// die Null (n≈85 je Quartil) — deshalb HINWEIS, kein Gate, und die
|
||||
// Beschriftung sagt „historisch", nicht „belegt".
|
||||
const vo = s.vola || {};
|
||||
const slipTxt = vo.stufe === "hoch"
|
||||
? " · ⚠ Vola zieht an — Slippage historisch ~4× höher"
|
||||
: vo.stufe === "ruhig" ? " · Vola ruhig" : "";
|
||||
$("m15-kosten").textContent = k.ratio == null
|
||||
? "② Kosten —"
|
||||
: `② Kosten ${f(k.ratio, 3)}×ATR · ${
|
||||
k.stufe === "guenstig" ? "günstig" : k.stufe === "normal" ? "normal" : "TEUER"}`;
|
||||
k.stufe === "guenstig" ? "günstig" : k.stufe === "normal" ? "normal" : "TEUER"}`
|
||||
+ slipTxt;
|
||||
|
||||
// ── Gate 3: Level in Reichweite ──────────────────────────────────────
|
||||
const z = (s.zonen || []);
|
||||
@@ -186,11 +195,25 @@ function renderM15(s) {
|
||||
: "③ kein Level in Sicht";
|
||||
|
||||
// ── Reichweite (Kegel) — sagt WIE WEIT, nicht wohin ──────────────────
|
||||
// ⚠ REINE GEOMETRIE: passt das Gegenlevel (= das Ziel) noch in das 80-%-Band
|
||||
// des jeweiligen Horizonts? Keine Bruch-Annahme — die wäre nicht belegt.
|
||||
const zi = s.ziel;
|
||||
const r = s.reichweite || [];
|
||||
$("m15-reich").className = "m15-item";
|
||||
$("m15-reich").textContent = r.length
|
||||
? "④ erwartete Spanne · " + r.map(x => `${x.minutes}′ ${f(x.lo, 2)}–${f(x.hi, 2)}`).join(" · ")
|
||||
: "④ erwartete Spanne —";
|
||||
if (zi && zi.horizonte && zi.horizonte.length) {
|
||||
const drin = zi.horizonte.filter(x => x.drin).map(x => x.minutes);
|
||||
const el = $("m15-reich");
|
||||
el.className = "m15-item " + (drin.length ? "ok" : "warn");
|
||||
el.textContent = drin.length
|
||||
? `④ Ziel ${f(zi.level, 3)} erreichbar ab ${Math.min(...drin)}′ `
|
||||
+ `(${zi.strecke} $ vom Kurs)`
|
||||
: `④ Ziel ${f(zi.level, 3)} liegt AUSSERHALB des 120′-Kegels `
|
||||
+ `(${zi.strecke} $) — CRV zeitlich nicht gedeckt`;
|
||||
} else {
|
||||
$("m15-reich").className = "m15-item";
|
||||
$("m15-reich").textContent = r.length
|
||||
? "④ erwartete Spanne · " + r.map(x => `${x.minutes}′ ${f(x.lo, 2)}–${f(x.hi, 2)}`).join(" · ")
|
||||
: "④ erwartete Spanne —";
|
||||
}
|
||||
|
||||
// ── Veto: die eigene Bilanz, JE LOT ──────────────────────────────────
|
||||
const v = s.veto || {};
|
||||
|
||||
+2
-2
@@ -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=153">
|
||||
<link rel="stylesheet" href="style.css?v=154">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Modul-Menü (ganz oben) -->
|
||||
@@ -340,6 +340,6 @@
|
||||
</div>
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
|
||||
<script src="app.js?v=153"></script>
|
||||
<script src="app.js?v=154"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user