Gesamtempfehlung: Selbst-Kalibrierung, voller Ausrichtungs-Split, Block-Gruende,
Elliott entmachtet (v=144)
Vier Verbesserungen, alle aus dem Gemessenen abgeleitet — kein neues Signal.
Ausgangspunkt: die Karte hat genau eine belegte Funktion, das Veto; als Prognose
ist sie ein Muenzwurf (48-51 % ueber 1.071 Episoden).
(1) SELBST-KALIBRIERUNG. Neue Tabelle rec_outcomes: eine Zeile je EPISODE
(Richtungswechsel, nicht je Minute), Auswertung ~alle 5 min gegen candles_m1
nach 30/60 min, Snapshot rec_track, Zeile #vd-selfcal. Gegen 50 % zu lesen.
Warum der groesste Hebel: die Qualitaet war bis heute unsichtbar und musste
auf Nachfrage rueckwirkend gemessen werden. Dasselbe Muster
(pbreak_predictions) hat den Ausfall des P(break)-Modells LIVE aufgedeckt.
Mit 4 Szenarien getestet (tests_rec_outcomes.py). Zwei Fehlschlaege dabei
waren Testfehler, keine Code-Fehler — dokumentiert.
(2) alignment_stats auf die VOLLE Historie (1.216 statt 40 Trades) und
zusaetzlich je Lot. Absolute Euro-Vergleiche sind bei wechselnder
Positionsgroesse ungueltig (Lehre 04.08.). Damit steht die einzige
beidhaelftig robuste Aussage der Karte auf ihrer vollen Stichprobe.
(3) Block-Gruende sichtbar (block_mix -> #vd-blocks): "heute X % ohne Block ·
entry_room … · min_conf …". Bisher nur per DB-Abfrage zu beantworten.
(4) ELLIOTT: Verdict-Gewicht 1,0 -> 0. Gemessen ueber 15.544 Zeilen hatte es mit
1.384 Episoden die groesste Stichprobe der Tabelle und ist darin flach
(-0,075/+0,032, 50 % Treffer) — bei 25,9 % Einfluss auf die Nadel, weil es
in nur 1 % der Zeilen schweigt. Chip bleibt, Stimme entfaellt.
⚠ Folge: die Nadel schlaegt staerker aus (real +0,33 -> +1,00).
⚠ Namenskollision beim Bau gefunden und behoben: die neue Klasse hiess zuerst
.vd-track — so heisst bereits die Schiene der Bias-Nadel; sie waere
ueberschrieben worden. Jetzt .vd-selfcal.
Live verifiziert: alignment je Lot, block_mix (93 von 1381 ohne Block),
rec_outcomes angelegt, Elliott weight=0 bei erhaltenem Chip, v=144 ausgeliefert,
eine Instanz auf Port 8000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4f85d2ce75
commit
874ee16e00
+161
-10
@@ -690,6 +690,8 @@ class TradingEngine:
|
||||
except Exception as ce: log.warning(f"candles.log: {ce}")
|
||||
try: self._evaluate_pbreak_predictions() # ~alle 5 min, self-throttled
|
||||
except Exception as pe: log.warning(f"pbreak_predictions: {pe}")
|
||||
try: self._evaluate_rec_outcomes() # ~alle 5 min, self-throttled
|
||||
except Exception as re_: log.warning(f"rec_outcomes: {re_}")
|
||||
# Wirtschaftskalender (self-throttled ~1 h, außerhalb des mt5_lock
|
||||
# unkritisch — reiner HTTP-Fetch mit Timeout)
|
||||
try: self.calendar.maybe_refresh()
|
||||
@@ -1675,6 +1677,30 @@ class TradingEngine:
|
||||
except Exception as e:
|
||||
log.warning(f"log_recommendation: {e}")
|
||||
|
||||
# ── Selbst-Kalibrierung: eine Zeile je EPISODE (2026-08-06) ──────────
|
||||
# WARUM: die Qualitaet der Empfehlung war bis heute unsichtbar — sie musste
|
||||
# auf Nachfrage rueckwirkend gemessen werden (`analyze_signal_live.py`:
|
||||
# 48–51 % Treffer ueber 1.071 Episoden). Dasselbe Muster wie
|
||||
# `pbreak_predictions` hat den Ausfall des P(break)-Modells LIVE aufgedeckt,
|
||||
# was kein Backtest konnte. Reine Messung, kein Eingriff in die Logik.
|
||||
# ⚠ Nur bei RICHTUNGSWECHSEL, nicht je Minute — sonst zaehlt man dieselbe
|
||||
# Marktlage dutzendfach (der Fehler, den die Episoden-Bildung behebt).
|
||||
try:
|
||||
_sig = rec.get("signal")
|
||||
if _sig in ("LONG", "SHORT") and _sig != getattr(self, "_rec_epi_sig", None):
|
||||
_atr = float((s.get("pb_feats") or {}).get("atr") or 0.0)
|
||||
_px = float((self.data.snapshot() or {}).get("bid") or 0.0)
|
||||
if _atr > 0 and _px > 0 and self.history:
|
||||
self.history.log_rec_outcome(
|
||||
signal=_sig, conf_pct=int(rec.get("conf_pct") or 0),
|
||||
tf=str(rec.get("tf") or self.wave.timeframe_name or ""),
|
||||
price=_px, atr=_atr)
|
||||
if _sig in ("LONG", "SHORT", "WARTEN"):
|
||||
# WARTEN re-armt: die naechste Richtung ist eine NEUE Episode.
|
||||
self._rec_epi_sig = _sig if _sig != "WARTEN" else None
|
||||
except Exception as e:
|
||||
log.debug(f"log_rec_outcome: {e}")
|
||||
|
||||
# Verdict-Stimmen-Logging (2026-07-24, Idee 2b): Modul-Votes 1×/min in die
|
||||
# DB (`verdict_votes`), damit die UNBELEGTEN Stimmen (KI-Copilot, Elliott)
|
||||
# nach ein paar Wochen datenbasiert bewertet werden können (behalten /
|
||||
@@ -2856,11 +2882,20 @@ class TradingEngine:
|
||||
if tgt is not None and bid:
|
||||
ev = -1 if tgt < bid else 1 if tgt > bid else 0
|
||||
edet = f"{(elliott or {}).get('wave', '—')} → {tgt:.3f}"
|
||||
# ⚠ Gewicht nur bei GERICHTETEM Ziel (2026-07-31, Modul-Audit): liegt das
|
||||
# Ziel exakt auf dem Kurs, ist ev=0 — das ist keine Aussage und darf die
|
||||
# Nadel nicht dämpfen. Betraf 1,3 % der Zeilen (klein, aber dieselbe Regel
|
||||
# wie bei Welle/Squeeze/M30/H1/KI — Konsistenz statt Sonderfall).
|
||||
ew = 1.0 if ev != 0 else 0.0
|
||||
# ⚠⚠ GEWICHT AUF 0 GESETZT (2026-08-06, gemessen
|
||||
# `analyze_verdict_modules.py` über 15.544 Zeilen). Elliott hatte mit
|
||||
# **1.384 Episoden die grösste Stichprobe** der ganzen Tabelle und ist
|
||||
# darin **flach**: Überschuss −0,075 / +0,032 ×ATR, **50 % Treffer**.
|
||||
# Gleichzeitig war es mit **25,9 % die zweitlauteste Stimme** der
|
||||
# Bias-Nadel — weil es praktisch nie schweigt (stumm in 1 % der Zeilen).
|
||||
# Zusammen mit H1 (38,1 %) bildete es einen Dauerhintergrund von ~64 %,
|
||||
# gegen den die episodischen Module erst anschieben mussten.
|
||||
# Ein Modul mit der grössten Stichprobe und ohne messbaren Inhalt darf
|
||||
# den Konsens nicht dominieren. Der CHIP bleibt (reine Anzeige, das Ziel
|
||||
# ist als Kontext nützlich) — nur die Stimme entfällt.
|
||||
# ⚠ Reine ANZEIGE-Änderung: die Order steuert die Welle über die
|
||||
# Headline, nicht der Konsens. Zurück: `ew = 1.0 if ev != 0 else 0.0`.
|
||||
ew = 0.0
|
||||
add("Elliott", ev, ew, edet)
|
||||
|
||||
# 7) Squeeze-Breakout — Event-Signal: stimmt NUR bei `active` mit (dann 2,0,
|
||||
@@ -3037,20 +3072,72 @@ class TradingEngine:
|
||||
return getattr(self, "_manual_cache", None)
|
||||
|
||||
def _alignment_cached(self) -> dict | None:
|
||||
"""Ausrichtungs-Split (mit/gegen/ohne Empfehlung, letzte 40 Trades) für den
|
||||
Order-Bestätigungsdialog — ehrliche eigene Zahlen statt generischem Hinweis
|
||||
(2026-07-24, Verhaltens-Hebel: Gegen-Signal-Trades sind die gemessene
|
||||
Kern-Leckage). DB-Query gecacht (~120 s), Snapshot bleibt billig."""
|
||||
"""Ausrichtungs-Split (mit/gegen/ohne Empfehlung) für den Order-Dialog —
|
||||
ehrliche eigene Zahlen statt generischem Hinweis (2026-07-24; Gegen-Signal-
|
||||
Trades sind die gemessene Kern-Leckage).
|
||||
⚠ Seit 2026-08-06 über die **volle Historie** (1.216 Trades) statt der
|
||||
letzten 40 — der Befund ist dort beidhälftig robust, über 40 war er eine
|
||||
Momentaufnahme. Zusätzlich je Lot normiert (`pnl_lot`), weil absolute
|
||||
Euro-Vergleiche bei wechselnder Positionsgröße ungültig sind.
|
||||
DB-Query gecacht (~120 s), Snapshot bleibt billig."""
|
||||
now = time.time()
|
||||
if now - getattr(self, "_align_ts", 0.0) > 120:
|
||||
try:
|
||||
self._align_cache = self.history.alignment_stats(40)
|
||||
self._align_cache = self.history.alignment_stats(0)
|
||||
except Exception as e:
|
||||
log.debug(f"alignment_stats: {e}")
|
||||
self._align_cache = None
|
||||
self._align_ts = now
|
||||
return getattr(self, "_align_cache", None)
|
||||
|
||||
def _block_mix_cached(self) -> dict | None:
|
||||
"""WELCHE Gates blocken heute, und wie oft? (2026-08-06)
|
||||
|
||||
WARUM SICHTBAR: auf die Frage „warum gab es heute so wenige Empfehlungen?"
|
||||
liess sich das nur per DB-Abfrage beantworten. Real am 06.08.: von 646
|
||||
Zeilen waren **6 ungeblockt** — entry_room 274 · min_conf 225 ·
|
||||
breakout_pending 88 · htf_counter 50. Die Karte sagt das jetzt selbst.
|
||||
Reine Anzeige, ~120 s gecacht."""
|
||||
now = time.time()
|
||||
if now - getattr(self, "_blockmix_ts", 0.0) > 120:
|
||||
try:
|
||||
import sqlite3
|
||||
con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0)
|
||||
rows = con.execute(
|
||||
"SELECT COALESCE(block_reason,'') b, COUNT(*) n FROM recommendations "
|
||||
"WHERE date(timestamp,'unixepoch','localtime') = date('now','localtime') "
|
||||
"GROUP BY b ORDER BY n DESC").fetchall()
|
||||
con.close()
|
||||
ges = sum(r[1] for r in rows)
|
||||
self._blockmix_cache = {
|
||||
"total": ges,
|
||||
"frei": next((r[1] for r in rows if not r[0]), 0),
|
||||
"top": [{"grund": r[0], "n": r[1]} for r in rows if r[0]][:4],
|
||||
} if ges else None
|
||||
except Exception as e:
|
||||
log.debug(f"block_mix: {e}")
|
||||
self._blockmix_cache = None
|
||||
self._blockmix_ts = now
|
||||
return getattr(self, "_blockmix_cache", None)
|
||||
|
||||
def _rec_track_cached(self) -> dict | None:
|
||||
"""Selbst-Kalibrierung: Trefferquote der letzten Empfehlungs-Episoden.
|
||||
|
||||
⚠ Gegen 50 % zu lesen, nicht gegen 0 — es ist eine Richtungsaussage.
|
||||
Gemessener Ausgangsstand (`analyze_signal_live.py`, 1.071 Episoden):
|
||||
48–51 %. Diese Zeile macht laufend sichtbar, ob sich das aendert, statt
|
||||
dass es erst auf Nachfrage rueckwirkend gemessen wird. ~120 s gecacht."""
|
||||
now = time.time()
|
||||
if now - getattr(self, "_rectrack_ts", 0.0) > 120:
|
||||
try:
|
||||
self._rectrack_cache = (self.history.rec_accuracy(200)
|
||||
if self.history else None)
|
||||
except Exception as e:
|
||||
log.debug(f"rec_accuracy: {e}")
|
||||
self._rectrack_cache = None
|
||||
self._rectrack_ts = now
|
||||
return getattr(self, "_rectrack_cache", None)
|
||||
|
||||
def _loss_streak_today(self) -> tuple[int, int]:
|
||||
"""(Verlust-Trades heute, Trades heute) — für den Mental-Check der Entry-
|
||||
Checkliste („nach 2 Losses Session beenden"). DB-Query ~120 s gecacht."""
|
||||
@@ -3276,6 +3363,68 @@ class TradingEngine:
|
||||
except Exception as e:
|
||||
log.debug(f"pbreak_predictions auswerten: {e}")
|
||||
|
||||
def _evaluate_rec_outcomes(self):
|
||||
"""Traegt fuer offene Empfehlungs-Episoden den Forward-Return nach.
|
||||
|
||||
⚠ `candles_m1.time` ist ROHE Broker-Zeit (UTC+3), `rec_outcomes.ts` ist
|
||||
lokale Epoch — der Offset muss drauf (dieselbe Falle wie bei
|
||||
`_evaluate_pbreak_predictions`). Genommen wird der letzte M1-Close bis
|
||||
zum Zielzeitpunkt; fehlen Bars (Wochenende, Broker-Ausfall), bleibt die
|
||||
Zeile offen statt falsch bewertet zu werden.
|
||||
|
||||
`hit` = 1, wenn der Kurs in Signalrichtung lief. Die Quote ist gegen 50 %
|
||||
zu lesen, nicht gegen 0."""
|
||||
now = time.time()
|
||||
if now - getattr(self, "_rec_eval_ts", 0.0) < 300:
|
||||
return
|
||||
self._rec_eval_ts = now
|
||||
try:
|
||||
import sqlite3
|
||||
off = self.trader._broker_offset_s()
|
||||
con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0)
|
||||
con.row_factory = sqlite3.Row
|
||||
offen = con.execute(
|
||||
"SELECT id, ts, signal, price, atr, ret30, ret60 FROM rec_outcomes "
|
||||
"WHERE done IS NULL OR done = 0").fetchall()
|
||||
for r in offen:
|
||||
d = 1 if r["signal"] == "LONG" else -1
|
||||
if not r["atr"] or r["atr"] <= 0:
|
||||
con.execute("UPDATE rec_outcomes SET done=1 WHERE id=?", (r["id"],))
|
||||
continue
|
||||
werte = {}
|
||||
for feld, minuten in (("ret30", 30), ("ret60", 60)):
|
||||
if r[feld] is not None:
|
||||
werte[feld] = r[feld]
|
||||
continue
|
||||
ziel = r["ts"] + minuten * 60
|
||||
if now < ziel:
|
||||
continue # noch nicht faellig
|
||||
row = con.execute(
|
||||
"SELECT c FROM candles_m1 WHERE time <= ? AND time > ? "
|
||||
"ORDER BY time DESC LIMIT 1",
|
||||
(ziel + off, ziel + off - 900)).fetchone()
|
||||
if row:
|
||||
werte[feld] = (row["c"] - r["price"]) * d / r["atr"]
|
||||
sets, args = [], []
|
||||
for feld in ("ret30", "ret60"):
|
||||
if feld in werte and r[feld] is None:
|
||||
sets.append(f"{feld}=?"); args.append(round(werte[feld], 4))
|
||||
sets.append(f"hit{feld[3:]}=?"); args.append(1 if werte[feld] > 0 else 0)
|
||||
faellig60 = now >= r["ts"] + 60 * 60
|
||||
if faellig60 and ("ret60" in werte or r["ret60"] is not None):
|
||||
sets.append("done=?"); args.append(1)
|
||||
elif now >= r["ts"] + 6 * 3600:
|
||||
# Nach 6 h keine Bars mehr zu erwarten (Wochenende/Ausfall) →
|
||||
# schliessen, damit die Zeile die Auswertung nicht ewig belegt.
|
||||
sets.append("done=?"); args.append(1)
|
||||
if sets:
|
||||
args.append(r["id"])
|
||||
con.execute(f"UPDATE rec_outcomes SET {', '.join(sets)} WHERE id=?",
|
||||
args)
|
||||
con.commit(); con.close()
|
||||
except Exception as e:
|
||||
log.debug(f"rec_outcomes auswerten: {e}")
|
||||
|
||||
def squeeze_monitor(self) -> dict:
|
||||
"""Auto-Squeeze-Monitor (B4) für das Dashboard: (1) LIVE-Trades (echt, DB —
|
||||
das Gesamtsystem inkl. realer Exits) und (2) die MECHANISCHE Neuberechnung
|
||||
@@ -3576,6 +3725,8 @@ class TradingEngine:
|
||||
"news_sentiment": (lambda sc: round(float(sc), 2) if sc is not None else None)(
|
||||
(self.news.sentiment or {}).get("score")),
|
||||
"alignment": self._alignment_cached(),
|
||||
"block_mix": self._block_mix_cached(),
|
||||
"rec_track": self._rec_track_cached(),
|
||||
# Kennzahlen der MANUELLEN Trades für die gleichnamige Karte
|
||||
# (2026-08-04). Rein deskriptiv; ~120 s gecacht, damit der heiße
|
||||
# Snapshot-Pfad nicht je Abruf die DB aggregiert.
|
||||
|
||||
+114
-30
@@ -145,6 +145,22 @@ SCHEMA = [
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS rec_outcomes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts INTEGER, -- lokale Epoch, Beginn der EPISODE
|
||||
signal TEXT, -- 'LONG' | 'SHORT'
|
||||
conf_pct INTEGER,
|
||||
tf TEXT,
|
||||
price REAL, -- Kurs bei Ausgabe der Empfehlung
|
||||
atr REAL, -- ATR_M5 zur Normierung
|
||||
ret30 REAL, -- (Kurs[+30min] - price) x Richtung / atr
|
||||
ret60 REAL,
|
||||
hit30 INTEGER, -- 1 = Kurs lief in Signalrichtung
|
||||
hit60 INTEGER,
|
||||
done INTEGER -- 1 = beide Horizonte ausgewertet
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS verdict_votes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts INTEGER, -- lokale Epoch
|
||||
@@ -420,6 +436,52 @@ class HistoryLogger:
|
||||
conn.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
def log_rec_outcome(self, *, signal: str, conf_pct: int, tf: str,
|
||||
price: float, atr: float) -> int:
|
||||
"""Loggt EINE Empfehlungs-EPISODE zur spaeteren Selbst-Kalibrierung.
|
||||
|
||||
⚠ WARUM EPISODEN UND NICHT ZEILEN: die Empfehlung wird ~1x/Minute
|
||||
geloggt; eine Marktlage erzeugt dutzende identische Zeilen. Wer die zaehlt,
|
||||
haelt 20.000 Faelle fuer eine Stichprobe, obwohl es 1.000 sind
|
||||
(`analyze_signal_live.py`). Hier wird genau EINE Zeile je Richtungswechsel
|
||||
geschrieben.
|
||||
|
||||
Ausgewertet wird separat (`engine._evaluate_rec_outcomes`) gegen
|
||||
`candles_m1` — dasselbe Muster wie `pbreak_predictions`, das den Ausfall
|
||||
des P(break)-Modells live aufgedeckt hat. Rueckgabe = Zeilen-ID."""
|
||||
with self._lock, self._connect() as conn:
|
||||
cur = conn.execute("""
|
||||
INSERT INTO rec_outcomes (ts, signal, conf_pct, tf, price, atr)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
""", (int(time.time()), signal, int(conf_pct or 0), tf or "",
|
||||
float(price or 0.0), float(atr or 0.0)))
|
||||
conn.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
def rec_accuracy(self, last_n: int = 200) -> dict:
|
||||
"""Trefferquote der letzten `last_n` ausgewerteten Empfehlungs-Episoden.
|
||||
|
||||
⚠ Die Trefferquote ist gegen 50 % zu lesen, nicht gegen 0 — und der
|
||||
Ø-Return gegen die Drift des Zeitraums. Gemessen ueber 1.071 Episoden lag
|
||||
die Richtungstreffer-Quote bei 48–51 % (`analyze_signal_live.py`); diese
|
||||
Zeile macht laufend sichtbar, ob sich daran etwas aendert."""
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT signal, ret30, ret60, hit30, hit60 FROM rec_outcomes
|
||||
WHERE done = 1 ORDER BY ts DESC LIMIT ?
|
||||
""", (int(last_n),)).fetchall()
|
||||
if not rows:
|
||||
return {"n": 0}
|
||||
def q(feld, hit):
|
||||
v = [r[feld] for r in rows if r[feld] is not None]
|
||||
h = [r[hit] for r in rows if r[hit] is not None]
|
||||
return ((round(100.0 * sum(h) / len(h), 1) if h else None),
|
||||
(round(sum(v) / len(v), 3) if v else None), len(h))
|
||||
wr30, avg30, n30 = q("ret30", "hit30")
|
||||
wr60, avg60, n60 = q("ret60", "hit60")
|
||||
return {"n": len(rows), "wr30": wr30, "avg30": avg30, "n30": n30,
|
||||
"wr60": wr60, "avg60": avg60, "n60": n60}
|
||||
|
||||
def pbreak_accuracy(self, period: str = "all") -> dict:
|
||||
"""Trefferquote der Abprall/Durchbruch-Prognose (nur ausgewertete Zeilen)."""
|
||||
since, until = self._range_to_ts(period)
|
||||
@@ -521,38 +583,60 @@ class HistoryLogger:
|
||||
"pct": round(100.0 * (r["c"] or 0) / (r["n"] or 1), 1)}
|
||||
return out
|
||||
|
||||
def alignment_stats(self, last_n: int = 40) -> dict:
|
||||
"""Ausrichtungs-Split der letzten N geschlossenen Trades: liefen sie MIT,
|
||||
GEGEN oder OHNE die Empfehlung (rec_signal beim Entry)? Für die ehrliche
|
||||
Verhaltens-Anzeige im Order-Bestätigungsdialog (B0 als Software — die
|
||||
gemessene Kern-Leckage sind diskretionäre Gegen-Signal-Trades)."""
|
||||
def alignment_stats(self, last_n: int = 0) -> dict:
|
||||
"""Ausrichtungs-Split: liefen die Trades MIT, GEGEN oder OHNE die Empfehlung?
|
||||
|
||||
⚠⚠ ZWEI AENDERUNGEN AM 2026-08-06, beide gemessen begruendet:
|
||||
(1) **Volle Historie statt der letzten 40 Trades** (`last_n=0`). Ueber
|
||||
1.216 geschlossene Trades ist der Befund beidhaelftig robust; ueber 40
|
||||
war er eine Momentaufnahme. Das ist die einzige Richtungsaussage ueber
|
||||
die Gesamtempfehlung, die den 2-Stichproben-Test besteht — sie sollte
|
||||
auf der ganzen Stichprobe stehen.
|
||||
(2) **Zusaetzlich JE LOT normiert** (`pnl_lot`). Absolute Euro-Vergleiche
|
||||
ueber Zeitraeume mit unterschiedlicher Positionsgroesse sind ungueltig
|
||||
— am 04.08. sah ein reiner Groessenzuwachs wie ein Strategie-Effekt aus
|
||||
(Ø +55 € gegen +19 €, je Lot aber identisch). Das Order-Dialog-Feld
|
||||
zeigt deshalb beides.
|
||||
|
||||
Gemessener Stand (06.08., ganze Historie, je Lot):
|
||||
MIT n=354 WR 56,5 % +3,22 €/Lot (H1 +0,37 / H2 +5,62)
|
||||
GEGEN n= 87 WR 40,2 % −6,60 €/Lot (H1 −7,01 / H2 −5,68)
|
||||
OHNE n=775 WR 56,6 % +0,36 €/Lot (kippt zwischen den Haelften)
|
||||
MIT und OHNE sind praktisch gleich — der Empfehlung zu FOLGEN bringt
|
||||
gemessen nichts, nur sie zu VERLETZEN kostet. Sie ist ein Veto, keine
|
||||
Prognose.
|
||||
"""
|
||||
sql = ("SELECT direction, rec_signal, pnl, lots FROM trades "
|
||||
"WHERE exit_time IS NOT NULL AND pnl IS NOT NULL AND lots > 0 "
|
||||
"ORDER BY exit_time DESC")
|
||||
args: tuple = ()
|
||||
if last_n and last_n > 0:
|
||||
sql += " LIMIT ?"
|
||||
args = (last_n,)
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT direction, rec_signal, pnl FROM trades
|
||||
WHERE exit_time IS NOT NULL AND pnl IS NOT NULL
|
||||
ORDER BY exit_time DESC LIMIT ?
|
||||
""", (last_n,)).fetchall()
|
||||
out = {}
|
||||
for key, match in (("with", True), ("against", False)):
|
||||
grp = []
|
||||
for r in rows:
|
||||
rec = (r["rec_signal"] or "").upper()
|
||||
if rec not in ("LONG", "SHORT"):
|
||||
continue
|
||||
aligned = (rec == "LONG") == (r["direction"] == "BUY")
|
||||
if aligned == match:
|
||||
grp.append(r["pnl"])
|
||||
rows = conn.execute(sql, args).fetchall()
|
||||
|
||||
def fasse(grp: list) -> dict:
|
||||
n = len(grp)
|
||||
out[key] = {"n": n,
|
||||
"wr": round(100 * sum(1 for p in grp if p > 0) / n) if n else None,
|
||||
"pnl": round(sum(grp), 2)}
|
||||
none_grp = [r["pnl"] for r in rows
|
||||
if (r["rec_signal"] or "").upper() not in ("LONG", "SHORT")]
|
||||
n = len(none_grp)
|
||||
out["none"] = {"n": n,
|
||||
"wr": round(100 * sum(1 for p in none_grp if p > 0) / n) if n else None,
|
||||
"pnl": round(sum(none_grp), 2)}
|
||||
return out
|
||||
if not n:
|
||||
return {"n": 0, "wr": None, "pnl": 0.0, "pnl_lot": None}
|
||||
pnl = sum(r["pnl"] for r in grp)
|
||||
lots = sum(r["lots"] for r in grp) or 0.0
|
||||
return {"n": n,
|
||||
"wr": round(100 * sum(1 for r in grp if r["pnl"] > 0) / n),
|
||||
"pnl": round(pnl, 2),
|
||||
"pnl_lot": round(pnl / lots, 2) if lots > 0 else None}
|
||||
|
||||
mit, gegen, ohne = [], [], []
|
||||
for r in rows:
|
||||
rec = (r["rec_signal"] or "").upper()
|
||||
if rec not in ("LONG", "SHORT"):
|
||||
ohne.append(r)
|
||||
elif (rec == "LONG") == (r["direction"] == "BUY"):
|
||||
mit.append(r)
|
||||
else:
|
||||
gegen.append(r)
|
||||
return {"with": fasse(mit), "against": fasse(gegen), "none": fasse(ohne)}
|
||||
|
||||
# ══════════════════════════════════════════
|
||||
# STATISTIK-QUERIES
|
||||
|
||||
Reference in New Issue
Block a user