Pending-Pfad fuer SIG: ruhende Stop-Order am Bestaetigungs-Level

SIG laeuft damit auf der gemessen positiven Variante (OeR +0,35/+0,32, PF 2,2)
statt auf der Market-Order (-0,09/-0,11).

Der heikelste Punkt war nicht die Order, sondern die Kollision beider Pfade:
trader.pending_orders() liefert ALLE magic-gleichen Orders, zwei getrennte
Manager haetten sich gegenseitig storniert - sporadisch und im Log kaum
erkennbar. Deshalb EIN gemeinsamer Ziel-Zustand (engine._pending_ziel), der
Manager gleicht nur noch ab. Vorrang Squeeze > Signal (unabhaengig validiert,
selten; das Signal feuert dauernd, es gibt einen Positions-Slot).

Zwei additive Ergaenzungen in wave_rec._confirm_breakout: das
Bestaetigungs-Level wird auch NACH der Bestaetigung veroeffentlicht (die
Nachjagd-Bremse braucht es genau dann; pending bleibt False, Frontend
unberuehrt), und conf = die Konfidenz VOR dem Stummschalten - sonst muesste
die ruhende Order blind platziert werden.

Nachjagd-Bremse jetzt auch fuer SIG (0,20 xATR, Haertetest-gedeckt).
12 Szenarien getestet, live am Snapshot verifiziert. Snapshot-Felder
squeeze_pending_levels -> pending_levels + pending_quelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-05 11:43:04 +02:00
co-authored by Claude Opus 5
parent 12fb11fa44
commit 90d77a2305
3 changed files with 168 additions and 42 deletions
+35 -4
View File
@@ -1401,10 +1401,41 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
also im gemessen negativen Fall. Wie oft das passiert, ist ungemessen. also im gemessen negativen Fall. Wie oft das passiert, ist ungemessen.
(4) `_pend` wird aktuell **nicht im Snapshot veröffentlicht** — das wäre die erste (4) `_pend` wird aktuell **nicht im Snapshot veröffentlicht** — das wäre die erste
Bauarbeit. Bauarbeit.
⚠⚠ **KONSEQUENZ FÜR DEN LAUFENDEN BETRIEB:** SIG steht seit dem 05.08. 08:13 auf ✅✅ **PENDING-PFAD FÜR SIG GEBAUT & LIVE (2026-08-05, User-Vorgabe),
AN (User-Toggle) und läuft dabei über die **Market-Order — also Variante A, die in `[trading] signal_pending_entry=true`.** SIG läuft damit auf Variante B statt A.
beiden Halbjahren negativ ist.** Solange der Pending-Pfad nicht gebaut ist, ist der ⚠⚠ **Der heikelste Punkt war NICHT die Order, sondern die Kollision der beiden
Schalter die gemessen schlechte Variante. Pfade.** `trader.pending_orders()` liefert ALLE magic-gleichen Orders — zwei
getrennte Manager (Squeeze/Signal) hätten sich **gegenseitig die Orders
storniert**, sporadisch und im Log kaum erkennbar. Deshalb **EIN gemeinsamer
Ziel-Zustand**: `engine._pending_ziel()` liefert `{order_type: preis}` + Quelle,
`_manage_squeeze_pending` gleicht nur noch ab. **VORRANG: Squeeze > Signal** — der
Squeeze ist unabhängig validiert und selten (~23×/Woche), das Signal feuert
dauernd; bei EINEM Positions-Slot ist die Reihenfolge damit eindeutig.
⚠ **Zwei additive Ergänzungen in `wave_rec._confirm_breakout` waren nötig:**
(1) Das Bestätigungs-Level wird jetzt **auch nach der Bestätigung** veröffentlicht
(`snap["breakout"]["level"]`, `confirmed=True`) — die Nachjagd-Bremse im
Market-Fallback braucht es genau dann. `pending` bleibt False, das Frontend rendert
nur bei `pending` und ist unberührt. (2) **`conf`** = die Konfidenz VOR dem
Stummschalten: der Rückgabewert setzt `conf_pct` auf 0 (das Signal gilt ja noch
nicht), die ruhende Order müsste sonst **blind** platziert werden. Damit wird die
Mindest-Konfidenz jeden Tick geprüft und die Order storniert, sobald sie fällt —
strenger als der Live-Check von vorher (der prüfte nur im Moment des Entries).
**Nachjagd-Bremse auch für SIG** (`squeeze_max_chase_atr`, 0,20): greift die
Pending nicht, wird nur noch bis 0,20×ATR über dem Bestätigungs-Level nachgekauft —
der Härtetest zeigt dort +0,148/+0,090, ab ~0,250,30 kippt es.
**Der wandernde Anker ist die operative Hauptlast:** `_pend` verankert sich bei
jeder k×ATR-Gegenbewegung, nach Timeout und bei Richtungswechsel neu — die Order
wird dann nachgezogen (Toleranz 0,02×ATR gegen Order-Churn). **Mit 12 Szenarien
getestet** (Vorrang · reiner Signal-Fall · SHORT · Konfidenz zu niedrig · Wechsel
Signal→Squeeze mit Storno · wanderndes Level · Toleranz · Gate · Fill · Feature aus
· Bestätigung eingetreten · Schonfrist). Live verifiziert am Snapshot:
`signal_pending=true`, `breakout` liefert `level`/`conf`/`confirmed`;
`pending_levels` leer, weil eine Position offen war = korrektes Verhalten.
Snapshot-Felder umbenannt: `squeeze_pending_levels`**`pending_levels`** +
**`pending_quelle`**. Zurück: `signal_pending_entry=false`.
**B4-Pflicht:** dieselbe Erfolgskontrolle wie beim Squeeze — der Abstand
Einstieg↔Level muss gegen 0 gehen. AUTOSIG-Trades sind über `setup=AUTOSIG_*`
separierbar.
**Methodik-Fehler beim Bau, gefunden und behoben:** der erste Entwurf liess **Methodik-Fehler beim Bau, gefunden und behoben:** der erste Entwurf liess
`pend["level"]` nach der Bestätigung über weitere Bars stehen — bei verzögertem `pend["level"]` nach der Bestätigung über weitere Bars stehen — bei verzögertem
Einstieg (Slot belegt, Konfidenz zu niedrig) wäre er zu einem Preis eingestiegen, Einstieg (Slot belegt, Konfidenz zu niedrig) wäre er zu einem Preis eingestiegen,
+118 -37
View File
@@ -348,9 +348,16 @@ class TradingEngine:
"squeeze_max_chase_atr", "0.20")) "squeeze_max_chase_atr", "0.20"))
except (TypeError, ValueError): except (TypeError, ValueError):
self._squeeze_max_chase = 0.20 self._squeeze_max_chase = 0.20
self._squeeze_pending_levels = None # Signal-Pfad bekommt dieselbe Mechanik (2026-08-05, gemessen
# `backtest_auto_signal_v3.py`: am Bestätigungs-Level ØR +0,35/+0,32 statt
# 0,09/0,11 am Markt — SIG besteht damit ERSTMALS die 2-Stichproben-Regel).
self._signal_pending = self.cfg["trading"].get(
"signal_pending_entry", "true").lower() == "true"
self._pending_levels = None
self._pending_quelle = ""
self._squeeze_pending_err = None self._squeeze_pending_err = None
self._squeeze_chase_logged = None self._squeeze_chase_logged = None
self._signal_chase_logged = None
self._squeeze_reverse_sig = None # Dedup Reverse-Close (Gegen-Trade) self._squeeze_reverse_sig = None # Dedup Reverse-Close (Gegen-Trade)
self._squeeze_entry_count = 0 self._squeeze_entry_count = 0
# ── Autonomer Entry auf die EMPFEHLUNG (User-Wunsch 2026-07-30) ─────────── # ── Autonomer Entry auf die EMPFEHLUNG (User-Wunsch 2026-07-30) ───────────
@@ -1073,21 +1080,26 @@ class TradingEngine:
tg["bot_token"], tg["chat_id"]) tg["bot_token"], tg["chat_id"])
except Exception as e: log.warning(f"S/R-Close-Telegram: {e}") except Exception as e: log.warning(f"S/R-Close-Telegram: {e}")
def _squeeze_guard(self) -> str | None: def _auto_guard(self, pfad: str) -> str | None:
"""Die NICHT richtungsabhängigen Gates für autonome Squeeze-Entries. """Die NICHT richtungsabhängigen Gates für autonome Entries.
→ Grundtext, wenn geblockt, sonst None. `pfad` = "squeeze" | "signal". → Grundtext, wenn geblockt, sonst None.
⚠ Bewusst EINE Funktion für beide Einstiegswege (ruhende Stop-Order UND ⚠ Bewusst EINE Funktion für ALLE Einstiegswege (ruhende Stop-Order UND
Market-Fallback). Zwei Kopien derselben Gates wären genau die Divergenz, die Market-Fallback, Squeeze UND Signal). Kopien derselben Gates wären genau die
dieses Projekt siebenmal als Deployment-Drift dokumentiert hat — und bei einer Divergenz, die dieses Projekt siebenmal als Deployment-Drift dokumentiert hat
RUHENDEN Order wiegt sie schwerer: eine vergessene Pending-Order feuert im — und bei einer RUHENDEN Order wiegt sie schwerer: eine vergessene
gesperrten Fenster, ohne dass jemand hinschaut.""" Pending-Order feuert im gesperrten Fenster, ohne dass jemand hinschaut."""
if not self._auto_squeeze: if pfad == "squeeze":
return "Auto-Squeeze aus" if not self._auto_squeeze:
return "Auto-Squeeze aus"
skip = self._squeeze_skip_night
else:
if not self._auto_signal:
return "Auto-Signal aus"
skip = self._auto_signal_skip_night
if self._circuit_halted(): if self._circuit_halted():
return "Circuit-Breaker" return "Circuit-Breaker"
if (self._squeeze_skip_night if skip and datetime.now(_BERLIN).hour in self._squeeze_night_hours:
and datetime.now(_BERLIN).hour in self._squeeze_night_hours):
return "Nacht-Fenster" return "Nacht-Fenster"
_bl = self.calendar.blackout() _bl = self.calendar.blackout()
if _bl: if _bl:
@@ -1099,6 +1111,48 @@ class TradingEngine:
self._news_block_logged = None self._news_block_logged = None
return None return None
def _squeeze_guard(self) -> str | None:
"""Alias — `_check_auto_squeeze` ruft weiterhin diesen Namen."""
return self._auto_guard("squeeze")
def _pending_ziel(self) -> tuple[dict, str]:
"""Welche Stop-Orders SOLLEN liegen? → ({order_type: preis}, Quelle).
⚠⚠ EIN gemeinsamer Ziel-Zustand für Squeeze UND Signal — nicht zwei Manager.
Zwei getrennte Manager würden über `trader.pending_orders()` jeweils die
Orders des anderen sehen und **gegenseitig stornieren**; der Bug wäre
sporadisch und im Log kaum zu erkennen.
VORRANG: Squeeze schlägt Signal. Der Squeeze ist unabhängig validiert und
seltener (Kompression ~23×/Woche), das Signal feuert dauernd — bei einem
Positions-Slot ist die Reihenfolge damit eindeutig. Ist der Squeeze `armed`,
legt das Signal nichts.
"""
ws = self.wave.snapshot() or {}
atr = (ws.get("pb_feats") or {}).get("atr") or 0.0
if atr <= 0:
return {}, ""
# ── 1) Squeeze: beide Box-Grenzen ────────────────────────────────
sq = ws.get("squeeze") or {}
hi, lo = sq.get("box_hi"), sq.get("box_lo")
if (self._squeeze_pending and sq.get("state") == "armed" and hi and lo
and self._auto_guard("squeeze") is None):
return ({mt5.ORDER_TYPE_BUY_STOP: round(float(hi) + _SQ_K_ENG * atr, 3),
mt5.ORDER_TYPE_SELL_STOP: round(float(lo) - _SQ_K_ENG * atr, 3)},
"squeeze")
# ── 2) Signal: EINE Order am Bestätigungs-Level ──────────────────
# Gemessen `backtest_auto_signal_v3.py`: am Level ØR +0,35/+0,32 (PF 2,2)
# gegen 0,09/0,11 am Markt — dieselbe Mechanik, die den Squeeze rettet.
bo = ws.get("breakout") or {}
if (self._signal_pending and bo.get("pending") and bo.get("level")
and bo.get("dir") in ("LONG", "SHORT")
and (bo.get("conf") or 0) >= self._auto_signal_min_conf
and self._auto_guard("signal") is None):
ot = (mt5.ORDER_TYPE_BUY_STOP if bo["dir"] == "LONG"
else mt5.ORDER_TYPE_SELL_STOP)
return {ot: round(float(bo["level"]), 3)}, "signal"
return {}, ""
def _manage_squeeze_pending(self): def _manage_squeeze_pending(self):
"""RUHENDE Stop-Orders an beiden Box-Grenzen, solange der Squeeze `armed` ist. """RUHENDE Stop-Orders an beiden Box-Grenzen, solange der Squeeze `armed` ist.
@@ -1119,33 +1173,30 @@ class TradingEngine:
return return
try: try:
ps = self.trader.snapshot() ps = self.trader.snapshot()
sq = (self.wave.snapshot() or {}).get("squeeze") or {}
atr = ((self.wave.snapshot() or {}).get("pb_feats") or {}).get("atr") or 0.0 atr = ((self.wave.snapshot() or {}).get("pb_feats") or {}).get("atr") or 0.0
hi, lo = sq.get("box_hi"), sq.get("box_lo") ziel, quelle = self._pending_ziel()
grund = self._squeeze_guard() # Orders sollen NUR liegen, wenn zusätzlich: flat · Schonfrist vorbei.
# Orders sollen NUR liegen, wenn: flat · kein Gate · Kompression aktiv · # Beides kann sich jederzeit ändern → JEDEN Tick neu bewertet, nicht
# Box bekannt · Schonfrist vorbei. Jede dieser Bedingungen kann sich
# jederzeit ändern — deshalb wird das JEDEN Tick neu bewertet und nicht
# beim Platzieren einmal geprüft. # beim Platzieren einmal geprüft.
soll = (not ps.get("ticket") and grund is None if ps.get("ticket") or self._in_startup_grace() or atr <= 0:
and sq.get("state") == "armed" ziel, quelle = {}, ""
and hi and lo and atr > 0
and not self._in_startup_grace())
offen = self.trader.pending_orders(sym) offen = self.trader.pending_orders(sym)
if not soll: if not ziel:
for o in offen: for o in offen:
err = self.trader.cancel_pending(o.ticket) err = self.trader.cancel_pending(o.ticket)
if not err: if not err:
log.info(f"Squeeze-Pending storniert (T={o.ticket}) — " log.info(f"⏸ Pending-Entry storniert (T={o.ticket}) — "
f"{grund or ('Position offen' if ps.get('ticket') else 'Squeeze geklärt')}") + ("Position offen" if ps.get("ticket")
else "Setup nicht mehr gültig"))
if offen: if offen:
self._squeeze_pending_levels = None self._pending_levels = None
self._pending_quelle = ""
return return
ziel = {mt5.ORDER_TYPE_BUY_STOP: round(float(hi) + _SQ_K_ENG * atr, 3),
mt5.ORDER_TYPE_SELL_STOP: round(float(lo) - _SQ_K_ENG * atr, 3)}
# Bestehende Orders behalten, wenn ihr Trigger noch passt (Toleranz # Bestehende Orders behalten, wenn ihr Trigger noch passt (Toleranz
# 0,02×ATR) — sonst löschen und neu setzen. Ohne die Toleranz würde # 0,02×ATR) — sonst löschen und neu setzen. Ohne die Toleranz würde
# jede ATR-Zuckung die Orders löschen/neu setzen (Broker-Last, Log-Flut). # jede ATR-Zuckung die Orders löschen/neu setzen (Broker-Last, Log-Flut).
# ⚠ Beim Signal-Pfad ist das wichtiger als beim Squeeze: `_pend`
# verankert sich bei jeder k×ATR-Gegenbewegung neu, das Level wandert.
tol = max(0.02 * atr, 0.001) tol = max(0.02 * atr, 0.001)
vorhanden = {} vorhanden = {}
for o in offen: for o in offen:
@@ -1157,17 +1208,20 @@ class TradingEngine:
for otype, preis in ziel.items(): for otype, preis in ziel.items():
if otype in vorhanden: if otype in vorhanden:
continue continue
t, err = self.trader.place_stop(sym, otype, preis, comment="SQZ-STOP") tag = "SQZ-STOP" if quelle == "squeeze" else "SIG-STOP"
t, err = self.trader.place_stop(sym, otype, preis, comment=tag)
if t: if t:
log.info(f"🎯 Squeeze-Pending gesetzt: " log.info(f"🎯 Pending-Entry ({quelle}) gesetzt: "
f"{'BUY_STOP' if otype == mt5.ORDER_TYPE_BUY_STOP else 'SELL_STOP'}" f"{'BUY_STOP' if otype == mt5.ORDER_TYPE_BUY_STOP else 'SELL_STOP'}"
f" @ {preis:.3f} (T={t})") f" @ {preis:.3f} (T={t})")
elif err and self._squeeze_pending_err != err: elif err and self._squeeze_pending_err != err:
# ⚠ Nur bei ÄNDERUNG loggen: „Kurs hat das Level bereits passiert" # ⚠ Nur bei ÄNDERUNG loggen: „Kurs hat das Level bereits passiert"
# tritt im 1-s-Takt auf, sobald der Kurs an einer Box-Grenze klebt. # tritt im 1-s-Takt auf, sobald der Kurs am Level klebt.
self._squeeze_pending_err = err self._squeeze_pending_err = err
log.info(f"Squeeze-Pending nicht platzierbar ({preis:.3f}): {err}") log.info(f"Pending-Entry ({quelle}) nicht platzierbar "
self._squeeze_pending_levels = ziel f"({preis:.3f}): {err}")
self._pending_levels = ziel
self._pending_quelle = quelle
except Exception as e: except Exception as e:
log.error(f"_manage_squeeze_pending: {e}", exc_info=True) log.error(f"_manage_squeeze_pending: {e}", exc_info=True)
@@ -1354,6 +1408,31 @@ class TradingEngine:
return return
if (d == self._pos_close_dir and now - self._pos_close_ts < _SQUEEZE_REENTRY_COOLDOWN_S): if (d == self._pos_close_dir and now - self._pos_close_ts < _SQUEEZE_REENTRY_COOLDOWN_S):
return return
# ── NACHJAGD-BREMSE (2026-08-05, gemessen `backtest_auto_signal_v3.py`).
# Der Market-Weg ist nur noch FALLBACK, wenn die ruhende Stop-Order am
# Bestätigungs-Level nicht gegriffen hat. Am Level ØR +0,35/+0,32, am
# Bar-Close 0,09/0,11 — und der Härtetest zeigt den Kipp-Punkt bei
# ~0,250,30×ATR Abstand (bei +0,20 noch +0,148/+0,090). Also nur bis
# `squeeze_max_chase_atr` nachkaufen, sonst diese Episode auslassen.
# ⚠ `_signal_last_sig` wird NICHT gesetzt → kommt der Kurs ans Level
# zurück, darf der Entry noch kommen.
ws = self.wave.snapshot() or {}
bo = ws.get("breakout") or {}
atr_m5 = (ws.get("pb_feats") or {}).get("atr") or 0.0
lvl = bo.get("level") if bo.get("dir") == d else None
tick = self.data.snapshot() or {}
px = tick.get("ask") if d == "LONG" else tick.get("bid")
if lvl and atr_m5 > 0 and px:
weg = (float(px) - float(lvl)) * (1 if d == "LONG" else -1) / atr_m5
if weg > self._squeeze_max_chase:
if self._signal_chase_logged != d:
self._signal_chase_logged = d
log.info(f"⏭ Auto-Signal übersprungen: Kurs steht schon "
f"{weg:.2f}×ATR über dem Bestätigungs-Level {lvl} "
f"(Grenze {self._squeeze_max_chase:.2f}) — "
f"gemessen ab hier kein Edge mehr")
return
self._signal_chase_logged = None
self._signal_last_sig = d self._signal_last_sig = d
if isinstance(self._last_rec, dict): if isinstance(self._last_rec, dict):
self._last_rec["setup"] = f"AUTOSIG_{d}" self._last_rec["setup"] = f"AUTOSIG_{d}"
@@ -3516,9 +3595,11 @@ class TradingEngine:
# Einstieg über ruhende Stop-Orders (2026-08-05) + die aktuell gesetzten # Einstieg über ruhende Stop-Orders (2026-08-05) + die aktuell gesetzten
# Trigger — macht im Snapshot prüfbar, ob wirklich Orders liegen. # Trigger — macht im Snapshot prüfbar, ob wirklich Orders liegen.
"squeeze_pending": self._squeeze_pending, "squeeze_pending": self._squeeze_pending,
"squeeze_pending_levels": ( "signal_pending": self._signal_pending,
sorted(round(v, 3) for v in self._squeeze_pending_levels.values()) "pending_levels": (
if self._squeeze_pending_levels else []), sorted(round(v, 3) for v in self._pending_levels.values())
if self._pending_levels else []),
"pending_quelle": self._pending_quelle,
"squeeze_max_chase_atr": self._squeeze_max_chase, "squeeze_max_chase_atr": self._squeeze_max_chase,
"squeeze_entry_count": self._squeeze_entry_count, "squeeze_entry_count": self._squeeze_entry_count,
"auto_signal": self._auto_signal, "auto_signal": self._auto_signal,
+15 -1
View File
@@ -571,20 +571,34 @@ class WaveRecommender:
p = {"dir": d, "level": cur + d * k * atr, "invalid": cur - d * k * atr, p = {"dir": d, "level": cur + d * k * atr, "invalid": cur - d * k * atr,
"confirmed": False, "start": time.time()} "confirmed": False, "start": time.time()}
self._pend = p self._pend = p
# ⚠ Das Bestätigungs-Level wird AUCH nach der Bestätigung veröffentlicht
# (2026-08-05). Zwei Konsumenten brauchen es: die ruhende Stop-Order (sie
# liegt dort) und die Nachjagd-Bremse im Market-Fallback (sie misst den
# Abstand dorthin). `pending` bleibt False — das Frontend rendert nur bei
# `pending`, ist also unberührt.
if p["confirmed"]: if p["confirmed"]:
snap["breakout"] = {"pending": False, "dir": sig, "need": None,
"level": round(p["level"], 3), "confirmed": True}
return rec # schon bestätigt → durchlassen return rec # schon bestätigt → durchlassen
broke = (cur >= p["level"]) if d > 0 else (cur <= p["level"]) broke = (cur >= p["level"]) if d > 0 else (cur <= p["level"])
against = (cur <= p["invalid"]) if d > 0 else (cur >= p["invalid"]) against = (cur <= p["invalid"]) if d > 0 else (cur >= p["invalid"])
if broke: if broke:
p["confirmed"] = True p["confirmed"] = True
snap["breakout"] = {"pending": False, "dir": sig, "need": None,
"level": round(p["level"], 3), "confirmed": True}
return rec return rec
if against or (time.time() - p["start"]) > self._breakout_timeout_s: if against or (time.time() - p["start"]) > self._breakout_timeout_s:
p = {"dir": d, "level": cur + d * k * atr, "invalid": cur - d * k * atr, p = {"dir": d, "level": cur + d * k * atr, "invalid": cur - d * k * atr,
"confirmed": False, "start": time.time()} # neu verankern "confirmed": False, "start": time.time()} # neu verankern
self._pend = p self._pend = p
need = abs(p["level"] - cur) need = abs(p["level"] - cur)
# ⚠ `conf` = die Konfidenz VOR dem Stummschalten. Der Rückgabewert unten
# setzt `conf_pct` auf 0 (das Signal gilt ja noch nicht) — wer die ruhende
# Stop-Order gegen die Mindest-Konfidenz prüfen will, hätte sonst KEINE
# Zahl und müsste die Order blind platzieren.
snap["breakout"] = {"pending": True, "dir": sig, snap["breakout"] = {"pending": True, "dir": sig,
"level": round(p["level"], 3), "need": round(need, 3)} "level": round(p["level"], 3), "need": round(need, 3),
"conf": rec.get("conf_pct") or 0, "confirmed": False}
return {"signal": "WARTEN", "conf_pct": 0, "score": 0.0, "setup": "WAVE", return {"signal": "WARTEN", "conf_pct": 0, "score": 0.0, "setup": "WAVE",
"regime": None, "rsi": None, "block": "breakout_pending", "regime": None, "rsi": None, "block": "breakout_pending",
"reasons": [f"warte auf Breakout (+{k:.1f}×ATR {sig}, noch {need:.2f})"]} "reasons": [f"warte auf Breakout (+{k:.1f}×ATR {sig}, noch {need:.2f})"]}