"""关键位箱体/收敛:止盈止损方案(Binance / Gate / OKX 共用)。""" KEY_SL_TP_MODES = frozenset({"standard", "box_1p5", "trend_manual"}) KEY_SL_TP_MODE_LABELS = { "standard": "标准突破", "box_1p5": "箱体1R·止盈1.5H", "trend_manual": "趋势单·自填止盈", } KEY_MONITOR_AUTO_TYPES_FOR_FORM = frozenset({"箱体突破", "收敛突破"}) def normalize_sl_tp_mode(raw): m = (raw or "standard").strip().lower() if m in ("box_1p5", "box15", "box-1.5", "box_1.5"): return "box_1p5" if m in ("trend_manual", "trend", "manual"): return "trend_manual" if m in KEY_SL_TP_MODES: return m return "standard" def sl_tp_mode_label(mode): return KEY_SL_TP_MODE_LABELS.get(normalize_sl_tp_mode(mode), normalize_sl_tp_mode(mode)) def sl_tp_mode_from_row(row, default="standard"): try: if hasattr(row, "keys") and "sl_tp_mode" in row.keys(): raw = row["sl_tp_mode"] else: raw = row.get("sl_tp_mode") if isinstance(row, dict) else None except Exception: raw = None return normalize_sl_tp_mode(raw if raw not in (None, "") else default) def breakeven_enabled_from_row(row, default=0): try: if hasattr(row, "keys") and "breakeven_enabled" in row.keys(): v = row["breakeven_enabled"] else: v = row.get("breakeven_enabled") if isinstance(row, dict) else None except Exception: v = None if v is None: return int(default) != 0 return int(v) != 0 def parse_breakeven_enabled_form(form_value): return 1 if (form_value or "").strip().lower() in ("1", "true", "on", "yes") else 0 def plan_key_sl_tp( mode, direction, upper, lower, checks, *, outside_pct, trend_outside_pct, manual_take_profit=None, ): """ 以确认 K 收盘 E 为「当前价」计算计划 SL/TP。 返回 (E, sl_raw, tp_raw, box_h) 或 None(几何无效 / 模式3缺止盈)。 """ try: E = float(checks["confirm_close"]) H = abs(float(upper) - float(lower)) except (TypeError, ValueError, KeyError): return None if H <= 0: return None direction = (direction or "long").strip().lower() mode = normalize_sl_tp_mode(mode) if mode == "box_1p5": if direction == "long": sl_raw = E - H tp_raw = E + 1.5 * H else: sl_raw = E + H tp_raw = E - 1.5 * H return E, sl_raw, tp_raw, H if mode == "trend_manual": try: br_hi = float(checks["breakout_high"]) br_lo = float(checks["breakout_low"]) tp_raw = float(manual_take_profit) except (TypeError, ValueError, KeyError): return None m = float(trend_outside_pct) / 100.0 if direction == "long": sl_raw = br_lo * (1.0 - m) if br_lo > 0 else 0.0 if tp_raw <= E or sl_raw <= 0: return None else: sl_raw = br_hi * (1.0 + m) if br_hi > 0 else 0.0 if tp_raw >= E or sl_raw <= 0: return None return E, sl_raw, tp_raw, H # standard:突破 K 极值外侧 + 止盈 E±1×H try: br_hi = float(checks["breakout_high"]) br_lo = float(checks["breakout_low"]) except (TypeError, ValueError, KeyError): return None om = float(outside_pct) / 100.0 if direction == "long": sl_raw = br_lo * (1.0 - om) if br_lo > 0 else 0.0 tp_raw = E + H else: sl_raw = br_hi * (1.0 + om) if br_hi > 0 else 0.0 tp_raw = E - H return E, sl_raw, tp_raw, H def sl_tp_plan_summary_text(mode, direction, E, sl_raw, tp_raw, box_h, *, outside_pct, trend_outside_pct): """微信/页面用一行计划 SL/TP 说明。""" mode = normalize_sl_tp_mode(mode) direction = (direction or "long").strip().lower() if mode == "box_1p5": return ( f"方案:{sl_tp_mode_label(mode)}|E={E}|SL=E∓1×H({box_h})|TP=E∓1.5×H" ) if mode == "trend_manual": return ( f"方案:{sl_tp_mode_label(mode)}|E={E}|SL=突破K极值外{trend_outside_pct}%|TP={tp_raw}(录入)" ) return ( f"方案:{sl_tp_mode_label(mode)}|E={E}|SL=突破K外{outside_pct}%|TP=E±1×H({box_h})" )