diff --git a/manual_trading_hub/agent.py b/manual_trading_hub/agent.py
index 6b406a8..af75afc 100644
--- a/manual_trading_hub/agent.py
+++ b/manual_trading_hub/agent.py
@@ -1,5 +1,5 @@
"""
-子账户极轻代理:GET /status、POST /emergency/close-all、POST /emergency/close-position,仅监听 127.0.0.1。
+子账户极轻代理:GET /status、挂单/条件单查询与撤销、POST /emergency/close-all、POST /emergency/close-position,仅监听 127.0.0.1。
与仓库内四个策略/监控目录一一对应时,典型用法(各目录自己的 .env 里已有密钥;子代理用环境变量 PORT,勿与 Flask 的 APP_PORT 相同):
EXCHANGE=binance → crypto_monitor_binance(BINANCE_*)
@@ -30,6 +30,13 @@ from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
+from exchange_orders import (
+ attach_orders_to_positions,
+ cancel_order as hub_cancel_order,
+ cancel_orders_for_symbol,
+ list_open_orders,
+)
+
HOST = os.getenv("HOST", "127.0.0.1")
PORT = int(os.getenv("PORT", "15200"))
CONTROL_TOKEN = (os.getenv("CONTROL_TOKEN") or "").strip()
@@ -281,6 +288,17 @@ class EmergencyClosePositionBody(BaseModel):
side: str
+class CancelOrderBody(BaseModel):
+ symbol: str
+ order_id: str
+ channel: str = "regular"
+
+
+class CancelSymbolOrdersBody(BaseModel):
+ symbol: str
+ scope: str = "all" # all | conditional | limit
+
+
def _close_position_market(
ex: Any, sym: str, side: str, contracts: float
) -> tuple[dict[str, Any] | None, str | None]:
@@ -522,6 +540,16 @@ def _status_inner(x_control_token: str | None) -> Any:
}
)
+ try:
+ attach_orders_to_positions(
+ positions_out,
+ list_open_orders(ex, EXCHANGE_KIND, None),
+ )
+ except Exception:
+ for p in positions_out:
+ p.setdefault("conditional_orders", [])
+ p.setdefault("regular_orders", [])
+
try:
pm = _position_mode_label()
except Exception:
@@ -536,6 +564,71 @@ def _status_inner(x_control_token: str | None) -> Any:
}
+@app.get("/open-orders")
+def open_orders(
+ symbol: str = "",
+ x_control_token: str | None = Header(default=None, alias="X-Control-Token"),
+):
+ _check_token(x_control_token)
+ try:
+ ex = get_exchange()
+ _ensure_markets()
+ sym = (symbol or "").strip() or None
+ orders = list_open_orders(ex, EXCHANGE_KIND, sym)
+ return {"ok": True, "exchange": EXCHANGE_KIND, "symbol": sym, "orders": orders}
+ except Exception as e:
+ return JSONResponse(
+ {"ok": False, "error": str(e), "exchange": EXCHANGE_KIND, "orders": []},
+ status_code=200,
+ )
+
+
+@app.post("/orders/cancel")
+def cancel_one_order(
+ body: CancelOrderBody,
+ x_control_token: str | None = Header(default=None, alias="X-Control-Token"),
+):
+ _check_token(x_control_token)
+ sym = (body.symbol or "").strip()
+ oid = (body.order_id or "").strip()
+ if not sym or not oid:
+ raise HTTPException(status_code=400, detail="symbol 与 order_id 必填")
+ try:
+ ex = get_exchange()
+ _ensure_markets()
+ hub_cancel_order(ex, EXCHANGE_KIND, sym, oid, body.channel or "regular")
+ return {"ok": True, "exchange": EXCHANGE_KIND, "cancelled": {"symbol": sym, "order_id": oid}}
+ except Exception as e:
+ return JSONResponse(
+ {"ok": False, "error": str(e), "exchange": EXCHANGE_KIND},
+ status_code=200,
+ )
+
+
+@app.post("/orders/cancel-symbol")
+def cancel_symbol_orders(
+ body: CancelSymbolOrdersBody,
+ x_control_token: str | None = Header(default=None, alias="X-Control-Token"),
+):
+ _check_token(x_control_token)
+ sym = (body.symbol or "").strip()
+ if not sym:
+ raise HTTPException(status_code=400, detail="symbol 必填")
+ scope = (body.scope or "all").strip().lower()
+ if scope not in ("all", "conditional", "limit"):
+ raise HTTPException(status_code=400, detail="scope 须为 all / conditional / limit")
+ try:
+ ex = get_exchange()
+ _ensure_markets()
+ n = cancel_orders_for_symbol(ex, EXCHANGE_KIND, sym, scope=scope)
+ return {"ok": True, "exchange": EXCHANGE_KIND, "cancelled_count": n, "scope": scope}
+ except Exception as e:
+ return JSONResponse(
+ {"ok": False, "error": str(e), "exchange": EXCHANGE_KIND, "cancelled_count": 0},
+ status_code=200,
+ )
+
+
@app.post("/emergency/close-all")
def emergency_close_all(x_control_token: str | None = Header(default=None, alias="X-Control-Token")):
_check_token(x_control_token)
diff --git a/manual_trading_hub/exchange_orders.py b/manual_trading_hub/exchange_orders.py
new file mode 100644
index 0000000..03642f1
--- /dev/null
+++ b/manual_trading_hub/exchange_orders.py
@@ -0,0 +1,355 @@
+"""
+中控子代理:拉取交易所挂单/条件单并规范化展示;撤销单笔或按合约批量撤销。
+"""
+from __future__ import annotations
+
+from typing import Any
+
+
+def _coerce_float(*values) -> float | None:
+ for v in values:
+ if v is None or v == "":
+ continue
+ try:
+ return float(v)
+ except (TypeError, ValueError):
+ continue
+ return None
+
+
+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
+ 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):
+ for key in ("orderType", "type", "origType", "algoType", "ordType"):
+ val = info.get(key)
+ if val:
+ return str(val).upper()
+ return str(order.get("type") or "").upper()
+
+
+def _is_conditional_type(typ: str) -> bool:
+ t = (typ or "").upper()
+ if not t:
+ return False
+ keys = ("STOP", "TAKE_PROFIT", "TRAIL", "TRIGGER", "CONDITIONAL")
+ return any(k in t for k in keys)
+
+
+def _order_label(typ: str, side: str, reduce_only: bool | None) -> str:
+ t = (typ or "").upper()
+ side_l = (side or "").lower()
+ parts = []
+ if "TAKE_PROFIT" in t:
+ parts.append("止盈")
+ elif "STOP" in t:
+ parts.append("止损")
+ elif "LIMIT" in t:
+ parts.append("限价")
+ elif "MARKET" in t:
+ parts.append("市价")
+ else:
+ parts.append(typ or "委托")
+ if side_l == "buy":
+ parts.append("买入")
+ elif side_l == "sell":
+ parts.append("卖出")
+ if reduce_only:
+ parts.append("·只减仓")
+ return " ".join(parts)
+
+
+def _normalize_raw_order(order: dict, *, channel: str) -> dict[str, Any] | None:
+ if not isinstance(order, dict):
+ return None
+ info = order.get("info") or {}
+ if not isinstance(info, dict):
+ info = {}
+ oid = order.get("id") or info.get("algoId") or info.get("orderId") or info.get("ordId")
+ if oid is None:
+ return None
+ sym = str(order.get("symbol") or info.get("symbol") or "")
+ typ = _order_type_str(order)
+ side = str(order.get("side") or info.get("side") or "").lower()
+ reduce_only = order.get("reduceOnly")
+ if reduce_only is None:
+ reduce_only = info.get("reduceOnly")
+ try:
+ reduce_only = bool(reduce_only) if reduce_only is not None else None
+ except (TypeError, ValueError):
+ reduce_only = None
+ trig = _coerce_float(
+ order.get("stopPrice"),
+ order.get("triggerPrice"),
+ info.get("triggerPrice"),
+ info.get("stopPrice"),
+ info.get("slTriggerPx"),
+ info.get("tpTriggerPx"),
+ )
+ price = _coerce_float(order.get("price"), info.get("price"))
+ amt = _coerce_float(order.get("amount"), order.get("remaining"), info.get("quantity"), info.get("origQty"))
+ category = "conditional" if _is_conditional_type(typ) or channel == "algo" else "limit"
+ return {
+ "id": str(oid),
+ "symbol": sym,
+ "channel": channel,
+ "category": category,
+ "label": _order_label(typ, side, reduce_only),
+ "type": typ,
+ "side": side,
+ "amount": amt,
+ "trigger_price": trig,
+ "price": price,
+ "reduce_only": reduce_only,
+ "status": str(order.get("status") or info.get("status") or "open"),
+ }
+
+
+def _binance_list(ex: Any, symbol: str | None) -> list[dict]:
+ ex.load_markets()
+ out: list[dict] = []
+ symbols: list[str] = []
+ if symbol:
+ try:
+ symbols = [ex.market(symbol)["symbol"]]
+ except Exception:
+ symbols = [symbol]
+ else:
+ symbols = []
+ try:
+ for p in ex.fetch_positions() or []:
+ sym = p.get("symbol")
+ if sym:
+ symbols.append(sym)
+ except Exception:
+ pass
+ if symbol and not symbols:
+ symbols = [symbol]
+
+ def collect(ex_sym: str) -> None:
+ market = ex.market(ex_sym)
+ contract_id = market.get("id")
+ try:
+ for o in ex.fetch_open_orders(ex_sym) or []:
+ item = dict(o)
+ item["_channel"] = "regular"
+ n = _normalize_raw_order(item, channel="regular")
+ if n:
+ out.append(n)
+ except Exception:
+ pass
+ try:
+ if contract_id and hasattr(ex, "fapiPrivateGetOpenAlgoOrders"):
+ raw = ex.fapiPrivateGetOpenAlgoOrders({"symbol": contract_id})
+ items = raw if isinstance(raw, list) else (raw.get("orders") or raw.get("data") or [])
+ for info in items or []:
+ if not isinstance(info, dict):
+ continue
+ wrapped = {
+ "id": info.get("algoId") or info.get("orderId"),
+ "symbol": ex_sym,
+ "info": info,
+ "type": info.get("orderType") or info.get("type"),
+ "side": (info.get("side") or "").lower(),
+ "amount": info.get("quantity") or info.get("origQty"),
+ "stopPrice": info.get("triggerPrice") or info.get("stopPrice"),
+ "reduceOnly": info.get("reduceOnly"),
+ }
+ n = _normalize_raw_order(wrapped, channel="algo")
+ if n:
+ out.append(n)
+ except Exception:
+ pass
+
+ if symbols:
+ seen = set()
+ for s in symbols:
+ if s in seen:
+ continue
+ seen.add(s)
+ collect(s)
+ return out
+
+
+def _okx_list(ex: Any, symbol: str | None) -> list[dict]:
+ ex.load_markets()
+ out: list[dict] = []
+ symbols: list[str] = []
+ if symbol:
+ try:
+ symbols = [ex.market(symbol)["symbol"]]
+ except Exception:
+ symbols = [symbol]
+ else:
+ try:
+ for p in ex.fetch_positions() or []:
+ sym = p.get("symbol")
+ if sym:
+ symbols.append(sym)
+ except Exception:
+ pass
+ if symbol and not symbols:
+ symbols = [symbol]
+ seen = set()
+ for sym in symbols:
+ if sym in seen:
+ continue
+ seen.add(sym)
+ try:
+ for o in ex.fetch_open_orders(sym) or []:
+ ch = "algo" if _is_conditional_type(_order_type_str(o)) else "regular"
+ n = _normalize_raw_order(dict(o), channel=ch)
+ if n:
+ out.append(n)
+ except Exception:
+ pass
+ return out
+
+
+def _gate_trigger_params(ex: Any) -> dict:
+ p = {"type": "swap", "trigger": True}
+ try:
+ ex.load_unified_status()
+ if ex.options.get("unifiedAccount"):
+ p["unifiedAccount"] = True
+ except Exception:
+ pass
+ return p
+
+
+def _gate_list(ex: Any, symbol: str | None) -> list[dict]:
+ ex.load_markets()
+ out: list[dict] = []
+ symbols: list[str] = []
+ if symbol:
+ try:
+ symbols = [ex.market(symbol)["symbol"]]
+ except Exception:
+ symbols = [symbol]
+ else:
+ try:
+ for p in ex.fetch_positions() or []:
+ sym = p.get("symbol")
+ if sym:
+ symbols.append(sym)
+ except Exception:
+ pass
+ if symbol and not symbols:
+ symbols = [symbol]
+ trig_params = _gate_trigger_params(ex)
+ seen = set()
+ for sym in symbols:
+ if sym in seen:
+ continue
+ seen.add(sym)
+ try:
+ for o in ex.fetch_open_orders(sym) or []:
+ n = _normalize_raw_order(dict(o), channel="regular")
+ if n:
+ out.append(n)
+ except Exception:
+ pass
+ try:
+ for o in ex.fetch_open_orders(sym, params=trig_params) or []:
+ item = dict(o)
+ item["type"] = item.get("type") or "trigger"
+ n = _normalize_raw_order(item, channel="algo")
+ if n:
+ out.append(n)
+ except Exception:
+ pass
+ return out
+
+
+def list_open_orders(ex: Any, exchange_kind: str, symbol: str | None = None) -> list[dict]:
+ kind = (exchange_kind or "binance").lower()
+ if kind == "binance":
+ orders = _binance_list(ex, symbol)
+ elif kind == "okx":
+ orders = _okx_list(ex, symbol)
+ else:
+ orders = _gate_list(ex, symbol)
+ if symbol:
+ orders = [o for o in orders if symbols_match(symbol, o.get("symbol") or "")]
+ # 去重 id+channel
+ seen: set[tuple[str, str]] = set()
+ uniq: list[dict] = []
+ for o in orders:
+ key = (o["id"], o["channel"])
+ if key in seen:
+ continue
+ seen.add(key)
+ uniq.append(o)
+ return uniq
+
+
+def attach_orders_to_positions(positions: list[dict], orders: list[dict]) -> None:
+ for p in positions:
+ sym = p.get("symbol") or ""
+ matched = [o for o in orders if symbols_match(sym, o.get("symbol") or "")]
+ p["conditional_orders"] = [o for o in matched if o.get("category") == "conditional"]
+ p["regular_orders"] = [o for o in matched if o.get("category") != "conditional"]
+
+
+def cancel_order(
+ ex: Any,
+ exchange_kind: str,
+ symbol: str,
+ order_id: str,
+ channel: str = "regular",
+) -> None:
+ kind = (exchange_kind or "binance").lower()
+ ex.load_markets()
+ market = ex.market(symbol)
+ unified = market["symbol"]
+ ch = (channel or "regular").lower()
+ if kind == "binance" and ch == "algo":
+ contract_id = market.get("id")
+ if contract_id and hasattr(ex, "fapiPrivateDeleteAlgoOrder"):
+ ex.fapiPrivateDeleteAlgoOrder({"symbol": contract_id, "algoId": str(order_id)})
+ return
+ params = None
+ if kind == "gate" and ch == "algo":
+ params = _gate_trigger_params(ex)
+ ex.cancel_order(str(order_id), unified, params)
+
+
+def cancel_orders_for_symbol(
+ ex: Any,
+ exchange_kind: str,
+ symbol: str,
+ *,
+ scope: str = "all",
+) -> int:
+ """scope: all | conditional | limit"""
+ orders = list_open_orders(ex, exchange_kind, symbol)
+ if scope == "conditional":
+ orders = [o for o in orders if o.get("category") == "conditional"]
+ elif scope == "limit":
+ orders = [o for o in orders if o.get("category") != "conditional"]
+ n = 0
+ for o in orders:
+ try:
+ cancel_order(ex, exchange_kind, symbol, o["id"], o.get("channel") or "regular")
+ n += 1
+ except Exception:
+ pass
+ return n
diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py
index 64289d2..227daee 100644
--- a/manual_trading_hub/hub.py
+++ b/manual_trading_hub/hub.py
@@ -43,7 +43,7 @@ HUB_BRIDGE_TOKEN = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN")
_trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower()
HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off")
DIR = Path(__file__).resolve().parent
-HUB_BUILD = "20260522-settings-fix"
+HUB_BUILD = "20260524-open-orders"
def _is_local(host: str | None) -> bool:
@@ -390,6 +390,71 @@ class ClosePositionBody(BaseModel):
side: str
+class CancelOrderBody(BaseModel):
+ symbol: str
+ order_id: str
+ channel: str = "regular"
+
+
+class CancelSymbolOrdersBody(BaseModel):
+ symbol: str
+ scope: str = "all"
+
+
+@app.post("/api/orders/{exchange_id}/cancel")
+async def api_cancel_order(exchange_id: str, body: CancelOrderBody):
+ ex = _find_exchange(exchange_id)
+ if not ex or not ex.get("enabled"):
+ raise HTTPException(status_code=404, detail="账户未启用")
+ url = f"{ex['agent_url'].rstrip('/')}/orders/cancel"
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ url,
+ headers=_agent_headers(),
+ json={
+ "symbol": body.symbol,
+ "order_id": body.order_id,
+ "channel": body.channel or "regular",
+ },
+ timeout=60.0,
+ )
+ try:
+ payload = r.json()
+ except Exception:
+ payload = {"raw": (r.text or "")[:2000]}
+ return {
+ "exchange": ex,
+ "status_code": r.status_code,
+ "payload": payload,
+ "ok": bool(isinstance(payload, dict) and payload.get("ok")),
+ }
+
+
+@app.post("/api/orders/{exchange_id}/cancel-symbol")
+async def api_cancel_symbol_orders(exchange_id: str, body: CancelSymbolOrdersBody):
+ ex = _find_exchange(exchange_id)
+ if not ex or not ex.get("enabled"):
+ raise HTTPException(status_code=404, detail="账户未启用")
+ url = f"{ex['agent_url'].rstrip('/')}/orders/cancel-symbol"
+ async with httpx.AsyncClient() as client:
+ r = await client.post(
+ url,
+ headers=_agent_headers(),
+ json={"symbol": body.symbol, "scope": body.scope or "all"},
+ timeout=120.0,
+ )
+ try:
+ payload = r.json()
+ except Exception:
+ payload = {"raw": (r.text or "")[:2000]}
+ return {
+ "exchange": ex,
+ "status_code": r.status_code,
+ "payload": payload,
+ "ok": bool(isinstance(payload, dict) and payload.get("ok")),
+ }
+
+
@app.post("/api/close/{exchange_id}/position")
async def api_close_position(exchange_id: str, body: ClosePositionBody):
ex = _find_exchange(exchange_id)
diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css
index 459390c..a0fac71 100644
--- a/manual_trading_hub/static/app.css
+++ b/manual_trading_hub/static/app.css
@@ -545,6 +545,55 @@ button:disabled {
margin-top: 0;
}
+.pos-block {
+ margin-bottom: 14px;
+ padding-bottom: 10px;
+ border-bottom: 1px dashed var(--border-soft);
+}
+
+.pos-block:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+}
+
+.pos-orders {
+ margin: 8px 0 0;
+ padding: 8px 10px;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ border: 1px solid var(--border-soft);
+}
+
+.pos-orders-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 6px;
+}
+
+.pos-orders-title {
+ font-size: 10px;
+ color: var(--muted);
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.data-table-sub {
+ font-size: 10px;
+}
+
+.data-table-sub th,
+.data-table-sub td {
+ padding: 5px 6px;
+}
+
+.order-empty {
+ font-size: 11px;
+ color: var(--muted);
+ padding: 6px 4px 8px;
+}
+
.data-table {
width: 100%;
border-collapse: collapse;
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index ed51207..bd8d16a 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -136,11 +136,124 @@
btn.onclick = () =>
closeOnePosition(btn.dataset.exId, btn.dataset.symbol, btn.dataset.side);
});
+ box.querySelectorAll(".btn-cancel-order").forEach((btn) => {
+ btn.onclick = () =>
+ cancelOneOrder(
+ btn.dataset.exId,
+ btn.dataset.symbol,
+ btn.dataset.orderId,
+ btn.dataset.channel
+ );
+ });
+ box.querySelectorAll(".btn-cancel-cond-all").forEach((btn) => {
+ btn.onclick = () =>
+ cancelSymbolOrders(btn.dataset.exId, btn.dataset.symbol, "conditional");
+ });
} catch (e) {
box.innerHTML = `
${esc(e)}
`;
}
}
+ function renderOrderRows(exchangeId, symbol, orders, kind) {
+ if (!orders || !orders.length) {
+ const hint =
+ kind === "conditional"
+ ? "暂无条件单(止盈/止损等)"
+ : "暂无普通委托";
+ return `${hint}
`;
+ }
+ const symAttr = esc(symbol || "").replace(/"/g, """);
+ const rows = orders
+ .map((o) => {
+ const oidAttr = esc(o.id || "").replace(/"/g, """);
+ const chAttr = esc(o.channel || "regular").replace(/"/g, """);
+ const trig =
+ o.trigger_price != null ? fmt(o.trigger_price, 4) : o.price != null ? fmt(o.price, 4) : "—";
+ return `
+ | ${esc(o.label || o.type || "委托")} |
+ ${fmt(o.amount, 4)} |
+ ${trig} |
+ |
+
`;
+ })
+ .join("");
+ return ``;
+ }
+
+ function renderPositionBlock(exchangeId, x) {
+ const symAttr = esc(x.symbol || "").replace(/"/g, """);
+ const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
+ const cond = Array.isArray(x.conditional_orders) ? x.conditional_orders : [];
+ const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
+ const condAllBtn =
+ cond.length > 0
+ ? ``
+ : "";
+ return `
+
| 合约 | 方向 | 张数 | 浮盈 | 操作 |
+
+ | ${esc(x.symbol)} |
+ ${esc(x.side)} |
+ ${fmt(x.contracts, 4)} |
+ ${fmt(x.unrealized_pnl, 4)} |
+ |
+
+
+
+
+ 条件单 · ${cond.length}
+ ${condAllBtn}
+
+ ${renderOrderRows(exchangeId, x.symbol, cond, "conditional")}
+
+ 普通委托 · ${reg.length}
+
+ ${renderOrderRows(exchangeId, x.symbol, reg, "limit")}
+
+
`;
+ }
+
+ async function cancelOneOrder(exchangeId, symbol, orderId, channel) {
+ if (!confirm(`撤销委托 ${symbol} #${orderId}?`)) return;
+ try {
+ const r = await apiFetch("/api/orders/" + encodeURIComponent(exchangeId) + "/cancel", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ symbol, order_id: orderId, channel: channel || "regular" }),
+ });
+ const j = await r.json();
+ const pl = j.payload || {};
+ const ok = j.ok && pl.ok !== false;
+ showToast(ok ? "已撤单" : pl.error || JSON.stringify(j), !ok);
+ loadMonitorBoard();
+ } catch (e) {
+ showToast(String(e), true);
+ }
+ }
+
+ async function cancelSymbolOrders(exchangeId, symbol, scope) {
+ const label = scope === "conditional" ? "全部条件单" : "全部委托";
+ if (!confirm(`确认撤销 ${symbol} 的${label}?`)) return;
+ try {
+ const r = await apiFetch(
+ "/api/orders/" + encodeURIComponent(exchangeId) + "/cancel-symbol",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ symbol, scope }),
+ }
+ );
+ const j = await r.json();
+ const pl = j.payload || {};
+ const ok = j.ok && pl.ok !== false;
+ const n = pl.cancelled_count != null ? pl.cancelled_count : "?";
+ showToast(ok ? `已撤销 ${n} 笔` : pl.error || JSON.stringify(j), !ok);
+ loadMonitorBoard();
+ } catch (e) {
+ showToast(String(e), true);
+ }
+ }
+
function renderMonitorCard(row) {
const ag = row.agent || {};
const pos = Array.isArray(ag.positions) ? ag.positions : [];
@@ -168,20 +281,7 @@
`;
inner += `交易所持仓
`;
if (pos.length) {
- const posRows = pos
- .map((x) => {
- const symAttr = esc(x.symbol || "").replace(/"/g, """);
- const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
- return `
- | ${esc(x.symbol)} |
- ${esc(x.side)} |
- ${fmt(x.contracts, 4)} |
- ${fmt(x.unrealized_pnl, 4)} |
- |
-
`;
- })
- .join("");
- inner += ``;
+ inner += pos.map((x) => renderPositionBlock(row.id, x)).join("");
} else {
inner += `无持仓
`;
}
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html
index 31e6e1c..556fb3a 100644
--- a/manual_trading_hub/static/index.html
+++ b/manual_trading_hub/static/index.html
@@ -7,7 +7,7 @@
-
+
@@ -80,6 +80,6 @@
-
+