Auto-Flip-Close gebaut (User-Wunsch) - trotz erneut negativer Messung
User: "schliesse den trade automatisch beim close signal". Dreht die
Empfehlung gegen die offene Position und steht sie >=0,5xATR im Plus ->
close(reason="flip_close").
VORHER NEU GEMESSEN, weil das alte Urteil (backtest_flipclose.py,
16.07.) auf einem VIERTEN Exit-Modell fusste: Trail fest 1,5 (live jetzt
1,0), keine Lock-Phase, kein Time-Stop, kein Initial-TP, _MAXH 288 statt
200, Phasen ueber den Bar-Close statt ueber das High-Water.
backtest_flipclose2.py (80k M5, 2 Halbjahre, Echtkosten, Exit aus
core/exit_model.py) bestaetigt das alte Urteil - deutlicher:
Basis ohne Flip H1 SigmaR -537 · H2 -140
Flip ab 0,0xATR Delta H1 -126 · H2 -221
Flip ab 0,3xATR Delta H1 -77 · H2 -216
Flip ab 0,5xATR Delta H1 -92 · H2 -160 <- mildeste, Default
Flip auch im Minus Delta H1 -205 · H2 -364
JEDE Variante ist in BEIDEN Haelften schlechter. Trefferquote steigt
39 -> 45 %, Ertrag faellt = Gewinner-Kappen (die nachlaufende EMA dreht
oft mitten im Pullback).
Gebaut wurde die mildeste Variante (0,5xATR Mindestgewinn), abschaltbar
ueber [trading] auto_flip_close.
Fuer die Neumessung bekam exit_model.simulate() einen stop_when-Hook
(+ ret_bar), damit die Phasen-Mechanik nicht zum fuenften Mal kopiert
werden musste. Rueckwaertskompatibilitaet verifiziert: ohne Hook bitgenau
identisch (die zunaechst gemeldeten Abweichungen kamen allein aus
LIVE.mult 1,5 -> 1,0, gegengeprueft mit mult=1.5 -> identisch).
Mit 10 synthetischen Szenarien getestet: beide Richtungen, aus, unter
Schwelle, gleichgerichtet, WARTEN, im Minus, flat, Startup-Schonfrist,
Ticket-Dedup - alle korrekt.
B4/B5-Pflicht: Live-Ertrag gegen die Erwartung halten, bei Drift
auto_flip_close=false.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f1cc3775f9
commit
fea91fddb3
@@ -574,6 +574,35 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
P(break)-Modell (hat „mit-Trend" als Merkmal) macht das schon richtig; es mit dem
|
P(break)-Modell (hat „mit-Trend" als Merkmal) macht das schon richtig; es mit dem
|
||||||
groben EMA-Signal zu übersteuern kostet ~⅔ des S/R-Close-Edges. **12. verworfener
|
groben EMA-Signal zu übersteuern kostet ~⅔ des S/R-Close-Edges. **12. verworfener
|
||||||
Signal-Eingriff.**
|
Signal-Eingriff.**
|
||||||
|
- **⚠⚠ AUTO-FLIP-CLOSE = TROTZ NEGATIVER MESSUNG GEBAUT (User-Vorgabe 2026-07-31
|
||||||
|
„schließe den Trade automatisch beim Close-Signal"), `[trading] auto_flip_close=true`,
|
||||||
|
`auto_flip_close_min_r=0.5`.** Dreht die Empfehlung GEGEN die offene Position und
|
||||||
|
steht sie ≥0,5×ATR im Plus → `trader.close(reason="flip_close")`.
|
||||||
|
**NEU GEMESSEN mit dem ECHTEN Exit (`backtest_flipclose2.py`, 80k M5, 2 Halbjahre,
|
||||||
|
Echtkosten, Exit aus `core/exit_model.py`) — bestätigt das alte Urteil, deutlicher:**
|
||||||
|
| Variante | Δ H1 | Δ H2 |
|
||||||
|
|---|---|---|
|
||||||
|
| Flip ab 0,0×ATR | −126 | −221 |
|
||||||
|
| Flip ab 0,3×ATR | −77 | −216 |
|
||||||
|
| **Flip ab 0,5×ATR** | **−92** | **−160** ← mildeste, deshalb Default |
|
||||||
|
| Flip auch im Minus | −205 | −364 |
|
||||||
|
Basis ohne Flip: H1 ΣR −537 · H2 −140. **JEDE Variante ist in BEIDEN Hälften
|
||||||
|
schlechter.** Mechanik: Trefferquote steigt 39 → 45 %, Ertrag fällt =
|
||||||
|
**Gewinner-Kappen** (die nachlaufende EMA dreht oft mitten im Pullback).
|
||||||
|
⚠ **Warum überhaupt neu gemessen:** das alte `backtest_flipclose.py` nutzte ein
|
||||||
|
**VIERTES Exit-Modell** (Trail fest 1,5 · KEINE Lock-Phase · KEIN Time-Stop ·
|
||||||
|
KEIN Initial-TP · `_MAXH` 288 · Phasen über den Bar-Close statt über das
|
||||||
|
High-Water) — auf dieser Grundlage wäre das Urteil für den Live-Betrieb nicht
|
||||||
|
bindend gewesen. Für die Neumessung bekam `exit_model.simulate()` einen
|
||||||
|
**`stop_when`-Hook** (+ `ret_bar`), damit die Phasen-Mechanik NICHT wieder kopiert
|
||||||
|
werden musste; Rückwärtskompatibilität verifiziert (ohne Hook bitgenau identisch).
|
||||||
|
Umsetzung `engine._check_flip_close` im `_pos_loop`: nur im PLUS, 1× je Ticket,
|
||||||
|
Startup-Schonfrist beachtet, Log 🔄 + Telegram, Snapshot `flip_close`/
|
||||||
|
`flip_close_min_r`/`flip_close_count`. Mit **10 synthetischen Szenarien** getestet
|
||||||
|
(beide Richtungen · aus · unter Schwelle · gleichgerichtet · WARTEN · im Minus ·
|
||||||
|
flat · Schonfrist · Ticket-Dedup). Der Alarm (Blinken/Ton/Telegram) bleibt
|
||||||
|
unabhängig davon. **B4/B5-Pflicht: Live-Ertrag gegen die Erwartung halten —
|
||||||
|
driftet es weg, `auto_flip_close=false`.**
|
||||||
- **Auto-Flip-Close = VERWORFEN (gemessen `backtest_flipclose.py`, 2 Halbjahre,
|
- **Auto-Flip-Close = VERWORFEN (gemessen `backtest_flipclose.py`, 2 Halbjahre,
|
||||||
sequentielle 1-Positions-Sim, 2026-07-16):** User-Taktik-Kern „Position im Plus
|
sequentielle 1-Positions-Sim, 2026-07-16):** User-Taktik-Kern „Position im Plus
|
||||||
automatisch schließen, wenn die Empfehlung dreht" (heute nur Alarm). KEINE Variante
|
automatisch schließen, wenn die Empfehlung dreht" (heute nur Alarm). KEINE Variante
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Auto-Flip-Close NEU gemessen — mit dem ECHTEN Exit (User-Wunsch 2026-07-31).
|
||||||
|
|
||||||
|
FRAGE: „Schließe den Trade automatisch beim Close-Signal" — also: dreht die
|
||||||
|
Empfehlung gegen die offene Position, sofort raus (heute nur Alarm + Ton).
|
||||||
|
|
||||||
|
⚠ DAS WURDE SCHON EINMAL VERWORFEN (`backtest_flipclose.py`, 2026-07-16): keine
|
||||||
|
Variante hielt in beiden Hälften, H2 kostete 17–32 % des Ertrags („Gewinner-Kappen",
|
||||||
|
weil die nachlaufende EMA oft mitten im Pullback dreht).
|
||||||
|
|
||||||
|
WARUM TROTZDEM NEU: Beim Stufe-3-Refactor (2026-07-31) kam heraus, dass das alte
|
||||||
|
Skript ein **viertes, eigenes Exit-Modell** benutzt, das dem Live-System in mindestens
|
||||||
|
fünf Punkten widerspricht:
|
||||||
|
Trail hart 1,5 (live seit heute **1,0**)
|
||||||
|
KEINE Lock-Phase (live: ab 3,5×ATR enger)
|
||||||
|
KEIN Time-Stop (live: 120 min in Phase Init)
|
||||||
|
KEIN Initial-TP (live: 3,5×ATR)
|
||||||
|
_MAXH 288 (live/kanonisch 200)
|
||||||
|
+ Phasen-Entscheidung über den BAR-CLOSE statt über das High-Water.
|
||||||
|
Ein Urteil auf dieser Grundlage ist für den Live-Betrieb nicht bindend.
|
||||||
|
|
||||||
|
HIER: derselbe sequentielle 1-Positions-Aufbau, aber der Exit kommt aus
|
||||||
|
`core/exit_model.simulate()` — inklusive des neuen `stop_when`-Hooks für den Flip,
|
||||||
|
damit die Phasen-Mechanik NICHT noch einmal kopiert wird (genau diese Kopiererei
|
||||||
|
hat die vier divergierenden Modelle erzeugt). Echtkosten, 2 Halbjahre.
|
||||||
|
|
||||||
|
VARIANTEN: Flip erst ab minR Gewinn (0 / 0,3 / 0,5×ATR) sowie „auch im Minus".
|
||||||
|
Urteil „bauen" nur, wenn eine Variante in BEIDEN Hälften ≥ Basis liegt.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
|
from core.analysis import calc_trend_angle
|
||||||
|
from core.exit_model import LIVE, simulate
|
||||||
|
from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD,
|
||||||
|
_REVERSAL_STRETCH, _STRETCH_MAX)
|
||||||
|
|
||||||
|
_ATRMIN = 0.12
|
||||||
|
|
||||||
|
|
||||||
|
def _ema_series(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 _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 signal_series(H, L, C, EF, ES, AT):
|
||||||
|
"""Richtung je Bar (+1/−1/0) — dieselbe Regel wie im Live-Signal."""
|
||||||
|
TH = _REVERSAL_STRETCH
|
||||||
|
sig = [0] * len(C)
|
||||||
|
for i in range(_N_BARS, len(C)):
|
||||||
|
atr = AT[i]
|
||||||
|
if not atr or atr <= 0:
|
||||||
|
continue
|
||||||
|
atr = max(atr, _ATRMIN); es = ES[i]; ef = EF[i]
|
||||||
|
stretch = (C[i] - es) / atr
|
||||||
|
ang = calc_trend_angle(C[i - _ANGLE_LR - 2:i], _ANGLE_LR); ad = ang - 90.0
|
||||||
|
if stretch <= -TH and ad >= _ANGLE_DEAD: sig[i] = 1
|
||||||
|
elif stretch >= TH and ad <= -_ANGLE_DEAD: sig[i] = -1
|
||||||
|
elif abs(stretch) < _STRETCH_MAX: sig[i] = 1 if ef > es else -1 if ef < es else 0
|
||||||
|
return sig
|
||||||
|
|
||||||
|
|
||||||
|
def run(sig, H, L, C, AT, SP, lo, hi, flip=False, min_r=0.0, flip_neg=False):
|
||||||
|
"""Sequentiell: eine Position, Entry bei Signal, Exit aus dem GEMEINSAMEN Kern."""
|
||||||
|
Rs = []
|
||||||
|
i = lo
|
||||||
|
while i < hi:
|
||||||
|
d = sig[i]
|
||||||
|
if d == 0:
|
||||||
|
i += 1; continue
|
||||||
|
atr = max(AT[i] or 0, _ATRMIN)
|
||||||
|
cost = (SP[i] if SP[i] > 0 else 0.0225) / atr
|
||||||
|
hook = None
|
||||||
|
if flip:
|
||||||
|
def hook(j, prof_atr, _d=d):
|
||||||
|
if sig[j] != -_d:
|
||||||
|
return False
|
||||||
|
return flip_neg or prof_atr > min_r
|
||||||
|
R, xb = simulate(C[i], d, atr, H, L, C, i + 1, LIVE,
|
||||||
|
stop_when=hook, ret_bar=True)
|
||||||
|
Rs.append(R - cost)
|
||||||
|
i = xb + 1
|
||||||
|
return Rs
|
||||||
|
|
||||||
|
|
||||||
|
def st(Rs):
|
||||||
|
if not Rs:
|
||||||
|
return " —"
|
||||||
|
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
|
||||||
|
g = sum(x for x in Rs if x > 0); l = -sum(x for x in Rs if x < 0)
|
||||||
|
return (f"n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} PF={(g/l if l > 0 else 9.99):>4.2f} "
|
||||||
|
f"ΣR={s:>+6.0f} Worst={min(Rs):+.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
|
||||||
|
mt5.initialize(); sym = None
|
||||||
|
for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
|
||||||
|
if mt5.symbol_info(c):
|
||||||
|
sym = c; break
|
||||||
|
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
|
||||||
|
si = mt5.symbol_info(sym); point = si.point
|
||||||
|
mt5.shutdown()
|
||||||
|
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
|
||||||
|
C = [float(b["close"]) for b in bars]; SP = [float(b["spread"]) * point for b in bars]
|
||||||
|
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW); AT = _atr_series(H, L, C)
|
||||||
|
sig = signal_series(H, L, C, EF, ES, AT)
|
||||||
|
mid = len(C) // 2
|
||||||
|
halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C) - LIVE.max_hold - 1)]
|
||||||
|
|
||||||
|
print("=" * 96)
|
||||||
|
print(f" AUTO-FLIP-CLOSE — {sym} M5 ({len(C)} Bars), Echtkosten")
|
||||||
|
print(f" Exit aus core/exit_model.py: SL {LIVE.sl_atr} · Trail {LIVE.mult} · "
|
||||||
|
f"BE {LIVE.be} · Lock {LIVE.lock_start} · Time-Stop {LIVE.timestop_bars}")
|
||||||
|
print(" (das alte Urteil von 2026-07-16 fusste auf einem abweichenden Exit)")
|
||||||
|
print("=" * 96)
|
||||||
|
|
||||||
|
variants = [("Basis — kein Flip-Close", dict(flip=False)),
|
||||||
|
("Flip ab 0,0×ATR Gewinn", dict(flip=True, min_r=0.0)),
|
||||||
|
("Flip ab 0,3×ATR Gewinn", dict(flip=True, min_r=0.3)),
|
||||||
|
("Flip ab 0,5×ATR Gewinn", dict(flip=True, min_r=0.5)),
|
||||||
|
("Flip AUCH im Minus", dict(flip=True, flip_neg=True))]
|
||||||
|
base = {}
|
||||||
|
for name, kw in variants:
|
||||||
|
print(f"\n{name}")
|
||||||
|
for lbl, a, b in halves:
|
||||||
|
Rs = run(sig, H, L, C, AT, SP, a, b, **kw)
|
||||||
|
s = sum(Rs)
|
||||||
|
if not kw.get("flip"):
|
||||||
|
base[lbl] = s
|
||||||
|
delta = f" Δ vs Basis {s - base.get(lbl, 0):+.0f}" if kw.get("flip") else ""
|
||||||
|
print(f" {lbl} {st(Rs)}{delta}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -318,6 +318,17 @@ class TradingEngine:
|
|||||||
self._open_pos_dir = None # Richtung der aktuell offenen Position (LONG/SHORT)
|
self._open_pos_dir = None # Richtung der aktuell offenen Position (LONG/SHORT)
|
||||||
self._pos_close_ts = 0.0 # Zeit + Richtung des letzten Positions-Close (JEDE
|
self._pos_close_ts = 0.0 # Zeit + Richtung des letzten Positions-Close (JEDE
|
||||||
self._news_block_logged = None # Dedup für die News-Blackout-Logzeile
|
self._news_block_logged = None # Dedup für die News-Blackout-Logzeile
|
||||||
|
# Auto-Flip-Close (User-Wunsch 2026-07-31) — ⚠ GEMESSEN NEGATIV in beiden
|
||||||
|
# Hälften (`backtest_flipclose2.py`), s. `_check_flip_close`. Default-Schwelle
|
||||||
|
# 0,5×ATR = die mildeste der gemessenen Varianten.
|
||||||
|
_tf_cfg = self.cfg["trading"]
|
||||||
|
self._flip_close = _tf_cfg.get("auto_flip_close", "false").lower() == "true"
|
||||||
|
try:
|
||||||
|
self._flip_close_min_r = float(_tf_cfg.get("auto_flip_close_min_r", "0.5"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self._flip_close_min_r = 0.5
|
||||||
|
self._flip_close_done_ticket = None
|
||||||
|
self._flip_close_count = 0
|
||||||
self._pos_close_dir = None # Art) → Squeeze-Wiedereinstiegs-Sperre (gegen die
|
self._pos_close_dir = None # Art) → Squeeze-Wiedereinstiegs-Sperre (gegen die
|
||||||
# Open→Instant-Close-Kaskade bei zu engem Notfall-Stop)
|
# Open→Instant-Close-Kaskade bei zu engem Notfall-Stop)
|
||||||
self._sr_close_min_gain_armed_ticket = None # Ticket, für das die %-Schwelle schon
|
self._sr_close_min_gain_armed_ticket = None # Ticket, für das die %-Schwelle schon
|
||||||
@@ -605,6 +616,7 @@ class TradingEngine:
|
|||||||
self._check_circuit_breaker() # Tagesverlust-Stopp (Überleben zuerst)
|
self._check_circuit_breaker() # Tagesverlust-Stopp (Überleben zuerst)
|
||||||
self._check_auto_close()
|
self._check_auto_close()
|
||||||
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
|
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
|
||||||
|
self._check_flip_close() # Empfehlung dreht → schließen
|
||||||
self._check_sr_close()
|
self._check_sr_close()
|
||||||
self._check_auto_squeeze() # autonomer Entry auf Squeeze-Ausbruch
|
self._check_auto_squeeze() # autonomer Entry auf Squeeze-Ausbruch
|
||||||
self._check_auto_signal() # autonomer Entry auf die Empfehlung
|
self._check_auto_signal() # autonomer Entry auf die Empfehlung
|
||||||
@@ -1420,6 +1432,85 @@ class TradingEngine:
|
|||||||
"news_score": None,
|
"news_score": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _check_flip_close(self):
|
||||||
|
"""Auto-Flip-Close: dreht die Empfehlung GEGEN die offene Position und steht
|
||||||
|
sie mindestens `min_r`×ATR im Plus → schließen. (`[trading] auto_flip_close`)
|
||||||
|
|
||||||
|
⚠⚠ **GEMESSEN NEGATIV — auf ausdrücklichen User-Wunsch trotzdem gebaut**
|
||||||
|
(2026-07-31 „schließe den Trade automatisch beim Close-Signal").
|
||||||
|
`backtest_flipclose2.py` (80k M5, 2 Halbjahre, Echtkosten, Exit aus
|
||||||
|
`core/exit_model.py`) — JEDE Variante ist in BEIDEN Hälften schlechter als
|
||||||
|
gar kein Flip-Close:
|
||||||
|
Basis (kein Flip) H1 ΣR −537 · H2 −140
|
||||||
|
Flip ab 0,0×ATR Δ H1 −126 · H2 −221
|
||||||
|
Flip ab 0,3×ATR Δ H1 −77 · H2 −216
|
||||||
|
Flip ab 0,5×ATR Δ H1 −92 · H2 −160 ← die MILDESTE, deshalb Default
|
||||||
|
Flip auch im Minus Δ H1 −205 · H2 −364
|
||||||
|
Mechanik: die Trefferquote steigt (39 → 45 %), der Ertrag fällt —
|
||||||
|
**Gewinner-Kappen**, weil die nachlaufende EMA oft mitten im Pullback dreht
|
||||||
|
und der Trade weitergelaufen wäre.
|
||||||
|
⚠ Das bestätigt das ältere Urteil (`backtest_flipclose.py`, 2026-07-16). Die
|
||||||
|
Neumessung war trotzdem nötig: das alte Skript nutzte ein VIERTES Exit-Modell
|
||||||
|
(Trail fest 1,5 · keine Lock-Phase · kein Time-Stop · kein Initial-TP ·
|
||||||
|
`_MAXH` 288 · Phasen über den Bar-Close statt über das High-Water) — auf
|
||||||
|
dieser Grundlage wäre das Urteil für den Live-Betrieb nicht bindend gewesen.
|
||||||
|
**B4/B5-Pflicht:** Live-Ertrag gegen die Erwartung halten; driftet es weg →
|
||||||
|
`auto_flip_close=false`. Der Alarm (Blinken/Ton/Telegram) bleibt unabhängig
|
||||||
|
davon bestehen.
|
||||||
|
"""
|
||||||
|
if not self._flip_close or self._in_startup_grace():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
ps = self.trader.snapshot()
|
||||||
|
ticket = ps.get("ticket")
|
||||||
|
if not ticket:
|
||||||
|
self._flip_close_done_ticket = None
|
||||||
|
return
|
||||||
|
if ticket == self._flip_close_done_ticket:
|
||||||
|
return # für dieses Ticket schon ausgelöst
|
||||||
|
pnl = float(ps.get("pnl") or 0.0)
|
||||||
|
if pnl <= 0:
|
||||||
|
return # nie im Minus (gemessen die schlechteste Variante)
|
||||||
|
sig = (self.wave.signal() or {}).get("signal")
|
||||||
|
if sig not in ("LONG", "SHORT"):
|
||||||
|
return
|
||||||
|
is_long = ps.get("order_type") == mt5.ORDER_TYPE_BUY
|
||||||
|
if (is_long and sig != "SHORT") or (not is_long and sig != "LONG"):
|
||||||
|
return # kein Gegen-Signal
|
||||||
|
# Mindestgewinn in ×ATR (M5, wie im Backtest)
|
||||||
|
atr = float(((self.wave.snapshot() or {}).get("pb_feats") or {}).get("atr") or 0.0)
|
||||||
|
entry = float(ps.get("entry_price") or 0.0)
|
||||||
|
bid = float((self.data.snapshot() or {}).get("bid") or 0.0)
|
||||||
|
if atr > 0 and entry and bid:
|
||||||
|
d = 1 if is_long else -1
|
||||||
|
prof_atr = (bid - entry) * d / max(atr, 0.06)
|
||||||
|
if prof_atr < self._flip_close_min_r:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return # ohne ATR keine Schwelle → nicht schließen
|
||||||
|
self._flip_close_done_ticket = ticket
|
||||||
|
log.info(f"🔄 AUTO-FLIP-CLOSE: Empfehlung dreht auf {sig} — "
|
||||||
|
f"{'LONG' if is_long else 'SHORT'} bei P&L {pnl:+.2f} € "
|
||||||
|
f"({prof_atr:.2f}×ATR) geschlossen")
|
||||||
|
err = self.trader.close(reason="flip_close")
|
||||||
|
if err:
|
||||||
|
log.error(f"Auto-Flip-Close fehlgeschlagen: {err}")
|
||||||
|
self._flip_close_done_ticket = None # nächster Tick erneut
|
||||||
|
return
|
||||||
|
self._flip_close_count += 1
|
||||||
|
tg = self.cfg["telegram"]
|
||||||
|
if tg.get("enabled", "false").lower() == "true" and tg.get("bot_token"):
|
||||||
|
try:
|
||||||
|
send_telegram(
|
||||||
|
f"🔄 <b>AUTO-FLIP-CLOSE</b>\n{self.data.symbol or ''} "
|
||||||
|
f"{'LONG' if is_long else 'SHORT'} geschlossen — Empfehlung "
|
||||||
|
f"dreht auf {sig}.\nGewinn {pnl:+.2f} € ({prof_atr:.2f}×ATR)",
|
||||||
|
tg["bot_token"], tg["chat_id"])
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Flip-Close-Telegram: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"_check_flip_close: {e}", exc_info=True)
|
||||||
|
|
||||||
def _check_close_alert(self, rec: dict):
|
def _check_close_alert(self, rec: dict):
|
||||||
"""Telegram-CLOSE-Alarm NUR bei Signal-Flip gegen die Position (echter
|
"""Telegram-CLOSE-Alarm NUR bei Signal-Flip gegen die Position (echter
|
||||||
Wende-Grund). Max. 1×/5 min (`_CLOSE_ALERT_COOLDOWN_S`) — sonst spammt es.
|
Wende-Grund). Max. 1×/5 min (`_CLOSE_ALERT_COOLDOWN_S`) — sonst spammt es.
|
||||||
@@ -2871,6 +2962,11 @@ class TradingEngine:
|
|||||||
"signal_entry_count": self._signal_entry_count,
|
"signal_entry_count": self._signal_entry_count,
|
||||||
"adverse15_atr": self._adverse15_atr,
|
"adverse15_atr": self._adverse15_atr,
|
||||||
"adverse15_count": self._adverse15_count,
|
"adverse15_count": self._adverse15_count,
|
||||||
|
# Auto-Flip-Close (gemessen negativ, auf User-Wunsch gebaut) — für den
|
||||||
|
# B4-Monitor sichtbar machen, wie oft er zuschlägt.
|
||||||
|
"flip_close": self._flip_close,
|
||||||
|
"flip_close_min_r": self._flip_close_min_r,
|
||||||
|
"flip_close_count": self._flip_close_count,
|
||||||
"last_trades": last_trades,
|
"last_trades": last_trades,
|
||||||
# Wirtschaftskalender: {blackout, next, enabled, n} — Anzeige + Grund,
|
# Wirtschaftskalender: {blackout, next, enabled, n} — Anzeige + Grund,
|
||||||
# warum der Bot gerade keine autonome Order eröffnet.
|
# warum der Bot gerade keine autonome Order eröffnet.
|
||||||
|
|||||||
+23
-5
@@ -88,7 +88,9 @@ def simulate(entry: float, d: int, atr: float,
|
|||||||
H: list, L: list, C: list, j0: int,
|
H: list, L: list, C: list, j0: int,
|
||||||
p: ExitParams = LIVE,
|
p: ExitParams = LIVE,
|
||||||
timestop: bool = True,
|
timestop: bool = True,
|
||||||
use_tp: bool = True) -> float:
|
use_tp: bool = True,
|
||||||
|
stop_when=None,
|
||||||
|
ret_bar: bool = False):
|
||||||
"""Kanonische Exit-Simulation → Ergebnis in R (×ATR), Kosten NICHT enthalten.
|
"""Kanonische Exit-Simulation → Ergebnis in R (×ATR), Kosten NICHT enthalten.
|
||||||
|
|
||||||
`d` = +1 LONG / −1 SHORT · `j0` = erster Bar NACH dem Einstieg.
|
`d` = +1 LONG / −1 SHORT · `j0` = erster Bar NACH dem Einstieg.
|
||||||
@@ -105,6 +107,14 @@ def simulate(entry: float, d: int, atr: float,
|
|||||||
(`timestop=True, use_tp=True`) ist also die einzige live-treue Variante. Die
|
(`timestop=True, use_tp=True`) ist also die einzige live-treue Variante. Die
|
||||||
Flags erlauben es, ein bestehendes Skript BITGENAU zu migrieren und dabei
|
Flags erlauben es, ein bestehendes Skript BITGENAU zu migrieren und dabei
|
||||||
sichtbar zu machen, welchen Ausschnitt es misst, statt ihn zu verstecken.
|
sichtbar zu machen, welchen Ausschnitt es misst, statt ihn zu verstecken.
|
||||||
|
|
||||||
|
`stop_when(j, profit_atr)` — optionaler Rueckruf fuer einen ZUSAETZLICHEN Exit
|
||||||
|
(z. B. Flip-Close: „Signal dreht"). Gibt er True zurueck, wird zum Close von Bar
|
||||||
|
`j` geschlossen. Ohne ihn ist das Ergebnis bitgenau wie zuvor. Damit laesst sich
|
||||||
|
eine Exit-Idee messen, OHNE die Phasen-Mechanik zu kopieren — genau diese
|
||||||
|
Kopiererei hat im Projekt VIER divergierende Exit-Modelle erzeugt.
|
||||||
|
`ret_bar=True` liefert `(R, exit_bar)` statt nur `R` (fuer sequentielle Sims,
|
||||||
|
die nach dem Exit weiterlaufen muessen).
|
||||||
"""
|
"""
|
||||||
if atr <= 0:
|
if atr <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
@@ -113,21 +123,29 @@ def simulate(entry: float, d: int, atr: float,
|
|||||||
hw = entry
|
hw = entry
|
||||||
rank = 0
|
rank = 0
|
||||||
end = min(j0 + p.max_hold, len(C) - 1)
|
end = min(j0 + p.max_hold, len(C) - 1)
|
||||||
|
def _out(px, bar):
|
||||||
|
r = (px - entry) * d / atr
|
||||||
|
return (r, bar) if ret_bar else r
|
||||||
|
|
||||||
for j in range(j0, end + 1):
|
for j in range(j0, end + 1):
|
||||||
hi, lo = H[j], L[j]
|
hi, lo = H[j], L[j]
|
||||||
if (lo <= sl) if d > 0 else (hi >= sl):
|
if (lo <= sl) if d > 0 else (hi >= sl):
|
||||||
return (sl - entry) * d / atr
|
return _out(sl, j)
|
||||||
if tp is not None and ((hi >= tp) if d > 0 else (lo <= tp)):
|
if tp is not None and ((hi >= tp) if d > 0 else (lo <= tp)):
|
||||||
return (tp - entry) * d / atr
|
return _out(tp, j)
|
||||||
hw = max(hw, hi) if d > 0 else min(hw, lo)
|
hw = max(hw, hi) if d > 0 else min(hw, lo)
|
||||||
profit = (hw - entry) * d
|
profit = (hw - entry) * d
|
||||||
|
# Zusaetzlicher Exit (Flip-Close o. AE.) — NACH SL/TP, denn die haben im
|
||||||
|
# selben Bar Vorrang (pessimistisch), und VOR der Trail-Nachfuehrung.
|
||||||
|
if stop_when is not None and stop_when(j, (C[j] - entry) * d / atr):
|
||||||
|
return _out(C[j], j)
|
||||||
ph = 0 if profit < p.trail_start * atr else (
|
ph = 0 if profit < p.trail_start * atr else (
|
||||||
1 if profit < p.lock_start * atr else 2)
|
1 if profit < p.lock_start * atr else 2)
|
||||||
if ph < rank:
|
if ph < rank:
|
||||||
ph = rank
|
ph = rank
|
||||||
rank = ph
|
rank = ph
|
||||||
if timestop and rank == 0 and (j - j0) >= p.timestop_bars:
|
if timestop and rank == 0 and (j - j0) >= p.timestop_bars:
|
||||||
return (C[j] - entry) * d / atr
|
return _out(C[j], j)
|
||||||
if ph == 0:
|
if ph == 0:
|
||||||
continue
|
continue
|
||||||
m = p.mult if ph == 1 else max(p.lock_min, p.mult * p.lock_scale)
|
m = p.mult if ph == 1 else max(p.lock_min, p.mult * p.lock_scale)
|
||||||
@@ -135,4 +153,4 @@ def simulate(entry: float, d: int, atr: float,
|
|||||||
if profit >= p.be * atr or ph == 2:
|
if profit >= p.be * atr or ph == 2:
|
||||||
cand = max(cand, entry) if d > 0 else min(cand, entry)
|
cand = max(cand, entry) if d > 0 else min(cand, entry)
|
||||||
sl = max(sl, cand) if d > 0 else min(sl, cand)
|
sl = max(sl, cand) if d > 0 else min(sl, cand)
|
||||||
return (C[end] - entry) * d / atr
|
return _out(C[end], end)
|
||||||
|
|||||||
Reference in New Issue
Block a user