增加单仓平仓
This commit is contained in:
+117
-28
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
子账户极轻代理:仅 GET /status + POST /emergency/close-all,仅监听 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_*)
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user