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 (
|
from order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
enrich_order_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 wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
||||||
from hub_auth import request_allowed as hub_request_allowed
|
from hub_auth import request_allowed as hub_request_allowed
|
||||||
@@ -6248,7 +6249,6 @@ def api_price_snapshot():
|
|||||||
order_rows = conn.execute(
|
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'"
|
"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()
|
).fetchall()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
symbol_set = set()
|
symbol_set = set()
|
||||||
for r in key_rows:
|
for r in key_rows:
|
||||||
@@ -6410,9 +6410,28 @@ def api_price_snapshot():
|
|||||||
take_profit=r["take_profit"],
|
take_profit=r["take_profit"],
|
||||||
calc_rr_ratio_fn=calc_rr_ratio,
|
calc_rr_ratio_fn=calc_rr_ratio,
|
||||||
exchange_tpsl=exchange_tpsl,
|
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)
|
order_prices.append(payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
conn.close()
|
||||||
|
|
||||||
from hub_position_metrics import build_position_marks_list
|
from hub_position_metrics import build_position_marks_list
|
||||||
|
|
||||||
position_marks = build_position_marks_list(
|
position_marks = build_position_marks_list(
|
||||||
|
|||||||
@@ -541,19 +541,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">止损</span>
|
<span class="pos-label">止损</span>
|
||||||
{% if o.stop_loss %}
|
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span>
|
||||||
<span class="pos-value">{{ price_fmt(o.symbol, o.stop_loss) }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="pos-value pos-val-dash">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">止盈</span>
|
<span class="pos-label">止盈</span>
|
||||||
{% if o.take_profit %}
|
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
|
||||||
<span class="pos-value">{{ price_fmt(o.symbol, o.take_profit) }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="pos-value pos-val-dash">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">盈亏比</span>
|
<span class="pos-label">盈亏比</span>
|
||||||
@@ -1914,6 +1906,24 @@ function paintBreakevenBadge(orderId, secured){
|
|||||||
if(!wrap) return;
|
if(!wrap) return;
|
||||||
wrap.style.display = secured ? "inline-flex" : "none";
|
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){
|
function paintPriceTrend(el, key, value){
|
||||||
if(!el) return;
|
if(!el) return;
|
||||||
@@ -2001,6 +2011,7 @@ function refreshPriceSnapshot(){
|
|||||||
}
|
}
|
||||||
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
||||||
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
|
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
|
||||||
|
paintPlanTpslDisplay(o.id, o);
|
||||||
});
|
});
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
@@ -2239,6 +2250,7 @@ function refreshPriceSnapshotConditional(){
|
|||||||
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
|
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
|
||||||
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
||||||
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
||||||
|
paintPlanTpslDisplay(o.id, o);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ from key_monitor_lib import (
|
|||||||
from order_monitor_display_lib import (
|
from order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
enrich_order_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 wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
||||||
from hub_auth import request_allowed as hub_request_allowed
|
from hub_auth import request_allowed as hub_request_allowed
|
||||||
@@ -6237,7 +6238,6 @@ def api_price_snapshot():
|
|||||||
order_rows = conn.execute(
|
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'"
|
"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()
|
).fetchall()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
@@ -6424,9 +6424,28 @@ def api_price_snapshot():
|
|||||||
take_profit=r["take_profit"],
|
take_profit=r["take_profit"],
|
||||||
calc_rr_ratio_fn=calc_rr_ratio,
|
calc_rr_ratio_fn=calc_rr_ratio,
|
||||||
exchange_tpsl=exchange_tpsl,
|
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)
|
order_prices.append(payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
conn.close()
|
||||||
|
|
||||||
from hub_position_metrics import build_position_marks_list
|
from hub_position_metrics import build_position_marks_list
|
||||||
|
|
||||||
position_marks = build_position_marks_list(
|
position_marks = build_position_marks_list(
|
||||||
|
|||||||
@@ -525,19 +525,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">止损</span>
|
<span class="pos-label">止损</span>
|
||||||
{% if o.stop_loss %}
|
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span>
|
||||||
<span class="pos-value">{{ price_fmt(o.symbol, o.stop_loss) }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="pos-value pos-val-dash">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">止盈</span>
|
<span class="pos-label">止盈</span>
|
||||||
{% if o.take_profit %}
|
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
|
||||||
<span class="pos-value">{{ price_fmt(o.symbol, o.take_profit) }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="pos-value pos-val-dash">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">盈亏比</span>
|
<span class="pos-label">盈亏比</span>
|
||||||
@@ -1898,6 +1890,24 @@ function paintBreakevenBadge(orderId, secured){
|
|||||||
if(!wrap) return;
|
if(!wrap) return;
|
||||||
wrap.style.display = secured ? "inline-flex" : "none";
|
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){
|
function paintPriceTrend(el, key, value){
|
||||||
if(!el) return;
|
if(!el) return;
|
||||||
@@ -1985,6 +1995,7 @@ function refreshPriceSnapshot(){
|
|||||||
}
|
}
|
||||||
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
||||||
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
|
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
|
||||||
|
paintPlanTpslDisplay(o.id, o);
|
||||||
});
|
});
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
@@ -2223,6 +2234,7 @@ function refreshPriceSnapshotConditional(){
|
|||||||
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
|
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
|
||||||
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
||||||
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
||||||
|
paintPlanTpslDisplay(o.id, o);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ from order_monitor_display_lib import (
|
|||||||
apply_order_live_price_display,
|
apply_order_live_price_display,
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
enrich_order_display_fields,
|
enrich_order_display_fields,
|
||||||
|
order_monitor_tpsl_needs_sync,
|
||||||
stop_is_profit_protecting,
|
stop_is_profit_protecting,
|
||||||
tpsl_slot_trigger_price,
|
tpsl_slot_trigger_price,
|
||||||
tpsl_update_passes_rr_gate,
|
tpsl_update_passes_rr_gate,
|
||||||
@@ -5699,45 +5700,30 @@ def api_price_snapshot():
|
|||||||
except Exception:
|
except Exception:
|
||||||
exchange_tpsl = {"sl": None, "tp": None}
|
exchange_tpsl = {"sl": None, "tp": None}
|
||||||
payload["exchange_tpsl"] = exchange_tpsl
|
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(
|
apply_order_price_display_fields(
|
||||||
payload,
|
payload,
|
||||||
direction=r["direction"],
|
direction=r["direction"],
|
||||||
entry_price=entry,
|
entry_price=entry,
|
||||||
initial_stop_loss=r["initial_stop_loss"],
|
initial_stop_loss=r["initial_stop_loss"],
|
||||||
stop_loss=disp_sl,
|
stop_loss=r["stop_loss"],
|
||||||
take_profit=disp_tp,
|
take_profit=r["take_profit"],
|
||||||
calc_rr_ratio_fn=calc_rr_ratio,
|
calc_rr_ratio_fn=calc_rr_ratio,
|
||||||
exchange_tpsl=exchange_tpsl,
|
exchange_tpsl=exchange_tpsl,
|
||||||
|
format_price_fn=format_price_for_symbol,
|
||||||
|
symbol=r["symbol"],
|
||||||
)
|
)
|
||||||
order_prices.append(payload)
|
new_sl, new_tp, changed = order_monitor_tpsl_needs_sync(
|
||||||
if live_sl is not None or live_tp is not None:
|
r["stop_loss"], r["take_profit"], exchange_tpsl
|
||||||
|
)
|
||||||
|
if changed:
|
||||||
try:
|
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(
|
conn.execute(
|
||||||
"UPDATE order_monitors SET stop_loss=?, take_profit=? WHERE id=?",
|
"UPDATE order_monitors SET stop_loss=?, take_profit=? WHERE id=?",
|
||||||
(new_sl, new_tp, int(r["id"])),
|
(new_sl, new_tp, int(r["id"])),
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
order_prices.append(payload)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -1723,14 +1723,16 @@ function paintPlanTpslDisplay(orderId, snap){
|
|||||||
const slEl = document.getElementById(`order-plan-sl-${orderId}`);
|
const slEl = document.getElementById(`order-plan-sl-${orderId}`);
|
||||||
const tpEl = document.getElementById(`order-plan-tp-${orderId}`);
|
const tpEl = document.getElementById(`order-plan-tp-${orderId}`);
|
||||||
const rrEl = document.getElementById(`order-rr-${orderId}`);
|
const rrEl = document.getElementById(`order-rr-${orderId}`);
|
||||||
const slDisp = snap.stop_loss_display;
|
const slRaw = snap.stop_loss_raw != null && snap.stop_loss_raw !== "" ? snap.stop_loss_raw : snap.stop_loss;
|
||||||
const tpDisp = snap.take_profit_display;
|
const tpRaw = snap.take_profit_raw != null && snap.take_profit_raw !== "" ? snap.take_profit_raw : snap.take_profit;
|
||||||
if(slEl && slDisp) slEl.innerText = slDisp;
|
const slDisp = snap.stop_loss_display || (slRaw != null && slRaw !== "" ? formatPriceForInput(slRaw) : null);
|
||||||
if(tpEl && tpDisp) tpEl.innerText = tpDisp;
|
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(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);
|
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);
|
else if(tpDisp) card.setAttribute('data-plan-tp', tpDisp);
|
||||||
}
|
}
|
||||||
if(rrEl && typeof snap.rr_ratio !== "undefined") rrEl.innerText = formatRrRatio(snap.rr_ratio);
|
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 (
|
from order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
enrich_order_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 wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
|
||||||
from hub_auth import request_allowed as hub_request_allowed
|
from hub_auth import request_allowed as hub_request_allowed
|
||||||
@@ -5935,7 +5936,6 @@ def api_price_snapshot():
|
|||||||
order_rows = conn.execute(
|
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'"
|
"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()
|
).fetchall()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
@@ -6122,9 +6122,28 @@ def api_price_snapshot():
|
|||||||
take_profit=r["take_profit"],
|
take_profit=r["take_profit"],
|
||||||
calc_rr_ratio_fn=calc_rr_ratio,
|
calc_rr_ratio_fn=calc_rr_ratio,
|
||||||
exchange_tpsl=exchange_tpsl,
|
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)
|
order_prices.append(payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
conn.close()
|
||||||
|
|
||||||
from hub_position_metrics import build_position_marks_list
|
from hub_position_metrics import build_position_marks_list
|
||||||
|
|
||||||
position_marks = build_position_marks_list(
|
position_marks = build_position_marks_list(
|
||||||
|
|||||||
@@ -550,19 +550,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">止损</span>
|
<span class="pos-label">止损</span>
|
||||||
{% if o.stop_loss %}
|
<span class="pos-value" id="order-plan-sl-{{ o.id }}">{{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}</span>
|
||||||
<span class="pos-value">{{ price_fmt(o.symbol, o.stop_loss) }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="pos-value pos-val-dash">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">止盈</span>
|
<span class="pos-label">止盈</span>
|
||||||
{% if o.take_profit %}
|
<span class="pos-value" id="order-plan-tp-{{ o.id }}">{{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}</span>
|
||||||
<span class="pos-value">{{ price_fmt(o.symbol, o.take_profit) }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="pos-value pos-val-dash">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">盈亏比</span>
|
<span class="pos-label">盈亏比</span>
|
||||||
@@ -1924,6 +1916,24 @@ function paintBreakevenBadge(orderId, secured){
|
|||||||
if(!wrap) return;
|
if(!wrap) return;
|
||||||
wrap.style.display = secured ? "inline-flex" : "none";
|
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){
|
function paintPriceTrend(el, key, value){
|
||||||
if(!el) return;
|
if(!el) return;
|
||||||
@@ -2011,6 +2021,7 @@ function refreshPriceSnapshot(){
|
|||||||
}
|
}
|
||||||
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
||||||
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
|
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
|
||||||
|
paintPlanTpslDisplay(o.id, o);
|
||||||
});
|
});
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
@@ -2281,6 +2292,7 @@ function refreshPriceSnapshotConditional(){
|
|||||||
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
|
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
|
||||||
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
||||||
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
||||||
|
paintPlanTpslDisplay(o.id, o);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
|
|||||||
@@ -1269,6 +1269,15 @@ def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) ->
|
|||||||
o["rr_ratio"] = op["rr_ratio"]
|
o["rr_ratio"] = op["rr_ratio"]
|
||||||
if "sl_breakeven_secured" in op:
|
if "sl_breakeven_secured" in op:
|
||||||
o["sl_breakeven_secured"] = bool(op["sl_breakeven_secured"])
|
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:
|
def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon: dict | None) -> None:
|
||||||
|
|||||||
@@ -1032,33 +1032,33 @@
|
|||||||
return out;
|
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) {
|
function condOrdersFromPosition(pos) {
|
||||||
const cond = dedupeCondOrdersByTrigger(
|
const cond = dedupeCondOrdersByTrigger(
|
||||||
Array.isArray(pos.conditional_orders) ? pos.conditional_orders : []
|
Array.isArray(pos.conditional_orders) ? pos.conditional_orders : []
|
||||||
);
|
);
|
||||||
if (cond.length) return cond;
|
|
||||||
const et = pos.exchange_tpsl;
|
const et = pos.exchange_tpsl;
|
||||||
if (!et) return [];
|
if (!et) return cond;
|
||||||
const out = [];
|
upsertExTpslCondOrder(cond, "sl", et.sl);
|
||||||
if (et.sl && et.sl.trigger_price != null) {
|
upsertExTpslCondOrder(cond, "tp", et.tp);
|
||||||
out.push({
|
return cond;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMonitorOrder(orders, symbol, side) {
|
function findMonitorOrder(orders, symbol, side) {
|
||||||
@@ -1459,8 +1459,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const inferred = inferTpslFromCondOrders(pos.side, cond, entryN);
|
const inferred = inferTpslFromCondOrders(pos.side, cond, entryN);
|
||||||
if (sl === "" || sl == null) sl = inferred.sl;
|
if (inferred.sl !== "" && inferred.sl != null) {
|
||||||
if (!tpMonitored && (takeProfit === "" || takeProfit == null)) takeProfit = inferred.tp;
|
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)) {
|
if (sl !== "" && takeProfit !== "" && Number(sl) === Number(takeProfit)) {
|
||||||
takeProfit = "";
|
takeProfit = "";
|
||||||
|
|||||||
@@ -146,6 +146,44 @@ def apply_order_live_price_display(
|
|||||||
return payload
|
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(
|
def apply_order_price_display_fields(
|
||||||
payload: dict[str, Any],
|
payload: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
@@ -156,7 +194,10 @@ def apply_order_price_display_fields(
|
|||||||
take_profit: Any,
|
take_profit: Any,
|
||||||
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
||||||
exchange_tpsl: Any = None,
|
exchange_tpsl: Any = None,
|
||||||
|
format_price_fn: Optional[Callable[[Any, Any], str]] = None,
|
||||||
|
symbol: Any = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
disp_sl, disp_tp, _, _ = resolve_live_tpsl_prices(stop_loss, take_profit, exchange_tpsl)
|
||||||
payload["rr_ratio"] = snapshot_rr(
|
payload["rr_ratio"] = snapshot_rr(
|
||||||
calc_rr_ratio_fn,
|
calc_rr_ratio_fn,
|
||||||
direction,
|
direction,
|
||||||
@@ -168,4 +209,19 @@ def apply_order_price_display_fields(
|
|||||||
payload["sl_breakeven_secured"] = sl_breakeven_from_exchange_tpsl(
|
payload["sl_breakeven_secured"] = sl_breakeven_from_exchange_tpsl(
|
||||||
direction, entry_price, 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
|
return payload
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
from order_monitor_display_lib import (
|
from order_monitor_display_lib import (
|
||||||
|
apply_order_price_display_fields,
|
||||||
is_sl_breakeven_secured,
|
is_sl_breakeven_secured,
|
||||||
|
order_monitor_tpsl_needs_sync,
|
||||||
|
resolve_live_tpsl_prices,
|
||||||
sl_breakeven_from_exchange_tpsl,
|
sl_breakeven_from_exchange_tpsl,
|
||||||
snapshot_rr,
|
snapshot_rr,
|
||||||
snapshot_stop_loss,
|
snapshot_stop_loss,
|
||||||
@@ -48,3 +51,46 @@ def test_sl_breakeven_from_exchange_tpsl():
|
|||||||
{"sl": {"trigger_price": 2.735}, "tp": {"trigger_price": 3.3}},
|
{"sl": {"trigger_price": 2.735}, "tp": {"trigger_price": 3.3}},
|
||||||
)
|
)
|
||||||
assert ok is True
|
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