Sieben Verbesserungen umgesetzt: Breaker scharf, B5 entschieden, 5 Werkzeuge
1) CIRCUIT-BREAKER SCHARF -- daily_loss_limit_pct 0 -> 8. Live verifiziert:
enabled=True, limit_eur 89,44. Steht nicht in runtime_state.json, die ini
greift direkt. Begruendung: MaxDD 82,8 % auf dem gemessenen IST-Pfad,
schlechtester Einzeltrade -381 EUR.
2) B5-DEADLOCK RECHNERISCH AUFGELOEST -- die Regel ist ENTSCHIEDEN, nicht
vertagt. Stand 11/20: Verhaeltnis 0,14 (Latte 1,0), PF 0,39 (Latte 1,0).
Was die 9 Rest-Trades leisten muessten:
9/0 Gewinner -> 165,72 EUR je Trade = 12,2x der bisherige Schnitt
7/2 -> 186,17 EUR
5/4 -> 222,99 EUR
Der groesste Squeeze-Gewinn des Zeitraums war 77,34 EUR -- selbst der beste
Fall verlangt 2,1x den Rekord, neunmal in Folge. Latte nicht mehr erreichbar.
auto_squeeze bleibt FALSE. Wiederaufnahme waere eine NEUE Entscheidung mit
NEUER, vorab fixierter Latte.
3) core/kontrolle.py -- Kontroll-Fixture mit den Referenzwerten. Bricht bewusst
NICHT ab: ein Backtest darf abweichen, er muss es nur wissen und sagen.
4) core/live_config.py -- EINE Quelle der Live-Parameter (ini + runtime_state in
der Vorrangfolge des Engine-Starts). Dritte Instanz derselben Fehlerklasse:
ATRMIN_STD 0,12 statt 0,06, fehlendes set_entry_room, nachgebauter Cluster.
5) stichprobe.paarweise()/zeig_paarweise() -- gepaarter Vergleich. Anlass:
trail_start sah sequentiell besser aus, gepaart kippte das Vorzeichen
(Selektions- statt Exit-Effekt). Selbsttest in drei Richtungen bestanden.
6) tools/check_konstanten.py -- Waechter fuer veraltete Zahlen.
Erster Lauf: zwei Befunde, BEIDE Fehler in meiner eigenen Ableitung (Margin
aus Balance geschaetzt statt vom Broker gelesen; Elliott ohne
Gueltigkeitsfenster gezaehlt). Beide korrigiert.
Danach ein ECHTER Fund: meine am 13.08. notierten "~405 EUR/Lot" sind falsch,
der exakte Broker-Wert ist 712,84 -- die urspruenglich notierten 670 lagen
naeher als meine Korrektur. Lehre: eine abgeleitete Zahl ist keine gemessene.
Stand jetzt 0 Befunde.
7) Puls-Waechter bedingungs-bewusst: PULS traegt je Zeile die Vorbedingung,
bedingte Tabellen erscheinen als Hinweis statt als Ausfall. 0 Zeilen bleibt
IMMER ein Befund (der rec_outcomes-Fall). Grund: ein Waechter mit
Fehlalarmen wird ignoriert -- so ist der Divergenz-Waechter verstummt.
Pipeline gruen (85 Tests), Deployment ueber deploy.py --feld circuit_breaker
verifiziert, genau eine Instanz je Port.
Nicht angefasst: Sizing (User-Entscheidung) und die 44 Backtests mit eigener
Exit-Kopie (Migrations-Regel).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9dbc5d0452
commit
792355e8f9
+31
-10
@@ -436,15 +436,27 @@ class HistoryLogger:
|
||||
# BEIDE Nutzer (Reminder-Popup + Tagesreport) brauchen ihn. Zwei Kopien
|
||||
# waeren genau die Divergenz, die im Projekt dreimal zugeschlagen hat.
|
||||
# Richtungsregel: Skripte duerfen `core` importieren, nicht umgekehrt.
|
||||
# ⚠⚠ BEDINGUNGS-KENNTNIS (2026-08-17). Der Puls hat `pbreak_predictions` als
|
||||
# „63,5 h still" gemeldet — dabei schreibt die Tabelle NUR bei frischem
|
||||
# Level-Touch auf der ZIEL-Seite einer OFFENEN Position. Ohne offene
|
||||
# Position kann sie legitim tagelang schweigen. Ein Waechter, der
|
||||
# Fehlalarme produziert, wird ignoriert — und mit ihm die eine echte
|
||||
# Meldung; genau das ist dem Divergenz-Waechter passiert (6 Tage
|
||||
# Dauerwarnung, 02.08.). Deshalb traegt jede Zeile jetzt ihre VORBEDINGUNG.
|
||||
# `bedingung=None` heisst „schreibt unbedingt, Stille ist immer ein Fehler".
|
||||
PULS = (
|
||||
# (Tabelle, Zeitspalte, erwartete Kadenz in Minuten, wozu)
|
||||
("recommendations", "timestamp", 1, "Empfehlungs-Log"),
|
||||
("candles_m1", "time", 1, "M1-Kerzen-Sammlung"),
|
||||
("verdict_votes", "ts", 1, "Modul-Stimmen"),
|
||||
("m15_states", "ts", 60, "M15-Karte Selbstmessung"),
|
||||
("cone_checks", "ts", 60, "Kegel-Kalibrierung (ACI)"),
|
||||
("pbreak_predictions", "ts", 60, "P(break)-Prognose-Tracking"),
|
||||
("rec_outcomes", "ts", 180, "Selbst-Kalibrierung Empfehlung"),
|
||||
# (Tabelle, Zeitspalte, Kadenz in Minuten, wozu, Vorbedingung)
|
||||
("recommendations", "timestamp", 1, "Empfehlungs-Log", None),
|
||||
("candles_m1", "time", 1, "M1-Kerzen-Sammlung", None),
|
||||
("verdict_votes", "ts", 1, "Modul-Stimmen", None),
|
||||
("m15_states", "ts", 60, "M15-Karte Selbstmessung", None),
|
||||
("cone_checks", "ts", 60, "Kegel-Kalibrierung (ACI)", None),
|
||||
("pbreak_predictions", "ts", 60, "P(break)-Prognose-Tracking",
|
||||
"braucht eine OFFENE Position und einen frischen Level-Touch auf der "
|
||||
"Ziel-Seite — ohne offene Position ist Stille normal"),
|
||||
("rec_outcomes", "ts", 180, "Selbst-Kalibrierung Empfehlung",
|
||||
"braucht einen Richtungs-WECHSEL der Empfehlung — bei ~93 % WARTEN "
|
||||
"kann das lange dauern"),
|
||||
)
|
||||
|
||||
def telemetrie_puls(self) -> list[str]:
|
||||
@@ -458,20 +470,29 @@ class HistoryLogger:
|
||||
try:
|
||||
jetzt = time.time()
|
||||
with self._lock, self._connect() as conn:
|
||||
for tab, sp, kadenz, wozu in self.PULS:
|
||||
for tab, sp, kadenz, wozu, bedingung in self.PULS:
|
||||
try:
|
||||
n = conn.execute(f"SELECT COUNT(*) FROM {tab}").fetchone()[0]
|
||||
letzt = conn.execute(f"SELECT MAX({sp}) FROM {tab}").fetchone()[0]
|
||||
except Exception:
|
||||
continue # Tabelle gibt es (noch) nicht
|
||||
if not n:
|
||||
# ⚠ 0 Zeilen ist IMMER ein Befund — auch mit Vorbedingung:
|
||||
# „hat nie geschrieben" laesst sich nicht wegerklaeren.
|
||||
out.append(f"{tab}: 0 Zeilen — Logger hat NIE geschrieben ({wozu})")
|
||||
continue
|
||||
# ⚠ `candles_m1.time` ist ROHE Brokerzeit (UTC+3) — ohne den
|
||||
# Abzug meldet der Puls dort dauerhaft 3 h Rueckstand.
|
||||
ts = letzt - 10800 if tab == "candles_m1" else letzt
|
||||
alter_min = (jetzt - ts) / 60.0
|
||||
if alter_min > kadenz * 20:
|
||||
if alter_min <= kadenz * 20:
|
||||
continue
|
||||
if bedingung:
|
||||
# Bedingte Tabellen: als HINWEIS melden, nicht als Ausfall —
|
||||
# sonst wird der Waechter zum Rauschen (s. Kommentar an PULS).
|
||||
out.append(f"ⓘ {tab}: still seit {alter_min/60:.1f} h — "
|
||||
f"{bedingung} ({wozu})")
|
||||
else:
|
||||
out.append(f"{tab}: letzte Zeile vor {alter_min/60:.1f} h "
|
||||
f"(erwartet ~alle {kadenz} min) — {wozu}")
|
||||
except Exception:
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""KONTROLL-FIXTURE: ein Backtest muss die Referenz reproduzieren, BEVOR er berichtet.
|
||||
|
||||
⚠⚠ WARUM ES DAS GIBT (2026-08-13). Zwei an einem Tag gebaute Messungen konnten
|
||||
den dokumentierten Squeeze-Referenzwert (+0,124/+0,292, `backtest_squeeze_
|
||||
touchfill.py`) NICHT reproduzieren — sie kamen auf −0,151/−0,079 bei einem
|
||||
Drittel der Trades. Beide Ergebnisse mussten deshalb auf den **relativen**
|
||||
Vergleich beschränkt werden; die absolute Aussage war nicht führbar. Und es
|
||||
blieb offen, ob der dokumentierte Wert oder die neue Pipeline danebenliegt.
|
||||
|
||||
`backtest_verworfene_v2.py` macht es vorbildlich von Hand („✅ Die Kontrolle
|
||||
reproduziert … Pipeline in Ordnung"). Hier ist dasselbe als geteilte Funktion,
|
||||
damit es niemand vergisst.
|
||||
|
||||
from core.kontrolle import pruefe_squeeze
|
||||
ok = pruefe_squeeze(h1_oer, h2_oer, n_h1, n_h2) # meldet und gibt bool
|
||||
|
||||
⚠ Es wird NICHT abgebrochen. Ein Backtest darf von der Referenz abweichen — er
|
||||
muss es nur WISSEN und sagen. Ein harter Abbruch würde legitime Varianten
|
||||
(andere Fill-Annahme, anderer Exit) unmöglich machen; eine stille Abweichung
|
||||
dagegen erzeugt genau die Fehlschlüsse, die hier vermieden werden sollen.
|
||||
|
||||
⚠ Die Referenzen sind MESSWERTE, keine Wunschzahlen. Wer sie ändert, muss die
|
||||
Quelle nennen — sonst wandert die Latte mit dem Ergebnis.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Referenz:
|
||||
name: str
|
||||
quelle: str
|
||||
h1: float
|
||||
h2: float
|
||||
n_h1: int
|
||||
n_h2: int
|
||||
toleranz: float = 0.06 # ØR-Abstand, ab dem gewarnt wird
|
||||
|
||||
|
||||
# ── Die dokumentierten Referenzwerte des Projekts ────────────────────────────
|
||||
SQUEEZE_TOUCHFILL = Referenz(
|
||||
name="Squeeze · ruhende Order am Level, Fill bei Beruehrung",
|
||||
quelle="backtest_squeeze_touchfill.py (2026-08-06)",
|
||||
h1=+0.124, h2=+0.292, n_h1=1092, n_h2=1807)
|
||||
|
||||
SQUEEZE_CLOSEBRUCH = Referenz(
|
||||
name="Squeeze · Fill bei Close-Bruch (die OPTIMISTISCHE Annahme)",
|
||||
quelle="backtest_squeeze_entry.py (2026-08-05)",
|
||||
h1=+0.456, h2=+0.611, n_h1=801, n_h2=1280)
|
||||
|
||||
SQUEEZE_MARKT = Referenz(
|
||||
name="Squeeze · Markt-Einstieg am Bar-Close",
|
||||
quelle="backtest_squeeze_entry.py (2026-08-05)",
|
||||
h1=-0.158, h2=-0.006, n_h1=801, n_h2=1280)
|
||||
|
||||
|
||||
def pruefe(ref: Referenz, h1: float, h2: float,
|
||||
n_h1: int | None = None, n_h2: int | None = None,
|
||||
still: bool = False) -> bool:
|
||||
"""Reproduziert der Lauf die Referenz? Meldet den Befund, gibt bool zurueck."""
|
||||
d1, d2 = abs(h1 - ref.h1), abs(h2 - ref.h2)
|
||||
ok = d1 <= ref.toleranz and d2 <= ref.toleranz
|
||||
if still:
|
||||
return ok
|
||||
print(f"\n ── KONTROLLE: {ref.name}")
|
||||
print(f" Referenz {ref.quelle}")
|
||||
print(f" erwartet H1 {ref.h1:+.3f} · H2 {ref.h2:+.3f}"
|
||||
f" (n {ref.n_h1}/{ref.n_h2})")
|
||||
n_txt = "" if n_h1 is None else f" (n {n_h1}/{n_h2})"
|
||||
print(f" gemessen H1 {h1:+.3f} · H2 {h2:+.3f}{n_txt}")
|
||||
print(f" Abstand H1 {d1:.3f} · H2 {d2:.3f} (Toleranz {ref.toleranz})")
|
||||
if ok:
|
||||
print(" ✅ reproduziert — die Pipeline ist in Ordnung, ABSOLUTWERTE "
|
||||
"sind belastbar.")
|
||||
else:
|
||||
print(" ❌ WEICHT AB. Dann gilt:")
|
||||
print(" · der RELATIVE Vergleich innerhalb dieses Laufs bleibt gueltig,")
|
||||
print(" solange alle Varianten dieselbe Entry-Liste benutzen;")
|
||||
print(" · die ABSOLUTWERTE sind es NICHT — sie duerfen nicht als")
|
||||
print(" Ertragsaussage berichtet werden.")
|
||||
if n_h1 is not None and (n_h1 < ref.n_h1 * 0.6 or n_h1 > ref.n_h1 * 1.6):
|
||||
print(f" · ⚠ auch die Trade-ZAHL weicht stark ab ({n_h1} gegen "
|
||||
f"{ref.n_h1}) — meist ein anderer Filter (ATR-Floor, Cooldown,")
|
||||
print(" Slot-Behandlung), nicht der Markt.")
|
||||
return ok
|
||||
|
||||
|
||||
def pruefe_squeeze(h1: float, h2: float, n_h1=None, n_h2=None,
|
||||
variante: str = "touchfill") -> bool:
|
||||
"""Bequemer Aufruf fuer die haeufigste Kontrolle."""
|
||||
ref = {"touchfill": SQUEEZE_TOUCHFILL,
|
||||
"closebruch": SQUEEZE_CLOSEBRUCH,
|
||||
"markt": SQUEEZE_MARKT}[variante]
|
||||
return pruefe(ref, h1, h2, n_h1, n_h2)
|
||||
@@ -0,0 +1,155 @@
|
||||
"""DIE Live-Parameter — EINE Quelle für Backtests.
|
||||
|
||||
⚠⚠ WARUM ES DAS GIBT. Ein Backtest, der einen geteilten Kern aufruft, aber
|
||||
dessen **Modul-Defaults** übernimmt, misst eine andere Welt als die laufende.
|
||||
Das ist im Projekt DREIMAL passiert:
|
||||
· 2026-08-05 `set_entry_room(0.6)` vergessen → `_room_gate` war ein No-op;
|
||||
die Population schrumpfte von 967 auf 295, als es korrigiert war.
|
||||
· 2026-08-07 der Level-Cluster war NACHGEBAUT und wich ab (4,5 % der
|
||||
Zeitpunkte hatten einen anderen Widerstand).
|
||||
· 2026-08-13 `squeeze_scan.ATRMIN_STD` = 0,12 genommen statt der **live**
|
||||
gültigen 0,06 → H1 127 gegen H2 483 Einstiege, also eine völlig
|
||||
andere Population. Aufgefallen nur an der Schieflage.
|
||||
**Ein aufgerufenes Gate ist nicht dasselbe wie ein aktives Gate.**
|
||||
|
||||
Deshalb: wer einen Backtest schreibt, holt die Werte HIER — nicht aus dem
|
||||
Gedächtnis, nicht aus dem Modul-Default, nicht aus CLAUDE.md.
|
||||
|
||||
from core.live_config import LIVE_CFG
|
||||
scan(..., atr_min=LIVE_CFG.squeeze_atr_min, box_n=LIVE_CFG.squeeze_box_n)
|
||||
|
||||
⚠ Gelesen wird die ECHTE `oil_widget_config.ini` PLUS `runtime_state.json` — und
|
||||
zwar in derselben Vorrangfolge wie beim Engine-Start (runtime gewinnt). Genau
|
||||
diese Reihenfolge hat am 31.07. dafür gesorgt, dass `auto_signal` unbemerkt
|
||||
lief, obwohl die ini `false` sagte.
|
||||
|
||||
⚠ Diese Datei trifft KEINE Entscheidungen und ändert nichts. Sie liest nur.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
_HIER = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
_INI = os.path.join(_HIER, "oil_widget_config.ini")
|
||||
_RT = os.path.join(_HIER, "runtime_state.json")
|
||||
|
||||
|
||||
def _lies() -> tuple[dict, dict]:
|
||||
c = configparser.ConfigParser(inline_comment_prefixes=("#", ";"))
|
||||
try:
|
||||
c.read(_INI, encoding="utf-8")
|
||||
t = dict(c["trading"]) if c.has_section("trading") else {}
|
||||
except Exception:
|
||||
t = {}
|
||||
try:
|
||||
with open(_RT, encoding="utf-8") as f:
|
||||
rt = json.load(f)
|
||||
except Exception:
|
||||
rt = {}
|
||||
return t, rt
|
||||
|
||||
|
||||
def _f(d: dict, k: str, std: float) -> float:
|
||||
try:
|
||||
return float(d.get(k, std))
|
||||
except (TypeError, ValueError):
|
||||
return std
|
||||
|
||||
|
||||
def _i(d: dict, k: str, std: int) -> int:
|
||||
try:
|
||||
return int(float(d.get(k, std)))
|
||||
except (TypeError, ValueError):
|
||||
return std
|
||||
|
||||
|
||||
def _b(d: dict, rt: dict, k: str, std: bool) -> bool:
|
||||
"""⚠ runtime_state gewinnt — genau wie beim Engine-Start."""
|
||||
if k in rt:
|
||||
return bool(rt[k])
|
||||
v = str(d.get(k, str(std))).strip().lower()
|
||||
return v in ("true", "1", "yes", "ja")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LiveConfig:
|
||||
# ── Squeeze (der einzige validierte Auto-Entry) ───────────────────────
|
||||
squeeze_box_n: int = 10 # `wave_rec._SQ_N` — NICHT squeeze_scan.BOX_N_STD (12)
|
||||
squeeze_k: float = 0.1
|
||||
squeeze_mult: float = 2.5
|
||||
squeeze_atr_min: float = 0.06 # ⚠ Modul-Default ist 0,12 — live gilt 0,06
|
||||
# ── Signal-Gates ─────────────────────────────────────────────────────
|
||||
breakout_k: float = 0.3
|
||||
entry_room_atr: float = 0.6
|
||||
min_conf: int = 55
|
||||
stretch_max: float = 3.5
|
||||
reversal_enabled: bool = False
|
||||
# ── Exit ─────────────────────────────────────────────────────────────
|
||||
sr_close_pbreak: float = 0.35
|
||||
adverse_15min_atr: float = 0.0
|
||||
auto_flip_close: bool = False
|
||||
# ── Ausfuehrung / Risiko ─────────────────────────────────────────────
|
||||
squeeze_pending_entry: bool = True
|
||||
squeeze_max_chase_atr: float = 0.20
|
||||
margin_buffer_pct: float = 95.0
|
||||
risk_pct: float = 0.0
|
||||
daily_loss_limit_pct: float = 0.0
|
||||
# ── Schalter (runtime_state hat Vorrang) ─────────────────────────────
|
||||
auto_squeeze: bool = False
|
||||
auto_sr_close: bool = True
|
||||
squeeze_night_hours: tuple = field(default_factory=tuple)
|
||||
# ── Abgeleitete Groessen, die Backtests brauchen ─────────────────────
|
||||
# Der Live-SL kommt aus ATR_M15; ueber 121 echte Trades (13.08.) betraegt
|
||||
# er im Median 3,4xATR_M5. Backtests rechnen in ATR_M5 — deshalb hier.
|
||||
sl_atr_m5_aequivalent: float = 3.4
|
||||
eur_pro_dollar_lot: float = 86.90 # aus 1.051 Trades geschaetzt (13.08.)
|
||||
|
||||
@classmethod
|
||||
def laden(cls) -> "LiveConfig":
|
||||
t, rt = _lies()
|
||||
std = cls()
|
||||
nh = str(t.get("auto_squeeze_night_hours", "")).strip()
|
||||
stunden = tuple(int(x) for x in nh.split(",") if x.strip().isdigit()) if nh else ()
|
||||
return cls(
|
||||
squeeze_atr_min=std.squeeze_atr_min, # lebt im Code, nicht in der ini
|
||||
squeeze_box_n=std.squeeze_box_n,
|
||||
squeeze_k=std.squeeze_k,
|
||||
squeeze_mult=std.squeeze_mult,
|
||||
breakout_k=_f(t, "breakout_k", std.breakout_k),
|
||||
entry_room_atr=_f(t, "entry_room_atr", std.entry_room_atr),
|
||||
min_conf=std.min_conf,
|
||||
stretch_max=std.stretch_max,
|
||||
reversal_enabled=_b(t, {}, "reversal_enabled", std.reversal_enabled),
|
||||
sr_close_pbreak=_f(t, "sr_close_pbreak", std.sr_close_pbreak),
|
||||
adverse_15min_atr=_f(t, "adverse_15min_atr", std.adverse_15min_atr),
|
||||
auto_flip_close=_b(t, {}, "auto_flip_close", std.auto_flip_close),
|
||||
squeeze_pending_entry=_b(t, {}, "squeeze_pending_entry",
|
||||
std.squeeze_pending_entry),
|
||||
squeeze_max_chase_atr=_f(t, "squeeze_max_chase_atr",
|
||||
std.squeeze_max_chase_atr),
|
||||
margin_buffer_pct=_f(t, "margin_buffer_pct", std.margin_buffer_pct),
|
||||
risk_pct=_f(t, "risk_pct", std.risk_pct),
|
||||
daily_loss_limit_pct=_f(t, "daily_loss_limit_pct",
|
||||
std.daily_loss_limit_pct),
|
||||
auto_squeeze=_b(t, rt, "auto_squeeze", std.auto_squeeze),
|
||||
auto_sr_close=_b(t, rt, "auto_sr_close", std.auto_sr_close),
|
||||
squeeze_night_hours=stunden,
|
||||
sl_atr_m5_aequivalent=std.sl_atr_m5_aequivalent,
|
||||
eur_pro_dollar_lot=std.eur_pro_dollar_lot,
|
||||
)
|
||||
|
||||
def zeig(self) -> str:
|
||||
z = [" LIVE-KONFIGURATION (aus ini + runtime_state, Vorrang wie beim Start)"]
|
||||
for k, v in sorted(self.__dict__.items()):
|
||||
z.append(f" {k:<26} {v}")
|
||||
return "\n".join(z)
|
||||
|
||||
|
||||
LIVE_CFG = LiveConfig.laden()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(LIVE_CFG.zeig())
|
||||
@@ -123,3 +123,49 @@ def schluessel_level_stunde(level_feld="level", ts_feld="ts"):
|
||||
ts = _feld(z, ts_feld)
|
||||
return (round(float(lvl or 0), 2), int(ts or 0) // 3600)
|
||||
return f
|
||||
|
||||
|
||||
def paarweise(basis: dict, variante: dict, wiederholungen: int = 3000,
|
||||
seed: int = 7):
|
||||
"""GEPAARTER Vergleich zweier Exit-/Regel-Varianten auf DENSELBEN Faellen.
|
||||
|
||||
⚠⚠ WARUM DAS NOETIG IST (2026-08-14, real passiert). Beim `trail_start`-Sweep
|
||||
sahen SEQUENTIELL vier Werte in beiden Haelften besser aus (Δ bis +0,101).
|
||||
Gepaart kippte in H2 das Vorzeichen. Ursache: eine sequentielle Sim mit EINEM
|
||||
Positions-Slot nimmt bei anderem Exit ANDERE Trades — der scheinbare Gewinn
|
||||
war ein **Selektionseffekt**, kein Exit-Effekt. Dieselbe Falle steckte am
|
||||
31.07. in `backtest_exit_combo.py` („verschiedene Trade-Folgen sind nicht
|
||||
vergleichbar").
|
||||
|
||||
⚠ Und der gepaarte Test ist nicht nur richtiger, sondern **trennschaerfer**:
|
||||
die Einzel-Intervalle messen die Streuung der TRADES, das gepaarte die des
|
||||
UNTERSCHIEDS — und die ist um ein Vielfaches kleiner.
|
||||
|
||||
`basis` / `variante`: {schluessel: ergebnis}. Der Schluessel identifiziert den
|
||||
Fall (z. B. der Einstiegs-Bar-Index). Verglichen wird nur die Schnittmenge.
|
||||
|
||||
Rueckgabe: (mittlere Differenz, lo, hi, n) — bei n < 20 (None, None, None, n).
|
||||
"""
|
||||
import random as _r
|
||||
gemeinsam = [k for k in basis if k in variante]
|
||||
dif = [variante[k] - basis[k] for k in gemeinsam]
|
||||
n = len(dif)
|
||||
if n < 20:
|
||||
return None, None, None, n
|
||||
m = sum(dif) / n
|
||||
r = _r.Random(seed)
|
||||
bs = sorted(sum(dif[r.randrange(n)] for _ in range(n)) / n
|
||||
for _ in range(wiederholungen))
|
||||
return m, bs[int(.025 * wiederholungen)], bs[int(.975 * wiederholungen)], n
|
||||
|
||||
|
||||
def zeig_paarweise(name: str, basis: dict, variante: dict) -> bool:
|
||||
"""Druckt den gepaarten Vergleich und sagt, ob er die Null ausschliesst."""
|
||||
m, lo, hi, n = paarweise(basis, variante)
|
||||
if m is None:
|
||||
print(f" {name:<28} zu wenige gemeinsame Faelle (n={n})")
|
||||
return False
|
||||
ok = lo > 0 or hi < 0
|
||||
print(f" {name:<28} Δ je Fall {m:>+.4f} KI [{lo:+.4f} … {hi:+.4f}] "
|
||||
f"n={n} {'✅ ohne 0' if ok else '⚠ enthaelt 0'}")
|
||||
return ok
|
||||
|
||||
Reference in New Issue
Block a user