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:
@@ -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(
|
||||
|
||||
@@ -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(()=>{});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(()=>{});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(()=>{});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user