analyze_srclose_live.py: kontrafaktische Live-Messung des S/R-Auto-Close
User-Frage "ist der Auto-Close beim S/R eher behindernd als dienlich?". Die rohe Statistik (99 % WR, +13,71 EUR/Trade) ist ein Konstruktions- Artefakt - der Close feuert nur im Plus. Das Skript misst stattdessen die Kontrafaktische: fuer jeden sr_close die naechsten 60 min aus candles_m1 (gleiches Fenster wie das Modelltraining, _BRK_W=12 M5-Bars), wie weit der Kurs noch IN Richtung lief (entgangen) bzw. GEGEN sie (vermieden), in EUR ueber die echte Lot-Groesse. EUR-je-Punkt wird aus den Trades selbst kalibriert statt geraten. Broker-Offset (candles_m1.time = rohe UTC+3) wird korrigiert. Befund (70 auswertbare Closes): entgangen 33,68 EUR/Trade vs vermieden 25,68 -> netto -8,00 EUR/Trade. 43 % klar zu frueh geschlossen, nur 26 % klar richtig. Deckt sich mit der Live-P(break)-Genauigkeit (58,8 % bei einer Basisrate von 59,3 % = kaum Trennschaerfe ueber "immer Abpraller"). ⚠ Einordnung: 89 der 98 Closes liefen noch auf M5-Levels; die M30- Umstellung ist 1 Tag alt (9 Closes). Die Messung beurteilt also vor allem die ALTE Konfiguration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a6f370ac02
commit
578711a290
@@ -0,0 +1,141 @@
|
|||||||
|
"""analyze_srclose_live.py — War der S/R-Auto-Close live dienlich oder hinderlich?
|
||||||
|
|
||||||
|
User-Frage 2026-07-31 („ist der Auto-Close beim S/R eher behindernd als dienlich?").
|
||||||
|
|
||||||
|
⚠ WARUM DIE ROHE STATISTIK NICHT REICHT: `closed_by='sr_close'` hat live 99 % WR
|
||||||
|
und Ø +13,71 € — das ist ein **Konstruktions-Artefakt**, der Close feuert nur im
|
||||||
|
Plus. Die Frage ist die KONTRAFAKTISCHE: was wäre passiert, wenn der Trade
|
||||||
|
weitergelaufen wäre?
|
||||||
|
|
||||||
|
METHODE: für jeden sr_close-Trade die nächsten 60 min aus `candles_m1` holen und
|
||||||
|
messen, wie weit der Kurs danach noch IN Trade-Richtung lief (= entgangener Gewinn)
|
||||||
|
bzw. GEGEN sie (= vermiedener Rückgang). Umgerechnet in EUR über die tatsächliche
|
||||||
|
Lot-Größe des Trades.
|
||||||
|
|
||||||
|
⚠ Grenzen (bewusst benannt):
|
||||||
|
* Der Vergleich „hätte weitergelaufen" ist eine OBERGRENZE für das Halten — er
|
||||||
|
unterstellt einen perfekten Ausstieg am besten Punkt. Real hätte das Trailing
|
||||||
|
einen Teil davon wieder abgegeben. Die MAE-Spalte zeigt die Gegenseite.
|
||||||
|
* 60-min-Fenster = dasselbe wie beim P(break)-Modelltraining (_BRK_W=12 M5-Bars).
|
||||||
|
* `candles_m1.time` ist ROHE Broker-Zeit (UTC+3), `trades.exit_time` lokale Epoch
|
||||||
|
— der Offset wird unten korrigiert.
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DB = "oil_widget_history.db"
|
||||||
|
WINDOW_MIN = 60 # wie beim Modelltraining (_BRK_W=12 M5-Bars)
|
||||||
|
|
||||||
|
|
||||||
|
def broker_offset_s(c) -> int:
|
||||||
|
"""Offset zwischen candles_m1.time (Broker, UTC+3) und lokaler Epoch.
|
||||||
|
|
||||||
|
Robust bestimmt: die letzte M1-Kerze ist ~jetzt, die Differenz zu time.time()
|
||||||
|
auf volle Stunden gerundet.
|
||||||
|
"""
|
||||||
|
c.execute("SELECT MAX(time) FROM candles_m1")
|
||||||
|
last = c.fetchone()[0]
|
||||||
|
if not last:
|
||||||
|
return 0
|
||||||
|
return round((last - time.time()) / 3600.0) * 3600
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
db = sqlite3.connect(DB)
|
||||||
|
c = db.cursor()
|
||||||
|
off = broker_offset_s(c)
|
||||||
|
print(f"Broker-Offset erkannt: {off/3600:+.0f} h\n")
|
||||||
|
|
||||||
|
c.execute("""SELECT ticket, direction, lots, exit_time, exit_price, pnl, entry_price
|
||||||
|
FROM trades
|
||||||
|
WHERE closed_by='sr_close' AND exit_time IS NOT NULL
|
||||||
|
AND exit_price IS NOT NULL AND lots > 0
|
||||||
|
ORDER BY exit_time""")
|
||||||
|
rows = c.fetchall()
|
||||||
|
if not rows:
|
||||||
|
print("Keine sr_close-Trades gefunden.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# EUR je 1,00 Kursbewegung und Lot — aus den Trades selbst kalibriert, damit
|
||||||
|
# keine Kontraktgroesse geraten werden muss.
|
||||||
|
cal = []
|
||||||
|
for tk, d, lots, xt, xp, pnl, ep in rows:
|
||||||
|
if ep and xp and lots and pnl is not None and abs(xp - ep) > 1e-9:
|
||||||
|
sgn = 1 if (d or "").upper().startswith("B") or d == "LONG" else -1
|
||||||
|
move = (xp - ep) * sgn
|
||||||
|
if abs(move) > 0.01:
|
||||||
|
cal.append(abs(pnl) / (abs(move) * lots))
|
||||||
|
cal.sort()
|
||||||
|
eur_per_point_lot = cal[len(cal)//2] if cal else 100.0
|
||||||
|
print(f"kalibriert: {eur_per_point_lot:.1f} EUR je 1.00 Kurs und Lot "
|
||||||
|
f"(Median aus {len(cal)} Trades)\n")
|
||||||
|
|
||||||
|
tot_missed = tot_saved = 0.0
|
||||||
|
n_missed = n_saved = 0
|
||||||
|
detail = []
|
||||||
|
|
||||||
|
for tk, d, lots, xt, xp, pnl, ep in rows:
|
||||||
|
long = (d == "LONG") or (d or "").upper().startswith("B")
|
||||||
|
sgn = 1 if long else -1
|
||||||
|
t0 = xt + off
|
||||||
|
t1 = t0 + WINDOW_MIN * 60
|
||||||
|
c.execute("SELECT MAX(h), MIN(l) FROM candles_m1 WHERE time > ? AND time <= ?",
|
||||||
|
(t0, t1))
|
||||||
|
hi, lo = c.fetchone()
|
||||||
|
if hi is None:
|
||||||
|
continue
|
||||||
|
# weiter IN Richtung (entgangen) / GEGEN Richtung (vermieden), in Kurspunkten
|
||||||
|
fav = (hi - xp) if long else (xp - lo)
|
||||||
|
adv = (xp - lo) if long else (hi - xp)
|
||||||
|
fav = max(0.0, fav)
|
||||||
|
adv = max(0.0, adv)
|
||||||
|
eur_fav = fav * lots * eur_per_point_lot
|
||||||
|
eur_adv = adv * lots * eur_per_point_lot
|
||||||
|
tot_missed += eur_fav
|
||||||
|
tot_saved += eur_adv
|
||||||
|
if eur_fav > 1:
|
||||||
|
n_missed += 1
|
||||||
|
if eur_adv > 1:
|
||||||
|
n_saved += 1
|
||||||
|
detail.append((xt, tk, pnl, eur_fav, eur_adv))
|
||||||
|
|
||||||
|
n = len(detail)
|
||||||
|
print(f"=== {n} S/R-Auto-Closes, Fenster {WINDOW_MIN} min danach ===\n")
|
||||||
|
realized = sum(x[2] for x in detail)
|
||||||
|
print(f"realisiert (tatsaechlich kassiert) : {realized:>10.2f} EUR")
|
||||||
|
print(f"entgangen (Kurs lief noch weiter) : {tot_missed:>10.2f} EUR "
|
||||||
|
f"(in {n_missed}/{n} Faellen)")
|
||||||
|
print(f"vermieden (Kurs drehte zurueck) : {tot_saved:>10.2f} EUR "
|
||||||
|
f"(in {n_saved}/{n} Faellen)")
|
||||||
|
print()
|
||||||
|
print(f"Ø entgangen je Trade : {tot_missed/n:>8.2f} EUR")
|
||||||
|
print(f"Ø vermieden je Trade : {tot_saved/n:>8.2f} EUR")
|
||||||
|
print(f"Netto (vermieden - entgangen): {(tot_saved-tot_missed)/n:+8.2f} EUR/Trade "
|
||||||
|
f"= {tot_saved-tot_missed:+.2f} EUR gesamt")
|
||||||
|
print()
|
||||||
|
print("⚠ 'entgangen' ist eine OBERGRENZE fuer das Halten (perfekter Ausstieg am")
|
||||||
|
print(" Hoch). 'vermieden' ist die Gegenseite: so weit waere der Trade")
|
||||||
|
print(" zurueckgefallen. Die Wahrheit liegt dazwischen — real haette das")
|
||||||
|
print(" Trailing einen Teil des Laufs mitgenommen und einen Teil abgegeben.")
|
||||||
|
|
||||||
|
# Verteilung: wie oft war der Close klar richtig / klar zu frueh?
|
||||||
|
clear_right = sum(1 for x in detail if x[4] > 2 * x[3])
|
||||||
|
clear_early = sum(1 for x in detail if x[3] > 2 * x[4])
|
||||||
|
print()
|
||||||
|
print(f"klar RICHTIG geschlossen (Rueckfall > 2x weiterer Lauf): {clear_right}/{n} "
|
||||||
|
f"= {100*clear_right/n:.0f}%")
|
||||||
|
print(f"klar ZU FRUEH (weiterer Lauf > 2x Rueckfall) : {clear_early}/{n} "
|
||||||
|
f"= {100*clear_early/n:.0f}%")
|
||||||
|
|
||||||
|
if "-v" in sys.argv:
|
||||||
|
print("\n--- Einzelfaelle ---")
|
||||||
|
for xt, tk, pnl, f, a in detail:
|
||||||
|
print(f"{datetime.fromtimestamp(xt):%d.%m %H:%M} #{tk} "
|
||||||
|
f"kassiert {pnl:+7.2f} | entgangen {f:6.2f} | vermieden {a:6.2f}")
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user