增加单仓平仓

This commit is contained in:
dekun
2026-05-23 16:52:28 +08:00
parent 80150227e3
commit a4c13fd8cd
5 changed files with 210 additions and 36 deletions
+117 -28
View File
@@ -1,5 +1,5 @@
"""
子账户极轻代理:GET /status + POST /emergency/close-all,仅监听 127.0.0.1。
子账户极轻代理:GET /statusPOST /emergency/close-all、POST /emergency/close-position,仅监听 127.0.0.1。
与仓库内四个策略/监控目录一一对应时,典型用法(各目录自己的 .env 里已有密钥;子代理用环境变量 PORT,勿与 Flask 的 APP_PORT 相同):
EXCHANGE=binance → crypto_monitor_binanceBINANCE_*
@@ -28,6 +28,7 @@ from typing import Any
import ccxt
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
HOST = os.getenv("HOST", "127.0.0.1")
PORT = int(os.getenv("PORT", "15200"))
@@ -275,6 +276,47 @@ def _cancel_symbol_orders(ex: Any, sym: str) -> None:
pass
class EmergencyClosePositionBody(BaseModel):
symbol: str
side: str
def _close_position_market(
ex: Any, sym: str, side: str, contracts: float
) -> tuple[dict[str, Any] | None, str | None]:
"""市价平掉指定合约、方向;返回 (closed_info, error_message)。"""
side_n = (side or "").strip().lower()
if side_n not in ("long", "short"):
return None, f"无效方向: {side}"
close_side = "sell" if side_n == "long" else "buy"
direction = side_n
try:
amt = float(ex.amount_to_precision(sym, abs(float(contracts))))
except Exception:
amt = abs(float(contracts))
if amt <= 0:
return None, f"{sym}: 可平张数为 0"
order_resp = None
last_err: Exception | None = None
for params in _close_param_candidates(direction):
try:
order_resp = ex.create_order(sym, "market", close_side, amt, None, params)
last_err = None
break
except Exception as e:
last_err = e
if _retryable_close_err(str(e)):
continue
return None, f"{sym}: {e}"
if order_resp is None:
return None, f"{sym}: {last_err or '下单失败'}"
_cancel_symbol_orders(ex, sym)
return (
{"symbol": sym, "side": side_n, "amount": amt, "order_id": order_resp.get("id")},
None,
)
def _is_local(host: str | None) -> bool:
if not host:
return False
@@ -526,38 +568,85 @@ def emergency_close_all(x_control_token: str | None = Header(default=None, alias
if not sym:
continue
side = _position_side(p, c)
close_side = "sell" if side == "long" else "buy"
direction = "long" if side == "long" else "short"
try:
amt = float(ex.amount_to_precision(sym, abs(c)))
except Exception:
amt = abs(c)
if amt <= 0:
continue
order_resp = None
last_err: Exception | None = None
for params in _close_param_candidates(direction):
try:
order_resp = ex.create_order(sym, "market", close_side, amt, None, params)
last_err = None
break
except Exception as e:
last_err = e
if _retryable_close_err(str(e)):
continue
errors.append(f"{sym}: {e}")
order_resp = None
break
if order_resp is None and last_err and sym not in "".join(errors):
errors.append(f"{sym}: {last_err}")
if order_resp is not None:
closed.append({"symbol": sym, "side": side, "amount": amt, "order_id": order_resp.get("id")})
_cancel_symbol_orders(ex, sym)
info, err = _close_position_market(ex, sym, side, abs(c))
if err:
errors.append(err)
elif info:
closed.append(info)
time.sleep(0.05)
return {"ok": len(errors) == 0, "closed": closed, "errors": errors, "exchange": EXCHANGE_KIND}
@app.post("/emergency/close-position")
def emergency_close_position(
body: EmergencyClosePositionBody,
x_control_token: str | None = Header(default=None, alias="X-Control-Token"),
):
_check_token(x_control_token)
sym = (body.symbol or "").strip()
want_side = (body.side or "").strip().lower()
if not sym:
raise HTTPException(status_code=400, detail="symbol 不能为空")
if want_side not in ("long", "short"):
raise HTTPException(status_code=400, detail="side 须为 long 或 short")
try:
ex = get_exchange()
except RuntimeError as e:
raise HTTPException(status_code=503, detail=str(e)) from e
try:
_ensure_markets()
except Exception as e:
return JSONResponse(
{
"ok": False,
"error": f"load_markets: {e}",
"closed": None,
"exchange": EXCHANGE_KIND,
},
status_code=200,
)
try:
raw = ex.fetch_positions() or []
except Exception as e:
raise HTTPException(status_code=502, detail=f"fetch_positions: {e}") from e
matched = None
for p in raw:
if not isinstance(p, dict):
continue
if (p.get("symbol") or "").strip() != sym:
continue
c = _position_contracts(p)
if abs(c) < 1e-12:
continue
side = _position_side(p, c)
if side != want_side:
continue
matched = (sym, side, abs(c))
break
if not matched:
return JSONResponse(
{
"ok": False,
"error": f"未找到持仓: {sym} {want_side}",
"closed": None,
"exchange": EXCHANGE_KIND,
},
status_code=200,
)
sym, side, c = matched
info, err = _close_position_market(ex, sym, side, c)
if err:
return JSONResponse(
{"ok": False, "error": err, "closed": None, "exchange": EXCHANGE_KIND},
status_code=200,
)
return {"ok": True, "closed": info, "errors": [], "exchange": EXCHANGE_KIND}
def main():
import uvicorn