Files
crypto_monitor/manual_trading_hub/agent.py
T
2026-06-04 20:20:36 +08:00

909 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
子账户极轻代理:GET /status、挂单/条件单查询与撤销、POST /emergency/close-all、POST /emergency/close-position,仅监听 127.0.0.1。
与仓库内四个策略/监控目录一一对应时,典型用法(各目录自己的 .env 里已有密钥;子代理用环境变量 PORT,勿与 Flask 的 APP_PORT 相同):
EXCHANGE=binance → crypto_monitor_binanceBINANCE_*
EXCHANGE=okx → crypto_monitor_okxOKX_*
EXCHANGE=gate → crypto_monitor_gate / crypto_monitor_gate_botGATE_*
环境变量:
EXCHANGE binance(默认)| okx | gate
PORT 默认 15200(与 crypto_monitor_* 的 Flask APP_PORT 错开;中控默认聚合 1520015203)
HOST 默认 127.0.0.1
CONTROL_TOKEN 可选;请求头 X-Control-Token
BinanceBINANCE_API_KEY / BINANCE_API_SECRET;余额为 **U 本位永续合约账户** USDT(与 `crypto_monitor_binance` 的合约口径一致,非现货钱包);BINANCE_POSITION_MODEBINANCE_MARGIN_MODE
OKXOKX_API_KEY / OKX_API_SECRET / OKX_API_PASSPHRASEOKX_TD_MODEOKX_POS_MODE
GateGATE_API_KEY / GATE_API_SECRETGATE_TD_MODEGATE_POS_MODE
代理与主项目一致时可设:BINANCE_SOCKS_PROXY / OKX_SOCKS_PROXY / GATE_SOCKS_PROXY(或 HTTP(S)_PROXY)。
"""
from __future__ import annotations
import math
import os
import sys
import time
from pathlib import Path
from typing import Any
_REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from hub_ohlcv_lib import format_price_by_tick, price_tick_from_market
from hub_position_metrics import (
parse_position_entry_price,
parse_position_mark_price,
parse_position_unrealized_pnl,
resolve_position_display_upnl,
)
import ccxt
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,
replace_position_tpsl,
symbols_match,
)
HOST = os.getenv("HOST", "127.0.0.1")
PORT = int(os.getenv("PORT", "15200"))
CONTROL_TOKEN = (os.getenv("CONTROL_TOKEN") or "").strip()
_raw_ex = (os.getenv("EXCHANGE") or "binance").strip().lower()
if _raw_ex in ("binance", "bnb", "ba"):
EXCHANGE_KIND = "binance"
elif _raw_ex in ("okx", "okex"):
EXCHANGE_KIND = "okx"
elif _raw_ex in ("gate", "gateio"):
EXCHANGE_KIND = "gate"
else:
EXCHANGE_KIND = "binance"
# —— Binance ——
_bin_pos = (os.getenv("BINANCE_POSITION_MODE") or "hedge").strip().lower()
BINANCE_POSITION_MODE = "hedge" if _bin_pos in ("hedge", "dual", "double", "hedged") else "oneway"
_bin_margin = (os.getenv("BINANCE_MARGIN_MODE") or "cross").strip().lower()
BINANCE_DEFAULT_MARGIN_MODE = "cross" if _bin_margin in ("cross", "cross_margin") else "isolated"
# —— OKX ——
OKX_TD_MODE = (os.getenv("OKX_TD_MODE") or "cross").strip()
_okx_pos = (os.getenv("OKX_POS_MODE") or "hedge").strip().lower()
OKX_POS_MODE = "hedge" if _okx_pos in ("hedge", "long_short_mode", "dual") else "net"
# —— Gate ——
_gate_td = (os.getenv("GATE_TD_MODE") or "cross").strip().lower()
GATE_DEFAULT_MARGIN_MODE = "cross" if _gate_td in ("cross", "cross_margin") else "isolated"
_gate_pos = (os.getenv("GATE_POS_MODE") or "hedge").strip().lower()
GATE_POS_MODE = "hedge" if _gate_pos in ("hedge", "dual", "double") else "single"
app = FastAPI(title="sub-agent", docs_url=None, redoc_url=None)
_ccxt_ex: Any = None
_markets_loaded = False
def _socks_proxy_url(prefix: str) -> str:
return (os.getenv(f"{prefix}_SOCKS_PROXY") or "").strip()
def _http_https_proxy(prefix: str) -> dict[str, str] | None:
http = (os.getenv(f"{prefix}_HTTP_PROXY") or "").strip()
https = (os.getenv(f"{prefix}_HTTPS_PROXY") or "").strip()
socks = _socks_proxy_url(prefix)
if socks:
return {"http": socks, "https": socks}
if http or https:
return {"http": http, "https": https}
return None
def _attach_proxies(ex: Any, prefix: str) -> None:
p = _http_https_proxy(prefix)
if p:
ex.proxies = p
def _make_exchange() -> Any:
if EXCHANGE_KIND == "binance":
key = (os.getenv("BINANCE_API_KEY") or "").strip()
secret = (os.getenv("BINANCE_API_SECRET") or "").strip()
if not key or not secret:
raise RuntimeError("缺少 BINANCE_API_KEY / BINANCE_API_SECRET")
ex = ccxt.binance(
{
"apiKey": key,
"secret": secret,
"enableRateLimit": True,
"options": {
"defaultType": "swap",
# ccxt 默认 fetch_balance 走现货;与监控项目一致,固定为 U 本位合约钱包
"fetchBalance": {"defaultType": "swap"},
"defaultMarginMode": BINANCE_DEFAULT_MARGIN_MODE,
"adjustForTimeDifference": True,
},
}
)
_attach_proxies(ex, "BINANCE")
return ex
if EXCHANGE_KIND == "okx":
key = (os.getenv("OKX_API_KEY") or "").strip()
secret = (os.getenv("OKX_API_SECRET") or "").strip()
password = (os.getenv("OKX_API_PASSPHRASE") or "").strip()
if not key or not secret or not password:
raise RuntimeError("缺少 OKX_API_KEY / OKX_API_SECRET / OKX_API_PASSPHRASE")
ex = ccxt.okx(
{
"apiKey": key,
"secret": secret,
"password": password,
"enableRateLimit": True,
"options": {
"defaultType": "swap",
"hedged": OKX_POS_MODE == "hedge",
},
}
)
_attach_proxies(ex, "OKX")
return ex
# gate
key = (os.getenv("GATE_API_KEY") or "").strip()
secret = (os.getenv("GATE_API_SECRET") or "").strip()
if not key or not secret:
raise RuntimeError("缺少 GATE_API_KEY / GATE_API_SECRET")
ex = ccxt.gateio(
{
"apiKey": key,
"secret": secret,
"enableRateLimit": True,
"options": {
"defaultType": "swap",
"defaultMarginMode": GATE_DEFAULT_MARGIN_MODE,
},
}
)
_attach_proxies(ex, "GATE")
return ex
def get_exchange() -> Any:
global _ccxt_ex
if _ccxt_ex is None:
_ccxt_ex = _make_exchange()
return _ccxt_ex
def _ensure_markets() -> None:
global _markets_loaded
if not _markets_loaded:
get_exchange().load_markets()
_markets_loaded = True
def _check_token(x_control_token: str | None) -> None:
if not CONTROL_TOKEN:
return
if (x_control_token or "").strip() != CONTROL_TOKEN:
raise HTTPException(status_code=401, detail="invalid token")
def _position_mode_label() -> str:
if EXCHANGE_KIND == "binance":
return BINANCE_POSITION_MODE
if EXCHANGE_KIND == "okx":
return OKX_POS_MODE
return GATE_POS_MODE
def _close_param_candidates_binance(direction: str) -> list[dict[str, Any]]:
ps = "LONG" if direction == "long" else "SHORT"
hedge_ro = {"positionSide": ps, "reduceOnly": True}
hedge_plain = {"positionSide": ps}
oneway_ro = {"reduceOnly": True}
oneway_plain: dict[str, Any] = {}
if BINANCE_POSITION_MODE == "hedge":
return [hedge_ro, hedge_plain, oneway_ro, oneway_plain]
return [oneway_ro, oneway_plain, hedge_ro, hedge_plain]
def _close_param_candidates_okx(direction: str) -> list[dict[str, Any]]:
base: dict[str, Any] = {"tdMode": OKX_TD_MODE}
out: list[dict[str, Any]] = []
if OKX_POS_MODE == "hedge":
ps = "long" if direction == "long" else "short"
out.extend(
[
{**base, "posSide": ps, "reduceOnly": True},
{**base, "posSide": ps},
]
)
out.extend([{**base, "reduceOnly": True}, dict(base)])
return out
def _close_param_candidates_gate(_direction: str) -> list[dict[str, Any]]:
return [{"reduceOnly": True}, {}]
def _close_param_candidates(direction: str) -> list[dict[str, Any]]:
if EXCHANGE_KIND == "binance":
return _close_param_candidates_binance(direction)
if EXCHANGE_KIND == "okx":
return _close_param_candidates_okx(direction)
return _close_param_candidates_gate(direction)
def _retryable_close_err(msg: str) -> bool:
s = (msg or "").lower()
if "-4061" in s:
return True
if "-1106" in s and "reduceonly" in s:
return True
if "reduceonly" in s or "reduce only" in s:
return True
if "position side" in s or "positionside" in s or "pos side" in s:
return True
if "dual side" in s or "position mode" in s:
return True
return False
def _position_contracts(p: dict[str, Any]) -> float:
raw = p.get("contracts")
if raw is not None:
try:
return float(raw)
except (TypeError, ValueError):
pass
info = p.get("info") or {}
for k in ("positionAmt", "positionamt", "pos", "size"):
if k in info:
try:
v = float(info[k])
if v != 0:
return v
except (TypeError, ValueError):
pass
return 0.0
def _position_side(p: dict[str, Any], contracts: float) -> str:
s = (p.get("side") or "").lower()
if s in ("long", "short"):
return s
if contracts > 0:
return "long"
if contracts < 0:
return "short"
return "long"
def _cancel_symbol_orders(ex: Any, sym: str) -> None:
try:
ex.cancel_all_orders(sym, params={})
except Exception:
pass
if EXCHANGE_KIND != "binance":
return
try:
m = ex.market(sym)
cid = m.get("id")
if cid and hasattr(ex, "fapiPrivateDeleteAlgoOpenOrders"):
ex.fapiPrivateDeleteAlgoOpenOrders({"symbol": cid})
except Exception:
pass
class EmergencyClosePositionBody(BaseModel):
symbol: str
side: str
class CancelOrderBody(BaseModel):
symbol: str
order_id: str
channel: str = "regular"
class CancelSymbolOrdersBody(BaseModel):
symbol: str
scope: str = "all" # all | conditional | limit
class PlaceTpslBody(BaseModel):
symbol: str
side: str # long | short
stop_loss: float
take_profit: float
contracts: float | None = None
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
h = host.lower()
return h in ("127.0.0.1", "::1", "localhost") or h.startswith("::ffff:127.0.0.1")
def _finite_or_none(x: Any) -> float | None:
try:
f = float(x)
return f if math.isfinite(f) else None
except (TypeError, ValueError):
return None
def _position_price_fmt(ex: Any, symbol: str, price: float | None) -> tuple[float | None, str | None, float | None]:
"""返回 (原价, 交易所精度字符串, price_tick)。"""
if price is None or price <= 0 or not symbol:
return None, None, None
tick: float | None = None
try:
ex.load_markets()
unified = ex.market(symbol)["symbol"]
tick = price_tick_from_market(ex, unified)
px_str = str(ex.price_to_precision(unified, price))
return _finite_or_none(float(px_str)), px_str, tick
except Exception:
return price, format_price_by_tick(price, tick), tick
def _position_entry_price(p: dict[str, Any]) -> float | None:
"""四所 ccxt 持仓统一解析开仓均价(Binance/OKX/Gate 字段名不一致)。"""
return parse_position_entry_price(p)
def _position_contract_size(ex: Any, symbol: str) -> float:
try:
market = ex.market((symbol or "").strip())
cs = float(market.get("contractSize") or 1)
return cs if cs > 0 else 1.0
except Exception:
return 1.0
def _position_mark_price(p: dict[str, Any]) -> float | None:
"""四所 ccxt 持仓统一解析标记价(与实例 parse_ccxt_position_metrics 一致)。"""
return parse_position_mark_price(p)
def _ticker_mark_price(ex: Any, symbol: str) -> float | None:
"""持仓行无 mark 时,用 ticker 补标记价(last/mark)。"""
sym = (symbol or "").strip()
if not sym:
return None
try:
t = ex.fetch_ticker(sym)
except Exception:
return None
if not isinstance(t, dict):
return None
info = t.get("info") if isinstance(t.get("info"), dict) else {}
for key in (
t.get("mark"),
t.get("last"),
t.get("close"),
info.get("markPrice"),
info.get("mark_price"),
info.get("markPx"),
):
px = _finite_or_none(key)
if px is not None and px > 0:
return px
return None
def _extract_usdt_total(balance: dict[str, Any]) -> float | None:
"""从 ccxt balance 结构中尽量取出 USDT 总额(与 crypto_monitor_binance 一致)。"""
usdt_info = balance.get("USDT") or {}
if not isinstance(usdt_info, dict):
usdt_info = {}
total_map = balance.get("total") or {}
if not isinstance(total_map, dict):
total_map = {}
free_map = balance.get("free") or {}
if not isinstance(free_map, dict):
free_map = {}
total = usdt_info.get("total")
if total is None:
total = usdt_info.get("equity")
if total is None:
total = total_map.get("USDT")
if total is None:
total = usdt_info.get("free")
if total is None:
total = free_map.get("USDT")
try:
return float(total) if total is not None else None
except (TypeError, ValueError):
return None
def _binance_futures_usdt_asset_row(balance: Any) -> dict[str, Any] | None:
"""U 本位合约 fetch_balance(type=swap) 的 info.assets 中 USDT 一行(与币安合约后台口径一致)。"""
if not isinstance(balance, dict):
return None
info = balance.get("info")
if not isinstance(info, dict):
return None
assets = info.get("assets")
if not isinstance(assets, list):
return None
for a in assets:
if isinstance(a, dict) and str(a.get("asset") or "").upper() == "USDT":
return a
return None
def _binance_swap_usdt_total(ex: Any) -> float | None:
"""仅 U 本位永续合约账户 USDT(显式 type=swap,不用现货余额)。"""
try:
bal = ex.fetch_balance({"type": "swap"})
except Exception:
return None
row = _binance_futures_usdt_asset_row(bal)
if row:
for k in ("marginBalance", "walletBalance", "crossWalletBalance", "balance"):
x = row.get(k)
if x is not None and str(x).strip() != "":
try:
fv = float(x)
if fv >= 0:
return fv
except (TypeError, ValueError):
pass
v = _extract_usdt_total(bal)
return float(v) if v is not None else None
@app.middleware("http")
async def local_only(request: Request, call_next):
if request.client and not _is_local(request.client.host):
return JSONResponse({"detail": "forbidden"}, status_code=403)
return await call_next(request)
@app.get("/health")
def health():
return {"ok": True, "exchange": EXCHANGE_KIND}
@app.get("/status")
def status(x_control_token: str | None = Header(default=None, alias="X-Control-Token")):
try:
return _status_inner(x_control_token)
except HTTPException:
raise
except Exception as e:
return JSONResponse(
{
"ok": False,
"error": f"status: {e}",
"exchange": EXCHANGE_KIND,
"balance_usdt": None,
"positions": [],
"total_unrealized_pnl": None,
},
status_code=200,
)
def _status_inner(x_control_token: str | None) -> Any:
_check_token(x_control_token)
try:
ex = get_exchange()
except RuntimeError as e:
return JSONResponse(
{
"ok": False,
"error": str(e),
"exchange": EXCHANGE_KIND,
"balance_usdt": None,
"positions": [],
"total_unrealized_pnl": None,
},
status_code=200,
)
try:
_ensure_markets()
except Exception as e:
return JSONResponse(
{
"ok": False,
"error": f"load_markets: {e}",
"exchange": EXCHANGE_KIND,
"balance_usdt": None,
"positions": [],
"total_unrealized_pnl": None,
},
status_code=200,
)
balance_usdt: float | None = None
try:
if EXCHANGE_KIND == "binance":
balance_usdt = _binance_swap_usdt_total(ex)
else:
bal = ex.fetch_balance()
u = bal.get("USDT") or {}
if isinstance(u, dict) and u.get("total") is not None:
balance_usdt = _finite_or_none(u["total"])
except Exception:
pass
positions_out: list[dict[str, Any]] = []
total_upnl = 0.0
try:
raw = ex.fetch_positions() or []
except Exception as e:
return JSONResponse(
{
"ok": False,
"error": str(e),
"exchange": EXCHANGE_KIND,
"balance_usdt": balance_usdt,
"positions": [],
"total_unrealized_pnl": None,
},
status_code=200,
)
for p in raw:
if not isinstance(p, dict):
continue
c = _position_contracts(p)
if abs(c) < 1e-12:
continue
sym = p.get("symbol") or ""
side = _position_side(p, c)
entry_f = _position_entry_price(p)
mark_f = _position_mark_price(p)
if mark_f is None and sym:
mark_f = _ticker_mark_price(ex, sym)
cs = _position_contract_size(ex, sym) if sym else 1.0
exchange_upnl = parse_position_unrealized_pnl(p)
upnl_f = resolve_position_display_upnl(
side,
entry_f,
mark_f,
abs(c),
cs,
exchange_upnl,
)
if upnl_f is None:
upnl_f = 0.0
total_upnl += upnl_f
notional = p.get("notional")
try:
notional_f = float(notional) if notional is not None else None
except (TypeError, ValueError):
notional_f = None
_, entry_fmt, price_tick = _position_price_fmt(ex, sym, entry_f)
_, mark_fmt, mark_tick = _position_price_fmt(ex, sym, mark_f)
if price_tick is None and mark_tick is not None:
price_tick = mark_tick
positions_out.append(
{
"symbol": sym,
"side": side,
"contracts": abs(c),
"contracts_signed": c,
"notional_usdt": _finite_or_none(notional_f) if notional_f is not None else None,
"unrealized_pnl": _finite_or_none(upnl_f),
"entry_price": entry_f,
"entry_price_fmt": entry_fmt,
"mark_price": mark_f,
"mark_price_fmt": mark_fmt,
"contract_size": _finite_or_none(cs),
"price_tick": _finite_or_none(price_tick) if price_tick is not None else None,
}
)
orders_fetch_error: str | None = None
try:
attach_orders_to_positions(
positions_out,
list_open_orders(ex, EXCHANGE_KIND, None),
)
except Exception as e:
orders_fetch_error = str(e)
for p in positions_out:
p.setdefault("conditional_orders", [])
p.setdefault("regular_orders", [])
try:
pm = _position_mode_label()
except Exception:
pm = EXCHANGE_KIND
out = {
"ok": True,
"exchange": EXCHANGE_KIND,
"balance_usdt": balance_usdt,
"positions": positions_out,
"total_unrealized_pnl": _finite_or_none(total_upnl),
"position_mode": pm,
}
if orders_fetch_error:
out["orders_fetch_error"] = orders_fetch_error
return out
@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("/orders/place-tpsl")
def place_tpsl_orders(
body: PlaceTpslBody,
x_control_token: str | None = Header(default=None, alias="X-Control-Token"),
):
"""先撤该合约全部条件单,再挂止盈+止损(与四实例策略逻辑一致)。"""
_check_token(x_control_token)
sym = (body.symbol or "").strip()
side = (body.side or "").strip().lower()
if not sym or side not in ("long", "short"):
raise HTTPException(status_code=400, detail="symbol 与 side(long/short) 必填")
try:
sl = float(body.stop_loss)
tp = float(body.take_profit)
except (TypeError, ValueError) as e:
raise HTTPException(status_code=400, detail="stop_loss / take_profit 须为数字") from e
try:
ex = get_exchange()
_ensure_markets()
amt = body.contracts
if amt is None or float(amt) <= 0:
raw = ex.fetch_positions() or []
found = None
for p in raw:
psym = p.get("symbol") or ""
if not symbols_match(sym, psym):
continue
c = abs(float(p.get("contracts") or 0))
if c <= 0:
continue
ps = (p.get("side") or "").lower()
if ps and ps != side:
continue
found = c
break
if found is None:
return JSONResponse(
{"ok": False, "error": f"未找到持仓 {sym} {side}", "exchange": EXCHANGE_KIND},
status_code=200,
)
amt = found
info = replace_position_tpsl(ex, EXCHANGE_KIND, sym, side, float(amt), sl, tp)
return {"ok": True, "exchange": EXCHANGE_KIND, "placed": info}
except HTTPException:
raise
except Exception as e:
return JSONResponse(
{"ok": False, "error": str(e), "exchange": EXCHANGE_KIND},
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)
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": [], "errors": [str(e)], "exchange": EXCHANGE_KIND},
status_code=200,
)
errors: list[str] = []
closed: list[dict[str, Any]] = []
try:
raw = ex.fetch_positions() or []
except Exception as e:
raise HTTPException(status_code=502, detail=f"fetch_positions: {e}") from e
for p in raw:
if not isinstance(p, dict):
continue
c = _position_contracts(p)
if abs(c) < 1e-12:
continue
sym = p.get("symbol")
if not sym:
continue
side = _position_side(p, c)
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
uvicorn.run(app, host=HOST, port=PORT, log_level="warning", access_log=False)
if __name__ == "__main__":
main()