From 32e8de85cc554c7a0bfa311c7c9a8d4778d48415 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 25 Jul 2026 12:57:16 +0800 Subject: [PATCH] Show option top-of-book size and open option before perp. Co-authored-by: Cursor --- backend/app/sim/matcher.py | 96 ++++++++++++++++++++++++------------- docs/策略说明.md | 4 +- frontend/src/pages/Plan.tsx | 14 +++++- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/backend/app/sim/matcher.py b/backend/app/sim/matcher.py index f995c7f..aaa73c9 100644 --- a/backend/app/sim/matcher.py +++ b/backend/app/sim/matcher.py @@ -107,14 +107,7 @@ class Matcher: ct_mult = self._ct_mult(option_inst_id) opt_contracts = contracts_for_eth(opt_qty, ct_mult) - pf = perp_fill( - side=perp_side, - action="open", - bid=float(snap.perp.bid), - ask=float(snap.perp.ask), - qty_eth=perp_qty, - fee_rate=fee_rate, - ) + # 1) 先成交期权(吃卖一);失败则整组不开 of = option_fill( action="open", bid=float(oq.bid or 0), @@ -124,18 +117,53 @@ class Matcher: ) initial_premium = of.fill_px * opt_qty # 锁定口径:成交价×名义,不含费 premium_cost = of.notional + of.fee - total_debit = premium_cost + pf.fee # 永续开仓只扣费;期权支付权利金+费 - try: self.ledger.apply_cash( - -total_debit, - kind="open_debit", + -premium_cost, + kind="open_option", group_id=group_id, - note=f"open {group_id}", + note=f"open option {group_id}", ) except RuntimeError as e: return OpenResult(ok=False, detail=str(e)) + # 2) 期权确认后再市价成交永续(重新取盘口) + snap2 = sess.snapshot() + if not snap2.perp or snap2.perp.bid is None or snap2.perp.ask is None: + self.ledger.apply_cash( + premium_cost, + kind="open_option_rollback", + group_id=group_id, + note=f"rollback option {group_id}: perp book missing", + ) + return OpenResult( + ok=False, + detail="期权已成交但永续盘口不可用,已回滚期权", + ) + pf = perp_fill( + side=perp_side, + action="open", + bid=float(snap2.perp.bid), + ask=float(snap2.perp.ask), + qty_eth=perp_qty, + fee_rate=fee_rate, + ) + try: + self.ledger.apply_cash( + -pf.fee, + kind="open_perp_fee", + group_id=group_id, + note=f"open perp {group_id}", + ) + except RuntimeError as e: + self.ledger.apply_cash( + premium_cost, + kind="open_option_rollback", + group_id=group_id, + note=f"rollback option {group_id}: {e}", + ) + return OpenResult(ok=False, detail=f"期权已成交但永续扣费失败并已回滚: {e}") + now = int(time.time() * 1000) with self.db._lock: self.db._conn.execute( @@ -160,26 +188,7 @@ class Matcher: pf.slip + of.slip, ), ) - 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, - "perp", - "open", - perp_side, - s.perp_inst_id, - perp_qty, - None, - pf.base_px, - pf.fill_px, - pf.fee, - pf.slip, - pf.notional, - now, - ), - ) + # 成交顺序:期权先、永续后(时间戳差 1ms 便于审计) 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) @@ -200,6 +209,26 @@ class Matcher: now, ), ) + 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, + "perp", + "open", + perp_side, + s.perp_inst_id, + perp_qty, + None, + pf.base_px, + pf.fill_px, + pf.fee, + pf.slip, + pf.notional, + now + 1, + ), + ) self.db._conn.execute( """UPDATE positions SET group_id=?, perp_side=?, perp_qty_eth=?, perp_entry_px=?, @@ -233,6 +262,7 @@ class Matcher: "option": of.to_dict(), "initial_premium": initial_premium, "fees": pf.fee + of.fee, + "open_sequence": ["option", "perp"], }, ) diff --git a/docs/策略说明.md b/docs/策略说明.md index 24f8212..40988fc 100644 --- a/docs/策略说明.md +++ b/docs/策略说明.md @@ -30,6 +30,8 @@ | 永续 ETH-USDT-SWAP | **1 ETH** | 见下表 | 市价(吃买一/卖一 + 滑点) | | 期权 ETH-USD_UM | **2 ETH** 名义 | 只买不卖 | 开仓吃卖一,平仓吃买一 | +**开仓顺序**:先成交期权 → 确认后再市价成交永续。永续盘口失败则回滚期权扣款,不留半边仓。 + 永续杠杆默认 **3×**(可配)。同时最多 **1 组**仓,禁止叠仓开下一组。 ### 2.1 开仓方向(ATM 相对现价优先) @@ -61,7 +63,7 @@ → 若开启 atm_open_offset_enabled:|ATM − 标的| ≤ max_atm_open_offset(默认 3)否则跳过 → 选向:ATM 偏下→Call+空;偏上→Put+多;贴平→卖一比价 → 期权杠杆 = 标的价 ÷ 卖一权利金 ≥ min_option_leverage(默认 100) - → 开永续 + 开期权(一组) + → 先开期权(吃卖一)→ 再市价开永续(一组) → 锁定 initial_premium = 期权成交价 × 期权名义(不含费) ``` diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index 85b1b56..3f9b9cf 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -6,6 +6,14 @@ function fmt(n: number | null | undefined, d = 2) { return n.toFixed(d); } +/** 卖一/买一 + 对应档量:8.80(12.5) / 7.20(8.0) */ +function fmtTop(px: number | null | undefined, sz: number | null | undefined) { + if (px == null || Number.isNaN(px)) return "—"; + if (sz == null || Number.isNaN(sz)) return fmt(px); + const s = sz >= 100 ? sz.toFixed(0) : sz >= 10 ? sz.toFixed(1) : sz.toFixed(2); + return `${fmt(px)}(${s})`; +} + function pnlClass(n: number | null | undefined) { if (n == null || Number.isNaN(n) || n === 0) return ""; return n > 0 ? "pos-pnl-profit" : "pos-pnl-loss"; @@ -398,13 +406,15 @@ export default function PlanPage() {
Call 卖一/买一 - {fmt(snap?.call?.ask)} / {fmt(snap?.call?.bid)} + {fmtTop(snap?.call?.ask, snap?.call?.ask_sz)} /{" "} + {fmtTop(snap?.call?.bid, snap?.call?.bid_sz)}
Put 卖一/买一 - {fmt(snap?.put?.ask)} / {fmt(snap?.put?.bid)} + {fmtTop(snap?.put?.ask, snap?.put?.ask_sz)} /{" "} + {fmtTop(snap?.put?.bid, snap?.put?.bid_sz)}