Add option-option hedge mode with SIM/LIVE parity.
Mutual hedge_mode, amplitude OTM selection, 1:1 risk sizing, win-leg/full close, dual audits and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -381,6 +381,609 @@ class Matcher:
|
||||
},
|
||||
)
|
||||
|
||||
def open_oo_group(
|
||||
self,
|
||||
*,
|
||||
group_id: str,
|
||||
call_inst_id: str,
|
||||
put_inst_id: str,
|
||||
call_strike: float,
|
||||
put_strike: float,
|
||||
entry_index_px: float,
|
||||
expiry_ymd: str | None = None,
|
||||
) -> OpenResult:
|
||||
"""期期:买 Call 再买 Put,无永续。"""
|
||||
if not get_settings().is_sim:
|
||||
return OpenResult(
|
||||
ok=False,
|
||||
detail="LIVE 期期开仓须走 LiveExecutor.open_oo_group",
|
||||
)
|
||||
pos = self.current_position()
|
||||
st = str(pos.get("status") or "flat")
|
||||
if st in BLOCKING_STATUSES and (
|
||||
st == "opening" or bool(pos.get("group_id") or pos.get("option_inst_id"))
|
||||
):
|
||||
return OpenResult(ok=False, detail=f"已有持仓状态({st}),请先平仓")
|
||||
if pos.get("status") == "open" and pos.get("group_id"):
|
||||
return OpenResult(ok=False, detail="已有持仓组,请先平仓")
|
||||
|
||||
ex = get_exchange()
|
||||
cq = ex.quote(call_inst_id)
|
||||
pq = ex.quote(put_inst_id)
|
||||
if cq is None or cq.ask is None:
|
||||
_, asks, _ = ex.fetch_book(call_inst_id, depth=5)
|
||||
if asks:
|
||||
from types import SimpleNamespace
|
||||
|
||||
cq = SimpleNamespace(ask=asks[0].px, bid=None)
|
||||
if pq is None or pq.ask is None:
|
||||
_, asks, _ = ex.fetch_book(put_inst_id, depth=5)
|
||||
if asks:
|
||||
from types import SimpleNamespace
|
||||
|
||||
pq = SimpleNamespace(ask=asks[0].px, bid=None)
|
||||
if not cq or cq.ask is None or not pq or pq.ask is None:
|
||||
return OpenResult(ok=False, detail="期期 Call/Put 卖一不可用")
|
||||
|
||||
fee_rate = self._fee_rate()
|
||||
opt_qty = self.ledger.get_setting_float("option_qty_eth", 0.1)
|
||||
if opt_qty < 0.1 - 1e-12:
|
||||
return OpenResult(ok=False, detail="期期名义 qty 无效")
|
||||
call_ct = self._ct_mult(call_inst_id)
|
||||
put_ct = self._ct_mult(put_inst_id)
|
||||
call_contracts = contracts_for_eth(opt_qty, call_ct)
|
||||
put_contracts = contracts_for_eth(opt_qty, put_ct)
|
||||
|
||||
cf = option_fill(
|
||||
action="open",
|
||||
bid=float(getattr(cq, "bid", None) or 0),
|
||||
ask=float(cq.ask),
|
||||
qty_eth=opt_qty,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
call_prem = cf.fill_px * opt_qty
|
||||
call_cost = cf.notional + cf.fee
|
||||
try:
|
||||
self.ledger.apply_cash(
|
||||
-call_cost,
|
||||
kind="open_option",
|
||||
group_id=group_id,
|
||||
note=f"open oo call {group_id}",
|
||||
)
|
||||
except RuntimeError as e:
|
||||
return OpenResult(ok=False, detail=str(e))
|
||||
|
||||
# 再买 Put;失败则尝试卖回 Call
|
||||
pq2 = ex.quote(put_inst_id) or pq
|
||||
ask2 = float(pq2.ask) if pq2 and pq2.ask else float(pq.ask)
|
||||
pf = option_fill(
|
||||
action="open",
|
||||
bid=float(getattr(pq2, "bid", None) or 0),
|
||||
ask=ask2,
|
||||
qty_eth=opt_qty,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
put_prem = pf.fill_px * opt_qty
|
||||
put_cost = pf.notional + pf.fee
|
||||
try:
|
||||
self.ledger.apply_cash(
|
||||
-put_cost,
|
||||
kind="open_option",
|
||||
group_id=group_id,
|
||||
note=f"open oo put {group_id}",
|
||||
)
|
||||
except RuntimeError as e:
|
||||
# 回滚 Call:按买一卖出估算
|
||||
bid = float(getattr(cq, "bid", None) or cf.fill_px)
|
||||
rb = option_fill(
|
||||
action="close",
|
||||
bid=bid,
|
||||
ask=float(cq.ask),
|
||||
qty_eth=opt_qty,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
self.ledger.apply_cash(
|
||||
rb.notional - rb.fee,
|
||||
kind="open_option_rollback",
|
||||
group_id=group_id,
|
||||
note=f"rollback oo call {group_id}: {e}",
|
||||
)
|
||||
return OpenResult(ok=False, detail=f"Call 已成交但 Put 扣费失败并已回滚: {e}")
|
||||
|
||||
now = int(time.time() * 1000)
|
||||
total_prem = call_prem + put_prem
|
||||
with self.db._lock:
|
||||
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, hedge_mode, option2_inst_id, option2_side, strike2, initial_premium2
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
"open",
|
||||
"option_option",
|
||||
"call",
|
||||
None,
|
||||
call_inst_id,
|
||||
None,
|
||||
float(call_strike),
|
||||
expiry_ymd,
|
||||
entry_index_px,
|
||||
call_prem,
|
||||
now,
|
||||
cf.fee + pf.fee,
|
||||
cf.slip + pf.slip,
|
||||
"SIM",
|
||||
"option_option",
|
||||
put_inst_id,
|
||||
"put",
|
||||
float(put_strike),
|
||||
put_prem,
|
||||
),
|
||||
)
|
||||
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",
|
||||
call_inst_id,
|
||||
opt_qty,
|
||||
call_contracts,
|
||||
cf.base_px,
|
||||
cf.fill_px,
|
||||
cf.fee,
|
||||
cf.slip,
|
||||
cf.notional,
|
||||
now,
|
||||
"SIM",
|
||||
),
|
||||
)
|
||||
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,
|
||||
"option2",
|
||||
"open",
|
||||
"long",
|
||||
put_inst_id,
|
||||
opt_qty,
|
||||
put_contracts,
|
||||
pf.base_px,
|
||||
pf.fill_px,
|
||||
pf.fee,
|
||||
pf.slip,
|
||||
pf.notional,
|
||||
now + 1,
|
||||
"SIM",
|
||||
),
|
||||
)
|
||||
self.db._conn.execute(
|
||||
"""UPDATE positions SET
|
||||
group_id=?, perp_side=NULL, 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=?,
|
||||
hedge_mode=?, option2_inst_id=?, option2_side=?, option2_qty_eth=?,
|
||||
option2_qty_contracts=?, option2_entry_px=?, strike2=?, initial_premium2=?
|
||||
WHERE id=1""",
|
||||
(
|
||||
group_id,
|
||||
call_inst_id,
|
||||
"call",
|
||||
opt_qty,
|
||||
call_contracts,
|
||||
cf.fill_px,
|
||||
entry_index_px,
|
||||
call_prem,
|
||||
"open",
|
||||
"option_option",
|
||||
put_inst_id,
|
||||
"put",
|
||||
opt_qty,
|
||||
put_contracts,
|
||||
pf.fill_px,
|
||||
float(put_strike),
|
||||
put_prem,
|
||||
),
|
||||
)
|
||||
self.db._conn.commit()
|
||||
|
||||
try:
|
||||
from ..strategy.exits import lock_trade_exit_target
|
||||
|
||||
lock_trade_exit_target(
|
||||
self.db, group_id=group_id, initial_premium=total_prem
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("lock exit target failed oo group=%s", group_id)
|
||||
|
||||
return OpenResult(
|
||||
ok=True,
|
||||
group_id=group_id,
|
||||
detail="opened_oo",
|
||||
data={
|
||||
"group_id": group_id,
|
||||
"hedge_mode": "option_option",
|
||||
"call_inst_id": call_inst_id,
|
||||
"put_inst_id": put_inst_id,
|
||||
"call_strike": float(call_strike),
|
||||
"put_strike": float(put_strike),
|
||||
"option_qty_eth": float(opt_qty),
|
||||
"initial_premium": total_prem,
|
||||
"fees": cf.fee + pf.fee,
|
||||
"open_sequence": ["call", "put"],
|
||||
},
|
||||
)
|
||||
|
||||
def close_winning_oo_leave_residual(
|
||||
self, *, reason: str = "target_oo_win", skip_market: bool = False
|
||||
) -> CloseResult:
|
||||
"""期期达标:平盈利腿,亏损腿进 residual。skip_market=True 时假定已在交易所卖掉盈利腿。"""
|
||||
pos = self.current_position()
|
||||
st = str(pos.get("status") or "")
|
||||
if st not in ("open", "closing") or not pos.get("group_id"):
|
||||
return CloseResult(ok=False, detail="无期期持仓可平")
|
||||
if str(pos.get("hedge_mode") or "") != "option_option":
|
||||
# 兼容:有 option2 即视为期期
|
||||
if not pos.get("option2_inst_id"):
|
||||
return CloseResult(ok=False, detail="非期期持仓")
|
||||
if st == "closing" and not skip_market:
|
||||
skip_market = True
|
||||
|
||||
group_id = str(pos["group_id"])
|
||||
call_id = str(pos.get("option_inst_id") or "")
|
||||
put_id = str(pos.get("option2_inst_id") or "")
|
||||
qty = float(pos.get("option_qty_eth") or 0)
|
||||
qty2 = float(pos.get("option2_qty_eth") or qty)
|
||||
if not call_id or not put_id or qty <= 0:
|
||||
return CloseResult(ok=False, detail="期期腿不完整")
|
||||
|
||||
upl = self.unrealized()
|
||||
call_upl = float(upl.get("option_upl") or 0)
|
||||
put_upl = float(upl.get("option2_upl") or 0)
|
||||
# 盈利腿:UPL 更高且 > 0
|
||||
if call_upl >= put_upl and call_upl > 0:
|
||||
win_leg, lose_leg = "option", "option2"
|
||||
win_id, lose_id = call_id, put_id
|
||||
win_side, lose_side = "call", "put"
|
||||
win_qty = qty
|
||||
lose_qty = qty2
|
||||
win_entry = float(pos.get("option_entry_px") or 0)
|
||||
lose_entry = float(pos.get("option2_entry_px") or 0)
|
||||
lose_strike = float(pos.get("strike2") or 0)
|
||||
lose_prem = float(pos.get("initial_premium2") or 0)
|
||||
win_contracts = float(pos.get("option_qty_contracts") or 0)
|
||||
lose_contracts = float(pos.get("option2_qty_contracts") or 0)
|
||||
elif put_upl > call_upl and put_upl > 0:
|
||||
win_leg, lose_leg = "option2", "option"
|
||||
win_id, lose_id = put_id, call_id
|
||||
win_side, lose_side = "put", "call"
|
||||
win_qty = qty2
|
||||
lose_qty = qty
|
||||
win_entry = float(pos.get("option2_entry_px") or 0)
|
||||
lose_entry = float(pos.get("option_entry_px") or 0)
|
||||
g = self.db.fetchone(
|
||||
"SELECT strike FROM groups WHERE group_id=?", (group_id,)
|
||||
)
|
||||
lose_strike = float(g["strike"] or 0) if g else 0.0
|
||||
lose_prem = float(pos.get("initial_premium") or 0)
|
||||
win_contracts = float(pos.get("option2_qty_contracts") or 0)
|
||||
lose_contracts = float(pos.get("option_qty_contracts") or 0)
|
||||
else:
|
||||
return CloseResult(ok=False, detail="无明确盈利腿,暂不平")
|
||||
|
||||
fee_rate = self._fee_rate()
|
||||
if skip_market:
|
||||
oq = self._quote_held_option(win_id)
|
||||
fill_px = float(oq.bid) if oq and oq.bid else float(win_entry)
|
||||
of = option_fill(
|
||||
action="close",
|
||||
bid=fill_px,
|
||||
ask=fill_px,
|
||||
qty_eth=win_qty,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
else:
|
||||
oq = self._quote_held_option(win_id)
|
||||
if oq is None or oq.bid is None or float(oq.bid) <= 0:
|
||||
return CloseResult(ok=False, detail="盈利腿买一不可用")
|
||||
gate = self._residual_bid_gate(
|
||||
{
|
||||
"option_inst_id": win_id,
|
||||
"option_qty_eth": win_qty,
|
||||
"initial_premium": win_entry * win_qty,
|
||||
},
|
||||
bid=float(oq.bid),
|
||||
oq=oq,
|
||||
require_premium_ratio=False,
|
||||
)
|
||||
if gate:
|
||||
return CloseResult(ok=False, detail=f"盈利腿流动性不足: {gate}")
|
||||
of = option_fill(
|
||||
action="close",
|
||||
bid=float(oq.bid),
|
||||
ask=float(oq.ask or oq.bid),
|
||||
qty_eth=win_qty,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
cash = of.notional - of.fee
|
||||
if get_settings().is_sim or not skip_market:
|
||||
self.ledger.apply_cash(
|
||||
cash, kind="close_option", group_id=group_id, note=f"oo win {win_leg}"
|
||||
)
|
||||
elif skip_market:
|
||||
# LIVE:交易所已成交,仍记本地账本现金(与其它 LIVE 平仓一致)
|
||||
try:
|
||||
self.ledger.apply_cash(
|
||||
cash,
|
||||
kind="close_option",
|
||||
group_id=group_id,
|
||||
note=f"oo win live {win_leg}",
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("oo win live ledger cash failed")
|
||||
|
||||
now = int(time.time() * 1000)
|
||||
expiry_ymd = None
|
||||
expiry_ms = None
|
||||
g = self.db.fetchone("SELECT * FROM groups WHERE group_id=?", (group_id,))
|
||||
if g:
|
||||
expiry_ymd = g["expiry_ymd"]
|
||||
try:
|
||||
from ..exchange.expiry import expiry_ms_from_ymd
|
||||
|
||||
if expiry_ymd:
|
||||
expiry_ms = int(expiry_ms_from_ymd(str(expiry_ymd)))
|
||||
except Exception:
|
||||
expiry_ms = None
|
||||
|
||||
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, exec_mode)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
win_leg,
|
||||
"close",
|
||||
"sell",
|
||||
win_id,
|
||||
win_qty,
|
||||
win_contracts,
|
||||
of.base_px,
|
||||
of.fill_px,
|
||||
of.fee,
|
||||
of.slip,
|
||||
of.notional,
|
||||
now,
|
||||
"SIM",
|
||||
),
|
||||
)
|
||||
self.db._conn.execute(
|
||||
"""INSERT INTO residual_options(
|
||||
group_id, option_inst_id, option_side, option_qty_eth, option_qty_contracts,
|
||||
option_entry_px, strike, expiry_ymd, expiry_ms, entry_index_px,
|
||||
initial_premium, status, created_at_ms, note
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
lose_id,
|
||||
lose_side,
|
||||
lose_qty,
|
||||
lose_contracts,
|
||||
lose_entry,
|
||||
lose_strike,
|
||||
expiry_ymd,
|
||||
expiry_ms,
|
||||
float(pos.get("entry_index_px") or 0),
|
||||
lose_prem,
|
||||
"pending",
|
||||
now,
|
||||
f"oo losing leg after {reason}; win={win_leg}",
|
||||
),
|
||||
)
|
||||
# 组:记部分实现盈亏(赢腿),状态 residual
|
||||
win_pnl = (of.fill_px - win_entry) * win_qty - of.fee
|
||||
self.db._conn.execute(
|
||||
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
|
||||
fees=COALESCE(fees,0)+?, note=?
|
||||
WHERE group_id=?""",
|
||||
(
|
||||
"option_residual",
|
||||
now,
|
||||
reason,
|
||||
float(win_pnl),
|
||||
float(of.fee),
|
||||
f"oo win closed {win_leg}; lose {lose_leg} residual",
|
||||
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, exit_target_usdt=NULL, status='flat',
|
||||
hedge_mode=NULL, option2_inst_id=NULL, option2_side=NULL,
|
||||
option2_qty_eth=0, option2_qty_contracts=0, option2_entry_px=NULL,
|
||||
strike2=NULL, initial_premium2=NULL
|
||||
WHERE id=1"""
|
||||
)
|
||||
self.db._conn.commit()
|
||||
|
||||
return CloseResult(
|
||||
ok=True,
|
||||
detail="oo_win_closed_lose_residual",
|
||||
data={
|
||||
"group_id": group_id,
|
||||
"reason": reason,
|
||||
"win_leg": win_leg,
|
||||
"lose_leg": lose_leg,
|
||||
"win_pnl": win_pnl,
|
||||
},
|
||||
)
|
||||
|
||||
def close_oo_full(
|
||||
self, *, reason: str = "expiry", bypass_liquidity: bool = False
|
||||
) -> CloseResult:
|
||||
"""期期全平两腿(到期/紧急);无永续。"""
|
||||
pos = self.current_position()
|
||||
if str(pos.get("status") or "") != "open" or not pos.get("group_id"):
|
||||
return CloseResult(ok=False, detail="无期期持仓可平")
|
||||
if not (
|
||||
str(pos.get("hedge_mode") or "") == "option_option"
|
||||
or pos.get("option2_inst_id")
|
||||
):
|
||||
return CloseResult(ok=False, detail="非期期持仓")
|
||||
|
||||
group_id = str(pos["group_id"])
|
||||
legs = [
|
||||
(
|
||||
"option",
|
||||
str(pos.get("option_inst_id") or ""),
|
||||
float(pos.get("option_qty_eth") or 0),
|
||||
float(pos.get("option_qty_contracts") or 0),
|
||||
float(pos.get("option_entry_px") or 0),
|
||||
float(pos.get("initial_premium") or 0),
|
||||
),
|
||||
(
|
||||
"option2",
|
||||
str(pos.get("option2_inst_id") or ""),
|
||||
float(pos.get("option2_qty_eth") or 0),
|
||||
float(pos.get("option2_qty_contracts") or 0),
|
||||
float(pos.get("option2_entry_px") or 0),
|
||||
float(pos.get("initial_premium2") or 0),
|
||||
),
|
||||
]
|
||||
fee_rate = self._fee_rate()
|
||||
now = int(time.time() * 1000)
|
||||
total_pnl = 0.0
|
||||
total_fees = 0.0
|
||||
for leg, inst, qty, contracts, entry, prem in legs:
|
||||
if not inst or qty <= 0:
|
||||
continue
|
||||
oq = self._quote_held_option(inst)
|
||||
if reason == "expiry":
|
||||
# 到期:尽量用买一,否则按 0 权利金结算
|
||||
bid = float(oq.bid) if oq and oq.bid is not None else 0.0
|
||||
ask = float(oq.ask) if oq and oq.ask is not None else bid
|
||||
else:
|
||||
if oq is None or oq.bid is None or float(oq.bid) <= 0:
|
||||
if not bypass_liquidity:
|
||||
return CloseResult(
|
||||
ok=False, detail=f"期期全平缺买一: {inst}"
|
||||
)
|
||||
bid = float(entry)
|
||||
ask = bid
|
||||
else:
|
||||
if not bypass_liquidity:
|
||||
gate = self._residual_bid_gate(
|
||||
{
|
||||
"option_inst_id": inst,
|
||||
"option_qty_eth": qty,
|
||||
"initial_premium": prem or entry * qty,
|
||||
},
|
||||
bid=float(oq.bid),
|
||||
oq=oq,
|
||||
require_premium_ratio=False,
|
||||
)
|
||||
if gate:
|
||||
return CloseResult(
|
||||
ok=False, detail=f"期期全平流动性: {gate}"
|
||||
)
|
||||
bid = float(oq.bid)
|
||||
ask = float(oq.ask or oq.bid)
|
||||
of = option_fill(
|
||||
action="close",
|
||||
bid=bid,
|
||||
ask=ask if ask > 0 else bid,
|
||||
qty_eth=qty,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
cash = of.notional - of.fee
|
||||
if get_settings().is_sim:
|
||||
self.ledger.apply_cash(
|
||||
cash,
|
||||
kind="close_option",
|
||||
group_id=group_id,
|
||||
note=f"oo full {leg}",
|
||||
)
|
||||
else:
|
||||
try:
|
||||
self.ledger.apply_cash(
|
||||
cash,
|
||||
kind="close_option",
|
||||
group_id=group_id,
|
||||
note=f"oo full {leg}",
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("oo full ledger cash failed leg=%s", leg)
|
||||
total_pnl += (of.fill_px * qty - (prem or entry * qty)) - of.fee
|
||||
total_fees += of.fee
|
||||
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, exec_mode)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
leg,
|
||||
"close",
|
||||
"sell",
|
||||
inst,
|
||||
qty,
|
||||
contracts,
|
||||
of.base_px,
|
||||
of.fill_px,
|
||||
of.fee,
|
||||
of.slip,
|
||||
of.notional,
|
||||
now,
|
||||
"SIM" if get_settings().is_sim else "LIVE",
|
||||
),
|
||||
)
|
||||
self.db._conn.commit()
|
||||
now += 1
|
||||
|
||||
with self.db._lock:
|
||||
self.db._conn.execute(
|
||||
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
|
||||
fees=COALESCE(fees,0)+?, note=?
|
||||
WHERE group_id=?""",
|
||||
(
|
||||
"closed",
|
||||
int(time.time() * 1000),
|
||||
reason,
|
||||
float(total_pnl),
|
||||
float(total_fees),
|
||||
f"oo full close {reason}",
|
||||
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, exit_target_usdt=NULL, status='flat',
|
||||
hedge_mode=NULL, option2_inst_id=NULL, option2_side=NULL,
|
||||
option2_qty_eth=0, option2_qty_contracts=0, option2_entry_px=NULL,
|
||||
strike2=NULL, initial_premium2=NULL
|
||||
WHERE id=1"""
|
||||
)
|
||||
self.db._conn.commit()
|
||||
return CloseResult(
|
||||
ok=True,
|
||||
detail="oo_full_closed",
|
||||
data={"group_id": group_id, "reason": reason, "net": total_pnl},
|
||||
)
|
||||
|
||||
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
|
||||
"""
|
||||
全平一组。默认校验期权买一深度 + 买一/标记偏差(默认≤30%)。
|
||||
@@ -1384,6 +1987,7 @@ class Matcher:
|
||||
"has_position": False,
|
||||
"perp_upl": 0.0,
|
||||
"option_upl": 0.0,
|
||||
"option2_upl": 0.0,
|
||||
"net_pnl": 0.0,
|
||||
"est_close_fees": 0.0,
|
||||
"index_px": None,
|
||||
@@ -1391,6 +1995,95 @@ class Matcher:
|
||||
"move_pct": 0.0,
|
||||
"premium_gap": None,
|
||||
}
|
||||
if str(pos.get("hedge_mode") or "") == "option_option" or pos.get(
|
||||
"option2_inst_id"
|
||||
):
|
||||
return self._unrealized_oo(pos)
|
||||
return self._unrealized_perp(pos)
|
||||
|
||||
def _unrealized_oo(self, pos: dict[str, Any]) -> dict[str, Any]:
|
||||
sess = get_session()
|
||||
snap = sess.snapshot()
|
||||
fee_rate = self._fee_rate()
|
||||
index_px = snap.index_px
|
||||
if index_px is None and snap.perp:
|
||||
index_px = snap.perp.mark_px
|
||||
qty = float(pos.get("option_qty_eth") or 0)
|
||||
qty2 = float(pos.get("option2_qty_eth") or qty)
|
||||
prem1 = float(pos.get("initial_premium") or 0)
|
||||
prem2 = float(pos.get("initial_premium2") or 0)
|
||||
call_id = str(pos.get("option_inst_id") or "")
|
||||
put_id = str(pos.get("option2_inst_id") or "")
|
||||
oq1 = self._quote_held_option(call_id) if call_id else None
|
||||
oq2 = self._quote_held_option(put_id) if put_id else None
|
||||
option_upl = 0.0
|
||||
option2_upl = 0.0
|
||||
fees = 0.0
|
||||
if oq1 and oq1.bid is not None and qty > 0:
|
||||
bid = float(oq1.bid)
|
||||
of = option_fill(
|
||||
action="close",
|
||||
bid=bid,
|
||||
ask=float(oq1.ask or bid),
|
||||
qty_eth=qty,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
fees += of.fee
|
||||
option_upl = bid * qty - prem1
|
||||
if oq2 and oq2.bid is not None and qty2 > 0:
|
||||
bid = float(oq2.bid)
|
||||
of = option_fill(
|
||||
action="close",
|
||||
bid=bid,
|
||||
ask=float(oq2.ask or bid),
|
||||
qty_eth=qty2,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
fees += of.fee
|
||||
option2_upl = bid * qty2 - prem2
|
||||
g = None
|
||||
gid = pos.get("group_id")
|
||||
if gid:
|
||||
g = self.db.fetchone("SELECT * FROM groups WHERE group_id=?", (gid,))
|
||||
paid = float(g["fees"] or 0) if g else 0.0
|
||||
net = option_upl + option2_upl - paid - fees
|
||||
entry_idx = float(pos.get("entry_index_px") or 0)
|
||||
move = abs(float(index_px) - entry_idx) if index_px is not None and entry_idx else 0.0
|
||||
move_pct = (move / entry_idx * 100.0) if entry_idx > 0 else 0.0
|
||||
return {
|
||||
"has_position": True,
|
||||
"hedge_mode": "option_option",
|
||||
"group_id": gid,
|
||||
"status": "open",
|
||||
"perp_upl": 0.0,
|
||||
"option_upl": option_upl,
|
||||
"option2_upl": option2_upl,
|
||||
"net_pnl": net,
|
||||
"fees_paid": paid,
|
||||
"est_close_fees": fees,
|
||||
"index_px": float(index_px) if index_px is not None else None,
|
||||
"entry_index_px": entry_idx,
|
||||
"move_points": move,
|
||||
"move_pct": move_pct,
|
||||
"initial_premium": prem1 + prem2,
|
||||
"option_inst_id": call_id,
|
||||
"option2_inst_id": put_id,
|
||||
"option_side": "call",
|
||||
"option2_side": "put",
|
||||
"option_qty_eth": qty,
|
||||
"option2_qty_eth": qty2,
|
||||
"option_entry_px": float(pos.get("option_entry_px") or 0),
|
||||
"option2_entry_px": float(pos.get("option2_entry_px") or 0),
|
||||
"strike": float(g["strike"]) if g and g["strike"] is not None else None,
|
||||
"strike2": float(pos.get("strike2") or 0) or None,
|
||||
"expiry_ymd": g["expiry_ymd"] if g else None,
|
||||
"open_at_ms": int(g["open_at_ms"]) if g and g["open_at_ms"] else None,
|
||||
"perp_side": None,
|
||||
"perp_qty_eth": 0.0,
|
||||
"premium_gap": None,
|
||||
}
|
||||
|
||||
def _unrealized_perp(self, pos: dict[str, Any]) -> dict[str, Any]:
|
||||
sess = get_session()
|
||||
snap = sess.snapshot()
|
||||
s = get_settings()
|
||||
|
||||
Reference in New Issue
Block a user