Send WeChat alerts on OKX options open and close.
Cover manual, target, and exchange/expiry sync with idempotent sent flags. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+7
-2
@@ -74,10 +74,15 @@ OKX_OPTIONS_API_PASSPHRASE=...
|
||||
|
||||
## 5. 微信提醒
|
||||
|
||||
当某笔持仓 **未实现盈亏 ≥ 已付权利金的 100%**(翻倍)时,会发 **一条** 企业微信提醒(同一笔只提醒一次).
|
||||
|
||||
需已配置 `WECHAT_WEBHOOK`.
|
||||
|
||||
| 场景 | 标题 | 说明 |
|
||||
|------|------|------|
|
||||
| **开仓** | 【OKX期权·开仓】 | 下单成功并写入本地后必发(幂等) |
|
||||
| **平仓** | 【OKX期权·平仓】 | 手动全平 / 目标位全平 / 到期或交易所平仓同步后必发(幂等) |
|
||||
| 浮盈翻倍 | 【OKX期权·翻倍提醒】 | 未实现盈亏 ≥ 已付权利金约 100%,同一笔只提醒一次 |
|
||||
| 挂单超时撤销 | 【OKX期权·挂单超时撤销】 | 平仓挂单超时被系统撤销 |
|
||||
|
||||
## 6. 与永续 / 对冲计划的关系
|
||||
|
||||
| | 永续(子账户) | 期权(主账户) |
|
||||
|
||||
@@ -95,6 +95,14 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
|
||||
ON options_target_monitors(status)
|
||||
"""
|
||||
)
|
||||
for ddl in (
|
||||
"ALTER TABLE options_trades ADD COLUMN wechat_open_sent INTEGER DEFAULT 0",
|
||||
"ALTER TABLE options_trades ADD COLUMN wechat_close_sent INTEGER DEFAULT 0",
|
||||
):
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
except Exception:
|
||||
pass
|
||||
init_options_review_tables(conn)
|
||||
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ def sync_open_options_trades(
|
||||
*,
|
||||
live_inst_ids: set[str],
|
||||
fetch_history_fn: Callable[[str], list[dict[str, Any]]],
|
||||
notify_cfg: dict[str, Any] | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
交易所已无持仓时,将本地 open 记录同步为 closed.
|
||||
@@ -319,6 +320,24 @@ def sync_open_options_trades(
|
||||
),
|
||||
)
|
||||
updated += 1
|
||||
if notify_cfg is not None:
|
||||
try:
|
||||
from lib.options.options_notify_lib import notify_options_close
|
||||
|
||||
reason = "到期结算" if close_reason == "expired" else "交易所平仓"
|
||||
notify_options_close(
|
||||
notify_cfg,
|
||||
conn,
|
||||
inst_id=inst_id,
|
||||
reason=reason,
|
||||
trade_id=int(row["id"]),
|
||||
premium_paid=paid,
|
||||
premium_received=prem_recv,
|
||||
realized_pnl=realized_pnl,
|
||||
close_quote=close_quote,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return updated
|
||||
|
||||
|
||||
@@ -414,6 +433,7 @@ 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},
|
||||
)
|
||||
if sync_trades_fn is not None:
|
||||
sync_trades_fn(conn)
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
"""OKX 期权开仓/平仓企业微信推送(必发,幂等落库标记)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
|
||||
def _fmt(v: Any, d: int = 4) -> str:
|
||||
try:
|
||||
if v is None or v == "":
|
||||
return "—"
|
||||
return f"{float(v):.{d}f}"
|
||||
except (TypeError, ValueError):
|
||||
return str(v)
|
||||
|
||||
|
||||
def _opt_type_label(opt_type: Any) -> str:
|
||||
t = str(opt_type or "").strip().upper()
|
||||
if t in ("C", "CALL"):
|
||||
return "Call"
|
||||
if t in ("P", "PUT"):
|
||||
return "Put"
|
||||
return t or "—"
|
||||
|
||||
|
||||
def ensure_options_notify_columns(conn: sqlite3.Connection) -> None:
|
||||
for ddl in (
|
||||
"ALTER TABLE options_trades ADD COLUMN wechat_open_sent INTEGER DEFAULT 0",
|
||||
"ALTER TABLE options_trades ADD COLUMN wechat_close_sent INTEGER DEFAULT 0",
|
||||
):
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def notify_options_send(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 build_options_open_message(
|
||||
*,
|
||||
account_label: str,
|
||||
inst_id: str,
|
||||
underlying: str = "",
|
||||
opt_type: Any = None,
|
||||
sheets: Any = None,
|
||||
premium_paid: Any = None,
|
||||
open_quote: Any = None,
|
||||
target_index: Any = None,
|
||||
signal_note: str = "",
|
||||
trade_id: Any = None,
|
||||
) -> str:
|
||||
lines = [
|
||||
"【OKX期权·开仓】",
|
||||
f"账户:{account_label or 'OKX期权'}",
|
||||
]
|
||||
if trade_id is not None:
|
||||
lines.append(f"本地单号:#{trade_id}")
|
||||
lines.extend(
|
||||
[
|
||||
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",
|
||||
]
|
||||
)
|
||||
if target_index is not None and str(target_index).strip() != "":
|
||||
try:
|
||||
lines.append(f"目标指数:{float(target_index):g}")
|
||||
except (TypeError, ValueError):
|
||||
lines.append(f"目标指数:{target_index}")
|
||||
if signal_note:
|
||||
lines.append(f"备注:{signal_note[:200]}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_options_close_message(
|
||||
*,
|
||||
account_label: str,
|
||||
inst_id: str,
|
||||
reason: str = "",
|
||||
underlying: str = "",
|
||||
opt_type: Any = None,
|
||||
sheets: Any = None,
|
||||
premium_paid: Any = None,
|
||||
premium_received: Any = None,
|
||||
realized_pnl: Any = None,
|
||||
close_quote: Any = None,
|
||||
target_index: Any = None,
|
||||
trigger_idx: Any = None,
|
||||
trade_id: Any = None,
|
||||
) -> str:
|
||||
lines = [
|
||||
"【OKX期权·平仓】",
|
||||
f"账户:{account_label or 'OKX期权'}",
|
||||
]
|
||||
if trade_id is not None:
|
||||
lines.append(f"本地单号:#{trade_id}")
|
||||
lines.extend(
|
||||
[
|
||||
f"合约:{inst_id}",
|
||||
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",
|
||||
]
|
||||
)
|
||||
if target_index is not None and str(target_index).strip() != "":
|
||||
try:
|
||||
lines.append(f"目标指数:{float(target_index):g}")
|
||||
except (TypeError, ValueError):
|
||||
lines.append(f"目标指数:{target_index}")
|
||||
if trigger_idx is not None and str(trigger_idx).strip() != "":
|
||||
try:
|
||||
lines.append(f"触发指数:{float(trigger_idx):g}")
|
||||
except (TypeError, ValueError):
|
||||
lines.append(f"触发指数:{trigger_idx}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def notify_options_open(
|
||||
cfg: dict[str, Any],
|
||||
conn: sqlite3.Connection | None,
|
||||
*,
|
||||
trade_id: int | None,
|
||||
inst_id: str,
|
||||
underlying: str = "",
|
||||
opt_type: Any = None,
|
||||
sheets: Any = None,
|
||||
premium_paid: Any = None,
|
||||
open_quote: Any = None,
|
||||
target_index: Any = None,
|
||||
signal_note: str = "",
|
||||
) -> bool:
|
||||
ensure_options_notify_columns(conn) if conn is not None else None
|
||||
if conn is not None and trade_id is not None:
|
||||
row = conn.execute(
|
||||
"SELECT wechat_open_sent FROM options_trades WHERE id=?",
|
||||
(int(trade_id),),
|
||||
).fetchone()
|
||||
if row and int(row["wechat_open_sent"] or 0):
|
||||
return False
|
||||
msg = build_options_open_message(
|
||||
account_label=str(cfg.get("account_label") or "OKX期权"),
|
||||
inst_id=inst_id,
|
||||
underlying=underlying,
|
||||
opt_type=opt_type,
|
||||
sheets=sheets,
|
||||
premium_paid=premium_paid,
|
||||
open_quote=open_quote,
|
||||
target_index=target_index,
|
||||
signal_note=signal_note,
|
||||
trade_id=trade_id,
|
||||
)
|
||||
ok = notify_options_send(cfg, msg)
|
||||
if ok and conn is not None and trade_id is not None:
|
||||
conn.execute(
|
||||
"UPDATE options_trades SET wechat_open_sent=1 WHERE id=?",
|
||||
(int(trade_id),),
|
||||
)
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
return ok
|
||||
|
||||
|
||||
def _load_trade_row(conn: sqlite3.Connection, trade_id: int) -> dict[str, Any] | None:
|
||||
row = conn.execute("SELECT * FROM options_trades WHERE id=?", (int(trade_id),)).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def notify_options_close(
|
||||
cfg: dict[str, Any],
|
||||
conn: sqlite3.Connection | None,
|
||||
*,
|
||||
inst_id: str,
|
||||
reason: str = "平仓",
|
||||
trade_id: int | None = None,
|
||||
underlying: str = "",
|
||||
opt_type: Any = None,
|
||||
sheets: Any = None,
|
||||
premium_paid: Any = None,
|
||||
premium_received: Any = None,
|
||||
realized_pnl: Any = None,
|
||||
close_quote: Any = None,
|
||||
target_index: Any = None,
|
||||
trigger_idx: Any = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
"""平仓必发.默认按 trade_id / 同合约未标记行幂等."""
|
||||
if conn is not None:
|
||||
ensure_options_notify_columns(conn)
|
||||
rows: list[dict[str, Any]] = []
|
||||
if conn is not None and trade_id is not None:
|
||||
r = _load_trade_row(conn, int(trade_id))
|
||||
if r:
|
||||
rows = [r]
|
||||
elif conn is not None and inst_id:
|
||||
q = conn.execute(
|
||||
"""
|
||||
SELECT * FROM options_trades
|
||||
WHERE inst_id=? AND status='closed'
|
||||
AND COALESCE(wechat_close_sent,0)=0
|
||||
ORDER BY id DESC
|
||||
LIMIT 20
|
||||
""",
|
||||
(inst_id,),
|
||||
).fetchall()
|
||||
rows = [dict(x) for x in q]
|
||||
if not rows and force:
|
||||
q2 = conn.execute(
|
||||
"""
|
||||
SELECT * FROM options_trades
|
||||
WHERE inst_id=? AND status='closed'
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""",
|
||||
(inst_id,),
|
||||
).fetchone()
|
||||
if q2:
|
||||
rows = [dict(q2)]
|
||||
|
||||
if rows:
|
||||
# 同次平仓可能多腿:合并一条推送,逐条标记
|
||||
total_paid = sum(float(r.get("premium_paid") or 0) for r in rows)
|
||||
total_recv = sum(float(r.get("premium_received") or 0) for r in rows if r.get("premium_received") is not None)
|
||||
pnls = [float(r["realized_pnl"]) for r in rows if r.get("realized_pnl") is not None]
|
||||
total_pnl = sum(pnls) if pnls else None
|
||||
if total_pnl is None and (premium_received is not None or realized_pnl is not None):
|
||||
total_pnl = realized_pnl
|
||||
total_recv = premium_received if premium_received is not None else total_recv
|
||||
total_paid = premium_paid if premium_paid is not None else total_paid
|
||||
head = rows[0]
|
||||
pending = [r for r in rows if not int(r.get("wechat_close_sent") or 0)]
|
||||
if not pending and not force:
|
||||
return False
|
||||
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 ""),
|
||||
reason=reason,
|
||||
underlying=underlying or str(head.get("underlying") or ""),
|
||||
opt_type=opt_type or head.get("opt_type"),
|
||||
sheets=sheets if sheets is not None else sum(int(r.get("sheets") or 0) for r in rows),
|
||||
premium_paid=total_paid,
|
||||
premium_received=total_recv if rows else premium_received,
|
||||
realized_pnl=total_pnl,
|
||||
close_quote=close_quote if close_quote is not None else head.get("close_quote"),
|
||||
target_index=target_index,
|
||||
trigger_idx=trigger_idx,
|
||||
trade_id=head.get("id") if len(rows) == 1 else None,
|
||||
)
|
||||
ok = notify_options_send(cfg, msg)
|
||||
if ok and conn is not None:
|
||||
for r in pending or rows:
|
||||
conn.execute(
|
||||
"UPDATE options_trades SET wechat_close_sent=1 WHERE id=?",
|
||||
(int(r["id"]),),
|
||||
)
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
return ok
|
||||
|
||||
# 无库行时仍发一条(尽量不丢提醒)
|
||||
msg = build_options_close_message(
|
||||
account_label=str(cfg.get("account_label") or "OKX期权"),
|
||||
inst_id=inst_id,
|
||||
reason=reason,
|
||||
underlying=underlying,
|
||||
opt_type=opt_type,
|
||||
sheets=sheets,
|
||||
premium_paid=premium_paid,
|
||||
premium_received=premium_received,
|
||||
realized_pnl=realized_pnl,
|
||||
close_quote=close_quote,
|
||||
target_index=target_index,
|
||||
trigger_idx=trigger_idx,
|
||||
trade_id=trade_id,
|
||||
)
|
||||
return notify_options_send(cfg, msg)
|
||||
|
||||
|
||||
def notify_options_close_trade_ids(
|
||||
cfg: dict[str, Any],
|
||||
conn: sqlite3.Connection,
|
||||
trade_ids: list[int],
|
||||
*,
|
||||
reason: str,
|
||||
) -> bool:
|
||||
ids = [int(x) for x in trade_ids if x is not None]
|
||||
if not ids:
|
||||
return False
|
||||
ensure_options_notify_columns(conn)
|
||||
placeholders = ",".join("?" for _ in ids)
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT * FROM options_trades
|
||||
WHERE id IN ({placeholders}) AND COALESCE(wechat_close_sent,0)=0
|
||||
""",
|
||||
ids,
|
||||
).fetchall()
|
||||
if not rows:
|
||||
return False
|
||||
first = dict(rows[0])
|
||||
return notify_options_close(
|
||||
cfg,
|
||||
conn,
|
||||
inst_id=str(first.get("inst_id") or ""),
|
||||
reason=reason,
|
||||
trade_id=int(first["id"]) if len(rows) == 1 else None,
|
||||
underlying=str(first.get("underlying") or ""),
|
||||
opt_type=first.get("opt_type"),
|
||||
sheets=sum(int(r["sheets"] or 0) for r in rows),
|
||||
premium_paid=sum(float(r["premium_paid"] or 0) for r in rows),
|
||||
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"),
|
||||
)
|
||||
@@ -640,11 +640,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
conn = cfg["get_db"]()
|
||||
trade_id = None
|
||||
target_mon = None
|
||||
open_underlying = ""
|
||||
open_opt_type = None
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
meta = q.get("meta") or {}
|
||||
u = str(meta.get("uly") or inst_id).split("-")[0]
|
||||
opt_type = meta.get("optType")
|
||||
open_underlying = u
|
||||
open_opt_type = opt_type
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO options_trades
|
||||
@@ -683,9 +687,30 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
finally:
|
||||
conn.close()
|
||||
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
|
||||
from lib.options.options_notify_lib import notify_options_open
|
||||
|
||||
invalidate_option_positions_cache()
|
||||
_sync_options_trades(cfg, force=True)
|
||||
try:
|
||||
conn_n = cfg["get_db"]()
|
||||
try:
|
||||
notify_options_open(
|
||||
cfg,
|
||||
conn_n,
|
||||
trade_id=trade_id,
|
||||
inst_id=inst_id,
|
||||
underlying=open_underlying,
|
||||
opt_type=open_opt_type,
|
||||
sheets=sheets,
|
||||
premium_paid=sizing.get("total_premium"),
|
||||
open_quote=float(ask) if ask is not None else None,
|
||||
target_index=target_index,
|
||||
signal_note=signal_note,
|
||||
)
|
||||
finally:
|
||||
conn_n.close()
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
@@ -938,11 +963,21 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
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
|
||||
|
||||
conn2 = cfg["get_db"]()
|
||||
try:
|
||||
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:
|
||||
@@ -1253,6 +1288,7 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
conn,
|
||||
live_inst_ids=live_ids,
|
||||
fetch_history_fn=lambda inst_id: fetch_option_position_history(ex, inst_id),
|
||||
notify_cfg=cfg,
|
||||
)
|
||||
|
||||
def _target_close(inst_id: str) -> dict[str, Any]:
|
||||
|
||||
@@ -292,6 +292,7 @@ def close_option_by_bid_depth(
|
||||
|
||||
|
||||
def _notify_target_close(
|
||||
cfg: dict[str, Any] | None,
|
||||
send_wechat: Callable[[str], None] | None,
|
||||
*,
|
||||
account_label: str,
|
||||
@@ -299,7 +300,28 @@ def _notify_target_close(
|
||||
target: float,
|
||||
idx: float,
|
||||
result: dict[str, Any],
|
||||
conn: Any = None,
|
||||
) -> None:
|
||||
"""目标位平仓推送:优先走统一平仓必发(幂等);无 cfg 时回退旧文案."""
|
||||
if result.get("fully_closed") or result.get("already_flat"):
|
||||
if cfg is not None:
|
||||
try:
|
||||
from lib.options.options_notify_lib import notify_options_close
|
||||
|
||||
notify_options_close(
|
||||
cfg,
|
||||
conn,
|
||||
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"),
|
||||
target_index=target,
|
||||
trigger_idx=idx,
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if not send_wechat:
|
||||
return
|
||||
try:
|
||||
@@ -313,6 +335,7 @@ def _notify_target_close(
|
||||
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"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}",
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -339,6 +362,7 @@ def run_options_target_closes(
|
||||
index_fn: Callable[[dict[str, Any]], float | None] | None = None,
|
||||
send_wechat: Callable[[str], None] | None = None,
|
||||
account_label: str = "OKX期权",
|
||||
cfg: dict[str, Any] | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
扫描 active 目标委托;指数到位后限价平仓.
|
||||
@@ -433,11 +457,13 @@ def run_options_target_closes(
|
||||
_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,
|
||||
)
|
||||
return triggered
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""期权开平仓微信文案."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from lib.options.options_notify_lib import (
|
||||
build_options_close_message,
|
||||
build_options_open_message,
|
||||
)
|
||||
|
||||
|
||||
class TestOptionsNotify(unittest.TestCase):
|
||||
def test_open_close_messages(self) -> None:
|
||||
open_msg = build_options_open_message(
|
||||
account_label="OKX期权",
|
||||
inst_id="ETH-USD-250725-3200-C",
|
||||
underlying="ETH",
|
||||
opt_type="C",
|
||||
sheets=2,
|
||||
premium_paid=8.5,
|
||||
open_quote=0.01,
|
||||
target_index=3400,
|
||||
signal_note="假突破",
|
||||
trade_id=12,
|
||||
)
|
||||
self.assertIn("【OKX期权·开仓】", open_msg)
|
||||
self.assertIn("ETH-USD-250725-3200-C", open_msg)
|
||||
self.assertIn("目标指数:3400", open_msg)
|
||||
|
||||
close_msg = build_options_close_message(
|
||||
account_label="OKX期权",
|
||||
inst_id="ETH-USD-250725-3200-C",
|
||||
reason="手动平仓",
|
||||
underlying="ETH",
|
||||
opt_type="C",
|
||||
sheets=2,
|
||||
premium_paid=8.5,
|
||||
premium_received=12.0,
|
||||
realized_pnl=3.5,
|
||||
)
|
||||
self.assertIn("【OKX期权·平仓】", close_msg)
|
||||
self.assertIn("手动平仓", close_msg)
|
||||
self.assertIn("3.5000", close_msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user