diff --git a/lib/options/options_coin_open_lib.py b/lib/options/options_coin_open_lib.py index 7853160..1ed86ca 100644 --- a/lib/options/options_coin_open_lib.py +++ b/lib/options/options_coin_open_lib.py @@ -372,6 +372,7 @@ def open_coin_option_buy_full( open_quote=float(ask), target_index=target_index, signal_note=signal_note, + premium_ccy=premium_ccy, ) except Exception: pass diff --git a/lib/options/options_monitor_lib.py b/lib/options/options_monitor_lib.py index e6e5093..3787f9a 100644 --- a/lib/options/options_monitor_lib.py +++ b/lib/options/options_monitor_lib.py @@ -30,7 +30,11 @@ def build_profit_alert_message( upl: float, upl_ratio: float | None, bid: float | None, + premium_ccy: str = "USDC", ) -> 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 "—" bid_txt = f"{bid:.4f}" if bid is not None else "—" return "\n".join( @@ -38,9 +42,9 @@ def build_profit_alert_message( "【OKX期权·翻倍提醒】", f"账户:{account_label}", f"合约:{inst_id}", - f"已付权利金:{premium_paid:.4f} USDC", - f"未实现盈亏:{upl:+.4f} USDC({pct})", - f"当前买一:{bid_txt}(可考虑限价平仓锁利)", + f"已付权利金:{premium_paid:.4f} {ccy}", + f"未实现盈亏:{upl:+.4f} {ccy}({pct})", + f"当前买一:{bid_txt} {ccy}(可考虑限价平仓锁利)", ] ) @@ -60,14 +64,24 @@ def run_options_profit_alerts( """ sent = 0 pos_by_inst = {str(p.get("inst_id") or p.get("instId") or ""): p for p in positions} - rows = conn.execute( - """ - SELECT id, inst_id, premium_paid, profit_alert_sent - FROM options_trades - WHERE status = 'open' - ORDER BY id ASC - """ - ).fetchall() + try: + rows = conn.execute( + """ + SELECT id, inst_id, premium_paid, profit_alert_sent, premium_ccy + FROM options_trades + WHERE status = 'open' + ORDER BY id ASC + """ + ).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]] = {} for row in rows: @@ -76,13 +90,18 @@ def run_options_profit_alerts( continue bucket = by_inst.setdefault( 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"])) prem = _safe_float(row["premium_paid"]) if prem is not None: bucket["premium"] += float(prem) 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): bucket["all_sent"] = False @@ -111,6 +130,7 @@ def run_options_profit_alerts( upl=upl or 0.0, upl_ratio=ratio, bid=bid, + premium_ccy=str(bucket.get("premium_ccy") or pos.get("premium_ccy") or ""), ) try: send_wechat(msg) @@ -450,6 +470,9 @@ def options_monitor_loop( account_label=account_label, 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: from lib.options.options_target_lib import run_options_target_closes @@ -459,14 +482,11 @@ def options_monitor_loop( close_fn=target_close_fn, send_wechat=send_wechat, account_label=account_label, - cfg={"send_wechat": send_wechat, "account_label": account_label}, + cfg=pe_cfg, ) if profit_exit_close_fn is not None: 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( conn, positions, diff --git a/lib/options/options_notify_lib.py b/lib/options/options_notify_lib.py index 7b32721..e8d5db7 100644 --- a/lib/options/options_notify_lib.py +++ b/lib/options/options_notify_lib.py @@ -23,6 +23,30 @@ def _opt_type_label(opt_type: Any) -> str: 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: for ddl in ( "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, signal_note: str = "", trade_id: Any = None, + premium_ccy: Any = None, ) -> str: + ccy = resolve_premium_ccy(premium_ccy, inst_id=inst_id, underlying=underlying) lines = [ "【OKX期权·开仓】", f"账户:{account_label or 'OKX期权'}", @@ -69,8 +95,8 @@ def build_options_open_message( f"合约:{inst_id}", f"标的:{(underlying or '—')} · {_opt_type_label(opt_type)}", f"张数:{sheets if sheets is not None else '—'}", - f"开仓报价:{_fmt(open_quote)} USDC", - f"权利金:{_fmt(premium_paid)} USDC", + f"开仓报价:{_fmt(open_quote)} {ccy}", + f"权利金:{_fmt(premium_paid)} {ccy}", ] ) if target_index is not None and str(target_index).strip() != "": @@ -98,7 +124,9 @@ def build_options_close_message( target_index: Any = None, trigger_idx: Any = None, trade_id: Any = None, + premium_ccy: Any = None, ) -> str: + ccy = resolve_premium_ccy(premium_ccy, inst_id=inst_id, underlying=underlying) lines = [ "【OKX期权·平仓】", f"账户:{account_label or 'OKX期权'}", @@ -111,9 +139,9 @@ def build_options_close_message( f"标的:{(underlying or '—')} · {_opt_type_label(opt_type)}", f"原因:{(reason or '平仓').strip()}", f"张数:{sheets if sheets is not None else '—'}", - f"平仓报价:{_fmt(close_quote)} USDC", - f"已付/收回:{_fmt(premium_paid)} / {_fmt(premium_received)} USDC", - f"实现盈亏:{_fmt(realized_pnl, 4)} USDC", + f"平仓报价:{_fmt(close_quote)} {ccy}", + f"已付/收回:{_fmt(premium_paid)} / {_fmt(premium_received)} {ccy}", + f"实现盈亏:{_fmt(realized_pnl, 4)} {ccy}", ] ) if target_index is not None and str(target_index).strip() != "": @@ -142,15 +170,23 @@ def notify_options_open( open_quote: Any = None, target_index: Any = None, signal_note: str = "", + premium_ccy: Any = None, ) -> bool: 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: 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),), ).fetchone() if row and int(row["wechat_open_sent"] or 0): 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( account_label=str(cfg.get("account_label") or "OKX期权"), inst_id=inst_id, @@ -162,6 +198,7 @@ def notify_options_open( target_index=target_index, signal_note=signal_note, trade_id=trade_id, + premium_ccy=ccy, ) ok = notify_options_send(cfg, msg) 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, trigger_idx: Any = None, force: bool = False, + premium_ccy: Any = None, ) -> bool: """平仓必发.默认按 trade_id / 同合约未标记行幂等.""" 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)] if not pending and not force: 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( account_label=str(cfg.get("account_label") or "OKX期权"), inst_id=inst_id or str(head.get("inst_id") or ""), @@ -259,6 +302,7 @@ def notify_options_close( target_index=target_index, trigger_idx=trigger_idx, trade_id=head.get("id") if len(rows) == 1 else None, + premium_ccy=ccy, ) ok = notify_options_send(cfg, msg) if ok and conn is not None: @@ -288,6 +332,7 @@ def notify_options_close( target_index=target_index, trigger_idx=trigger_idx, trade_id=trade_id, + premium_ccy=resolve_premium_ccy(premium_ccy, inst_id=inst_id, underlying=underlying), ) return notify_options_send(cfg, msg) diff --git a/lib/options/options_profit_exit_lib.py b/lib/options/options_profit_exit_lib.py index c71c986..7723ad7 100644 --- a/lib/options/options_profit_exit_lib.py +++ b/lib/options/options_profit_exit_lib.py @@ -233,6 +233,9 @@ def _notify_profit_exit_close( result: dict[str, Any], conn: Any = 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 cfg is not None: try: @@ -246,6 +249,7 @@ def _notify_profit_exit_close( sheets=result.get("submitted_sheets"), premium_received=result.get("premium_received"), close_quote=result.get("locked_bid_px") or result.get("bid"), + premium_ccy=ccy, ) return except Exception: @@ -260,8 +264,8 @@ def _notify_profit_exit_close( f"账户:{account_label}", f"合约:{inst_id}", f"倍数:{mult:g}(1倍=盈利=权利金)", - f"权利金:{premium_paid if premium_paid is not None else '—'}", - f"可回收:{recycle if recycle is not None else '—'}", + f"权利金:{premium_paid if premium_paid is not None else '—'} {ccy}", + f"可回收:{recycle if recycle is not None else '—'} {ccy}", f"提交张数:{result.get('submitted_sheets') or '—'}", f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}", ] diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 633e4d5..316f50a 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -1281,6 +1281,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: open_quote=fill_px, target_index=target_index, signal_note=signal_note, + premium_ccy=sizing.get("premium_ccy") or "USDC", ) finally: conn_n.close() @@ -1639,28 +1640,30 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: invalidate_option_positions_cache() _sync_options_trades(cfg, force=True) - if result.get("fully_closed"): - try: - from lib.options.options_target_lib import cancel_target_monitor - from lib.options.options_notify_lib import notify_options_close + try: + from lib.options.options_target_lib import cancel_target_monitor + from lib.options.options_notify_lib import notify_options_close - conn2 = cfg["get_db"]() - try: + conn2 = cfg["get_db"]() + try: + if result.get("fully_closed"): cancel_target_monitor(conn2, inst_id=inst_id) conn2.commit() - notify_options_close( - cfg, - conn2, - inst_id=inst_id, - reason="手动平仓", - sheets=result.get("submitted_sheets"), - premium_received=result.get("premium_received"), - close_quote=result.get("locked_bid_px") or result.get("bid"), - ) - finally: - conn2.close() - except Exception: - pass + reason = "手动平仓" if result.get("fully_closed") else "手动平仓(部分)" + notify_options_close( + cfg, + conn2, + inst_id=inst_id, + reason=reason, + sheets=result.get("submitted_sheets"), + premium_received=result.get("premium_received"), + close_quote=result.get("locked_bid_px") or result.get("bid"), + ) + finally: + conn2.close() + except Exception: + pass + if result.get("fully_closed"): try: from lib.options.options_coin_open_lib import maybe_sell_spot_after_close diff --git a/lib/options/options_target_lib.py b/lib/options/options_target_lib.py index ecbb8c6..4375e09 100644 --- a/lib/options/options_target_lib.py +++ b/lib/options/options_target_lib.py @@ -303,6 +303,9 @@ def _notify_target_close( conn: Any = None, ) -> None: """目标位平仓推送:优先走统一平仓必发(幂等);无 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 cfg is not None: try: @@ -318,6 +321,7 @@ def _notify_target_close( close_quote=result.get("locked_bid_px") or result.get("bid"), target_index=target, trigger_idx=idx, + premium_ccy=ccy, ) return except Exception: @@ -325,6 +329,7 @@ def _notify_target_close( if not send_wechat: return try: + recv = result.get("premium_received") send_wechat( "\n".join( [ @@ -334,7 +339,7 @@ def _notify_target_close( f"目标指数:{target:g}", f"触发指数:{idx:g}", 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 '挂单中/部分'}", ] )