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:
Axel Hocks
2026-07-31 17:55:06 +02:00
co-authored by Claude Opus 5
parent f1cc3775f9
commit fea91fddb3
4 changed files with 301 additions and 5 deletions
+29
View File
@@ -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
+153
View File
@@ -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 1732 % 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()
+96
View File
@@ -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
View File
@@ -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)