fix: sync live TP/SL to position cards after entrust changes

Use exchange TP/SL for display and DB sync on price_snapshot polls, refresh instance UI cells on each tick, and merge live values into hub monitor board.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-09 11:06:27 +08:00
parent 7cb55f6557
commit f7d94f67d7
12 changed files with 291 additions and 89 deletions
+20 -1
View File
@@ -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(
+22 -10
View File
@@ -541,19 +541,11 @@
</div>
<div class="pos-cell">
<span class="pos-label">止损</span>
{% if o.stop_loss %}
<span class="pos-value">{{ price_fmt(o.symbol, o.stop_loss) }}</span>
{% else %}
<span class="pos-value pos-val-dash"></span>
{% endif %}
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">止盈</span>
{% if o.take_profit %}
<span class="pos-value">{{ price_fmt(o.symbol, o.take_profit) }}</span>
{% else %}
<span class="pos-value pos-val-dash"></span>
{% endif %}
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">盈亏比</span>
@@ -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(()=>{});
+20 -1
View File
@@ -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(
+22 -10
View File
@@ -525,19 +525,11 @@
</div>
<div class="pos-cell">
<span class="pos-label">止损</span>
{% if o.stop_loss %}
<span class="pos-value">{{ price_fmt(o.symbol, o.stop_loss) }}</span>
{% else %}
<span class="pos-value pos-val-dash"></span>
{% endif %}
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">止盈</span>
{% if o.take_profit %}
<span class="pos-value">{{ price_fmt(o.symbol, o.take_profit) }}</span>
{% else %}
<span class="pos-value pos-val-dash"></span>
{% endif %}
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">盈亏比</span>
@@ -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(()=>{});
+12 -26
View File
@@ -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()
+8 -6
View File
@@ -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);
+20 -1
View File
@@ -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(
+22 -10
View File
@@ -550,19 +550,11 @@
</div>
<div class="pos-cell">
<span class="pos-label">止损</span>
{% if o.stop_loss %}
<span class="pos-value">{{ price_fmt(o.symbol, o.stop_loss) }}</span>
{% else %}
<span class="pos-value pos-val-dash"></span>
{% endif %}
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">止盈</span>
{% if o.take_profit %}
<span class="pos-value">{{ price_fmt(o.symbol, o.take_profit) }}</span>
{% else %}
<span class="pos-value pos-val-dash"></span>
{% endif %}
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
</div>
<div class="pos-cell">
<span class="pos-label">盈亏比</span>
@@ -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(()=>{});
+9
View File
@@ -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:
+34 -24
View File
@@ -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 = "";
+56
View File
@@ -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
+46
View File
@@ -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