Implement dual target-close paths and residual option expiry.
Document and enforce: A full dual-leg close, B perp-only when deep OTM with residual archive that does not block next open, and expiry settlement when target is missed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from typing import Annotated, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from ..models.db import get_db
|
||||
from ..sim.pnl import summarize_fills_pnl
|
||||
from .auth import require_user
|
||||
|
||||
router = APIRouter(prefix="/api/trades", tags=["trades"])
|
||||
@@ -14,68 +15,6 @@ def _row(r: Any) -> dict:
|
||||
return dict(r)
|
||||
|
||||
|
||||
def summarize_fills_pnl(fills: list[Any]) -> dict[str, float | None]:
|
||||
"""
|
||||
从成交明细重算腿盈亏与净盈亏。
|
||||
价差盈亏按 fill_px(成交价);手续费另扣。
|
||||
净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费(开+平)。
|
||||
"""
|
||||
rows = [dict(x) for x in fills]
|
||||
opt_open = next(
|
||||
(f for f in rows if f.get("leg") == "option" and f.get("action") == "open"),
|
||||
None,
|
||||
)
|
||||
opt_close = next(
|
||||
(f for f in rows if f.get("leg") == "option" and f.get("action") == "close"),
|
||||
None,
|
||||
)
|
||||
perp_open = next(
|
||||
(f for f in rows if f.get("leg") == "perp" and f.get("action") == "open"),
|
||||
None,
|
||||
)
|
||||
perp_close = next(
|
||||
(f for f in rows if f.get("leg") == "perp" and f.get("action") == "close"),
|
||||
None,
|
||||
)
|
||||
|
||||
option_pnl: float | None = None
|
||||
if opt_open and opt_close:
|
||||
qty = float(opt_open.get("qty_eth") or opt_close.get("qty_eth") or 0)
|
||||
option_pnl = (float(opt_close["fill_px"]) - float(opt_open["fill_px"])) * qty
|
||||
|
||||
perp_pnl: float | None = None
|
||||
if perp_open and perp_close:
|
||||
qty = float(perp_open.get("qty_eth") or perp_close.get("qty_eth") or 0)
|
||||
side = str(perp_open.get("side") or "")
|
||||
o = float(perp_open["fill_px"])
|
||||
c = float(perp_close["fill_px"])
|
||||
if side == "long":
|
||||
perp_pnl = (c - o) * qty
|
||||
else:
|
||||
perp_pnl = (o - c) * qty
|
||||
|
||||
fees_total = sum(float(f.get("fee") or 0) for f in rows)
|
||||
gross = None
|
||||
net = None
|
||||
if option_pnl is not None and perp_pnl is not None:
|
||||
gross = option_pnl + perp_pnl
|
||||
net = gross - fees_total
|
||||
elif option_pnl is not None:
|
||||
gross = option_pnl
|
||||
net = option_pnl - fees_total
|
||||
elif perp_pnl is not None:
|
||||
gross = perp_pnl
|
||||
net = perp_pnl - fees_total
|
||||
|
||||
return {
|
||||
"option_pnl": option_pnl,
|
||||
"perp_pnl": perp_pnl,
|
||||
"fees_total": fees_total,
|
||||
"gross_pnl": gross,
|
||||
"net_pnl": net,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/groups")
|
||||
async def list_groups(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
db = get_db()
|
||||
|
||||
@@ -97,6 +97,28 @@ CREATE TABLE IF NOT EXISTS strategy_state (
|
||||
last_error TEXT,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS residual_options (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
group_id TEXT NOT NULL UNIQUE,
|
||||
option_inst_id TEXT NOT NULL,
|
||||
option_side TEXT NOT NULL,
|
||||
option_qty_eth REAL NOT NULL,
|
||||
option_qty_contracts REAL,
|
||||
option_entry_px REAL NOT NULL,
|
||||
strike REAL,
|
||||
expiry_ymd TEXT,
|
||||
expiry_ms INTEGER,
|
||||
entry_index_px REAL,
|
||||
initial_premium REAL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
settled_at_ms INTEGER,
|
||||
settle_px REAL,
|
||||
settle_pnl REAL,
|
||||
note TEXT,
|
||||
FOREIGN KEY(group_id) REFERENCES groups(group_id)
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from ..strategy.session import get_session
|
||||
from .ledger import Ledger
|
||||
from .liquidity import bid_covers_eth, bid_mark_ok, contracts_for_eth
|
||||
from .pricing import (
|
||||
is_deep_otm,
|
||||
option_expiry_settle,
|
||||
option_fill,
|
||||
option_intrinsic,
|
||||
@@ -576,6 +577,317 @@ class Matcher:
|
||||
},
|
||||
)
|
||||
|
||||
def option_is_deep_otm(self) -> bool:
|
||||
"""活跃组期权是否远虚(内在价值≈0)。"""
|
||||
pos = self.current_position()
|
||||
if pos.get("status") != "open" or not pos.get("group_id"):
|
||||
return False
|
||||
group_id = str(pos["group_id"])
|
||||
option_inst_id = str(pos.get("option_inst_id") or "")
|
||||
option_side = str(pos.get("option_side") or "")
|
||||
strike = self._group_strike(group_id, option_inst_id)
|
||||
spot = self._close_spot_px(get_session().snapshot())
|
||||
if strike is None or spot is None:
|
||||
return False
|
||||
return is_deep_otm(
|
||||
option_side=option_side, strike=float(strike), spot=float(spot)
|
||||
)
|
||||
|
||||
def close_perp_abandon_option(self, *, reason: str = "target_perp_only") -> CloseResult:
|
||||
"""
|
||||
目标平仓 B:只平永续,期权归档为到期残留(不再盯盘、不挡新开)。
|
||||
"""
|
||||
s = get_settings()
|
||||
pos = self.current_position()
|
||||
if pos.get("status") != "open" or not pos.get("group_id"):
|
||||
return CloseResult(ok=False, detail="无持仓可平")
|
||||
|
||||
group_id = str(pos["group_id"])
|
||||
sess = get_session()
|
||||
snap = sess.snapshot()
|
||||
if not snap.perp or snap.perp.bid is None or snap.perp.ask is None:
|
||||
return CloseResult(ok=False, detail="永续盘口不可用")
|
||||
|
||||
option_inst_id = str(pos["option_inst_id"])
|
||||
option_side = str(pos["option_side"])
|
||||
strike = self._group_strike(group_id, option_inst_id)
|
||||
spot = self._close_spot_px(snap)
|
||||
if strike is None or spot is None:
|
||||
return CloseResult(ok=False, detail="无法判断远虚:缺行权价或标的价")
|
||||
if not is_deep_otm(
|
||||
option_side=option_side, strike=float(strike), spot=float(spot)
|
||||
):
|
||||
return CloseResult(ok=False, detail="期权非远虚,应走双腿全平")
|
||||
|
||||
fee_rate = self._fee_rate()
|
||||
perp_side = str(pos["perp_side"])
|
||||
perp_qty = float(pos["perp_qty_eth"])
|
||||
perp_entry = float(pos["perp_entry_px"])
|
||||
pf = perp_fill(
|
||||
side=perp_side,
|
||||
action="close",
|
||||
bid=float(snap.perp.bid),
|
||||
ask=float(snap.perp.ask),
|
||||
qty_eth=perp_qty,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
if perp_side == "long":
|
||||
perp_pnl = (pf.fill_px - perp_entry) * perp_qty
|
||||
else:
|
||||
perp_pnl = (perp_entry - pf.fill_px) * perp_qty
|
||||
self.ledger.apply_cash(
|
||||
perp_pnl - pf.fee,
|
||||
kind="close_perp",
|
||||
group_id=group_id,
|
||||
note=f"close perp {reason} abandon option",
|
||||
)
|
||||
|
||||
g = self.db.fetchone("SELECT * FROM groups WHERE group_id=?", (group_id,))
|
||||
expiry_ymd = str(g["expiry_ymd"]) if g and g["expiry_ymd"] else None
|
||||
expiry_ms = None
|
||||
if expiry_ymd:
|
||||
try:
|
||||
from ..exchange.expiry import expiry_ms_from_ymd
|
||||
|
||||
expiry_ms = int(expiry_ms_from_ymd(expiry_ymd))
|
||||
except Exception:
|
||||
expiry_ms = None
|
||||
|
||||
now = int(time.time() * 1000)
|
||||
open_fees = float((g["fees"] if g else 0) or 0)
|
||||
fees = open_fees + pf.fee
|
||||
slip = float((g["slip_cost"] if g else 0) or 0) + pf.slip
|
||||
# 暂记永续段实现盈亏;期权到期结算后再按全部成交重算
|
||||
interim_net = perp_pnl - open_fees - pf.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)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
"perp",
|
||||
"close",
|
||||
"flat",
|
||||
s.perp_inst_id,
|
||||
perp_qty,
|
||||
None,
|
||||
pf.base_px,
|
||||
pf.fill_px,
|
||||
pf.fee,
|
||||
pf.slip,
|
||||
pf.notional,
|
||||
now,
|
||||
),
|
||||
)
|
||||
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,
|
||||
option_inst_id,
|
||||
option_side,
|
||||
float(pos["option_qty_eth"]),
|
||||
float(pos["option_qty_contracts"] or 0),
|
||||
float(pos["option_entry_px"]),
|
||||
float(strike),
|
||||
expiry_ymd,
|
||||
expiry_ms,
|
||||
float(pos["entry_index_px"] or 0),
|
||||
float(pos["initial_premium"] or 0),
|
||||
"pending",
|
||||
now,
|
||||
f"abandoned after {reason}; deep_otm spot={spot:.4f} K={strike}",
|
||||
),
|
||||
)
|
||||
self.db._conn.execute(
|
||||
"""UPDATE groups SET status=?, close_reason=?, realized_pnl=?,
|
||||
fees=?, slip_cost=?, note=? WHERE group_id=?""",
|
||||
(
|
||||
"option_residual",
|
||||
reason,
|
||||
interim_net,
|
||||
fees,
|
||||
slip,
|
||||
f"perp_closed; option residual until expiry",
|
||||
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, status='flat'
|
||||
WHERE id=1"""
|
||||
)
|
||||
self.db._conn.commit()
|
||||
|
||||
return CloseResult(
|
||||
ok=True,
|
||||
detail="perp_closed_option_residual",
|
||||
data={
|
||||
"group_id": group_id,
|
||||
"reason": reason,
|
||||
"mode": "target_perp_only",
|
||||
"perp_pnl": perp_pnl,
|
||||
"interim_net": interim_net,
|
||||
"option_abandoned": True,
|
||||
"strike": float(strike),
|
||||
"spot": float(spot),
|
||||
},
|
||||
)
|
||||
|
||||
def list_residual_options(self, *, pending_only: bool = True) -> list[dict[str, Any]]:
|
||||
if pending_only:
|
||||
rows = self.db.fetchall(
|
||||
"SELECT * FROM residual_options WHERE status='pending' ORDER BY created_at_ms ASC"
|
||||
)
|
||||
else:
|
||||
rows = self.db.fetchall(
|
||||
"SELECT * FROM residual_options ORDER BY created_at_ms DESC LIMIT 100"
|
||||
)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def settle_due_residuals(self, *, now_ms: int | None = None) -> list[dict[str, Any]]:
|
||||
"""到期结算所有 pending 残留期权(不扫描进活跃组平仓)。"""
|
||||
now = int(now_ms if now_ms is not None else time.time() * 1000)
|
||||
pending = self.db.fetchall(
|
||||
"SELECT * FROM residual_options WHERE status='pending' ORDER BY id ASC"
|
||||
)
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in pending:
|
||||
ems = row["expiry_ms"]
|
||||
if ems is None:
|
||||
ymd = row["expiry_ymd"]
|
||||
if ymd:
|
||||
try:
|
||||
from ..exchange.expiry import expiry_ms_from_ymd
|
||||
|
||||
ems = int(expiry_ms_from_ymd(str(ymd)))
|
||||
except Exception:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
if now < int(ems):
|
||||
continue
|
||||
r = self._settle_one_residual(dict(row), now_ms=now)
|
||||
if r:
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
def settle_all_residuals_now(self) -> list[dict[str, Any]]:
|
||||
"""紧急:立即按内在价值结算全部残留(不等到期)。"""
|
||||
pending = self.db.fetchall(
|
||||
"SELECT * FROM residual_options WHERE status='pending' ORDER BY id ASC"
|
||||
)
|
||||
now = int(time.time() * 1000)
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in pending:
|
||||
r = self._settle_one_residual(dict(row), now_ms=now, force=True)
|
||||
if r:
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
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"])
|
||||
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()
|
||||
intrinsic = option_intrinsic(
|
||||
option_side=str(row["option_side"]),
|
||||
strike=float(strike),
|
||||
spot=float(spot),
|
||||
)
|
||||
of = option_expiry_settle(
|
||||
intrinsic=float(intrinsic),
|
||||
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
|
||||
self.ledger.apply_cash(
|
||||
opt_cash,
|
||||
kind="close_option",
|
||||
group_id=group_id,
|
||||
note=f"residual option expiry settle{' force' if force else ''}",
|
||||
)
|
||||
|
||||
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=?, 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 unrealized(self) -> dict[str, Any]:
|
||||
pos = self.current_position()
|
||||
if pos.get("status") != "open":
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""从成交明细汇总腿盈亏与净盈亏。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def summarize_fills_pnl(fills: list[Any]) -> dict[str, float | None]:
|
||||
"""
|
||||
价差盈亏按 fill_px;手续费另扣。
|
||||
净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费(开+平)。
|
||||
允许只有永续已平、期权尚未结算的半组。
|
||||
"""
|
||||
rows = [dict(x) for x in fills]
|
||||
opt_open = next(
|
||||
(f for f in rows if f.get("leg") == "option" and f.get("action") == "open"),
|
||||
None,
|
||||
)
|
||||
opt_close = next(
|
||||
(f for f in rows if f.get("leg") == "option" and f.get("action") == "close"),
|
||||
None,
|
||||
)
|
||||
perp_open = next(
|
||||
(f for f in rows if f.get("leg") == "perp" and f.get("action") == "open"),
|
||||
None,
|
||||
)
|
||||
perp_close = next(
|
||||
(f for f in rows if f.get("leg") == "perp" and f.get("action") == "close"),
|
||||
None,
|
||||
)
|
||||
|
||||
option_pnl: float | None = None
|
||||
if opt_open and opt_close:
|
||||
qty = float(opt_open.get("qty_eth") or opt_close.get("qty_eth") or 0)
|
||||
option_pnl = (float(opt_close["fill_px"]) - float(opt_open["fill_px"])) * qty
|
||||
|
||||
perp_pnl: float | None = None
|
||||
if perp_open and perp_close:
|
||||
qty = float(perp_open.get("qty_eth") or perp_close.get("qty_eth") or 0)
|
||||
side = str(perp_open.get("side") or "")
|
||||
o = float(perp_open["fill_px"])
|
||||
c = float(perp_close["fill_px"])
|
||||
if side == "long":
|
||||
perp_pnl = (c - o) * qty
|
||||
else:
|
||||
perp_pnl = (o - c) * qty
|
||||
|
||||
fees_total = sum(float(f.get("fee") or 0) for f in rows)
|
||||
gross = None
|
||||
net = None
|
||||
if option_pnl is not None and perp_pnl is not None:
|
||||
gross = option_pnl + perp_pnl
|
||||
net = gross - fees_total
|
||||
elif option_pnl is not None:
|
||||
gross = option_pnl
|
||||
net = option_pnl - fees_total
|
||||
elif perp_pnl is not None:
|
||||
gross = perp_pnl
|
||||
net = perp_pnl - fees_total
|
||||
|
||||
return {
|
||||
"option_pnl": option_pnl,
|
||||
"perp_pnl": perp_pnl,
|
||||
"fees_total": fees_total,
|
||||
"gross_pnl": gross,
|
||||
"net_pnl": net,
|
||||
}
|
||||
@@ -29,6 +29,22 @@ def option_intrinsic(*, option_side: str, strike: float, spot: float) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
def is_deep_otm(
|
||||
*,
|
||||
option_side: str,
|
||||
strike: float,
|
||||
spot: float,
|
||||
max_intrinsic: float = 0.01,
|
||||
) -> bool:
|
||||
"""
|
||||
远虚:内在价值≈0(多头期权已无行权价值)。
|
||||
100×杠杆 ATM 在标的波动约1%后常落入此状态。
|
||||
"""
|
||||
return option_intrinsic(
|
||||
option_side=option_side, strike=strike, spot=spot
|
||||
) <= float(max_intrinsic)
|
||||
|
||||
|
||||
def option_expiry_settle(
|
||||
*,
|
||||
intrinsic: float,
|
||||
|
||||
@@ -88,6 +88,7 @@ class StrategyEngine:
|
||||
"can_open": allow_open,
|
||||
"last_error": last_error,
|
||||
"position": upl,
|
||||
"residuals": self.matcher.list_residual_options(pending_only=True),
|
||||
"ledger": self.ledger.snapshot(),
|
||||
}
|
||||
|
||||
@@ -118,16 +119,24 @@ class StrategyEngine:
|
||||
|
||||
async def emergency_close(self) -> dict[str, Any]:
|
||||
async with self._lock:
|
||||
# 紧急全平:绕过期权流动性/偏差校验
|
||||
r = self.matcher.close_group(reason="emergency", bypass_liquidity=True)
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
close_data: dict[str, Any] | None = None
|
||||
detail = "flat"
|
||||
ok = True
|
||||
pos = self.matcher.current_position()
|
||||
if pos.get("status") == "open":
|
||||
r = self.matcher.close_group(reason="emergency", bypass_liquidity=True)
|
||||
ok = r.ok
|
||||
detail = r.detail
|
||||
close_data = r.data
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
residuals = self.matcher.settle_all_residuals_now()
|
||||
return {
|
||||
"close": {
|
||||
"ok": r.ok,
|
||||
"detail": r.detail,
|
||||
"liquidity_wait": r.liquidity_wait,
|
||||
"data": r.data,
|
||||
"ok": ok,
|
||||
"detail": detail,
|
||||
"data": close_data,
|
||||
"residuals_settled": residuals,
|
||||
},
|
||||
"state": self.state(),
|
||||
}
|
||||
@@ -175,9 +184,27 @@ class StrategyEngine:
|
||||
reason: str,
|
||||
bypass_liquidity: bool,
|
||||
pending_close: bool,
|
||||
abandon_if_deep_otm: bool = False,
|
||||
) -> None:
|
||||
if not pending_close:
|
||||
self._set_state(phase="closing", last_error=None)
|
||||
|
||||
# 目标平仓 B:远虚 → 只平永续,期权归档
|
||||
if abandon_if_deep_otm and reason != "expiry" and self.matcher.option_is_deep_otm():
|
||||
r = await asyncio.to_thread(
|
||||
self.matcher.close_perp_abandon_option,
|
||||
reason="target_perp_only",
|
||||
)
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
self._set_state(
|
||||
last_error=None,
|
||||
phase="resting",
|
||||
)
|
||||
else:
|
||||
self._set_state(phase="closing", last_error=r.detail)
|
||||
return
|
||||
|
||||
r = await asyncio.to_thread(
|
||||
self.matcher.close_group,
|
||||
reason=reason,
|
||||
@@ -186,12 +213,25 @@ class StrategyEngine:
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
elif r.liquidity_wait and not bypass_liquidity:
|
||||
# 等待期间若已变成远虚,下一 tick 走归档
|
||||
if self.matcher.option_is_deep_otm():
|
||||
r2 = await asyncio.to_thread(
|
||||
self.matcher.close_perp_abandon_option,
|
||||
reason="target_perp_only",
|
||||
)
|
||||
if r2.ok:
|
||||
self._after_close()
|
||||
return
|
||||
self._set_state(phase="liquidity_wait", last_error=r.detail)
|
||||
else:
|
||||
self._set_state(phase="closing", last_error=r.detail)
|
||||
|
||||
async def _settle_residuals(self) -> None:
|
||||
await asyncio.to_thread(self.matcher.settle_due_residuals)
|
||||
|
||||
async def _maybe_expiry_close(self) -> bool:
|
||||
"""若持仓已到期则强制全平。返回是否触发到期平仓。"""
|
||||
await self._settle_residuals()
|
||||
pos = self.matcher.current_position()
|
||||
if pos.get("status") != "open":
|
||||
return False
|
||||
@@ -206,6 +246,7 @@ class StrategyEngine:
|
||||
reason="expiry",
|
||||
bypass_liquidity=True,
|
||||
pending_close=pending,
|
||||
abandon_if_deep_otm=False,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -242,6 +283,9 @@ class StrategyEngine:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _tick_async(self) -> None:
|
||||
# 残留期权到期结算(与活跃组隔离,不挡开仓)
|
||||
await self._settle_residuals()
|
||||
|
||||
s = get_settings()
|
||||
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
|
||||
assert st is not None
|
||||
@@ -260,7 +304,7 @@ class StrategyEngine:
|
||||
)
|
||||
pos = self.matcher.current_position()
|
||||
|
||||
# 有未平仓:只盯平仓,绝不开下一组
|
||||
# 有活跃持仓:只盯当前组平仓;残留期权不在此扫描
|
||||
if pos.get("status") == "open":
|
||||
upl = self.matcher.unrealized()
|
||||
expired = check_expiry_close(expiry_ms=self._position_expiry_ms(upl))
|
||||
@@ -276,13 +320,17 @@ class StrategyEngine:
|
||||
if expired.should_close:
|
||||
reason = "expiry"
|
||||
bypass = True
|
||||
abandon = False
|
||||
else:
|
||||
reason = decision.reason or "liquidity_retry"
|
||||
bypass = False
|
||||
# 目标达标(或流动性等待重试)时:远虚走只平永续
|
||||
abandon = bool(decision.should_close or pending_close)
|
||||
await self._close_open_position(
|
||||
reason=reason,
|
||||
bypass_liquidity=bypass,
|
||||
pending_close=pending_close,
|
||||
abandon_if_deep_otm=abandon,
|
||||
)
|
||||
else:
|
||||
self._set_state(phase="open", last_error=None)
|
||||
|
||||
@@ -144,6 +144,18 @@ def test_expiry_close() -> None:
|
||||
assert d3.should_close is True
|
||||
|
||||
|
||||
def test_deep_otm_and_expiry_settle() -> None:
|
||||
from app.sim.pricing import is_deep_otm, option_expiry_settle, option_intrinsic
|
||||
|
||||
assert is_deep_otm(option_side="call", strike=1860, spot=1840) is True
|
||||
assert is_deep_otm(option_side="call", strike=1860, spot=1882) is False
|
||||
assert is_deep_otm(option_side="put", strike=1860, spot=1882) is True
|
||||
assert option_intrinsic(option_side="call", strike=1860, spot=1840) == 0.0
|
||||
settled = option_expiry_settle(intrinsic=0.0, qty_eth=2.0, fee_rate=0.0005)
|
||||
assert settled.fill_px == 0.0
|
||||
assert settled.notional == 0.0
|
||||
|
||||
|
||||
def test_option_intrinsic_and_close_bid_floor() -> None:
|
||||
from app.sim.pricing import (
|
||||
option_expiry_settle,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.0.1">
|
||||
<node TEXT="★ ETH 永续 + 期权对冲"><node TEXT="① 期权"><node TEXT="优势"><node TEXT="亏损有界"/><node TEXT="大波动弹性强"/><node TEXT="租金模型"/><node TEXT="1 ETH 好算"/><node TEXT="只做买方"/></node><node TEXT="劣势"><node TEXT="横盘磨损"/><node TEXT="盘口薄"/><node TEXT="买一依赖"/><node TEXT="ATM 偏离"/><node TEXT="临期恶化"/></node></node><node TEXT="② 永续合约"><node TEXT="优势"><node TEXT="流动性深"/><node TEXT="对冲清晰"/><node TEXT="杠杆可配"/><node TEXT="组成对冲组"/></node><node TEXT="劣势"><node TEXT="强平风险"/><node TEXT="资金费率"/><node TEXT="保证金挤压"/><node TEXT="横盘无助"/><node TEXT="1:2 非精确"/></node></node><node TEXT="③ 设计理念"><node TEXT="ATM 买方 + 反向永续"/><node TEXT="波动兑现 · 磨损认账"/><node TEXT="到期全平 · 概率样本"/><node TEXT="权利金 = 租金"/><node TEXT="薄腿优先 · 单组纪律"/></node><node TEXT="④ 资金要求"><node TEXT="默认 1+2 ETH · 3x"/><node TEXT="推荐权益 ≥ 5000"/><node TEXT="舒适 ≥ 10000"/><node TEXT="试跑 0.1 / 0.2"/></node><node TEXT="⑤ 操盘思路"><node TEXT="心法与节奏"><node TEXT="有波动多做"/><node TEXT="目标 15U"/><node TEXT="休息 5 分钟"/><node TEXT="最多 1 组"/></node><node TEXT="仓位结构"><node TEXT="永续 1 · 期权 2"/><node TEXT="先期权后永续"/><node TEXT="ATM 选向"/></node><node TEXT="开仓"><node TEXT="前置检查"/><node TEXT="选约门槛"/><node TEXT="四步执行"/></node><node TEXT="平仓"><node TEXT="净利达标"/><node TEXT="流动性闸门"/><node TEXT="到期全平"/></node><node TEXT="关键参数"><node TEXT="仓位/杠杆/费率"/><node TEXT="目标/休息/周末"/><node TEXT="选约/偏差"/></node></node><node TEXT="⑥ 风险评估"><node TEXT="横盘磨损"/><node TEXT="SIM≠实盘"/><node TEXT="方向/流动性/保证金"/><node TEXT="不做卖方与叠仓"/></node><node TEXT="⑦ 纪律要求"><node TEXT="单组 · 休息 · 周末"/><node TEXT="不硬砸 · 看净利"/><node TEXT="不报复 · 不叠仓"/><node TEXT="SIM 后再放大"/></node></node>
|
||||
<node TEXT="★ ETH 永续 + 期权对冲"><node TEXT="① 期权"><node TEXT="优势"><node TEXT="亏损有界"/><node TEXT="大波动弹性强"/><node TEXT="租金模型"/><node TEXT="1 ETH 好算"/><node TEXT="只做买方"/></node><node TEXT="劣势"><node TEXT="横盘磨损"/><node TEXT="盘口薄"/><node TEXT="买一依赖"/><node TEXT="ATM 偏离"/><node TEXT="临期恶化"/></node></node><node TEXT="② 永续合约"><node TEXT="优势"><node TEXT="流动性深"/><node TEXT="对冲清晰"/><node TEXT="杠杆可配"/><node TEXT="组成对冲组"/></node><node TEXT="劣势"><node TEXT="强平风险"/><node TEXT="资金费率"/><node TEXT="保证金挤压"/><node TEXT="横盘无助"/><node TEXT="1:2 非精确"/></node></node><node TEXT="③ 设计理念"><node TEXT="ATM 买方 + 反向永续"/><node TEXT="波动兑现 · 磨损认账"/><node TEXT="到期全平 · 概率样本"/><node TEXT="权利金 = 租金"/><node TEXT="薄腿优先 · 单组纪律"/></node><node TEXT="④ 资金要求"><node TEXT="默认 1+2 ETH · 3x"/><node TEXT="推荐权益 ≥ 5000"/><node TEXT="舒适 ≥ 10000"/><node TEXT="试跑 0.1 / 0.2"/></node><node TEXT="⑤ 操盘思路"><node TEXT="心法与节奏"><node TEXT="有波动多做"/><node TEXT="目标 15U"/><node TEXT="休息 5 分钟"/><node TEXT="最多 1 组"/></node><node TEXT="仓位结构"><node TEXT="永续 1 · 期权 2"/><node TEXT="先期权后永续"/><node TEXT="ATM 选向"/></node><node TEXT="开仓"><node TEXT="前置检查"/><node TEXT="选约门槛"/><node TEXT="四步执行"/></node><node TEXT="平仓"><node TEXT="目标A双腿"/><node TEXT="目标B只平永续"/><node TEXT="到期结算"/></node><node TEXT="关键参数"><node TEXT="仓位/杠杆/费率"/><node TEXT="目标/休息/周末"/><node TEXT="选约/偏差"/></node></node><node TEXT="⑥ 风险评估"><node TEXT="横盘磨损"/><node TEXT="SIM≠实盘"/><node TEXT="方向/流动性/保证金"/><node TEXT="残留不挡新开"/></node><node TEXT="⑦ 纪律要求"><node TEXT="活跃1组 · 残留可共存"/><node TEXT="远虚归档 · 不挡新开"/><node TEXT="看净利 · 不报复"/><node TEXT="SIM 后再放大"/></node></node>
|
||||
</map>
|
||||
|
||||
Binary file not shown.
+58
-15
@@ -101,11 +101,12 @@
|
||||
|
||||
## 4. 平仓机制
|
||||
|
||||
任一触发 → **该组永续 + 期权全部平掉**(先期权、后永续)。
|
||||
平仓分两大类:**① 目标平仓**(净盈利达标);**② 到期平仓**(目标未达标,拖到到期)。
|
||||
目标平仓又分两种落地方式(双腿全平 / 只平永续+期权归档到期)。
|
||||
|
||||
### 4.1 净盈利达标(主出场)
|
||||
### 4.1 目标平仓(第一优先级)
|
||||
|
||||
**净盈利**口径(盯盘用):
|
||||
**净盈利**口径(盯盘用,主要看买一,不看标记价):
|
||||
|
||||
```
|
||||
净盈利 ≈ 永续浮盈 + 期权浮盈 − 预估平仓手续费
|
||||
@@ -119,32 +120,72 @@
|
||||
|
||||
说明:权利金倍数模式下,**1× = 赚回一倍权利金金额的净利**(例如权利金 20.41,目标约 20.41,不是 40.82)。
|
||||
|
||||
### 4.2 到期自动全平
|
||||
达标后按期权状态走下面 **A / B** 两套(不是互相替换,按能否平掉期权分支):
|
||||
|
||||
#### 4.1.A 双腿全平(期权仍有可平价值 / 非远虚)
|
||||
|
||||
- 条件:净利达标,且期权 **不是远虚**(仍可能有买一可平)。
|
||||
- 动作:校验买一深度 + 买一/标记偏差(默认 ≤30%)→ **先平期权、再平永续**。
|
||||
- 通不过流动性 → `liquidity_wait`,继续等待(不改开仓组)。
|
||||
|
||||
#### 4.1.B 只平永续 + 期权归档到期(远虚)
|
||||
|
||||
筛选约 **100× 杠杆** 期权时,标的波动约 **1%** 量级后,买方腿常变为**远虚**(内在价值≈0),买一往往枯死、整组硬平会卡死。
|
||||
|
||||
- 条件:净利达标,且期权已是 **虚值且远虚**(内在价值 ≈ 0)。
|
||||
- 动作:
|
||||
1. **只市价平掉永续**,兑现净利里永续那一截;
|
||||
2. 本张期权 **不再盯盘、不再参与平仓扫描**,归档为「到期残留」;
|
||||
3. 因期权 **逐仓**,残留 **不占用活跃持仓**,**不挡住下一组开仓**;
|
||||
4. 下一组开平仓 **只扫当前活跃组期权**,不扫描历史残留腿;
|
||||
5. 残留期权到到期日再按 **内在价值** 单独结算(多半接近 0)。
|
||||
|
||||
> 直觉:能双腿平就走 A;期权已经远虚平不掉就走 B,别把永续利润和下一单机会一起卡死。
|
||||
|
||||
### 4.2 到期平仓(目标未达标)
|
||||
|
||||
- 净利一直未达标 → **不主动砍仓**,持有到期权到期。
|
||||
- 到达期权到期时刻(OKX:UTC 08:00 = **上海 16:00**)→ 原因 `expiry`。
|
||||
- **对齐实盘**:期权按标的结算价的 **内在价值** 入账(不吃盘口、无价差滑点);永续仍市价平掉。
|
||||
- 策略 **暂停时仍执行**,避免拖过期。
|
||||
- **活跃组**:期权按标的结算价的 **内在价值** 入账;若永续仍在则市价平掉。
|
||||
- **残留组**(曾走 4.1.B):仅结算归档期权,不碰当前活跃仓。
|
||||
- 策略 **暂停时仍执行到期结算**,避免拖过期。
|
||||
- 波动小、拖到到期:权利金亏损视为 **预算内成本**,可接受。
|
||||
|
||||
### 4.3 平仓流动性闸门(常规出场)
|
||||
### 4.3 平仓流动性闸门(仅 4.1.A)
|
||||
|
||||
常规净盈利平仓时:
|
||||
双腿全平时:
|
||||
|
||||
- 期权买一深度需覆盖平仓名义;
|
||||
- 买一相对标记偏差默认 ≤ **30%**(`close_bid_mark_max_pct`);
|
||||
- 不满足 → `liquidity_wait`,继续等待,不改开仓。
|
||||
- 不满足 → `liquidity_wait`,继续等待。
|
||||
|
||||
**紧急全平**:绕过上述闸门;期权价取 max(买一, 标记, 内在价值)。
|
||||
**4.1.B / 到期 / 紧急全平**:不适用「必须卖掉期权买一」这套闸门(到期与残留按内在价值;紧急可绕过)。
|
||||
|
||||
### 4.4 其它平仓入口
|
||||
**紧急全平**:活跃组尽量双腿平掉;残留期权一并按内在价值结算。
|
||||
|
||||
### 4.4 其它平仓入口 / 原因码
|
||||
|
||||
| 原因码 | 含义 |
|
||||
|--------|------|
|
||||
| `fixed_usdt` / `premium_multiple` | 净盈利达标 |
|
||||
| `expiry` | 到期自动全平 |
|
||||
| `fixed_usdt` / `premium_multiple` | 目标平仓 · 双腿全平(4.1.A) |
|
||||
| `target_perp_only` | 目标平仓 · 只平永续,期权归档到期(4.1.B) |
|
||||
| `expiry` | 到期结算(活跃组或残留期权) |
|
||||
| `emergency` | 界面紧急全平 |
|
||||
| `manual` | 手动平仓 |
|
||||
|
||||
### 4.5 流程总览
|
||||
|
||||
```
|
||||
有活跃持仓
|
||||
├─ 净利达标?
|
||||
│ ├─ 是 · 期权远虚(内在价值≈0)→ 平永续,期权归档到期,可开下一组
|
||||
│ └─ 是 · 非远虚 → 试双腿全平(流动性闸门)→ 失败则等待
|
||||
└─ 否 → 持有直到到期 → 内在价值结算(+ 若有永续则平永续)
|
||||
|
||||
残留期权(已归档)
|
||||
└─ 仅到期结算;不参与盯盘、不参与下一组平仓扫描
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 策略侧重点
|
||||
@@ -155,8 +196,9 @@
|
||||
| **概率与轮次** | 波动日可能多轮;磨盘日可能零轮或一轮认亏 |
|
||||
| **确定性时间规则** | 用周末跳过、到期全平,**不做**难以精准的横盘识别 |
|
||||
| **费用后净利** | 出场看扣完预估平仓费后的净盈利,避免「账面赚、平完亏」 |
|
||||
| **单组纪律** | 有仓不开下一组,防止重叠风险 |
|
||||
| **单组纪律** | 活跃持仓最多 1 组;远虚归档的残留期权不挡下一组(逐仓) |
|
||||
| **权利金是成本预算** | 小波动拖到期亏权利金,属于策略设计内损耗,不是异常 bug |
|
||||
| **目标平仓两分支** | 能平期权则双腿全平;远虚则只平永续、期权到期归档 |
|
||||
|
||||
适合对外表述:
|
||||
|
||||
@@ -173,7 +215,7 @@
|
||||
| **横盘 / 低波动** | 高 | Theta 消耗权利金;永续几乎不贡献利润 | 周末跳过;到期认亏全平;接受磨损日 |
|
||||
| **方向选错** | 中高 | 卖一比价并非预测圣杯;错向时期权可能失利,永续端也可能承压 | 净盈利达标才兑现;到期强平止损时间 |
|
||||
| **开平滑点与手续费** | 中 | 双边 taker + 滑点侵蚀小目标利润 | 目标默认 15U 需覆盖费用;勿把目标设过低 |
|
||||
| **期权流动性** | 中 | 平仓买一薄、偏差大 → 等待 | 流动性闸门;到期/紧急可绕过 |
|
||||
| **期权流动性** | 中 | 远虚时买一枯死 | 4.1.B 只平永续+归档;到期内在价值结算 |
|
||||
| **杠杆与保证金** | 中 | 永续 3×,标的急跌/急涨挤压保证金 | 控制名义、保留资金缓冲;实盘注意强平线 |
|
||||
| **同到期 ATM 拥挤** | 低–中 | 临近到期价差与盘口恶化 | `min_option_hours`;到期强平 |
|
||||
| **SIM≠实盘** | 高(若直接外推) | 本地撮合、深度与成交假设简化 | 小资金实盘验证后再放大 |
|
||||
@@ -258,3 +300,4 @@
|
||||
| 2026-07-25 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 |
|
||||
| 2026-07-25 | 平仓顺序改为先期权后永续(与开仓同理:薄腿优先) |
|
||||
| 2026-07-26 | 到期按内在价值结算(对齐实盘);紧急平仓仍用 max(买一,标记,内在价值) |
|
||||
| 2026-07-26 | 明确两套目标平仓:双腿全平 / 远虚只平永续+期权归档到期;到期为未达标路径 |
|
||||
|
||||
@@ -166,6 +166,14 @@ export type PlanState = {
|
||||
initial_premium?: number;
|
||||
premium_gap?: number;
|
||||
};
|
||||
residuals?: {
|
||||
group_id: string;
|
||||
option_inst_id: string;
|
||||
option_side: string;
|
||||
strike: number | null;
|
||||
expiry_ymd: string | null;
|
||||
status: string;
|
||||
}[];
|
||||
ledger: { equity: number; available: number; reserved: number };
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const STATUS_ZH: Record<string, string> = {
|
||||
open: "持仓中",
|
||||
closed: "已平仓",
|
||||
option_residual: "期权残留待到期",
|
||||
};
|
||||
|
||||
const SIDE_ZH: Record<string, string> = {
|
||||
@@ -23,9 +24,10 @@ const ACTION_ZH: Record<string, string> = {
|
||||
};
|
||||
|
||||
const CLOSE_REASON_ZH: Record<string, string> = {
|
||||
fixed_usdt: "固定净盈利达标",
|
||||
premium_multiple: "权利金倍数达标",
|
||||
expiry: "到期自动全平",
|
||||
fixed_usdt: "固定净盈利达标·双腿全平",
|
||||
premium_multiple: "权利金倍数达标·双腿全平",
|
||||
target_perp_only: "净盈利达标·只平永续(期权归档)",
|
||||
expiry: "到期结算",
|
||||
emergency: "紧急全平",
|
||||
manual: "手动平仓",
|
||||
liquidity_retry: "流动性等待后续平仓",
|
||||
|
||||
@@ -134,7 +134,8 @@ export default function PlanPage() {
|
||||
<p style={{ color: "var(--muted)", marginTop: -8 }}>
|
||||
SIM 本地撮合 · 行情{" "}
|
||||
{(snap?.exchange || "okx").toUpperCase()}
|
||||
{snap?.perp_inst_id ? ` · ${snap.perp_inst_id}` : ""} · 净盈利达标或到期全平
|
||||
{snap?.perp_inst_id ? ` · ${snap.perp_inst_id}` : ""} ·
|
||||
目标平仓(双腿/远虚只平永续) · 未达标则到期结算
|
||||
</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
|
||||
@@ -228,6 +229,19 @@ export default function PlanPage() {
|
||||
<span>当前组</span>
|
||||
<span className="mono">{pos?.group_id || "—"}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>残留期权(待到期)</span>
|
||||
<span className="mono">
|
||||
{plan?.residuals && plan.residuals.length > 0
|
||||
? plan.residuals
|
||||
.map(
|
||||
(r) =>
|
||||
`${r.group_id}:${r.option_inst_id || "?"}@${r.expiry_ymd || "?"}`
|
||||
)
|
||||
.join(" · ")
|
||||
: "无"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>信号方向</span>
|
||||
<span className="mono">{biasTag}</span>
|
||||
|
||||
@@ -395,41 +395,35 @@ def bodies() -> dict[str, list[str]]:
|
||||
topic(
|
||||
"平仓",
|
||||
[
|
||||
L("触发即全平 · 先期权后永续"),
|
||||
L("两大类:目标平仓 · 到期平仓"),
|
||||
G(
|
||||
"主出场",
|
||||
"目标A · 双腿全平",
|
||||
[
|
||||
"净利 = 双腿浮盈 − 预估平仓费",
|
||||
"默认目标 ≥ 15 USDT",
|
||||
"或 ≥ 权利金 × 1.0",
|
||||
"净利达标 + 期权非远虚",
|
||||
"先期权后永续",
|
||||
"流动性闸门:深度 + 标记偏差30%",
|
||||
],
|
||||
),
|
||||
G(
|
||||
"流动性闸门",
|
||||
"目标B · 只平永续",
|
||||
[
|
||||
"买一深度足够",
|
||||
"买一偏离标记 ≤ 30%",
|
||||
"不满足则等待",
|
||||
"净利达标 + 期权远虚(内在约0)",
|
||||
"100x杠杆下波动约1%常远虚",
|
||||
"期权归档到期 · 不再盯盘",
|
||||
"逐仓不挡下一组开仓",
|
||||
"下一组不扫描残留期权",
|
||||
],
|
||||
),
|
||||
G(
|
||||
"到期全平",
|
||||
"到期平仓",
|
||||
[
|
||||
"上海 16:00(OKX)",
|
||||
"绕过闸门 · 暂停也执行",
|
||||
"亏权利金 = 预算成本",
|
||||
"目标未达标 → 持有到到期",
|
||||
"期权按内在价值结算",
|
||||
"活跃组若有永续一并平",
|
||||
"残留组只结归档期权",
|
||||
],
|
||||
),
|
||||
L("另有:紧急全平 / 手动平仓"),
|
||||
G(
|
||||
"执行",
|
||||
[
|
||||
"① 确认期权买一",
|
||||
"② 平期权入账",
|
||||
"③ 瞬时平永续",
|
||||
"④ 失败则回滚",
|
||||
],
|
||||
),
|
||||
],
|
||||
style_id="sty-ops_close",
|
||||
marker="arrow-down",
|
||||
@@ -551,15 +545,15 @@ def mm_flat() -> list[str]:
|
||||
mm("心法与节奏", [mm(x) for x in ["有波动多做", "目标 15U", "休息 5 分钟", "最多 1 组"]]),
|
||||
mm("仓位结构", [mm(x) for x in ["永续 1 · 期权 2", "先期权后永续", "ATM 选向"]]),
|
||||
mm("开仓", [mm(x) for x in ["前置检查", "选约门槛", "四步执行"]]),
|
||||
mm("平仓", [mm(x) for x in ["净利达标", "流动性闸门", "到期全平"]]),
|
||||
mm("平仓", [mm(x) for x in ["目标A双腿", "目标B只平永续", "到期结算"]]),
|
||||
mm("关键参数", [mm(x) for x in ["仓位/杠杆/费率", "目标/休息/周末", "选约/偏差"]]),
|
||||
],
|
||||
),
|
||||
mm("⑥ 风险评估", [mm(x) for x in ["横盘磨损", "SIM≠实盘", "方向/流动性/保证金", "不做卖方与叠仓"]]),
|
||||
mm("⑥ 风险评估", [mm(x) for x in ["横盘磨损", "SIM≠实盘", "方向/流动性/保证金", "残留不挡新开"]]),
|
||||
mm("⑦ 纪律要求", [mm(x) for x in [
|
||||
"单组 · 休息 · 周末",
|
||||
"不硬砸 · 看净利",
|
||||
"不报复 · 不叠仓",
|
||||
"活跃1组 · 残留可共存",
|
||||
"远虚归档 · 不挡新开",
|
||||
"看净利 · 不报复",
|
||||
"SIM 后再放大",
|
||||
]]),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user