fix: 中控改委托后同步计划价并去重条件单展示

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-04 21:12:00 +08:00
parent 54c1984ec7
commit be51eee73f
8 changed files with 376 additions and 33 deletions
+4 -23
View File
@@ -8,6 +8,7 @@ import time
from typing import Any
from lib.exchange.okx_orders_lib import fetch_okx_all_open_orders
from lib.hub.hub_symbol_lib import symbols_match
def _coerce_float(*values) -> float | None:
@@ -37,28 +38,6 @@ def _symbol_base_coin(symbol: str) -> str:
return s
def symbols_match(position_symbol: str, order_symbol: str) -> bool:
a = (position_symbol or "").strip().upper()
b = (order_symbol or "").strip().upper()
if not a or not b:
return False
if a == b:
return True
ba, bb = _symbol_base_coin(a), _symbol_base_coin(b)
if ba and bb and ba == bb:
return True
for suf in (":USDT", "/USDT:USDT", "/USDT"):
a2 = a.replace(suf, "")
b2 = b.replace(suf, "")
if f"{a2}/USDT" == b or f"{a2}/USDT:USDT" == b:
return True
if f"{b2}/USDT" == a or f"{b2}/USDT:USDT" == a:
return True
if a2 == b2:
return True
return False
def _order_type_str(order: dict) -> str:
info = order.get("info") or {}
if isinstance(info, dict):
@@ -424,7 +403,9 @@ def attach_orders_to_positions(positions: list[dict], orders: list[dict]) -> Non
matched = [o for o in orders if symbols_match(sym, o.get("symbol") or "")]
cond = [o for o in matched if o.get("category") == "conditional"]
_enrich_gate_conditional_labels(cond, p.get("side") or "long")
p["conditional_orders"] = cond
from lib.hub.hub_order_sync_lib import dedupe_conditional_orders_by_role
p["conditional_orders"] = dedupe_conditional_orders_by_role(cond)
p["regular_orders"] = [o for o in matched if o.get("category") != "conditional"]
+45 -8
View File
@@ -14,6 +14,11 @@ _REPO_ROOT = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from lib.hub.hub_order_sync_lib import (
cond_order_role,
dedupe_conditional_orders_by_role,
exchange_tpsl_from_cond_orders,
)
from lib.hub.hub_kline_store import format_ohlcv_detail, resolve_chart_bars, retention_days
from lib.hub.hub_ohlcv_lib import (
CHART_TIMEFRAME_ORDER,
@@ -1941,7 +1946,7 @@ def _merge_flask_position_mark_price(
def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict | None) -> None:
"""子代理挂单为空时,用实例 Flask 已算好的 exchange_tpsl 补全展示"""
"""子代理条件单优先;Flask exchange_tpsl 仅补缺失槽位,避免重复止损/止盈行"""
ag = agent_row.get("agent")
if not isinstance(ag, dict):
return
@@ -1949,8 +1954,8 @@ def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict
if not isinstance(positions, list) or not positions:
return
if not isinstance(snap, dict):
return
order_prices = snap.get("order_prices") or []
snap = None
order_prices = (snap or {}).get("order_prices") or []
hub_orders = []
if isinstance(hub_mon, dict):
hub_orders = hub_mon.get("orders") or []
@@ -1959,15 +1964,31 @@ def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict
continue
sym = p.get("symbol") or ""
side = p.get("side") or ""
et = _find_exchange_tpsl_for_position(sym, side, order_prices, hub_orders)
if not et:
et = _exchange_tpsl_from_hub_order(hub_orders, sym, side)
cond = dedupe_conditional_orders_by_role(p.get("conditional_orders") or [])
roles_in_cond = {
r for row in cond if (r := cond_order_role(row)) in ("sl", "tp")
}
et = exchange_tpsl_from_cond_orders(cond)
if not et or roles_in_cond != {"sl", "tp"}:
flask_et = _find_exchange_tpsl_for_position(sym, side, order_prices, hub_orders)
if not flask_et:
flask_et = _exchange_tpsl_from_hub_order(hub_orders, sym, side)
if flask_et:
if et:
for role in ("sl", "tp"):
if not et.get(role) and flask_et.get(role):
et[role] = flask_et[role]
else:
et = flask_et
if not et:
p["conditional_orders"] = cond
continue
p["exchange_tpsl"] = et
cond = p.get("conditional_orders") or []
merged = _tpsl_slots_to_conditional_orders(et, sym)
p["conditional_orders"] = _merge_conditional_orders_no_dup(cond, merged)
extra = [r for r in merged if cond_order_role(r) not in roles_in_cond]
p["conditional_orders"] = dedupe_conditional_orders_by_role(
_merge_conditional_orders_no_dup(cond, extra)
)
async def _fetch_exchange_flask_bundle(
@@ -2392,6 +2413,22 @@ async def api_place_tpsl(exchange_id: str, body: PlaceTpslBody):
"payload": payload,
"ok": bool(isinstance(payload, dict) and payload.get("ok")),
}
if out.get("ok") and (ex.get("flask_url") or "").strip():
async with httpx.AsyncClient() as flask_client:
sync_parsed = await _fetch_flask_json(
flask_client,
ex,
"/api/hub/order/sync-tpsl",
method="POST",
json_body={
"symbol": body.symbol,
"side": body.side,
"stop_loss": body.stop_loss,
"take_profit": body.take_profit,
},
)
if isinstance(sync_parsed, dict):
out["order_sync"] = sync_parsed
_schedule_board_refresh()
return out
+25 -1
View File
@@ -1831,6 +1831,29 @@
return localStorage.getItem(ordersCollapseKey(exchangeId, symbol)) === "1";
}
function condOrderRole(o) {
const lb = (o && o.label) || "";
if (/止盈止损/.test(lb)) return null;
if (/止损/.test(lb)) return "sl";
if (/止盈/.test(lb)) return "tp";
return null;
}
function dedupeCondOrdersByRole(orders) {
const list = Array.isArray(orders) ? orders : [];
const byRole = {};
const others = [];
for (const o of list) {
const role = condOrderRole(o);
if (role) byRole[role] = o;
else others.push(o);
}
const out = others.slice();
if (byRole.tp) out.push(byRole.tp);
if (byRole.sl) out.push(byRole.sl);
return out;
}
function dedupeCondOrdersByTrigger(orders) {
const list = Array.isArray(orders) ? orders : [];
const seen = new Set();
@@ -1869,9 +1892,10 @@
}
function condOrdersFromPosition(pos) {
const cond = dedupeCondOrdersByTrigger(
let cond = dedupeCondOrdersByRole(
Array.isArray(pos.conditional_orders) ? pos.conditional_orders : []
);
cond = dedupeCondOrdersByTrigger(cond);
const et = pos.exchange_tpsl;
if (!et) return cond;
upsertExTpslCondOrder(cond, "sl", et.sl);