补齐币本位期权开平仓微信推送:单位用ETH/BTC,手动/目标/翻倍平仓与翻倍提醒均必达。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-20 17:57:58 +08:00
parent a7d0c2f875
commit 440778e9e6
6 changed files with 122 additions and 44 deletions
+1
View File
@@ -372,6 +372,7 @@ def open_coin_option_buy_full(
open_quote=float(ask), open_quote=float(ask),
target_index=target_index, target_index=target_index,
signal_note=signal_note, signal_note=signal_note,
premium_ccy=premium_ccy,
) )
except Exception: except Exception:
pass pass
+36 -16
View File
@@ -30,7 +30,11 @@ def build_profit_alert_message(
upl: float, upl: float,
upl_ratio: float | None, upl_ratio: float | None,
bid: float | None, bid: float | None,
premium_ccy: str = "USDC",
) -> str: ) -> str:
from lib.options.options_notify_lib import resolve_premium_ccy
ccy = resolve_premium_ccy(premium_ccy, inst_id=inst_id)
pct = f"{upl_ratio * 100:.1f}%" if upl_ratio is not None else "" pct = f"{upl_ratio * 100:.1f}%" if upl_ratio is not None else ""
bid_txt = f"{bid:.4f}" if bid is not None else "" bid_txt = f"{bid:.4f}" if bid is not None else ""
return "\n".join( return "\n".join(
@@ -38,9 +42,9 @@ def build_profit_alert_message(
"【OKX期权·翻倍提醒】", "【OKX期权·翻倍提醒】",
f"账户:{account_label}", f"账户:{account_label}",
f"合约:{inst_id}", f"合约:{inst_id}",
f"已付权利金:{premium_paid:.4f} USDC", f"已付权利金:{premium_paid:.4f} {ccy}",
f"未实现盈亏:{upl:+.4f} USDC({pct})", f"未实现盈亏:{upl:+.4f} {ccy}({pct})",
f"当前买一:{bid_txt}(可考虑限价平仓锁利)", f"当前买一:{bid_txt} {ccy}(可考虑限价平仓锁利)",
] ]
) )
@@ -60,14 +64,24 @@ def run_options_profit_alerts(
""" """
sent = 0 sent = 0
pos_by_inst = {str(p.get("inst_id") or p.get("instId") or ""): p for p in positions} pos_by_inst = {str(p.get("inst_id") or p.get("instId") or ""): p for p in positions}
rows = conn.execute( try:
""" rows = conn.execute(
SELECT id, inst_id, premium_paid, profit_alert_sent """
FROM options_trades SELECT id, inst_id, premium_paid, profit_alert_sent, premium_ccy
WHERE status = 'open' FROM options_trades
ORDER BY id ASC WHERE status = 'open'
""" ORDER BY id ASC
).fetchall() """
).fetchall()
except Exception:
rows = conn.execute(
"""
SELECT id, inst_id, premium_paid, profit_alert_sent
FROM options_trades
WHERE status = 'open'
ORDER BY id ASC
"""
).fetchall()
# 同合约多腿加仓:按合约汇总权利金,整仓只告警一次 # 同合约多腿加仓:按合约汇总权利金,整仓只告警一次
by_inst: dict[str, dict[str, Any]] = {} by_inst: dict[str, dict[str, Any]] = {}
for row in rows: for row in rows:
@@ -76,13 +90,18 @@ def run_options_profit_alerts(
continue continue
bucket = by_inst.setdefault( bucket = by_inst.setdefault(
inst_id, inst_id,
{"ids": [], "premium": 0.0, "all_sent": True, "has_prem": False}, {"ids": [], "premium": 0.0, "all_sent": True, "has_prem": False, "premium_ccy": None},
) )
bucket["ids"].append(int(row["id"])) bucket["ids"].append(int(row["id"]))
prem = _safe_float(row["premium_paid"]) prem = _safe_float(row["premium_paid"])
if prem is not None: if prem is not None:
bucket["premium"] += float(prem) bucket["premium"] += float(prem)
bucket["has_prem"] = True bucket["has_prem"] = True
if not bucket.get("premium_ccy"):
try:
bucket["premium_ccy"] = row["premium_ccy"]
except (KeyError, IndexError, TypeError):
bucket["premium_ccy"] = None
if not int(row["profit_alert_sent"] or 0): if not int(row["profit_alert_sent"] or 0):
bucket["all_sent"] = False bucket["all_sent"] = False
@@ -111,6 +130,7 @@ def run_options_profit_alerts(
upl=upl or 0.0, upl=upl or 0.0,
upl_ratio=ratio, upl_ratio=ratio,
bid=bid, bid=bid,
premium_ccy=str(bucket.get("premium_ccy") or pos.get("premium_ccy") or ""),
) )
try: try:
send_wechat(msg) send_wechat(msg)
@@ -450,6 +470,9 @@ def options_monitor_loop(
account_label=account_label, account_label=account_label,
ticker_bid_fn=ticker_bid_fn, ticker_bid_fn=ticker_bid_fn,
) )
pe_cfg = dict(profit_exit_cfg or {})
pe_cfg.setdefault("send_wechat", send_wechat)
pe_cfg.setdefault("account_label", account_label)
if target_close_fn is not None: if target_close_fn is not None:
from lib.options.options_target_lib import run_options_target_closes from lib.options.options_target_lib import run_options_target_closes
@@ -459,14 +482,11 @@ def options_monitor_loop(
close_fn=target_close_fn, close_fn=target_close_fn,
send_wechat=send_wechat, send_wechat=send_wechat,
account_label=account_label, account_label=account_label,
cfg={"send_wechat": send_wechat, "account_label": account_label}, cfg=pe_cfg,
) )
if profit_exit_close_fn is not None: if profit_exit_close_fn is not None:
from lib.options.options_profit_exit_lib import run_options_profit_exits from lib.options.options_profit_exit_lib import run_options_profit_exits
pe_cfg = dict(profit_exit_cfg or {})
pe_cfg.setdefault("send_wechat", send_wechat)
pe_cfg.setdefault("account_label", account_label)
run_options_profit_exits( run_options_profit_exits(
conn, conn,
positions, positions,
+51 -6
View File
@@ -23,6 +23,30 @@ def _opt_type_label(opt_type: Any) -> str:
return t or "" return t or ""
def resolve_premium_ccy(
raw: Any = None,
*,
inst_id: str = "",
underlying: str = "",
) -> str:
"""权利金计价币种:币本位 ETH/BTC,U 本位 USDC."""
s = str(raw or "").strip().upper()
if s:
return s
inst = (inst_id or "").strip()
u = (underlying or "").strip().upper()
if not u and inst:
u = inst.split("-")[0].upper() if "-" in inst else "ETH"
try:
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
if inst:
return premium_ccy_for_mode(margin_mode_from_inst_id(inst), u or "ETH")
except Exception:
pass
return u if u in ("ETH", "BTC") else "USDC"
def ensure_options_notify_columns(conn: sqlite3.Connection) -> None: def ensure_options_notify_columns(conn: sqlite3.Connection) -> None:
for ddl in ( for ddl in (
"ALTER TABLE options_trades ADD COLUMN wechat_open_sent INTEGER DEFAULT 0", "ALTER TABLE options_trades ADD COLUMN wechat_open_sent INTEGER DEFAULT 0",
@@ -57,7 +81,9 @@ def build_options_open_message(
target_index: Any = None, target_index: Any = None,
signal_note: str = "", signal_note: str = "",
trade_id: Any = None, trade_id: Any = None,
premium_ccy: Any = None,
) -> str: ) -> str:
ccy = resolve_premium_ccy(premium_ccy, inst_id=inst_id, underlying=underlying)
lines = [ lines = [
"【OKX期权·开仓】", "【OKX期权·开仓】",
f"账户:{account_label or 'OKX期权'}", f"账户:{account_label or 'OKX期权'}",
@@ -69,8 +95,8 @@ def build_options_open_message(
f"合约:{inst_id}", f"合约:{inst_id}",
f"标的:{(underlying or '')} · {_opt_type_label(opt_type)}", f"标的:{(underlying or '')} · {_opt_type_label(opt_type)}",
f"张数:{sheets if sheets is not None else ''}", f"张数:{sheets if sheets is not None else ''}",
f"开仓报价:{_fmt(open_quote)} USDC", f"开仓报价:{_fmt(open_quote)} {ccy}",
f"权利金:{_fmt(premium_paid)} USDC", f"权利金:{_fmt(premium_paid)} {ccy}",
] ]
) )
if target_index is not None and str(target_index).strip() != "": if target_index is not None and str(target_index).strip() != "":
@@ -98,7 +124,9 @@ def build_options_close_message(
target_index: Any = None, target_index: Any = None,
trigger_idx: Any = None, trigger_idx: Any = None,
trade_id: Any = None, trade_id: Any = None,
premium_ccy: Any = None,
) -> str: ) -> str:
ccy = resolve_premium_ccy(premium_ccy, inst_id=inst_id, underlying=underlying)
lines = [ lines = [
"【OKX期权·平仓】", "【OKX期权·平仓】",
f"账户:{account_label or 'OKX期权'}", f"账户:{account_label or 'OKX期权'}",
@@ -111,9 +139,9 @@ def build_options_close_message(
f"标的:{(underlying or '')} · {_opt_type_label(opt_type)}", f"标的:{(underlying or '')} · {_opt_type_label(opt_type)}",
f"原因:{(reason or '平仓').strip()}", f"原因:{(reason or '平仓').strip()}",
f"张数:{sheets if sheets is not None else ''}", f"张数:{sheets if sheets is not None else ''}",
f"平仓报价:{_fmt(close_quote)} USDC", f"平仓报价:{_fmt(close_quote)} {ccy}",
f"已付/收回:{_fmt(premium_paid)} / {_fmt(premium_received)} USDC", f"已付/收回:{_fmt(premium_paid)} / {_fmt(premium_received)} {ccy}",
f"实现盈亏:{_fmt(realized_pnl, 4)} USDC", f"实现盈亏:{_fmt(realized_pnl, 4)} {ccy}",
] ]
) )
if target_index is not None and str(target_index).strip() != "": if target_index is not None and str(target_index).strip() != "":
@@ -142,15 +170,23 @@ def notify_options_open(
open_quote: Any = None, open_quote: Any = None,
target_index: Any = None, target_index: Any = None,
signal_note: str = "", signal_note: str = "",
premium_ccy: Any = None,
) -> bool: ) -> bool:
ensure_options_notify_columns(conn) if conn is not None else None ensure_options_notify_columns(conn) if conn is not None else None
row_ccy = premium_ccy
if conn is not None and trade_id is not None: if conn is not None and trade_id is not None:
row = conn.execute( row = conn.execute(
"SELECT wechat_open_sent FROM options_trades WHERE id=?", "SELECT wechat_open_sent, premium_ccy, underlying FROM options_trades WHERE id=?",
(int(trade_id),), (int(trade_id),),
).fetchone() ).fetchone()
if row and int(row["wechat_open_sent"] or 0): if row and int(row["wechat_open_sent"] or 0):
return False return False
if row is not None:
if not row_ccy:
row_ccy = row["premium_ccy"] if "premium_ccy" in row.keys() else None
if not underlying:
underlying = str(row["underlying"] or "") if "underlying" in row.keys() else underlying
ccy = resolve_premium_ccy(row_ccy, inst_id=inst_id, underlying=underlying)
msg = build_options_open_message( msg = build_options_open_message(
account_label=str(cfg.get("account_label") or "OKX期权"), account_label=str(cfg.get("account_label") or "OKX期权"),
inst_id=inst_id, inst_id=inst_id,
@@ -162,6 +198,7 @@ def notify_options_open(
target_index=target_index, target_index=target_index,
signal_note=signal_note, signal_note=signal_note,
trade_id=trade_id, trade_id=trade_id,
premium_ccy=ccy,
) )
ok = notify_options_send(cfg, msg) ok = notify_options_send(cfg, msg)
if ok and conn is not None and trade_id is not None: if ok and conn is not None and trade_id is not None:
@@ -198,6 +235,7 @@ def notify_options_close(
target_index: Any = None, target_index: Any = None,
trigger_idx: Any = None, trigger_idx: Any = None,
force: bool = False, force: bool = False,
premium_ccy: Any = None,
) -> bool: ) -> bool:
"""平仓必发.默认按 trade_id / 同合约未标记行幂等.""" """平仓必发.默认按 trade_id / 同合约未标记行幂等."""
if conn is not None: if conn is not None:
@@ -245,6 +283,11 @@ def notify_options_close(
pending = [r for r in rows if not int(r.get("wechat_close_sent") or 0)] pending = [r for r in rows if not int(r.get("wechat_close_sent") or 0)]
if not pending and not force: if not pending and not force:
return False return False
ccy = resolve_premium_ccy(
premium_ccy or head.get("premium_ccy"),
inst_id=inst_id or str(head.get("inst_id") or ""),
underlying=underlying or str(head.get("underlying") or ""),
)
msg = build_options_close_message( msg = build_options_close_message(
account_label=str(cfg.get("account_label") or "OKX期权"), account_label=str(cfg.get("account_label") or "OKX期权"),
inst_id=inst_id or str(head.get("inst_id") or ""), inst_id=inst_id or str(head.get("inst_id") or ""),
@@ -259,6 +302,7 @@ def notify_options_close(
target_index=target_index, target_index=target_index,
trigger_idx=trigger_idx, trigger_idx=trigger_idx,
trade_id=head.get("id") if len(rows) == 1 else None, trade_id=head.get("id") if len(rows) == 1 else None,
premium_ccy=ccy,
) )
ok = notify_options_send(cfg, msg) ok = notify_options_send(cfg, msg)
if ok and conn is not None: if ok and conn is not None:
@@ -288,6 +332,7 @@ def notify_options_close(
target_index=target_index, target_index=target_index,
trigger_idx=trigger_idx, trigger_idx=trigger_idx,
trade_id=trade_id, trade_id=trade_id,
premium_ccy=resolve_premium_ccy(premium_ccy, inst_id=inst_id, underlying=underlying),
) )
return notify_options_send(cfg, msg) return notify_options_send(cfg, msg)
+6 -2
View File
@@ -233,6 +233,9 @@ def _notify_profit_exit_close(
result: dict[str, Any], result: dict[str, Any],
conn: Any = None, conn: Any = None,
) -> None: ) -> None:
from lib.options.options_notify_lib import resolve_premium_ccy
ccy = resolve_premium_ccy(inst_id=inst_id)
if result.get("fully_closed") or result.get("already_flat"): if result.get("fully_closed") or result.get("already_flat"):
if cfg is not None: if cfg is not None:
try: try:
@@ -246,6 +249,7 @@ def _notify_profit_exit_close(
sheets=result.get("submitted_sheets"), sheets=result.get("submitted_sheets"),
premium_received=result.get("premium_received"), premium_received=result.get("premium_received"),
close_quote=result.get("locked_bid_px") or result.get("bid"), close_quote=result.get("locked_bid_px") or result.get("bid"),
premium_ccy=ccy,
) )
return return
except Exception: except Exception:
@@ -260,8 +264,8 @@ def _notify_profit_exit_close(
f"账户:{account_label}", f"账户:{account_label}",
f"合约:{inst_id}", f"合约:{inst_id}",
f"倍数:{mult:g}(1倍=盈利=权利金)", f"倍数:{mult:g}(1倍=盈利=权利金)",
f"权利金:{premium_paid if premium_paid is not None else ''}", f"权利金:{premium_paid if premium_paid is not None else ''} {ccy}",
f"可回收:{recycle if recycle is not None else ''}", f"可回收:{recycle if recycle is not None else ''} {ccy}",
f"提交张数:{result.get('submitted_sheets') or ''}", f"提交张数:{result.get('submitted_sheets') or ''}",
f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}", f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}",
] ]
+22 -19
View File
@@ -1281,6 +1281,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
open_quote=fill_px, open_quote=fill_px,
target_index=target_index, target_index=target_index,
signal_note=signal_note, signal_note=signal_note,
premium_ccy=sizing.get("premium_ccy") or "USDC",
) )
finally: finally:
conn_n.close() conn_n.close()
@@ -1639,28 +1640,30 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
invalidate_option_positions_cache() invalidate_option_positions_cache()
_sync_options_trades(cfg, force=True) _sync_options_trades(cfg, force=True)
if result.get("fully_closed"): try:
try: from lib.options.options_target_lib import cancel_target_monitor
from lib.options.options_target_lib import cancel_target_monitor from lib.options.options_notify_lib import notify_options_close
from lib.options.options_notify_lib import notify_options_close
conn2 = cfg["get_db"]() conn2 = cfg["get_db"]()
try: try:
if result.get("fully_closed"):
cancel_target_monitor(conn2, inst_id=inst_id) cancel_target_monitor(conn2, inst_id=inst_id)
conn2.commit() conn2.commit()
notify_options_close( reason = "手动平仓" if result.get("fully_closed") else "手动平仓(部分)"
cfg, notify_options_close(
conn2, cfg,
inst_id=inst_id, conn2,
reason="手动平仓", inst_id=inst_id,
sheets=result.get("submitted_sheets"), reason=reason,
premium_received=result.get("premium_received"), sheets=result.get("submitted_sheets"),
close_quote=result.get("locked_bid_px") or result.get("bid"), premium_received=result.get("premium_received"),
) close_quote=result.get("locked_bid_px") or result.get("bid"),
finally: )
conn2.close() finally:
except Exception: conn2.close()
pass except Exception:
pass
if result.get("fully_closed"):
try: try:
from lib.options.options_coin_open_lib import maybe_sell_spot_after_close from lib.options.options_coin_open_lib import maybe_sell_spot_after_close
+6 -1
View File
@@ -303,6 +303,9 @@ def _notify_target_close(
conn: Any = None, conn: Any = None,
) -> None: ) -> None:
"""目标位平仓推送:优先走统一平仓必发(幂等);无 cfg 时回退旧文案.""" """目标位平仓推送:优先走统一平仓必发(幂等);无 cfg 时回退旧文案."""
from lib.options.options_notify_lib import resolve_premium_ccy
ccy = resolve_premium_ccy(inst_id=inst_id)
if result.get("fully_closed") or result.get("already_flat"): if result.get("fully_closed") or result.get("already_flat"):
if cfg is not None: if cfg is not None:
try: try:
@@ -318,6 +321,7 @@ def _notify_target_close(
close_quote=result.get("locked_bid_px") or result.get("bid"), close_quote=result.get("locked_bid_px") or result.get("bid"),
target_index=target, target_index=target,
trigger_idx=idx, trigger_idx=idx,
premium_ccy=ccy,
) )
return return
except Exception: except Exception:
@@ -325,6 +329,7 @@ def _notify_target_close(
if not send_wechat: if not send_wechat:
return return
try: try:
recv = result.get("premium_received")
send_wechat( send_wechat(
"\n".join( "\n".join(
[ [
@@ -334,7 +339,7 @@ def _notify_target_close(
f"目标指数:{target:g}", f"目标指数:{target:g}",
f"触发指数:{idx:g}", f"触发指数:{idx:g}",
f"提交张数:{result.get('submitted_sheets') or ''}", f"提交张数:{result.get('submitted_sheets') or ''}",
f"预估收回:{result.get('premium_received') if result.get('premium_received') is not None else ''} USDC", f"预估收回:{recv if recv is not None else ''} {ccy}",
f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}", f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}",
] ]
) )