fix: 交易安全审计修复 — 补偿平仓、中控同步、滚仓/趋势防护

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-04 22:44:16 +08:00
parent df28e6dfb8
commit eb975b0133
11 changed files with 675 additions and 162 deletions
+81 -34
View File
@@ -1535,6 +1535,50 @@ async def _notify_instance_user_close(
)
async def _sync_flask_after_position_close(
client: httpx.AsyncClient,
ex: dict,
*,
symbol: str,
side: str,
) -> dict:
"""中控/agent 平仓后同步 Flask order_monitors、趋势与滚仓状态。"""
sym = (symbol or "").strip()
side_l = (side or "").strip().lower()
out: dict = {}
if not sym or side_l not in ("long", "short"):
return out
order_sync = await _fetch_flask_json(
client,
ex,
"/api/hub/order/sync-flat",
method="POST",
json_body={"symbol": sym, "side": side_l},
)
if isinstance(order_sync, dict):
out["order_sync"] = order_sync
if "trend" in (ex.get("capabilities") or []):
sync_parsed = await _fetch_flask_json(
client,
ex,
"/api/hub/trend/sync-flat",
method="POST",
json_body={"symbol": sym, "side": side_l},
)
if isinstance(sync_parsed, dict):
out["trend_sync"] = sync_parsed
roll_sync = await _fetch_flask_json(
client,
ex,
"/api/hub/roll/sync-flat",
method="POST",
json_body={"symbol": sym, "side": side_l},
)
if isinstance(roll_sync, dict):
out["roll_sync"] = roll_sync
return out
def _flask_error_from_hub_mon(hub_mon: dict | None) -> str | None:
if not isinstance(hub_mon, dict) or hub_mon.get("ok") is not False:
return None
@@ -2346,37 +2390,11 @@ async def api_close_position(exchange_id: str, body: ClosePositionBody):
"ok": bool(isinstance(payload, dict) and payload.get("ok")),
}
if out.get("ok"):
ex_key = (ex.get("key") or "").strip().lower()
async with httpx.AsyncClient() as flask_client:
if ex_key == "gate":
order_sync = await _fetch_flask_json(
flask_client,
ex,
"/api/hub/order/sync-flat",
method="POST",
json_body={"symbol": sym, "side": side},
)
if isinstance(order_sync, dict):
out["order_sync"] = order_sync
if "trend" in (ex.get("capabilities") or []):
sync_parsed = await _fetch_flask_json(
flask_client,
ex,
"/api/hub/trend/sync-flat",
method="POST",
json_body={"symbol": sym, "side": side},
)
if isinstance(sync_parsed, dict):
out["trend_sync"] = sync_parsed
roll_sync = await _fetch_flask_json(
flask_client,
ex,
"/api/hub/roll/sync-flat",
method="POST",
json_body={"symbol": sym, "side": side},
sync_bundle = await _sync_flask_after_position_close(
flask_client, ex, symbol=sym, side=side
)
if isinstance(roll_sync, dict):
out["roll_sync"] = roll_sync
out.update(sync_bundle)
risk_sync = await _notify_instance_user_close(flask_client, ex, count=1)
if isinstance(risk_sync, dict):
out["risk_sync"] = risk_sync
@@ -2414,6 +2432,14 @@ async def api_place_tpsl(exchange_id: str, body: PlaceTpslBody):
"ok": bool(isinstance(payload, dict) and payload.get("ok")),
}
if out.get("ok") and (ex.get("flask_url") or "").strip():
placed = payload.get("placed") if isinstance(payload, dict) else None
sl_sync = body.stop_loss
tp_sync = body.take_profit
if isinstance(placed, dict):
if placed.get("stop_loss") is not None:
sl_sync = placed["stop_loss"]
if placed.get("take_profit") is not None:
tp_sync = placed["take_profit"]
async with httpx.AsyncClient() as flask_client:
sync_parsed = await _fetch_flask_json(
flask_client,
@@ -2423,8 +2449,8 @@ async def api_place_tpsl(exchange_id: str, body: PlaceTpslBody):
json_body={
"symbol": body.symbol,
"side": body.side,
"stop_loss": body.stop_loss,
"take_profit": body.take_profit,
"stop_loss": sl_sync,
"take_profit": tp_sync,
},
)
if isinstance(sync_parsed, dict):
@@ -2451,9 +2477,19 @@ async def api_close_exchange(exchange_id: str):
closed = body.get("closed") or []
n = len(closed) if isinstance(closed, list) else 0
if n > 0:
risk_sync = await _notify_instance_user_close(client, ex, count=n)
if isinstance(risk_sync, dict):
out["risk_sync"] = risk_sync
async with httpx.AsyncClient() as flask_client:
for item in closed:
if not isinstance(item, dict):
continue
sym_i = (item.get("symbol") or "").strip()
side_i = (item.get("side") or "").strip().lower()
if sym_i and side_i in ("long", "short"):
await _sync_flask_after_position_close(
flask_client, ex, symbol=sym_i, side=side_i
)
risk_sync = await _notify_instance_user_close(flask_client, ex, count=n)
if isinstance(risk_sync, dict):
out["risk_sync"] = risk_sync
_schedule_board_refresh()
return out
@@ -2478,6 +2514,17 @@ async def api_close_all(body: CloseAllBody | None = Body(default=None)):
closed = payload.get("closed") or []
n = len(closed) if isinstance(closed, list) else 0
if n > 0:
for item in closed:
if not isinstance(item, dict):
continue
sym_i = (item.get("symbol") or "").strip()
side_i = (item.get("side") or "").strip().lower()
if sym_i and side_i in ("long", "short"):
sync_bundle = await _sync_flask_after_position_close(
client, ex, symbol=sym_i, side=side_i
)
if sync_bundle:
row["flask_sync"] = sync_bundle
risk_sync = await _notify_instance_user_close(client, ex, count=n)
if isinstance(risk_sync, dict):
row["risk_sync"] = risk_sync