Show option top-of-book size and open option before perp.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+63
-33
@@ -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
@@ -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 = 期权成交价 × 期权名义(不含费)
|
||||
```
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user