diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 6025f61..463dd79 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -141,6 +141,7 @@ from key_monitor_lib import ( from order_monitor_display_lib import ( apply_order_price_display_fields, enrich_order_display_fields, + order_monitor_tpsl_needs_sync, ) from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook from hub_auth import request_allowed as hub_request_allowed @@ -6248,7 +6249,6 @@ def api_price_snapshot(): order_rows = conn.execute( "SELECT id,symbol,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'" ).fetchall() - conn.close() symbol_set = set() for r in key_rows: @@ -6410,9 +6410,28 @@ def api_price_snapshot(): take_profit=r["take_profit"], calc_rr_ratio_fn=calc_rr_ratio, exchange_tpsl=exchange_tpsl, + format_price_fn=format_price_for_symbol, + symbol=r["symbol"], ) + new_sl, new_tp, changed = order_monitor_tpsl_needs_sync( + r["stop_loss"], r["take_profit"], exchange_tpsl + ) + if changed: + try: + conn.execute( + "UPDATE order_monitors SET stop_loss=?, take_profit=? WHERE id=?", + (new_sl, new_tp, int(r["id"])), + ) + except Exception: + pass order_prices.append(payload) + try: + conn.commit() + except Exception: + pass + conn.close() + from hub_position_metrics import build_position_marks_list position_marks = build_position_marks_list( diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 411b8ec..f143a05 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -541,19 +541,11 @@
止损 - {% if o.stop_loss %} - {{ price_fmt(o.symbol, o.stop_loss) }} - {% else %} - - {% endif %} + {{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}
止盈 - {% if o.take_profit %} - {{ price_fmt(o.symbol, o.take_profit) }} - {% else %} - - {% endif %} + {{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}
盈亏比 @@ -1914,6 +1906,24 @@ function paintBreakevenBadge(orderId, secured){ if(!wrap) return; wrap.style.display = secured ? "inline-flex" : "none"; } +function paintPlanTpslDisplay(orderId, snap){ + if(!snap) return; + const card = document.getElementById(`order-row-${orderId}`); + const slEl = document.getElementById(`order-plan-sl-${orderId}`); + const tpEl = document.getElementById(`order-plan-tp-${orderId}`); + const slRaw = snap.stop_loss_raw != null && snap.stop_loss_raw !== "" ? snap.stop_loss_raw : snap.stop_loss; + const tpRaw = snap.take_profit_raw != null && snap.take_profit_raw !== "" ? snap.take_profit_raw : snap.take_profit; + const slDisp = snap.stop_loss_display || (slRaw != null && slRaw !== "" ? formatPriceForInput(slRaw) : null); + const tpDisp = snap.take_profit_display || (tpRaw != null && tpRaw !== "" ? formatPriceForInput(tpRaw) : null); + if(slEl) slEl.innerText = slDisp || "—"; + if(tpEl) tpEl.innerText = tpDisp || "—"; + if(card){ + if(slRaw != null && slRaw !== "") card.setAttribute("data-plan-sl", formatPriceForInput(slRaw)); + else if(slDisp) card.setAttribute("data-plan-sl", slDisp); + if(tpRaw != null && tpRaw !== "") card.setAttribute("data-plan-tp", formatPriceForInput(tpRaw)); + else if(tpDisp) card.setAttribute("data-plan-tp", tpDisp); + } +} function paintPriceTrend(el, key, value){ if(!el) return; @@ -2001,6 +2011,7 @@ function refreshPriceSnapshot(){ } paintBreakevenBadge(o.id, o.sl_breakeven_secured); if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl); + paintPlanTpslDisplay(o.id, o); }); }).catch(()=>{}); } @@ -2239,6 +2250,7 @@ function refreshPriceSnapshotConditional(){ if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio); paintBreakevenBadge(o.id, o.sl_breakeven_secured); paintExchangeTpslRow(o.id, o.exchange_tpsl || {}); + paintPlanTpslDisplay(o.id, o); }); } }).catch(()=>{}); diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 2aba636..3230e5f 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -140,6 +140,7 @@ from key_monitor_lib import ( from order_monitor_display_lib import ( apply_order_price_display_fields, enrich_order_display_fields, + order_monitor_tpsl_needs_sync, ) from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook from hub_auth import request_allowed as hub_request_allowed @@ -6237,7 +6238,6 @@ def api_price_snapshot(): order_rows = conn.execute( "SELECT id,symbol,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'" ).fetchall() - conn.close() try: ensure_markets_loaded() @@ -6424,9 +6424,28 @@ def api_price_snapshot(): take_profit=r["take_profit"], calc_rr_ratio_fn=calc_rr_ratio, exchange_tpsl=exchange_tpsl, + format_price_fn=format_price_for_symbol, + symbol=r["symbol"], ) + new_sl, new_tp, changed = order_monitor_tpsl_needs_sync( + r["stop_loss"], r["take_profit"], exchange_tpsl + ) + if changed: + try: + conn.execute( + "UPDATE order_monitors SET stop_loss=?, take_profit=? WHERE id=?", + (new_sl, new_tp, int(r["id"])), + ) + except Exception: + pass order_prices.append(payload) + try: + conn.commit() + except Exception: + pass + conn.close() + from hub_position_metrics import build_position_marks_list position_marks = build_position_marks_list( diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index e1450ce..2fba849 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -525,19 +525,11 @@
止损 - {% if o.stop_loss %} - {{ price_fmt(o.symbol, o.stop_loss) }} - {% else %} - - {% endif %} + {{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}
止盈 - {% if o.take_profit %} - {{ price_fmt(o.symbol, o.take_profit) }} - {% else %} - - {% endif %} + {{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}
盈亏比 @@ -1898,6 +1890,24 @@ function paintBreakevenBadge(orderId, secured){ if(!wrap) return; wrap.style.display = secured ? "inline-flex" : "none"; } +function paintPlanTpslDisplay(orderId, snap){ + if(!snap) return; + const card = document.getElementById(`order-row-${orderId}`); + const slEl = document.getElementById(`order-plan-sl-${orderId}`); + const tpEl = document.getElementById(`order-plan-tp-${orderId}`); + const slRaw = snap.stop_loss_raw != null && snap.stop_loss_raw !== "" ? snap.stop_loss_raw : snap.stop_loss; + const tpRaw = snap.take_profit_raw != null && snap.take_profit_raw !== "" ? snap.take_profit_raw : snap.take_profit; + const slDisp = snap.stop_loss_display || (slRaw != null && slRaw !== "" ? formatPriceForInput(slRaw) : null); + const tpDisp = snap.take_profit_display || (tpRaw != null && tpRaw !== "" ? formatPriceForInput(tpRaw) : null); + if(slEl) slEl.innerText = slDisp || "—"; + if(tpEl) tpEl.innerText = tpDisp || "—"; + if(card){ + if(slRaw != null && slRaw !== "") card.setAttribute("data-plan-sl", formatPriceForInput(slRaw)); + else if(slDisp) card.setAttribute("data-plan-sl", slDisp); + if(tpRaw != null && tpRaw !== "") card.setAttribute("data-plan-tp", formatPriceForInput(tpRaw)); + else if(tpDisp) card.setAttribute("data-plan-tp", tpDisp); + } +} function paintPriceTrend(el, key, value){ if(!el) return; @@ -1985,6 +1995,7 @@ function refreshPriceSnapshot(){ } paintBreakevenBadge(o.id, o.sl_breakeven_secured); if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl); + paintPlanTpslDisplay(o.id, o); }); }).catch(()=>{}); } @@ -2223,6 +2234,7 @@ function refreshPriceSnapshotConditional(){ if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio); paintBreakevenBadge(o.id, o.sl_breakeven_secured); paintExchangeTpslRow(o.id, o.exchange_tpsl || {}); + paintPlanTpslDisplay(o.id, o); }); } }).catch(()=>{}); diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 2f51e42..dadf55a 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -66,6 +66,7 @@ from order_monitor_display_lib import ( apply_order_live_price_display, apply_order_price_display_fields, enrich_order_display_fields, + order_monitor_tpsl_needs_sync, stop_is_profit_protecting, tpsl_slot_trigger_price, tpsl_update_passes_rr_gate, @@ -5699,45 +5700,30 @@ def api_price_snapshot(): except Exception: exchange_tpsl = {"sl": None, "tp": None} payload["exchange_tpsl"] = exchange_tpsl - live_sl = tpsl_slot_trigger_price(exchange_tpsl.get("sl")) - live_tp = tpsl_slot_trigger_price(exchange_tpsl.get("tp")) - disp_sl = live_sl if live_sl is not None else r["stop_loss"] - disp_tp = live_tp if live_tp is not None else r["take_profit"] - sym = r["symbol"] - payload["stop_loss_raw"] = disp_sl - payload["take_profit_raw"] = disp_tp - payload["stop_loss_display"] = ( - format_price_for_symbol(sym, disp_sl) if disp_sl not in (None, "") else "—" - ) - payload["take_profit_display"] = ( - format_price_for_symbol(sym, disp_tp) if disp_tp not in (None, "") else "—" - ) apply_order_price_display_fields( payload, direction=r["direction"], entry_price=entry, initial_stop_loss=r["initial_stop_loss"], - stop_loss=disp_sl, - take_profit=disp_tp, + stop_loss=r["stop_loss"], + take_profit=r["take_profit"], calc_rr_ratio_fn=calc_rr_ratio, exchange_tpsl=exchange_tpsl, + format_price_fn=format_price_for_symbol, + symbol=r["symbol"], ) - order_prices.append(payload) - if live_sl is not None or live_tp is not None: + new_sl, new_tp, changed = order_monitor_tpsl_needs_sync( + r["stop_loss"], r["take_profit"], exchange_tpsl + ) + if changed: try: - cur_sl = float(r["stop_loss"] or 0) - cur_tp = float(r["take_profit"] or 0) - except (TypeError, ValueError): - cur_sl, cur_tp = 0.0, 0.0 - new_sl = live_sl if live_sl is not None else cur_sl - new_tp = live_tp if live_tp is not None else cur_tp - if (live_sl is not None and abs(new_sl - cur_sl) > 1e-12) or ( - live_tp is not None and abs(new_tp - cur_tp) > 1e-12 - ): conn.execute( "UPDATE order_monitors SET stop_loss=?, take_profit=? WHERE id=?", (new_sl, new_tp, int(r["id"])), ) + except Exception: + pass + order_prices.append(payload) try: conn.commit() diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index c8bc76f..f8d8935 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -1723,14 +1723,16 @@ function paintPlanTpslDisplay(orderId, snap){ const slEl = document.getElementById(`order-plan-sl-${orderId}`); const tpEl = document.getElementById(`order-plan-tp-${orderId}`); const rrEl = document.getElementById(`order-rr-${orderId}`); - const slDisp = snap.stop_loss_display; - const tpDisp = snap.take_profit_display; - if(slEl && slDisp) slEl.innerText = slDisp; - if(tpEl && tpDisp) tpEl.innerText = tpDisp; + const slRaw = snap.stop_loss_raw != null && snap.stop_loss_raw !== "" ? snap.stop_loss_raw : snap.stop_loss; + const tpRaw = snap.take_profit_raw != null && snap.take_profit_raw !== "" ? snap.take_profit_raw : snap.take_profit; + const slDisp = snap.stop_loss_display || (slRaw != null && slRaw !== "" ? formatPriceForInput(slRaw) : null); + const tpDisp = snap.take_profit_display || (tpRaw != null && tpRaw !== "" ? formatPriceForInput(tpRaw) : null); + if(slEl) slEl.innerText = slDisp || "—"; + if(tpEl) tpEl.innerText = tpDisp || "—"; if(card){ - if(snap.stop_loss_raw != null && snap.stop_loss_raw !== "") card.setAttribute('data-plan-sl', formatPriceForInput(snap.stop_loss_raw)); + if(slRaw != null && slRaw !== "") card.setAttribute('data-plan-sl', formatPriceForInput(slRaw)); else if(slDisp) card.setAttribute('data-plan-sl', slDisp); - if(snap.take_profit_raw != null && snap.take_profit_raw !== "") card.setAttribute('data-plan-tp', formatPriceForInput(snap.take_profit_raw)); + if(tpRaw != null && tpRaw !== "") card.setAttribute('data-plan-tp', formatPriceForInput(tpRaw)); else if(tpDisp) card.setAttribute('data-plan-tp', tpDisp); } if(rrEl && typeof snap.rr_ratio !== "undefined") rrEl.innerText = formatRrRatio(snap.rr_ratio); diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 2672ece..bb390fc 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -140,6 +140,7 @@ from key_monitor_lib import ( from order_monitor_display_lib import ( apply_order_price_display_fields, enrich_order_display_fields, + order_monitor_tpsl_needs_sync, ) from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook from hub_auth import request_allowed as hub_request_allowed @@ -5935,7 +5936,6 @@ def api_price_snapshot(): order_rows = conn.execute( "SELECT id,symbol,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'" ).fetchall() - conn.close() try: ensure_markets_loaded() @@ -6122,9 +6122,28 @@ def api_price_snapshot(): take_profit=r["take_profit"], calc_rr_ratio_fn=calc_rr_ratio, exchange_tpsl=exchange_tpsl, + format_price_fn=format_price_for_symbol, + symbol=r["symbol"], ) + new_sl, new_tp, changed = order_monitor_tpsl_needs_sync( + r["stop_loss"], r["take_profit"], exchange_tpsl + ) + if changed: + try: + conn.execute( + "UPDATE order_monitors SET stop_loss=?, take_profit=? WHERE id=?", + (new_sl, new_tp, int(r["id"])), + ) + except Exception: + pass order_prices.append(payload) + try: + conn.commit() + except Exception: + pass + conn.close() + from hub_position_metrics import build_position_marks_list position_marks = build_position_marks_list( diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index 962c431..3503bde 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -550,19 +550,11 @@
止损 - {% if o.stop_loss %} - {{ price_fmt(o.symbol, o.stop_loss) }} - {% else %} - - {% endif %} + {{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}
止盈 - {% if o.take_profit %} - {{ price_fmt(o.symbol, o.take_profit) }} - {% else %} - - {% endif %} + {{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}
盈亏比 @@ -1924,6 +1916,24 @@ function paintBreakevenBadge(orderId, secured){ if(!wrap) return; wrap.style.display = secured ? "inline-flex" : "none"; } +function paintPlanTpslDisplay(orderId, snap){ + if(!snap) return; + const card = document.getElementById(`order-row-${orderId}`); + const slEl = document.getElementById(`order-plan-sl-${orderId}`); + const tpEl = document.getElementById(`order-plan-tp-${orderId}`); + const slRaw = snap.stop_loss_raw != null && snap.stop_loss_raw !== "" ? snap.stop_loss_raw : snap.stop_loss; + const tpRaw = snap.take_profit_raw != null && snap.take_profit_raw !== "" ? snap.take_profit_raw : snap.take_profit; + const slDisp = snap.stop_loss_display || (slRaw != null && slRaw !== "" ? formatPriceForInput(slRaw) : null); + const tpDisp = snap.take_profit_display || (tpRaw != null && tpRaw !== "" ? formatPriceForInput(tpRaw) : null); + if(slEl) slEl.innerText = slDisp || "—"; + if(tpEl) tpEl.innerText = tpDisp || "—"; + if(card){ + if(slRaw != null && slRaw !== "") card.setAttribute("data-plan-sl", formatPriceForInput(slRaw)); + else if(slDisp) card.setAttribute("data-plan-sl", slDisp); + if(tpRaw != null && tpRaw !== "") card.setAttribute("data-plan-tp", formatPriceForInput(tpRaw)); + else if(tpDisp) card.setAttribute("data-plan-tp", tpDisp); + } +} function paintPriceTrend(el, key, value){ if(!el) return; @@ -2011,6 +2021,7 @@ function refreshPriceSnapshot(){ } paintBreakevenBadge(o.id, o.sl_breakeven_secured); if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl); + paintPlanTpslDisplay(o.id, o); }); }).catch(()=>{}); } @@ -2281,6 +2292,7 @@ function refreshPriceSnapshotConditional(){ if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio); paintBreakevenBadge(o.id, o.sl_breakeven_secured); paintExchangeTpslRow(o.id, o.exchange_tpsl || {}); + paintPlanTpslDisplay(o.id, o); }); } }).catch(()=>{}); diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 3adf80c..8381708 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -1269,6 +1269,15 @@ def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) -> o["rr_ratio"] = op["rr_ratio"] if "sl_breakeven_secured" in op: o["sl_breakeven_secured"] = bool(op["sl_breakeven_secured"]) + for key in ( + "stop_loss", + "take_profit", + "stop_loss_display", + "take_profit_display", + "display_rr_ratio", + ): + if key in op and op[key] not in (None, ""): + o[key] = op[key] def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon: dict | None) -> None: diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 35f9ef7..d45a724 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -1032,33 +1032,33 @@ return out; } + function upsertExTpslCondOrder(cond, role, slot) { + if (!slot || slot.trigger_price == null || slot.trigger_price === "") return; + const label = role === "sl" ? "止损" : "止盈"; + const item = { + label: label, + trigger_price: Number(slot.trigger_price), + amount: slot.amount != null ? slot.amount : null, + id: slot.order_id || "", + channel: "algo", + }; + const idx = cond.findIndex(function (o) { + const lb = o.label || ""; + return role === "sl" ? /^止损\b/.test(lb) || lb.includes("止损") : /^止盈\b/.test(lb) || lb.includes("止盈"); + }); + if (idx >= 0) cond[idx] = Object.assign({}, cond[idx], item); + else cond.push(item); + } + function condOrdersFromPosition(pos) { const cond = dedupeCondOrdersByTrigger( Array.isArray(pos.conditional_orders) ? pos.conditional_orders : [] ); - if (cond.length) return cond; const et = pos.exchange_tpsl; - if (!et) return []; - const out = []; - if (et.sl && et.sl.trigger_price != null) { - out.push({ - label: "止损", - trigger_price: Number(et.sl.trigger_price), - amount: null, - id: et.sl.order_id, - channel: "algo", - }); - } - if (et.tp && et.tp.trigger_price != null) { - out.push({ - label: "止盈", - trigger_price: Number(et.tp.trigger_price), - amount: null, - id: et.tp.order_id, - channel: "algo", - }); - } - return out; + if (!et) return cond; + upsertExTpslCondOrder(cond, "sl", et.sl); + upsertExTpslCondOrder(cond, "tp", et.tp); + return cond; } function findMonitorOrder(orders, symbol, side) { @@ -1459,8 +1459,18 @@ } const inferred = inferTpslFromCondOrders(pos.side, cond, entryN); - if (sl === "" || sl == null) sl = inferred.sl; - if (!tpMonitored && (takeProfit === "" || takeProfit == null)) takeProfit = inferred.tp; + if (inferred.sl !== "" && inferred.sl != null) { + sl = inferred.sl; + } else if (sl === "" || sl == null) { + sl = inferred.sl; + } + if (!tpMonitored) { + if (inferred.tp !== "" && inferred.tp != null) { + takeProfit = inferred.tp; + } else if (takeProfit === "" || takeProfit == null) { + takeProfit = inferred.tp; + } + } if (sl !== "" && takeProfit !== "" && Number(sl) === Number(takeProfit)) { takeProfit = ""; diff --git a/order_monitor_display_lib.py b/order_monitor_display_lib.py index 4a8fbfc..3d02ef7 100644 --- a/order_monitor_display_lib.py +++ b/order_monitor_display_lib.py @@ -146,6 +146,44 @@ def apply_order_live_price_display( return payload +def resolve_live_tpsl_prices( + plan_sl: Any, + plan_tp: Any, + exchange_tpsl: Any, +) -> tuple[Optional[float], Optional[float], Optional[float], Optional[float]]: + """返回 (展示用止损, 展示用止盈, 交易所止损, 交易所止盈)。""" + ex_sl = ex_tp = None + if isinstance(exchange_tpsl, dict): + ex_sl = tpsl_slot_trigger_price(exchange_tpsl.get("sl")) + ex_tp = tpsl_slot_trigger_price(exchange_tpsl.get("tp")) + disp_sl = ex_sl if ex_sl is not None else _positive_float(plan_sl) + disp_tp = ex_tp if ex_tp is not None else _positive_float(plan_tp) + return disp_sl, disp_tp, ex_sl, ex_tp + + +def order_monitor_tpsl_needs_sync( + plan_sl: Any, + plan_tp: Any, + exchange_tpsl: Any, + *, + eps: float = 1e-12, +) -> tuple[Optional[float], Optional[float], bool]: + """若交易所 TP/SL 与库中不一致,返回应写回的 (sl, tp) 及是否需更新。""" + _, _, ex_sl, ex_tp = resolve_live_tpsl_prices(plan_sl, plan_tp, exchange_tpsl) + try: + cur_sl = float(plan_sl or 0) + cur_tp = float(plan_tp or 0) + except (TypeError, ValueError): + cur_sl, cur_tp = 0.0, 0.0 + new_sl = ex_sl if ex_sl is not None else cur_sl + new_tp = ex_tp if ex_tp is not None else cur_tp + changed = ( + (ex_sl is not None and abs(new_sl - cur_sl) > eps) + or (ex_tp is not None and abs(new_tp - cur_tp) > eps) + ) + return new_sl, new_tp, changed + + def apply_order_price_display_fields( payload: dict[str, Any], *, @@ -156,7 +194,10 @@ def apply_order_price_display_fields( take_profit: Any, calc_rr_ratio_fn: Callable[..., Optional[float]], exchange_tpsl: Any = None, + format_price_fn: Optional[Callable[[Any, Any], str]] = None, + symbol: Any = None, ) -> dict[str, Any]: + disp_sl, disp_tp, _, _ = resolve_live_tpsl_prices(stop_loss, take_profit, exchange_tpsl) payload["rr_ratio"] = snapshot_rr( calc_rr_ratio_fn, direction, @@ -168,4 +209,19 @@ def apply_order_price_display_fields( payload["sl_breakeven_secured"] = sl_breakeven_from_exchange_tpsl( direction, entry_price, exchange_tpsl ) + payload["stop_loss"] = disp_sl + payload["take_profit"] = disp_tp + if disp_sl is not None and disp_tp is not None: + payload["display_rr_ratio"] = calc_rr_ratio_fn( + direction or "long", entry_price, disp_sl, disp_tp + ) + else: + payload["display_rr_ratio"] = None + if format_price_fn is not None and symbol is not None: + payload["stop_loss_display"] = ( + format_price_fn(symbol, disp_sl) if disp_sl is not None else "—" + ) + payload["take_profit_display"] = ( + format_price_fn(symbol, disp_tp) if disp_tp is not None else "—" + ) return payload diff --git a/tests/test_order_monitor_display_lib.py b/tests/test_order_monitor_display_lib.py index b6d0c67..d7084ef 100644 --- a/tests/test_order_monitor_display_lib.py +++ b/tests/test_order_monitor_display_lib.py @@ -1,5 +1,8 @@ from order_monitor_display_lib import ( + apply_order_price_display_fields, is_sl_breakeven_secured, + order_monitor_tpsl_needs_sync, + resolve_live_tpsl_prices, sl_breakeven_from_exchange_tpsl, snapshot_rr, snapshot_stop_loss, @@ -48,3 +51,46 @@ def test_sl_breakeven_from_exchange_tpsl(): {"sl": {"trigger_price": 2.735}, "tp": {"trigger_price": 3.3}}, ) assert ok is True + + +def test_resolve_live_tpsl_prefers_exchange(): + disp_sl, disp_tp, ex_sl, ex_tp = resolve_live_tpsl_prices( + 1674, + 1647.65, + {"sl": {"trigger_price": 1661}, "tp": {"trigger_price": 1647.65}}, + ) + assert disp_sl == 1661 + assert disp_tp == 1647.65 + assert ex_sl == 1661 + assert ex_tp == 1647.65 + + +def test_order_monitor_tpsl_needs_sync_detects_sl_change(): + new_sl, new_tp, changed = order_monitor_tpsl_needs_sync( + 1674, + 1647.65, + {"sl": {"trigger_price": 1661}, "tp": {"trigger_price": 1647.65}}, + ) + assert changed is True + assert new_sl == 1661 + assert new_tp == 1647.65 + + +def test_apply_order_price_display_fields_live_sl(): + payload = {} + apply_order_price_display_fields( + payload, + direction="short", + entry_price=1663.45, + initial_stop_loss=1674, + stop_loss=1674, + take_profit=1647.65, + calc_rr_ratio_fn=_calc_rr, + exchange_tpsl={"sl": {"trigger_price": 1661}, "tp": {"trigger_price": 1647.65}}, + format_price_fn=lambda _s, v: f"{v:.2f}", + symbol="ETH/USDT:USDT", + ) + assert payload["stop_loss"] == 1661 + assert payload["stop_loss_display"] == "1661.00" + assert payload["sl_breakeven_secured"] is True + assert payload["rr_ratio"] is not None