Add Binance live trading, anti-stuck open/close recovery, and configurable rate limits.
OKX/Binance LIVE share half_open and option_closed_perp_pending repair paths; private REST throttles default to 1s and are tunable in settings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+406
-62
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from ..config import get_settings
|
||||
from ..env_store import live_ready
|
||||
@@ -54,9 +53,12 @@ class OkxLiveExecutor(Matcher):
|
||||
return OpenResult(ok=False, detail=err)
|
||||
|
||||
s = get_settings()
|
||||
pos = self.current_position()
|
||||
if pos.get("status") == "open" and pos.get("group_id"):
|
||||
return OpenResult(ok=False, detail="已有持仓组,请先平仓")
|
||||
if self.has_open_position():
|
||||
st = self.position_status()
|
||||
return OpenResult(
|
||||
ok=False,
|
||||
detail=f"已有持仓/半仓状态({st}),请先修复或平仓",
|
||||
)
|
||||
|
||||
client = self._client()
|
||||
perp_qty = self.ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth)
|
||||
@@ -76,7 +78,7 @@ class OkxLiveExecutor(Matcher):
|
||||
logger.exception("live open option failed")
|
||||
return OpenResult(ok=False, detail=f"实盘开期权失败: {e}")
|
||||
|
||||
# 永续:按仓位方向
|
||||
# 永续市价:按产品假设,失败原因实质为保证金不足 → 必须回滚期权
|
||||
try:
|
||||
ct_val = client.get_ct_val(s.perp_inst_id, inst_type="SWAP")
|
||||
perp_sz = max(1, int(round(perp_qty / ct_val)))
|
||||
@@ -92,7 +94,7 @@ class OkxLiveExecutor(Matcher):
|
||||
pos_side=pos_side,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("live open perp failed; attempting option close")
|
||||
logger.exception("live open perp failed (likely margin); rollback option")
|
||||
try:
|
||||
client.place_market(
|
||||
inst_id=option_inst_id,
|
||||
@@ -103,11 +105,30 @@ class OkxLiveExecutor(Matcher):
|
||||
)
|
||||
except Exception as e2:
|
||||
logger.exception("live option rollback failed: %s", e2)
|
||||
self._persist_half_open(
|
||||
group_id=group_id,
|
||||
bias=bias,
|
||||
option_side=option_side,
|
||||
perp_side=perp_side,
|
||||
option_inst_id=option_inst_id,
|
||||
entry_index_px=entry_index_px,
|
||||
strike=strike,
|
||||
expiry_ymd=expiry_ymd,
|
||||
opt_qty=opt_qty,
|
||||
opt_contracts=opt_contracts,
|
||||
of_px=float(opt_fill.avg_px),
|
||||
of_fee=float(opt_fill.fee),
|
||||
detail=f"保证金开永续失败且期权回滚失败: {e} / {e2}",
|
||||
)
|
||||
return OpenResult(
|
||||
ok=False,
|
||||
detail=f"永续开仓失败且期权回滚失败: {e} / {e2}",
|
||||
group_id=group_id,
|
||||
detail=f"永续开仓失败(保证金)且期权回滚失败,已标记 half_open: {e} / {e2}",
|
||||
)
|
||||
return OpenResult(ok=False, detail=f"永续开仓失败,已尝试平期权: {e}")
|
||||
return OpenResult(
|
||||
ok=False,
|
||||
detail=f"永续开仓失败(多为保证金不足),已回滚期权: {e}",
|
||||
)
|
||||
|
||||
of_px = float(opt_fill.avg_px)
|
||||
pf_px = float(perp_fill_live.avg_px)
|
||||
@@ -117,21 +138,21 @@ class OkxLiveExecutor(Matcher):
|
||||
of_notional = of_px * opt_qty
|
||||
pf_notional = pf_px * perp_qty
|
||||
|
||||
try:
|
||||
self.ledger.apply_cash(
|
||||
-(of_notional + of_fee),
|
||||
kind="open_option",
|
||||
group_id=group_id,
|
||||
note=f"LIVE open option {group_id}",
|
||||
)
|
||||
self.ledger.apply_cash(
|
||||
-pf_fee,
|
||||
kind="open_perp_fee",
|
||||
group_id=group_id,
|
||||
note=f"LIVE open perp {group_id}",
|
||||
)
|
||||
except RuntimeError as e:
|
||||
return OpenResult(ok=False, detail=str(e))
|
||||
# LIVE:交易所已成交,本地账本允许透支镜像,禁止因账本拒记导致「交易所有仓、DB 空」
|
||||
self.ledger.apply_cash(
|
||||
-(of_notional + of_fee),
|
||||
kind="open_option",
|
||||
group_id=group_id,
|
||||
note=f"LIVE open option {group_id}",
|
||||
allow_negative=True,
|
||||
)
|
||||
self.ledger.apply_cash(
|
||||
-pf_fee,
|
||||
kind="open_perp_fee",
|
||||
group_id=group_id,
|
||||
note=f"LIVE open perp {group_id}",
|
||||
allow_negative=True,
|
||||
)
|
||||
|
||||
now = int(time.time() * 1000)
|
||||
with self.db._lock:
|
||||
@@ -238,6 +259,192 @@ class OkxLiveExecutor(Matcher):
|
||||
},
|
||||
)
|
||||
|
||||
def _persist_half_open(
|
||||
self,
|
||||
*,
|
||||
group_id: str,
|
||||
bias: str,
|
||||
option_side: str,
|
||||
perp_side: str,
|
||||
option_inst_id: str,
|
||||
entry_index_px: float,
|
||||
strike: float | None,
|
||||
expiry_ymd: str | None,
|
||||
opt_qty: float,
|
||||
opt_contracts: float,
|
||||
of_px: float,
|
||||
of_fee: float,
|
||||
detail: str,
|
||||
) -> None:
|
||||
"""期权已成交、永续未开且回滚失败 → 落 half_open,禁止新开,待 repair。"""
|
||||
s = get_settings()
|
||||
initial_premium = of_px * opt_qty
|
||||
self.ledger.apply_cash(
|
||||
-(of_px * opt_qty + of_fee),
|
||||
kind="open_option",
|
||||
group_id=group_id,
|
||||
note=f"LIVE half_open option {group_id}",
|
||||
allow_negative=True,
|
||||
)
|
||||
now = int(time.time() * 1000)
|
||||
with self.db._lock:
|
||||
existing = self.db._conn.execute(
|
||||
"SELECT group_id FROM groups WHERE group_id=?", (group_id,)
|
||||
).fetchone()
|
||||
if existing is None:
|
||||
self.db._conn.execute(
|
||||
"""INSERT INTO groups(
|
||||
group_id, status, bias, option_side, perp_side, option_inst_id, perp_inst_id,
|
||||
strike, expiry_ymd, entry_index_px, initial_premium, open_at_ms, fees, slip_cost,
|
||||
exec_mode, note
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
"half_open",
|
||||
bias,
|
||||
option_side,
|
||||
perp_side,
|
||||
option_inst_id,
|
||||
s.perp_inst_id,
|
||||
strike,
|
||||
expiry_ymd,
|
||||
entry_index_px,
|
||||
initial_premium,
|
||||
now,
|
||||
of_fee,
|
||||
0.0,
|
||||
"LIVE",
|
||||
detail[:200],
|
||||
),
|
||||
)
|
||||
self.db._conn.execute(
|
||||
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
|
||||
base_px, fill_px, fee, slip, notional, ts_ms, exec_mode)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
"option",
|
||||
"open",
|
||||
"long",
|
||||
option_inst_id,
|
||||
opt_qty,
|
||||
opt_contracts,
|
||||
of_px,
|
||||
of_px,
|
||||
of_fee,
|
||||
0.0,
|
||||
of_px * opt_qty,
|
||||
now,
|
||||
"LIVE",
|
||||
),
|
||||
)
|
||||
self.db._conn.execute(
|
||||
"""UPDATE positions SET
|
||||
group_id=?, perp_side=?, perp_qty_eth=0, perp_entry_px=NULL,
|
||||
option_inst_id=?, option_side=?, option_qty_eth=?, option_qty_contracts=?,
|
||||
option_entry_px=?, entry_index_px=?, initial_premium=?, status='half_open'
|
||||
WHERE id=1""",
|
||||
(
|
||||
group_id,
|
||||
perp_side,
|
||||
option_inst_id,
|
||||
option_side,
|
||||
opt_qty,
|
||||
opt_contracts,
|
||||
of_px,
|
||||
entry_index_px,
|
||||
initial_premium,
|
||||
),
|
||||
)
|
||||
self.db._conn.commit()
|
||||
|
||||
def repair_half_open(self) -> CloseResult:
|
||||
"""卖出 half_open 残留期权,清本地状态。"""
|
||||
err = self._guard_live()
|
||||
if err:
|
||||
return CloseResult(ok=False, detail=err)
|
||||
pos = self.current_position()
|
||||
if pos.get("status") != "half_open":
|
||||
return CloseResult(ok=False, detail="非 half_open 状态")
|
||||
group_id = str(pos.get("group_id") or "")
|
||||
option_inst_id = str(pos.get("option_inst_id") or "")
|
||||
opt_contracts = float(pos.get("option_qty_contracts") or 0)
|
||||
opt_qty = float(pos.get("option_qty_eth") or 0)
|
||||
if not option_inst_id or opt_contracts <= 0:
|
||||
return CloseResult(ok=False, detail="half_open 缺期权合约信息")
|
||||
client = self._client()
|
||||
try:
|
||||
opt_live = client.place_market(
|
||||
inst_id=option_inst_id,
|
||||
side="sell",
|
||||
sz=str(int(round(opt_contracts))),
|
||||
td_mode="cash",
|
||||
reduce_only=True,
|
||||
)
|
||||
except Exception as e:
|
||||
return CloseResult(ok=False, detail=f"half_open 平期权失败: {e}")
|
||||
of_px = float(opt_live.avg_px)
|
||||
of_fee = float(opt_live.fee)
|
||||
of_notional = of_px * opt_qty
|
||||
opt_entry = float(pos.get("option_entry_px") or of_px)
|
||||
self.ledger.apply_cash(
|
||||
of_notional - of_fee,
|
||||
kind="close_option",
|
||||
group_id=group_id or None,
|
||||
note="LIVE repair half_open",
|
||||
allow_negative=True,
|
||||
)
|
||||
now = int(time.time() * 1000)
|
||||
with self.db._lock:
|
||||
if group_id:
|
||||
self.db._conn.execute(
|
||||
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
|
||||
base_px, fill_px, fee, slip, notional, ts_ms, exec_mode)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
"option",
|
||||
"close",
|
||||
"flat",
|
||||
option_inst_id,
|
||||
opt_qty,
|
||||
opt_contracts,
|
||||
of_px,
|
||||
of_px,
|
||||
of_fee,
|
||||
0.0,
|
||||
of_notional,
|
||||
now,
|
||||
"LIVE",
|
||||
),
|
||||
)
|
||||
opt_pnl = (of_px - opt_entry) * opt_qty - of_fee
|
||||
self.db._conn.execute(
|
||||
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?, note=?
|
||||
WHERE group_id=?""",
|
||||
(
|
||||
"closed",
|
||||
now,
|
||||
"half_open_repair",
|
||||
float(opt_pnl),
|
||||
"repaired half_open",
|
||||
group_id,
|
||||
),
|
||||
)
|
||||
self.db._conn.execute(
|
||||
"""UPDATE positions SET
|
||||
group_id=NULL, perp_side=NULL, perp_qty_eth=0, perp_entry_px=NULL,
|
||||
option_inst_id=NULL, option_side=NULL, option_qty_eth=0, option_qty_contracts=0,
|
||||
option_entry_px=NULL, entry_index_px=NULL, initial_premium=0, status='flat'
|
||||
WHERE id=1"""
|
||||
)
|
||||
self.db._conn.commit()
|
||||
return CloseResult(
|
||||
ok=True,
|
||||
detail="half_open_repaired",
|
||||
data={"group_id": group_id, "exec_mode": "LIVE"},
|
||||
)
|
||||
|
||||
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
|
||||
err = self._guard_live()
|
||||
if err:
|
||||
@@ -245,7 +452,10 @@ class OkxLiveExecutor(Matcher):
|
||||
|
||||
s = get_settings()
|
||||
pos = self.current_position()
|
||||
if pos.get("status") != "open" or not pos.get("group_id"):
|
||||
st = str(pos.get("status") or "")
|
||||
if st == "half_open":
|
||||
return self.repair_half_open()
|
||||
if st not in ("open", "option_closed_perp_pending") or not pos.get("group_id"):
|
||||
return CloseResult(ok=False, detail="无持仓可平")
|
||||
|
||||
group_id = str(pos["group_id"])
|
||||
@@ -258,6 +468,7 @@ class OkxLiveExecutor(Matcher):
|
||||
client = self._client()
|
||||
is_expiry = reason == "expiry"
|
||||
fee_rate = self._fee_rate()
|
||||
pending_perp_only = st == "option_closed_perp_pending"
|
||||
|
||||
sess = get_session()
|
||||
snap = sess.snapshot()
|
||||
@@ -274,7 +485,24 @@ class OkxLiveExecutor(Matcher):
|
||||
of_slip = 0.0
|
||||
of_notional = 0.0
|
||||
|
||||
if is_expiry:
|
||||
if pending_perp_only:
|
||||
# 期权已在上次成交并入账;只读上次平期权 fill
|
||||
prev = self.db.fetchone(
|
||||
"""SELECT fill_px, fee, notional, slip FROM fills
|
||||
WHERE group_id=? AND leg='option' AND action='close'
|
||||
ORDER BY id DESC LIMIT 1""",
|
||||
(group_id,),
|
||||
)
|
||||
if prev is None:
|
||||
return CloseResult(
|
||||
ok=False,
|
||||
detail="option_closed_perp_pending 缺期权平仓记录,请人工核对",
|
||||
)
|
||||
of_px = float(prev["fill_px"])
|
||||
of_fee = float(prev["fee"] or 0)
|
||||
of_notional = float(prev["notional"] or (of_px * opt_qty))
|
||||
of_slip = float(prev["slip"] or 0)
|
||||
elif is_expiry:
|
||||
if intrinsic is None:
|
||||
return CloseResult(ok=False, detail="到期结算失败:缺行权价或标的价")
|
||||
of = option_expiry_settle(
|
||||
@@ -302,6 +530,20 @@ class OkxLiveExecutor(Matcher):
|
||||
)
|
||||
return CloseResult(ok=False, detail=f"实盘平期权失败: {e}")
|
||||
|
||||
# 期权已平:立刻落 pending,避免永续失败后重试再卖期权
|
||||
self._mark_option_closed_perp_pending(
|
||||
group_id=group_id,
|
||||
option_inst_id=option_inst_id,
|
||||
opt_qty=opt_qty,
|
||||
opt_contracts=opt_contracts,
|
||||
of_px=of_px,
|
||||
of_fee=of_fee,
|
||||
of_notional=of_notional,
|
||||
of_slip=of_slip,
|
||||
reason=reason,
|
||||
)
|
||||
pending_perp_only = True
|
||||
|
||||
try:
|
||||
ct_val = client.get_ct_val(s.perp_inst_id, inst_type="SWAP")
|
||||
perp_sz = max(1, int(round(perp_qty / ct_val)))
|
||||
@@ -320,35 +562,55 @@ class OkxLiveExecutor(Matcher):
|
||||
pf_px = float(perp_live.avg_px)
|
||||
pf_fee = float(perp_live.fee)
|
||||
except Exception as e:
|
||||
return CloseResult(ok=False, detail=f"期权已平但永续平仓失败: {e}")
|
||||
return CloseResult(
|
||||
ok=False,
|
||||
detail=f"期权已平,永续待平(option_closed_perp_pending): {e}",
|
||||
)
|
||||
|
||||
opt_entry = float(pos["option_entry_px"])
|
||||
perp_entry = float(pos["perp_entry_px"])
|
||||
opt_pnl = (of_px - opt_entry) * opt_qty
|
||||
if perp_side == "long":
|
||||
perp_pnl = (pf_px - perp_entry) * perp_qty
|
||||
else:
|
||||
perp_pnl = (perp_entry - pf_px) * perp_qty
|
||||
return self._finalize_dual_close(
|
||||
pos=pos,
|
||||
group_id=group_id,
|
||||
option_inst_id=option_inst_id,
|
||||
opt_qty=opt_qty,
|
||||
opt_contracts=opt_contracts,
|
||||
of_px=of_px,
|
||||
of_fee=of_fee,
|
||||
of_slip=of_slip,
|
||||
of_notional=of_notional,
|
||||
pf_px=pf_px,
|
||||
pf_fee=pf_fee,
|
||||
reason=reason,
|
||||
option_fill_already_written=(
|
||||
st == "option_closed_perp_pending"
|
||||
or (pending_perp_only and not is_expiry)
|
||||
),
|
||||
skip_option_cash=(
|
||||
st == "option_closed_perp_pending"
|
||||
or (pending_perp_only and not is_expiry)
|
||||
),
|
||||
)
|
||||
|
||||
def _mark_option_closed_perp_pending(
|
||||
self,
|
||||
*,
|
||||
group_id: str,
|
||||
option_inst_id: str,
|
||||
opt_qty: float,
|
||||
opt_contracts: float,
|
||||
of_px: float,
|
||||
of_fee: float,
|
||||
of_notional: float,
|
||||
of_slip: float,
|
||||
reason: str,
|
||||
) -> None:
|
||||
self.ledger.apply_cash(
|
||||
of_notional - of_fee,
|
||||
kind="close_option",
|
||||
group_id=group_id,
|
||||
note=f"LIVE close option {reason}",
|
||||
note=f"LIVE close option pending perp {reason}",
|
||||
allow_negative=True,
|
||||
)
|
||||
self.ledger.apply_cash(
|
||||
perp_pnl - pf_fee,
|
||||
kind="close_perp",
|
||||
group_id=group_id,
|
||||
note=f"LIVE close perp {reason}",
|
||||
)
|
||||
|
||||
now = int(time.time() * 1000)
|
||||
g = self.db.fetchone("SELECT * FROM groups WHERE group_id=?", (group_id,))
|
||||
fees = float((g["fees"] if g else 0) or 0) + of_fee + pf_fee
|
||||
slip = float((g["slip_cost"] if g else 0) or 0) + of_slip
|
||||
from ..sim.pnl import summarize_fills_pnl
|
||||
|
||||
with self.db._lock:
|
||||
self.db._conn.execute(
|
||||
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
|
||||
@@ -371,6 +633,92 @@ class OkxLiveExecutor(Matcher):
|
||||
"LIVE",
|
||||
),
|
||||
)
|
||||
self.db._conn.execute(
|
||||
"UPDATE positions SET status='option_closed_perp_pending' WHERE id=1"
|
||||
)
|
||||
self.db._conn.execute(
|
||||
"UPDATE groups SET fees=COALESCE(fees,0)+?, note=? WHERE group_id=?",
|
||||
(of_fee, f"option_closed_perp_pending:{reason}", group_id),
|
||||
)
|
||||
self.db._conn.commit()
|
||||
|
||||
def _finalize_dual_close(
|
||||
self,
|
||||
*,
|
||||
pos: dict,
|
||||
group_id: str,
|
||||
option_inst_id: str,
|
||||
opt_qty: float,
|
||||
opt_contracts: float,
|
||||
of_px: float,
|
||||
of_fee: float,
|
||||
of_slip: float,
|
||||
of_notional: float,
|
||||
pf_px: float,
|
||||
pf_fee: float,
|
||||
reason: str,
|
||||
option_fill_already_written: bool,
|
||||
skip_option_cash: bool,
|
||||
) -> CloseResult:
|
||||
s = get_settings()
|
||||
perp_side = str(pos["perp_side"])
|
||||
perp_qty = float(pos["perp_qty_eth"])
|
||||
opt_entry = float(pos["option_entry_px"])
|
||||
perp_entry = float(pos["perp_entry_px"] or pf_px)
|
||||
opt_pnl = (of_px - opt_entry) * opt_qty
|
||||
if perp_side == "long":
|
||||
perp_pnl = (pf_px - perp_entry) * perp_qty
|
||||
else:
|
||||
perp_pnl = (perp_entry - pf_px) * perp_qty
|
||||
|
||||
if not skip_option_cash:
|
||||
self.ledger.apply_cash(
|
||||
of_notional - of_fee,
|
||||
kind="close_option",
|
||||
group_id=group_id,
|
||||
note=f"LIVE close option {reason}",
|
||||
allow_negative=True,
|
||||
)
|
||||
self.ledger.apply_cash(
|
||||
perp_pnl - pf_fee,
|
||||
kind="close_perp",
|
||||
group_id=group_id,
|
||||
note=f"LIVE close perp {reason}",
|
||||
allow_negative=True,
|
||||
)
|
||||
|
||||
now = int(time.time() * 1000)
|
||||
g = self.db.fetchone("SELECT * FROM groups WHERE group_id=?", (group_id,))
|
||||
base_fees = float((g["fees"] if g else 0) or 0)
|
||||
fees = base_fees + (0.0 if skip_option_cash else of_fee) + pf_fee
|
||||
slip = float((g["slip_cost"] if g else 0) or 0) + (
|
||||
0.0 if option_fill_already_written else of_slip
|
||||
)
|
||||
from ..sim.pnl import summarize_fills_pnl
|
||||
|
||||
with self.db._lock:
|
||||
if not option_fill_already_written:
|
||||
self.db._conn.execute(
|
||||
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
|
||||
base_px, fill_px, fee, slip, notional, ts_ms, exec_mode)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
"option",
|
||||
"close",
|
||||
"flat",
|
||||
option_inst_id,
|
||||
opt_qty,
|
||||
opt_contracts,
|
||||
of_px,
|
||||
of_px,
|
||||
of_fee,
|
||||
of_slip,
|
||||
of_notional,
|
||||
now,
|
||||
"LIVE",
|
||||
),
|
||||
)
|
||||
self.db._conn.execute(
|
||||
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
|
||||
base_px, fill_px, fee, slip, notional, ts_ms, exec_mode)
|
||||
@@ -419,18 +767,23 @@ class OkxLiveExecutor(Matcher):
|
||||
data={"group_id": group_id, "reason": reason, "net_pnl": net, "exec_mode": "LIVE"},
|
||||
)
|
||||
|
||||
def close_perp_abandon_option(self, *, reason: str = "target_perp_only") -> CloseResult:
|
||||
def close_perp_abandon_option(
|
||||
self, *, reason: str = "target_perp_only", require_deep_otm: bool = True
|
||||
) -> CloseResult:
|
||||
err = self._guard_live()
|
||||
if err:
|
||||
return CloseResult(ok=False, detail=err)
|
||||
# 先校验远虚,再实盘只平永续,其余写入复用父类逻辑的简化版:
|
||||
if not self.option_is_deep_otm():
|
||||
if require_deep_otm and not self.option_is_deep_otm():
|
||||
return CloseResult(ok=False, detail="期权非远虚,应走双腿全平")
|
||||
|
||||
s = get_settings()
|
||||
pos = self.current_position()
|
||||
if pos.get("status") != "open" or not pos.get("group_id"):
|
||||
st = str(pos.get("status") or "")
|
||||
if st not in ("open", "option_closed_perp_pending") or not pos.get("group_id"):
|
||||
return CloseResult(ok=False, detail="无持仓可平")
|
||||
# 若期权已平只剩永续,走 close_group 续平即可
|
||||
if st == "option_closed_perp_pending":
|
||||
return self.close_group(reason=reason, bypass_liquidity=True)
|
||||
|
||||
group_id = str(pos["group_id"])
|
||||
perp_side = str(pos["perp_side"])
|
||||
@@ -567,17 +920,6 @@ class OkxLiveExecutor(Matcher):
|
||||
)
|
||||
|
||||
|
||||
class BinanceLiveStub(Matcher):
|
||||
def open_group(self, **kwargs: Any) -> OpenResult: # type: ignore[override]
|
||||
return OpenResult(ok=False, detail="币安实盘下单尚未接入,请使用 OKX 或切回 SIM")
|
||||
|
||||
def close_group(self, **kwargs: Any) -> CloseResult: # type: ignore[override]
|
||||
return CloseResult(ok=False, detail="币安实盘下单尚未接入,请使用 OKX 或切回 SIM")
|
||||
|
||||
def close_perp_abandon_option(self, **kwargs: Any) -> CloseResult: # type: ignore[override]
|
||||
return CloseResult(ok=False, detail="币安实盘下单尚未接入,请使用 OKX 或切回 SIM")
|
||||
|
||||
|
||||
def get_executor(db=None) -> Matcher:
|
||||
"""按 MODE + 交易所返回执行器。"""
|
||||
from ..models.db import get_db
|
||||
@@ -588,5 +930,7 @@ def get_executor(db=None) -> Matcher:
|
||||
return Matcher(database)
|
||||
ex = load_runtime_settings().exchange
|
||||
if ex == "binance":
|
||||
return BinanceLiveStub(database)
|
||||
from .binance_executor import BinanceLiveExecutor
|
||||
|
||||
return BinanceLiveExecutor(database)
|
||||
return OkxLiveExecutor(database)
|
||||
|
||||
Reference in New Issue
Block a user