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