币本位期权开仓/翻倍提醒/翻倍平仓/目标平仓/手动平仓微信推送:按ETH/BTC计价并补齐全平必发

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-20 18:02:12 +08:00
parent 9bb05113f3
commit 8b5080bdda
6 changed files with 256 additions and 19 deletions
+2
View File
@@ -378,6 +378,8 @@ def open_coin_option_buy_full(
open_quote=float(ask),
target_index=target_index,
signal_note=signal_note,
premium_ccy=premium_ccy,
margin_mode="coin",
)
except Exception:
pass
+10 -3
View File
@@ -30,16 +30,23 @@ def build_profit_alert_message(
upl: float,
upl_ratio: float | None,
bid: float | None,
premium_ccy: str | None = None,
) -> str:
from lib.options.options_notify_lib import resolve_options_premium_ccy
ccy = resolve_options_premium_ccy(inst_id=inst_id, premium_ccy=premium_ccy)
d = 6 if ccy in ("ETH", "BTC") else 4
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:.{d}f}" if bid is not None else ""
mode = "币本位" if ccy != "USDC" else "USDC"
return "\n".join(
[
"【OKX期权·翻倍提醒】",
f"账户:{account_label}",
f"本位:{mode}",
f"合约:{inst_id}",
f"已付权利金:{premium_paid:.4f} USDC",
f"未实现盈亏:{upl:+.4f} USDC({pct})",
f"已付权利金:{premium_paid:.{d}f} {ccy}",
f"未实现盈亏:{upl:+.{d}f} {ccy}({pct})",
f"当前买一:{bid_txt}(可考虑限价平仓锁利)",
]
)
+119 -7
View File
@@ -1,4 +1,4 @@
"""OKX 期权开仓/平仓企业微信推送(必发,幂等落库标记)."""
"""OKX 期权开仓/平仓企业微信推送(必发,幂等落库标记).支持 USDC / 币本位(ETH/BTC)."""
from __future__ import annotations
import sqlite3
@@ -23,6 +23,63 @@ def _opt_type_label(opt_type: Any) -> str:
return t or ""
def resolve_options_premium_ccy(
*,
inst_id: str = "",
underlying: str = "",
premium_ccy: Any = None,
margin_mode: Any = None,
row: dict[str, Any] | None = None,
) -> str:
"""权利金计价币种:USDC 或 ETH/BTC."""
raw = premium_ccy
if (raw is None or str(raw).strip() == "") and row:
raw = row.get("premium_ccy")
ccy = str(raw or "").strip().upper()
if ccy:
return ccy
try:
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
mid = str(inst_id or (row or {}).get("inst_id") or "").strip()
mode = margin_mode if margin_mode is not None else (row or {}).get("margin_mode")
if mode is None and mid:
mode = margin_mode_from_inst_id(mid)
u = str(
underlying
or (row or {}).get("underlying")
or (mid.split("-")[0] if mid else "ETH")
or "ETH"
).strip().upper() or "ETH"
return premium_ccy_for_mode(str(mode or "usdc"), u)
except Exception:
return "USDC"
def _amount_decimals(ccy: str) -> int:
c = (ccy or "USDC").strip().upper()
if c in ("ETH", "BTC"):
return 6
return 4
def _mode_tag(*, inst_id: str = "", premium_ccy: str = "", margin_mode: Any = None) -> str:
ccy = (premium_ccy or "").strip().upper()
if ccy and ccy != "USDC":
return "币本位"
try:
from lib.options.options_margin_mode_lib import is_coin_margin_mode, margin_mode_from_inst_id
mode = margin_mode
if mode is None and inst_id:
mode = margin_mode_from_inst_id(inst_id)
if is_coin_margin_mode(mode):
return "币本位"
except Exception:
pass
return "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,10 +114,21 @@ def build_options_open_message(
target_index: Any = None,
signal_note: str = "",
trade_id: Any = None,
premium_ccy: Any = None,
margin_mode: Any = None,
) -> str:
ccy = resolve_options_premium_ccy(
inst_id=inst_id,
underlying=underlying,
premium_ccy=premium_ccy,
margin_mode=margin_mode,
)
d = _amount_decimals(ccy)
mode = _mode_tag(inst_id=inst_id, premium_ccy=ccy, margin_mode=margin_mode)
lines = [
"【OKX期权·开仓】",
f"账户:{account_label or 'OKX期权'}",
f"本位:{mode}",
]
if trade_id is not None:
lines.append(f"本地单号:#{trade_id}")
@@ -69,8 +137,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, d)} {ccy}",
f"权利金:{_fmt(premium_paid, d)} {ccy}",
]
)
if target_index is not None and str(target_index).strip() != "":
@@ -98,10 +166,21 @@ def build_options_close_message(
target_index: Any = None,
trigger_idx: Any = None,
trade_id: Any = None,
premium_ccy: Any = None,
margin_mode: Any = None,
) -> str:
ccy = resolve_options_premium_ccy(
inst_id=inst_id,
underlying=underlying,
premium_ccy=premium_ccy,
margin_mode=margin_mode,
)
d = _amount_decimals(ccy)
mode = _mode_tag(inst_id=inst_id, premium_ccy=ccy, margin_mode=margin_mode)
lines = [
"【OKX期权·平仓】",
f"账户:{account_label or 'OKX期权'}",
f"本位:{mode}",
]
if trade_id is not None:
lines.append(f"本地单号:#{trade_id}")
@@ -111,9 +190,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, d)} {ccy}",
f"已付/收回:{_fmt(premium_paid, d)} / {_fmt(premium_received, d)} {ccy}",
f"实现盈亏:{_fmt(realized_pnl, d)} {ccy}",
]
)
if target_index is not None and str(target_index).strip() != "":
@@ -142,15 +221,26 @@ def notify_options_open(
open_quote: Any = None,
target_index: Any = None,
signal_note: str = "",
premium_ccy: Any = None,
margin_mode: Any = None,
) -> bool:
ensure_options_notify_columns(conn) if conn is not None else None
row_ccy = premium_ccy
row_mode = margin_mode
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, margin_mode, underlying FROM options_trades WHERE id=?",
(int(trade_id),),
).fetchone()
if row and int(row["wechat_open_sent"] or 0):
return False
if row:
if row_ccy is None:
row_ccy = row["premium_ccy"] if "premium_ccy" in row.keys() else None
if row_mode is None:
row_mode = row["margin_mode"] if "margin_mode" in row.keys() else None
if not underlying:
underlying = str(row["underlying"] or "") if "underlying" in row.keys() else underlying
msg = build_options_open_message(
account_label=str(cfg.get("account_label") or "OKX期权"),
inst_id=inst_id,
@@ -162,6 +252,8 @@ def notify_options_open(
target_index=target_index,
signal_note=signal_note,
trade_id=trade_id,
premium_ccy=row_ccy,
margin_mode=row_mode,
)
ok = notify_options_send(cfg, msg)
if ok and conn is not None and trade_id is not None:
@@ -197,6 +289,8 @@ def notify_options_close(
close_quote: Any = None,
target_index: Any = None,
trigger_idx: Any = None,
premium_ccy: Any = None,
margin_mode: Any = None,
force: bool = False,
) -> bool:
"""平仓必发.默认按 trade_id / 同合约未标记行幂等."""
@@ -230,6 +324,18 @@ def notify_options_close(
).fetchone()
if q2:
rows = [dict(q2)]
if not rows:
# 已有平仓记录且均已推送:幂等跳过,避免再走「无库行」重复推
exists = conn.execute(
"""
SELECT 1 FROM options_trades
WHERE inst_id=? AND status='closed'
LIMIT 1
""",
(inst_id,),
).fetchone()
if exists:
return False
if rows:
# 同次平仓可能多腿:合并一条推送,逐条标记
@@ -259,6 +365,8 @@ 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=premium_ccy or head.get("premium_ccy"),
margin_mode=margin_mode or head.get("margin_mode"),
)
ok = notify_options_send(cfg, msg)
if ok and conn is not None:
@@ -288,6 +396,8 @@ def notify_options_close(
target_index=target_index,
trigger_idx=trigger_idx,
trade_id=trade_id,
premium_ccy=premium_ccy,
margin_mode=margin_mode,
)
return notify_options_send(cfg, msg)
@@ -327,4 +437,6 @@ def notify_options_close_trade_ids(
premium_received=sum(float(r["premium_received"] or 0) for r in rows if r["premium_received"] is not None),
realized_pnl=sum(float(r["realized_pnl"]) for r in rows if r["realized_pnl"] is not None),
close_quote=first.get("close_quote"),
premium_ccy=first.get("premium_ccy"),
margin_mode=first.get("margin_mode"),
)
+37 -3
View File
@@ -233,6 +233,12 @@ def _notify_profit_exit_close(
result: dict[str, Any],
conn: Any = None,
) -> None:
from lib.options.options_notify_lib import resolve_options_premium_ccy
ccy = resolve_options_premium_ccy(inst_id=inst_id)
d = 6 if ccy in ("ETH", "BTC") else 4
mode = "币本位" if ccy != "USDC" else "USDC"
reason = f"翻倍出场({mult:g}倍)"
if result.get("fully_closed") or result.get("already_flat"):
if cfg is not None:
try:
@@ -242,26 +248,31 @@ def _notify_profit_exit_close(
cfg,
conn,
inst_id=inst_id,
reason=f"翻倍出场({mult:g}倍)",
reason=reason,
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,
)
# 无论首次/幂等跳过,全平路径不再走下方 fallback,避免重复推
return
except Exception:
pass
if not send_wechat:
return
try:
prem_txt = f"{float(premium_paid):.{d}f}" if premium_paid is not None else ""
recv_txt = f"{float(recycle):.{d}f}" if recycle is not None else ""
send_wechat(
"\n".join(
[
"【OKX期权·翻倍出场】",
f"账户:{account_label}",
f"本位:{mode}",
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"权利金:{prem_txt} {ccy}",
f"可回收:{recv_txt} {ccy}",
f"提交张数:{result.get('submitted_sheets') or ''}",
f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}",
]
@@ -330,6 +341,17 @@ def run_options_profit_exits(
if result.get("already_flat") or _result_fully_done(result):
_mark_state(conn, inst_id, "done")
_commit(conn)
_notify_profit_exit_close(
cfg,
send_wechat,
account_label=account_label,
inst_id=inst_id,
mult=mult,
premium_paid=prem,
recycle=None,
result={**result, "fully_closed": True},
conn=conn,
)
else:
_mark_state(conn, inst_id, "closing")
_commit(conn)
@@ -353,6 +375,18 @@ def run_options_profit_exits(
if result.get("already_flat"):
_mark_state(conn, inst_id, "done")
_commit(conn)
triggered += 1
_notify_profit_exit_close(
cfg,
send_wechat,
account_label=account_label,
inst_id=inst_id,
mult=mult,
premium_paid=prem,
recycle=recycle,
result=result,
conn=conn,
)
continue
if not result.get("ok"):
_mark_state(conn, inst_id, "active")
+46 -2
View File
@@ -309,6 +309,11 @@ def _notify_target_close(
conn: Any = None,
) -> None:
"""目标位平仓推送:优先走统一平仓必发(幂等);无 cfg 时回退旧文案."""
from lib.options.options_notify_lib import resolve_options_premium_ccy
ccy = resolve_options_premium_ccy(inst_id=inst_id)
d = 6 if ccy in ("ETH", "BTC") else 4
mode = "币本位" if ccy != "USDC" else "USDC"
if result.get("fully_closed") or result.get("already_flat"):
if cfg is not None:
try:
@@ -324,23 +329,28 @@ def _notify_target_close(
close_quote=result.get("locked_bid_px") or result.get("bid"),
target_index=target,
trigger_idx=idx,
premium_ccy=ccy,
)
# 无论首次/幂等跳过,全平路径不再走下方 fallback,避免重复推
return
except Exception:
pass
if not send_wechat:
return
try:
recv = result.get("premium_received")
recv_txt = f"{float(recv):.{d}f} {ccy}" if recv is not None else f"{ccy}"
send_wechat(
"\n".join(
[
"【OKX期权·目标位平仓】",
f"账户:{account_label}",
f"本位:{mode}",
f"合约:{inst_id}",
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_txt}",
f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}",
]
)
@@ -391,7 +401,7 @@ def run_options_target_closes(
cancel_orphans_without_position(conn, live_inst_ids=live_ids)
_commit_monitor(conn)
# 先处理已挂单等待成交的,绝不再发微信
# 先处理已挂单等待成交的;首次触发已推过「挂单中」,此处仅在全平时走幂等平仓推送
for mon in list_closing_targets(conn):
inst_id = str(mon.get("inst_id") or "")
if not inst_id:
@@ -408,6 +418,18 @@ def run_options_target_closes(
if inst_id not in pos_by_inst:
mark_monitor(conn, int(mon["id"]), status="expired", message="持仓已平")
_commit_monitor(conn)
target = _safe_float(mon.get("target_index"))
idx = _safe_float(mon.get("trigger_idx"))
_notify_target_close(
cfg,
send_wechat,
account_label=account_label,
inst_id=inst_id,
target=float(target) if target is not None else 0.0,
idx=float(idx) if idx is not None else 0.0,
result={"already_flat": True, "fully_closed": True, "ok": True},
conn=conn,
)
continue
result = close_fn(inst_id)
idx = _safe_float(pos_by_inst[inst_id].get("idx_px") or pos_by_inst[inst_id].get("idxPx"))
@@ -421,6 +443,17 @@ def run_options_target_closes(
message="目标位限价平仓完成",
)
_commit_monitor(conn)
target = _safe_float(mon.get("target_index"))
_notify_target_close(
cfg,
send_wechat,
account_label=account_label,
inst_id=inst_id,
target=float(target) if target is not None else 0.0,
idx=float(idx) if idx is not None else 0.0,
result={**result, "fully_closed": True},
conn=conn,
)
continue
mark_monitor(
conn,
@@ -464,6 +497,17 @@ def run_options_target_closes(
if result.get("already_flat"):
mark_monitor(conn, int(mon["id"]), status="expired", trigger_idx=idx, message="持仓已平")
_commit_monitor(conn)
triggered += 1
_notify_target_close(
cfg,
send_wechat,
account_label=account_label,
inst_id=inst_id,
target=target,
idx=idx,
result=result,
conn=conn,
)
continue
if not result.get("ok"):
mark_monitor(