Show option top-of-book size and open option before perp.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-25 12:57:16 +08:00
parent 21ca5cbfe0
commit 32e8de85cc
3 changed files with 78 additions and 36 deletions
+63 -33
View File
@@ -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"],
},
)
+3 -1
View File
@@ -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 = 期权成交价 × 期权名义(不含费)
```
+12 -2
View File
@@ -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() {
<div className="kv">
<span>Call /</span>
<span className="mono">
{fmt(snap?.call?.ask)} / {fmt(snap?.call?.bid)}
{fmtTop(snap?.call?.ask, snap?.call?.ask_sz)} /{" "}
{fmtTop(snap?.call?.bid, snap?.call?.bid_sz)}
</span>
</div>
<div className="kv">
<span>Put /</span>
<span className="mono">
{fmt(snap?.put?.ask)} / {fmt(snap?.put?.bid)}
{fmtTop(snap?.put?.ask, snap?.put?.ask_sz)} /{" "}
{fmtTop(snap?.put?.bid, snap?.put?.bid_sz)}
</span>
</div>
<div className="kv">