Add hedge-plan WeChat start/end alerts and expiry settlement.

Wire idempotent notify on open/close/partial fail, settle OO at expiry, and close orphaned TP option legs without rewriting plan totals.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-14 13:23:32 +08:00
parent 982497d65a
commit e6907dfbbe
6 changed files with 682 additions and 27 deletions
+180 -23
View File
@@ -1,12 +1,14 @@
"""对冲计划监控:永期 TP/SL期期目标价/到期收口."""
"""对冲计划监控:永期 TP/SL期期目标价到期结算与微信收口推送."""
from __future__ import annotations
import os
from datetime import datetime, timezone
from typing import Any, Optional
from lib.hedge_plan.hedge_plan_db import get_plan_legs, list_plans, update_plan
from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, list_plans, update_plan
from lib.hedge_plan.hedge_plan_notify_lib import notify_hedge, notify_plan_end, build_hedge_alert_message
from lib.hedge_plan.hedge_plan_orders_lib import _sell_option
from lib.hedge_plan.hedge_plan_settle_lib import leg_is_expired, settle_option_leg_at_spot
def _now() -> str:
@@ -47,11 +49,21 @@ def _index_px(cfg: dict[str, Any], underlying: str) -> Optional[float]:
return fn(ex, underlying)
except Exception:
return None
# 无期权账户时回退永续 ticker
ex_perp = cfg.get("exchange")
if ex_perp is not None:
try:
base = (underlying or "ETH").upper()
sym = f"{base}/USDT:USDT"
t = ex_perp.fetch_ticker(sym)
return _sf((t.get("info") or {}).get("markPx")) or _sf(t.get("last"))
except Exception:
return None
return None
def tick_active_plans(cfg: dict[str, Any]) -> dict[str, Any]:
"""扫描 active 计划并按规则收口.返回处理摘要."""
"""扫描 active 计划 + 止盈后遗留期权到期收口.返回处理摘要."""
get_db = cfg.get("get_db")
if not callable(get_db):
return {"ok": False, "msg": "get_db missing"}
@@ -61,31 +73,44 @@ def tick_active_plans(cfg: dict[str, Any]) -> dict[str, Any]:
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables
init_hedge_plan_tables(conn)
plans = list_plans(conn, status="active", limit=20)
plans = list_plans(conn, status="active", limit=40)
for plan in plans:
r = _tick_one(cfg, conn, plan)
if r:
acted.append(r)
orphaned = _settle_orphaned_after_tp(cfg, conn)
acted.extend(orphaned)
conn.commit()
finally:
conn.close()
return {"ok": True, "acted": acted}
def _notify_end_reload(cfg: dict[str, Any], conn: Any, plan_id: int) -> None:
plan = get_plan(conn, int(plan_id))
if plan:
notify_plan_end(cfg, conn, plan)
def _tick_one(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> Optional[dict[str, Any]]:
pt = plan.get("plan_type")
legs = get_plan_legs(conn, int(plan["id"]))
if pt == "perp_options":
return _tick_po(cfg, conn, plan, legs)
# 先判断期权是否已过期且永续仍在(罕见);主路径仍是永续平仓侦测
r = _tick_po(cfg, conn, plan, legs)
return r
if pt == "options_options":
return _tick_oo(cfg, conn, plan, legs)
r = _tick_oo_expiry(cfg, conn, plan, legs)
if r:
return r
return _tick_oo_target(cfg, conn, plan, legs)
return None
def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]) -> Optional[dict[str, Any]]:
perp = next((x for x in legs if x.get("leg_role") == "perp"), None)
opt = next((x for x in legs if x.get("leg_role") == "option_hedge"), None)
if not perp:
if not perp or perp.get("status") != "open":
return None
symbol = perp.get("symbol") or ""
direction = (plan.get("direction") or "long").lower()
@@ -117,7 +142,6 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
elif direction == "short" and mark <= tp:
reason = "perp_tp"
premium = float(plan.get("premium_total") or 0)
# 粗算永续已实现
cs = float(cfg.get("default_contract_size") or 0.01)
get_cs = cfg.get("get_contract_size")
if callable(get_cs) and symbol:
@@ -140,13 +164,20 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
inst_id=str(opt.get("inst_id") or ""),
sheets=float(opt.get("size") or 1),
)
# 简化:平仓失败仍结束计划并记 −权利金
if not close_r.get("ok"):
notify_hedge(
cfg,
build_hedge_alert_message(
title="永续止损后期权强制平仓失败",
plan_id=plan.get("id"),
detail=str(close_r.get("msg") or close_r),
),
)
if close_r.get("ok"):
# 无法精确拿到卖出价差时仍用 −premium 作为下限;有 bid 则近似
bid = _sf(close_r.get("bid"))
ask_open = _sf(opt.get("avg_open"))
if bid is not None and ask_open is not None:
ct = 0.01
ct = float(opt.get("ct_mult") or 0.01)
opt_pnl = (bid - ask_open) * float(opt.get("size") or 1) * ct
else:
opt_pnl = -premium
@@ -155,9 +186,17 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
("closed", reason, _now(), opt_pnl, opt["id"]),
)
elif reason == "perp_tp" and opt:
# 止盈默认不平期权
if _env_bool("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", False):
_sell_option(cfg, inst_id=str(opt.get("inst_id") or ""), sheets=float(opt.get("size") or 1))
close_r = _sell_option(cfg, inst_id=str(opt.get("inst_id") or ""), sheets=float(opt.get("size") or 1))
if not close_r.get("ok"):
notify_hedge(
cfg,
build_hedge_alert_message(
title="永续止盈后期权平仓失败",
plan_id=plan.get("id"),
detail=str(close_r.get("msg") or close_r),
),
)
conn.execute(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=? WHERE id=?",
("closed", reason, _now(), opt["id"]),
@@ -170,9 +209,9 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
opt_pnl = -premium
if reason == "perp_tp":
total = perp_pnl + opt_pnl # = 止盈盈利 权利金
total = perp_pnl + opt_pnl
else:
total = opt_pnl + perp_pnl # 期权盈亏 + 永续盈亏
total = opt_pnl + perp_pnl
conn.execute(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
@@ -189,33 +228,32 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
stats_bucket="tp" if reason == "perp_tp" else "sl",
closed_at=_now(),
)
_notify_end_reload(cfg, conn, int(plan["id"]))
return {"plan_id": plan["id"], "close_reason": reason, "total": total}
def _tick_oo(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]) -> Optional[dict[str, Any]]:
def _tick_oo_target(
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
) -> Optional[dict[str, Any]]:
target = _sf(plan.get("target_price"))
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
if target is None or idx is None:
return None
# 简化:接近目标价(相对 0.15%)时平盈利腿
if abs(idx - target) / max(target, 1) > 0.0015 and not (idx >= target or idx <= target):
pass
near = abs(idx - target) / max(abs(target), 1.0) <= 0.002
if not near:
return None
if not _env_bool("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", True):
return None
open_legs = [x for x in legs if x.get("status") == "open" and x.get("leg_role", "").startswith("option")]
open_legs = [x for x in legs if x.get("status") == "open" and str(x.get("leg_role") or "").startswith("option")]
if len(open_legs) < 2:
return None
# 用内在价值粗判盈利腿
winners = []
for leg in open_legs:
strike = _sf(leg.get("strike")) or 0
o = (leg.get("opt_type") or "").upper()
intrinsic = max(0.0, idx - strike) if o == "C" else max(0.0, strike - idx)
premium = float(leg.get("premium") or 0)
pnl = intrinsic * float(leg.get("size") or 1) * 0.01 - premium
pnl = intrinsic * float(leg.get("size") or 1) * float(leg.get("ct_mult") or 0.01) - premium
winners.append((pnl, leg))
winners.sort(key=lambda x: x[0], reverse=True)
best_pnl, best = winners[0]
@@ -223,11 +261,130 @@ def _tick_oo(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
return None
close_r = _sell_option(cfg, inst_id=str(best.get("inst_id") or ""), sheets=float(best.get("size") or 1))
if not close_r.get("ok"):
notify_hedge(
cfg,
build_hedge_alert_message(
title="期期平盈利腿失败",
plan_id=plan.get("id"),
detail=str(close_r.get("msg") or close_r),
),
)
return {"plan_id": plan["id"], "msg": "平盈利腿失败", "close": close_r}
conn.execute(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
("closed", "target_win_leg", _now(), best_pnl, best["id"]),
)
# 计划暂不 closed,等另一腿到期;先标 note
update_plan(conn, int(plan["id"]), close_reason="target_win_leg")
mid = dict(plan)
mid["close_reason"] = "target_win_leg"
mid["status"] = "active"
notify_plan_end(cfg, conn, mid)
return {"plan_id": plan["id"], "close_reason": "target_win_leg", "closed_leg": best.get("id")}
def _tick_oo_expiry(
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
) -> Optional[dict[str, Any]]:
"""期期:剩余期权腿全部到期 → 结算合计并结束计划."""
pending = [
x
for x in legs
if str(x.get("leg_role") or "").startswith("option")
and str(x.get("status") or "") in ("open", "hold_to_expiry")
]
if not pending:
# 若腿已全部 closed 但计划仍 active(异常残留)则用腿合计收口
closed_opts = [
x for x in legs if str(x.get("leg_role") or "").startswith("option") and x.get("status") == "closed"
]
if len(closed_opts) < 1:
return None
total_opts = sum(float(x.get("realized_pnl") or 0) for x in closed_opts)
reason = "oo_expiry_loss" if total_opts <= 0 else "oo_expiry_win"
update_plan(
conn,
int(plan["id"]),
status="closed",
close_reason=reason,
realized_pnl_options=round(total_opts, 4),
realized_pnl_total=round(total_opts, 4),
stats_bucket=reason if reason == "oo_expiry_loss" else "oo_target",
closed_at=_now(),
)
_notify_end_reload(cfg, conn, int(plan["id"]))
return {"plan_id": plan["id"], "close_reason": reason, "total": total_opts}
if not all(leg_is_expired(x) for x in pending):
return None
spot = _index_px(cfg, str(plan.get("underlying") or "ETH"))
if spot is None:
return None
settled_sum = 0.0
for leg in pending:
pnl = settle_option_leg_at_spot(leg, float(spot))
settled_sum += pnl
conn.execute(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
("closed", "expiry", _now(), round(pnl, 4), leg["id"]),
)
already = sum(
float(x.get("realized_pnl") or 0)
for x in legs
if str(x.get("leg_role") or "").startswith("option") and x.get("status") == "closed"
)
total = already + settled_sum
reason = "oo_expiry_loss" if total <= 0 else "oo_expiry_win"
bucket = "oo_expiry_loss" if reason == "oo_expiry_loss" else "oo_target"
update_plan(
conn,
int(plan["id"]),
status="closed",
close_reason=reason,
realized_pnl_options=round(total, 4),
realized_pnl_total=round(total, 4),
stats_bucket=bucket,
closed_at=_now(),
)
_notify_end_reload(cfg, conn, int(plan["id"]))
return {"plan_id": plan["id"], "close_reason": reason, "total": total, "spot": spot}
def _settle_orphaned_after_tp(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]:
"""永期止盈后 hold_to_expiry 期权到期:只更新腿,不回写计划合计."""
rows = conn.execute(
"""
SELECT l.id AS leg_id, l.plan_id, l.inst_id, l.opt_type, l.strike, l.size, l.premium, l.status,
p.underlying, p.status AS plan_status
FROM hedge_plan_legs l
JOIN hedge_plans p ON p.id = l.plan_id
WHERE l.status = 'hold_to_expiry' AND l.close_reason = 'orphaned_after_tp'
LIMIT 40
"""
).fetchall()
acted: list[dict[str, Any]] = []
for row in rows:
leg = dict(row)
if not leg_is_expired(leg):
continue
spot = _index_px(cfg, str(leg.get("underlying") or "ETH"))
if spot is None:
continue
pnl = settle_option_leg_at_spot(leg, float(spot))
conn.execute(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
("closed", "expiry", _now(), round(pnl, 4), leg["leg_id"]),
)
# 故意不 UPDATE hedge_plans.realized_pnl_*
acted.append(
{
"plan_id": leg["plan_id"],
"close_reason": "orphaned_option_expiry",
"leg_id": leg["leg_id"],
"leg_pnl": round(pnl, 4),
"note": "不回写计划合计",
}
)
return acted
+173
View File
@@ -0,0 +1,173 @@
"""对冲计划企业微信推送(起止必发,幂等落库标记)."""
from __future__ import annotations
from typing import Any, Callable, Optional
from lib.hedge_plan.hedge_plan_db import update_plan
def _fmt(v: Any, d: int = 2) -> str:
try:
if v is None or v == "":
return ""
return f"{float(v):.{d}f}"
except (TypeError, ValueError):
return str(v)
def _type_label(plan_type: str) -> str:
return "永期对冲" if (plan_type or "") == "perp_options" else "期期对冲"
def _dir_label(direction: str) -> str:
d = (direction or "").lower()
if d == "long":
return "做多"
if d == "short":
return "做空"
return ""
def build_hedge_start_message(plan: dict[str, Any], *, legs: Optional[list[dict[str, Any]]] = None) -> str:
pt = plan.get("plan_type") or ""
lines = [
f"🟢 对冲计划启动 #{plan.get('id')}",
f"📌 类型:{_type_label(pt)}",
f"🪙 标的:{plan.get('underlying') or ''}",
]
if pt == "perp_options":
lines.extend(
[
f"📈 方向:{_dir_label(plan.get('direction') or '')}",
f"💵 开仓参考:{_fmt(plan.get('entry_mark'))}",
f"🎯 止盈:{_fmt(plan.get('tp'))}|止损:{_fmt(plan.get('sl'))}",
f"📦 永续张数:{_fmt(plan.get('perp_size'), 4)}|杠杆:{_fmt(plan.get('leverage'), 0)}x",
f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC",
]
)
else:
lines.extend(
[
f"🎯 目标价 S*:{_fmt(plan.get('target_price'))}",
f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC",
]
)
if legs:
for leg in legs:
role = leg.get("leg_role") or ""
if role == "perp":
lines.append(f"· 永续腿 {leg.get('symbol') or ''} ×{_fmt(leg.get('size'), 4)}")
else:
lines.append(
f"· {role} {(leg.get('opt_type') or '')} K{_fmt(leg.get('strike'), 0)} "
f"×{_fmt(leg.get('size'), 0)}{leg.get('inst_id') or ''}"
)
lines.append("📎 独立模块推送,不进普通交易复盘")
return "\n".join(lines)
def build_hedge_end_message(plan: dict[str, Any]) -> str:
reason = plan.get("close_reason") or ""
total = plan.get("realized_pnl_total")
try:
tv = float(total) if total is not None else None
except (TypeError, ValueError):
tv = None
head = "🔴" if (tv is not None and tv < 0) else "🟢"
reason_map = {
"perp_tp": "永续止盈(期权默认不平)",
"perp_sl": "永续止损(期权强制平)",
"target_win_leg": "期期已平盈利腿(中间态)",
"oo_expiry_loss": "期期到期无盈利·总亏损",
"oo_expiry_win": "期期到期仍盈利",
"expiry": "到期收口",
"manual": "人工结束",
"partial_fail": "半腿失败收尾",
"cancelled": "已取消",
}
lines = [
f"{head} 对冲计划结束 #{plan.get('id')}",
f"📌 类型:{_type_label(plan.get('plan_type') or '')}",
f"🪙 标的:{plan.get('underlying') or ''}",
f"📎 原因:{reason_map.get(reason, reason)}",
f"💰 合计≈U:{_fmt(total)}",
f"· 永续分项:{_fmt(plan.get('realized_pnl_perp'))} USDT",
f"· 期权分项:{_fmt(plan.get('realized_pnl_options'))} USDC(≈U 1:1)",
f"⏱ 开仓:{plan.get('opened_at') or ''}|结束:{plan.get('closed_at') or ''}",
]
return "\n".join(lines)
def build_hedge_alert_message(
*,
title: str,
plan_id: Any = None,
detail: str = "",
) -> str:
lines = [f"⚠️ 对冲计划告警{(' #' + str(plan_id)) if plan_id else ''}", f"📌 {title}"]
if detail:
lines.append(str(detail)[:800])
return "\n".join(lines)
def notify_hedge(
cfg: dict[str, Any],
content: str,
) -> bool:
send: Optional[Callable[[str], Any]] = cfg.get("send_wechat")
if not callable(send):
return False
try:
send(content)
return True
except Exception:
return False
def notify_plan_start(
cfg: dict[str, Any],
conn: Any,
plan: dict[str, Any],
legs: Optional[list[dict[str, Any]]] = None,
) -> bool:
if int(plan.get("wechat_start_sent") or 0):
return False
ok = notify_hedge(cfg, build_hedge_start_message(plan, legs=legs))
if ok and plan.get("id") is not None:
update_plan(conn, int(plan["id"]), wechat_start_sent=1)
plan["wechat_start_sent"] = 1
return ok
def notify_plan_end(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> bool:
if int(plan.get("wechat_end_sent") or 0):
return False
# 中间态 target_win_leg 不算正式结束推送(用告警)
if (plan.get("close_reason") or "") == "target_win_leg" and (plan.get("status") or "") != "closed":
notify_hedge(
cfg,
build_hedge_alert_message(
title="期期已平盈利腿,亏损腿继续持有至到期",
plan_id=plan.get("id"),
detail=f"目标价 {_fmt(plan.get('target_price'))}",
),
)
return True
ok = notify_hedge(cfg, build_hedge_end_message(plan))
if ok and plan.get("id") is not None:
update_plan(conn, int(plan["id"]), wechat_end_sent=1)
plan["wechat_end_sent"] = 1
return ok
def notify_partial_fail(cfg: dict[str, Any], *, plan_type: str, msg: str, results: Any = None) -> bool:
detail = msg
if results:
try:
detail = f"{msg}\n路径结果:{results}"[:800]
except Exception:
pass
return notify_hedge(
cfg,
build_hedge_alert_message(title=f"{_type_label(plan_type)}半腿失败", detail=detail),
)
+20 -2
View File
@@ -278,9 +278,19 @@ def execute_perp_options_start(
sheets=float(opt_res.get("sheets") or body.get("sheets") or 1),
)
results.append({"step": "options_auto_close_on_perp_fail", **close_r})
msg = perp_res.get("msg") or "永续开仓失败"
if not dry_run:
try:
from lib.hedge_plan.hedge_plan_notify_lib import notify_partial_fail
notify_partial_fail(
cfg, plan_type="perp_options", msg=msg, results=results
)
except Exception:
pass
return {
"ok": False,
"msg": perp_res.get("msg") or "永续开仓失败",
"msg": msg,
"path": path,
"results": results,
"partial": True,
@@ -322,9 +332,17 @@ def execute_options_options_start(
if not dry_run and _env_bool("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", True):
close_r = _sell_option(cfg, inst_id=str(a_res.get("inst_id") or ""), sheets=float(a_res.get("sheets") or 1))
results.append({"step": "options_auto_close_leg_a", **close_r})
msg = b_res.get("msg") or "腿B开仓失败"
if not dry_run:
try:
from lib.hedge_plan.hedge_plan_notify_lib import notify_partial_fail
notify_partial_fail(cfg, plan_type="options_options", msg=msg, results=results)
except Exception:
pass
return {
"ok": False,
"msg": b_res.get("msg") or "腿B开仓失败",
"msg": msg,
"path": path,
"results": results,
"partial": True,
+27 -2
View File
@@ -98,6 +98,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
"perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(),
"options_account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "期权账户").strip(),
"live_trading": _env_bool("LIVE_TRADING_ENABLED", False),
"send_wechat": getattr(app_module, "send_wechat_msg", None),
}
@@ -171,7 +172,14 @@ def _maybe_start_monitor(cfg: dict[str, Any]) -> None:
def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, insert_leg, insert_plan
from lib.hedge_plan.hedge_plan_db import (
get_plan,
get_plan_legs,
init_hedge_plan_tables,
insert_leg,
insert_plan,
)
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
conn = cfg["get_db"]()
try:
@@ -229,13 +237,25 @@ def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
},
)
conn.commit()
plan = get_plan(conn, plan_id)
legs = get_plan_legs(conn, plan_id)
if plan:
notify_plan_start(cfg, conn, plan, legs)
conn.commit()
return plan_id
finally:
conn.close()
def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, insert_leg, insert_plan
from lib.hedge_plan.hedge_plan_db import (
get_plan,
get_plan_legs,
init_hedge_plan_tables,
insert_leg,
insert_plan,
)
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
conn = cfg["get_db"]()
try:
@@ -274,6 +294,11 @@ def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
},
)
conn.commit()
plan = get_plan(conn, plan_id)
legs = get_plan_legs(conn, plan_id)
if plan:
notify_plan_start(cfg, conn, plan, legs)
conn.commit()
return plan_id
finally:
conn.close()
+62
View File
@@ -0,0 +1,62 @@
"""对冲计划结算辅助:到期内在价值与期权腿收口."""
from __future__ import annotations
import time
from typing import Any, Optional
from lib.exchange.okx_options_lib import normalize_option_exp_ms
from lib.hedge_plan.hedge_plan_calc_lib import option_expiry_pnl
def _sf(v: Any) -> Optional[float]:
try:
if v is None or v == "":
return None
return float(v)
except (TypeError, ValueError):
return None
def leg_exp_ms(leg: dict[str, Any]) -> Optional[int]:
return normalize_option_exp_ms(leg.get("exp_time"), str(leg.get("inst_id") or ""))
def leg_is_expired(leg: dict[str, Any], *, now_ms: Optional[int] = None) -> bool:
exp = leg_exp_ms(leg)
if exp is None:
return False
now = int(now_ms if now_ms is not None else time.time() * 1000)
return now >= int(exp)
def settle_option_leg_at_spot(leg: dict[str, Any], spot: float) -> float:
"""按到期结算口径估算腿盈亏(USDC)."""
premium = float(leg.get("premium") or 0)
strike = _sf(leg.get("strike"))
if strike is None:
return -premium
sheets = float(leg.get("size") or 1)
# ct_mult 未入库时默认 0.01
ct = float(leg.get("ct_mult") or 0.01)
return float(
option_expiry_pnl(
opt_type=str(leg.get("opt_type") or "P"),
strike=float(strike),
spot=float(spot),
sheets=sheets,
ct_mult=ct,
premium_paid=premium,
)
)
def all_option_legs_expired(legs: list[dict[str, Any]], *, now_ms: Optional[int] = None) -> bool:
opts = [
x
for x in legs
if str(x.get("leg_role") or "").startswith("option")
and str(x.get("status") or "") in ("open", "hold_to_expiry")
]
if not opts:
return False
return all(leg_is_expired(x, now_ms=now_ms) for x in opts)