Harden residual mid-close: IOC partial fills, exchange reconcile, atomic book.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 14:41:00 +08:00
parent 640ecc9530
commit 469e7a258a
8 changed files with 729 additions and 238 deletions
+12 -8
View File
@@ -28,10 +28,12 @@ class Ledger:
group_id: str | None = None,
note: str = "",
allow_negative: bool = False,
commit: bool = True,
) -> float:
"""amount>0 入账;amount<0 出账。返回余额。
LIVE 实盘成交后本地账本仅作镜像,须 allow_negative=True,避免「交易所已成交、本地拒记」导致卡仓。
commit=False:由调用方持锁并统一提交(与持仓/残留状态同事务)。
"""
now = int(time.time() * 1000)
with self.db._lock:
@@ -49,15 +51,17 @@ class Ledger:
"INSERT INTO ledger_entries(group_id, kind, amount, balance_after, note, ts_ms) VALUES (?,?,?,?,?,?)",
(group_id, kind, float(amount), equity, note, now),
)
self.db._conn.commit()
try:
from ..config import get_settings
from .funds_wallets import SimFundsWallets
if commit:
self.db._conn.commit()
if commit:
try:
from ..config import get_settings
from .funds_wallets import SimFundsWallets
if get_settings().is_sim:
SimFundsWallets(self.db).mirror_cash(float(amount), kind=kind)
except Exception:
pass
if get_settings().is_sim:
SimFundsWallets(self.db).mirror_cash(float(amount), kind=kind)
except Exception:
pass
return equity
def reset_equity(self, amount: float, *, note: str = "重置模拟资金") -> float:
+259 -199
View File
@@ -12,7 +12,7 @@ from ..exchange import get_exchange
from ..models.db import Database, get_db
from ..strategy.session import get_session
from .ledger import Ledger
from .liquidity import bid_covers_eth, bid_mark_ok, contracts_for_eth
from .liquidity import bid_covers_eth, bid_mark_ok, contracts_for_eth, eth_from_contracts
from .pricing import (
is_deep_otm,
option_expiry_settle,
@@ -837,72 +837,64 @@ class Matcher:
)
)
def _residual_bid_gate(
self, row: dict[str, Any], *, bid: float, oq: Any
) -> str | None:
"""权利金比例 + 深度 + 买一/标记偏差。通过返回 None。"""
s = get_settings()
initial_premium = float(row.get("initial_premium") or 0)
opt_qty = float(row.get("option_qty_eth") or 0)
if initial_premium <= 0 or opt_qty <= 0:
return "invalid_initial_premium_or_qty"
if bid <= 0:
return "option_bid_unavailable"
current_premium = float(bid) * opt_qty
min_pct = self._residual_min_premium_pct()
threshold = initial_premium * (min_pct / 100.0)
if current_premium + 1e-12 < threshold:
return (
f"premium_below_threshold curr={current_premium:.4f} "
f"need>={threshold:.4f} ({min_pct:g}%)"
)
option_inst_id = str(row.get("option_inst_id") or "")
ct_mult = self._ct_mult(option_inst_id)
if not bid_covers_eth(
bid_sz_contracts=getattr(oq, "bid_sz", None),
ct_mult=ct_mult,
need_eth=opt_qty,
):
return "option_bid_liquidity_insufficient"
max_dev = self.ledger.get_setting_float(
"close_bid_mark_max_pct", s.close_bid_mark_max_pct
)
ok_dev, why = bid_mark_ok(
bid=float(bid),
mark=getattr(oq, "mark_px", None),
max_dev_pct=max_dev,
)
if not ok_dev:
return why or "bid_mark_deviation"
return None
def _evaluate_residual_premium_close(
self, row: dict[str, Any]
) -> tuple[str | None, float | None, Any]:
"""
残留中途平前置:权利金比例 + 买一流动性。
返回 (skip_reason, close_bid, option_quote)skip_reason 非空则本轮不卖。
成交价口径:最新买一(不再抬到内在价值)。
"""
s = get_settings()
initial_premium = float(row.get("initial_premium") or 0)
opt_qty = float(row.get("option_qty_eth") or 0)
if initial_premium <= 0 or opt_qty <= 0:
return ("invalid_initial_premium_or_qty", None, None)
option_inst_id = str(row.get("option_inst_id") or "")
if not option_inst_id:
return ("missing_option_inst", None, None)
oq = self._quote_held_option(option_inst_id)
if oq is None or oq.bid is None:
return ("option_bid_unavailable", None, None)
close_bid = float(oq.bid)
current_premium = close_bid * opt_qty
min_pct = self._residual_min_premium_pct()
threshold = initial_premium * (min_pct / 100.0)
if current_premium + 1e-12 < threshold:
return (
f"premium_below_threshold curr={current_premium:.4f} "
f"need>={threshold:.4f} ({min_pct:g}%)",
None,
None,
)
ct_mult = self._ct_mult(option_inst_id)
if not bid_covers_eth(
bid_sz_contracts=oq.bid_sz,
ct_mult=ct_mult,
need_eth=opt_qty,
):
return ("option_bid_liquidity_insufficient", None, None)
max_dev = self.ledger.get_setting_float(
"close_bid_mark_max_pct", s.close_bid_mark_max_pct
)
ok_dev, why = bid_mark_ok(bid=close_bid, mark=oq.mark_px, max_dev_pct=max_dev)
if not ok_dev:
return (why or "bid_mark_deviation", None, None)
strike = row.get("strike")
spot = self._close_spot_px(get_session().snapshot())
intrinsic: float | None = None
if strike is not None and spot is not None:
intrinsic = option_intrinsic(
option_side=str(row["option_side"]),
strike=float(strike),
spot=float(spot),
)
resolved = resolve_option_close_bid(
bid=close_bid,
mark=oq.mark_px,
intrinsic=intrinsic,
bypass_liquidity=False,
)
if resolved is None:
return ("option_close_px_unavailable", None, None)
return (None, float(resolved), oq)
skip = self._residual_bid_gate(row, bid=close_bid, oq=oq)
if skip:
return (skip, None, None)
return (None, close_bid, oq)
def _book_residual_market_close(
self,
@@ -915,50 +907,108 @@ class Matcher:
now_ms: int,
note: str,
exec_mode: str | None = None,
) -> dict[str, Any]:
"""买一卖出残留后的入账与结清(SIM/LIVE 共用)。"""
filled_contracts: float | None = None,
remaining_contracts: float | None = None,
close_reason: str = "residual_premium_close",
) -> dict[str, Any] | None:
"""买一卖出残留后的入账(与 pending 状态同事务)。支持部分成交扣减数量。"""
group_id = str(row["group_id"])
opt_qty = float(row["option_qty_eth"])
option_inst_id = str(row["option_inst_id"])
ct_mult = self._ct_mult(option_inst_id)
local_c = float(row.get("option_qty_contracts") or 0)
local_eth = float(row.get("option_qty_eth") or 0)
zero_fill_ok = (
filled_contracts is not None
and float(filled_contracts) <= 1e-12
and remaining_contracts is not None
and float(remaining_contracts) <= 1e-12
)
if filled_contracts is not None and float(filled_contracts) > 0:
fill_c = float(filled_contracts)
fill_eth = eth_from_contracts(fill_c, ct_mult)
elif zero_fill_ok:
fill_c = 0.0
fill_eth = 0.0
else:
fill_eth = local_eth
fill_c = local_c if local_c > 0 else contracts_for_eth(fill_eth, ct_mult)
if fill_eth <= 0 and not zero_fill_ok:
return None
fill_notional = (
float(notional)
if float(notional) > 0
else float(fill_px) * fill_eth
)
opt_entry = float(row["option_entry_px"])
opt_pnl = (float(fill_px) - opt_entry) * opt_qty
opt_cash = float(notional) - float(fee)
self.ledger.apply_cash(
opt_cash,
kind="close_option",
group_id=group_id,
note=note,
allow_negative=not get_settings().is_sim,
)
opt_pnl = (float(fill_px) - opt_entry) * fill_eth if fill_eth > 0 else 0.0
opt_cash = fill_notional - float(fee)
allow_neg = not get_settings().is_sim
fill_cols = (
"group_id, leg, action, side, inst_id, qty_eth, qty_contracts, "
"base_px, fill_px, fee, slip, notional, ts_ms"
)
fill_vals: list[Any] = [
group_id,
"option",
"close",
"flat",
str(row["option_inst_id"]),
opt_qty,
float(row["option_qty_contracts"] or 0),
float(fill_px),
float(fill_px),
float(fee),
float(slip),
float(notional),
now_ms,
]
if exec_mode:
fill_cols += ", exec_mode"
fill_vals.append(exec_mode)
if remaining_contracts is not None:
rem_c = max(0.0, float(remaining_contracts))
else:
rem_c = max(0.0, local_c - fill_c) if local_c > 0 else 0.0
rem_eth = eth_from_contracts(rem_c, ct_mult) if rem_c > 0 else 0.0
fully_done = rem_c <= 1e-8
with self.db._lock:
self.db._conn.execute(
f"""INSERT INTO fills({fill_cols})
VALUES ({",".join("?" for _ in fill_vals)})""",
tuple(fill_vals),
)
pending = self.db._conn.execute(
"SELECT * FROM residual_options WHERE group_id=? AND status='pending'",
(group_id,),
).fetchone()
if pending is None:
logger.warning(
"residual book skip %s: not pending (already settled?)", group_id
)
return None
if abs(opt_cash) > 1e-12:
self.ledger.apply_cash(
opt_cash,
kind="close_option",
group_id=group_id,
note=note,
allow_negative=allow_neg,
commit=False,
)
if get_settings().is_sim:
try:
from .funds_wallets import SimFundsWallets
SimFundsWallets(self.db).mirror_cash(
float(opt_cash), kind="close_option"
)
except Exception:
pass
if fill_eth > 1e-12 or float(fee) > 1e-12:
fill_cols = (
"group_id, leg, action, side, inst_id, qty_eth, qty_contracts, "
"base_px, fill_px, fee, slip, notional, ts_ms"
)
fill_vals: list[Any] = [
group_id,
"option",
"close",
"flat",
option_inst_id,
fill_eth,
fill_c,
float(fill_px),
float(fill_px),
float(fee),
float(slip),
fill_notional,
now_ms,
]
if exec_mode:
fill_cols += ", exec_mode"
fill_vals.append(exec_mode)
self.db._conn.execute(
f"""INSERT INTO fills({fill_cols})
VALUES ({",".join("?" for _ in fill_vals)})""",
tuple(fill_vals),
)
fills = self.db._conn.execute(
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
).fetchall()
@@ -973,32 +1023,62 @@ class Matcher:
).fetchone()
fees = float(g["fees"] or 0) + float(fee) if g else float(fee)
slip_total = float(g["slip_cost"] or 0) + float(slip) if g else float(slip)
self.db._conn.execute(
"""UPDATE residual_options SET status=?, settled_at_ms=?, settle_px=?, settle_pnl=?, note=?
WHERE group_id=?""",
(
"settled",
now_ms,
float(fill_px),
opt_pnl,
note,
group_id,
),
)
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=COALESCE(close_at_ms, ?),
close_reason=COALESCE(close_reason, ?), realized_pnl=?, fees=?, slip_cost=?
WHERE group_id=?""",
(
"closed",
now_ms,
"residual_premium_close",
float(net),
fees,
slip_total,
group_id,
),
)
if fully_done:
cur = self.db._conn.execute(
"""UPDATE residual_options SET status=?, settled_at_ms=?, settle_px=?, settle_pnl=?, note=?
WHERE group_id=? AND status='pending'""",
(
"settled",
now_ms,
float(fill_px),
opt_pnl,
note,
group_id,
),
)
if cur.rowcount != 1:
self.db._conn.rollback()
logger.warning("residual settle race %s", group_id)
return None
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=COALESCE(close_at_ms, ?),
close_reason=?, realized_pnl=?, fees=?, slip_cost=?
WHERE group_id=?""",
(
"closed",
now_ms,
close_reason,
float(net),
fees,
slip_total,
group_id,
),
)
else:
# 按初始权利金比例缩减门槛基准,避免部分成交后永远达不到原 20%
init_prem = float(pending["initial_premium"] or 0)
if local_eth > 1e-12 and rem_eth > 0:
init_prem = init_prem * (rem_eth / local_eth)
cur = self.db._conn.execute(
"""UPDATE residual_options SET
option_qty_eth=?, option_qty_contracts=?, initial_premium=?, note=?
WHERE group_id=? AND status='pending'""",
(
rem_eth,
rem_c,
init_prem,
f"{note}; partial rem_c={rem_c}",
group_id,
),
)
if cur.rowcount != 1:
self.db._conn.rollback()
return None
self.db._conn.execute(
"""UPDATE groups SET realized_pnl=?, fees=?, slip_cost=? WHERE group_id=?""",
(float(net), fees, slip_total, group_id),
)
self.db._conn.commit()
return {
@@ -1006,9 +1086,12 @@ class Matcher:
"option_pnl": opt_pnl,
"settle_px": float(fill_px),
"net_pnl": float(net),
"reason": "residual_premium_close",
"current_premium": float(fill_px) * opt_qty,
"reason": close_reason,
"current_premium": float(fill_px) * fill_eth,
"initial_premium": float(row.get("initial_premium") or 0),
"filled_contracts": fill_c,
"remaining_contracts": rem_c,
"fully_done": fully_done,
}
def try_close_one_residual(self, row: dict[str, Any]) -> dict[str, Any] | None:
@@ -1022,10 +1105,22 @@ class Matcher:
skip,
)
return None
# 下单前再刷买一并重跑门槛
option_inst_id = str(row.get("option_inst_id") or "")
oq2 = self._quote_held_option(option_inst_id) or oq
bid2 = float(oq2.bid) if oq2.bid is not None else float(close_bid)
skip2 = self._residual_bid_gate(row, bid=bid2, oq=oq2)
if skip2:
logger.debug(
"residual premium close recheck skip %s: %s",
row.get("group_id"),
skip2,
)
return None
of = option_fill(
action="close",
bid=float(close_bid),
ask=float(oq.ask or close_bid),
bid=float(bid2),
ask=float(oq2.ask or bid2),
qty_eth=float(row["option_qty_eth"]),
fee_rate=self._fee_rate(),
)
@@ -1037,7 +1132,9 @@ class Matcher:
notional=of.notional,
slip=of.slip,
now_ms=now_ms,
note=f"residual mid-close at bid px={close_bid}",
note=f"residual mid-close at bid px={bid2}",
filled_contracts=float(row.get("option_qty_contracts") or 0) or None,
remaining_contracts=0.0,
)
def try_close_pending_residuals(self) -> list[dict[str, Any]]:
@@ -1095,16 +1192,44 @@ class Matcher:
out.append(r)
return out
def _try_exchange_flatten_residual(
self, row: dict[str, Any], *, force: bool = False
) -> dict[str, Any] | None:
"""LIVE 覆盖:尽量在交易所卖掉残留。成功返回 fill 字段字典。"""
return None
def _settle_one_residual(
self, row: dict[str, Any], *, now_ms: int, force: bool = False
) -> dict[str, Any] | None:
group_id = str(row["group_id"])
# LIVE:优先交易所卖出再入账
ex_fill = self._try_exchange_flatten_residual(row, force=force)
if ex_fill is not None:
booked = self._book_residual_market_close(
row,
fill_px=float(ex_fill["fill_px"]),
fee=float(ex_fill.get("fee") or 0),
notional=float(ex_fill["notional"]),
slip=float(ex_fill.get("slip") or 0),
now_ms=now_ms,
note=str(ex_fill.get("note") or "residual exchange settle"),
exec_mode=ex_fill.get("exec_mode"),
filled_contracts=ex_fill.get("filled_contracts"),
remaining_contracts=float(ex_fill.get("remaining_contracts") or 0),
close_reason=str(
ex_fill.get("close_reason")
or ("emergency" if force else "expiry")
),
)
if booked is not None:
booked["forced"] = force
return booked
sess = get_session()
snap = sess.snapshot()
spot = self._close_spot_px(snap)
strike = row["strike"]
if strike is None or spot is None:
logger = __import__("logging").getLogger(__name__)
logger.warning("residual settle skip %s: no strike/spot", group_id)
return None
fee_rate = self._fee_rate()
@@ -1118,84 +1243,19 @@ class Matcher:
qty_eth=float(row["option_qty_eth"]),
fee_rate=fee_rate,
)
opt_entry = float(row["option_entry_px"])
opt_qty = float(row["option_qty_eth"])
opt_pnl = (of.fill_px - opt_entry) * opt_qty
opt_cash = of.notional - of.fee
from ..config import get_settings
self.ledger.apply_cash(
opt_cash,
kind="close_option",
group_id=group_id,
return self._book_residual_market_close(
row,
fill_px=of.fill_px,
fee=of.fee,
notional=of.notional,
slip=of.slip,
now_ms=now_ms,
note=f"residual option expiry settle{' force' if force else ''}",
# LIVE 本地账本仅镜像;拒记会导致 residual 永久 pending
allow_negative=not get_settings().is_sim,
filled_contracts=float(row.get("option_qty_contracts") or 0) or None,
remaining_contracts=0.0,
close_reason="expiry" if not force else "emergency",
)
with self.db._lock:
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)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
"option",
"close",
"flat",
str(row["option_inst_id"]),
opt_qty,
float(row["option_qty_contracts"] or 0),
of.base_px,
of.fill_px,
of.fee,
of.slip,
of.notional,
now_ms,
),
)
fills = self.db._conn.execute(
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
).fetchall()
from ..sim.pnl import summarize_fills_pnl
summary = summarize_fills_pnl(list(fills))
net = summary.get("net_pnl")
if net is None:
net = opt_pnl - of.fee
g = self.db._conn.execute(
"SELECT fees, slip_cost FROM groups WHERE group_id=?", (group_id,)
).fetchone()
fees = float(g["fees"] or 0) + of.fee
slip = float(g["slip_cost"] or 0) + of.slip
self.db._conn.execute(
"""UPDATE residual_options SET status=?, settled_at_ms=?, settle_px=?, settle_pnl=?, note=?
WHERE group_id=?""",
(
"settled",
now_ms,
of.fill_px,
opt_pnl,
"settled at intrinsic",
group_id,
),
)
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=COALESCE(close_at_ms, ?),
realized_pnl=?, fees=?, slip_cost=?
WHERE group_id=?""",
("closed", now_ms, float(net), fees, slip, group_id),
)
self.db._conn.commit()
return {
"group_id": group_id,
"option_pnl": opt_pnl,
"settle_px": of.fill_px,
"net_pnl": net,
"forced": force,
}
def _quote_held_option(self, option_inst_id: str):
"""只取持仓合约盘口;缺失时 REST 补一次,绝不借用 ATM 对。"""
if not option_inst_id: