fix: 交易安全审计修复 — 补偿平仓、中控同步、滚仓/趋势防护
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+81
-34
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user