Enrich WeCom open/close notifies with Chinese qty, margin, and PnL.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -161,9 +161,11 @@ async def sim_open_group(
|
||||
extra={
|
||||
"bias": bias,
|
||||
"option_side": option_side,
|
||||
"perp_side": perp_side,
|
||||
"option_inst_id": option_inst,
|
||||
"strike": pick.pair.strike,
|
||||
"expiry_ymd": pick.pair.expiry_ymd,
|
||||
**(r.data or {}),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -371,6 +371,12 @@ class BinanceLiveExecutor(Matcher):
|
||||
except Exception:
|
||||
logger.exception("lock exit target failed group=%s", group_id)
|
||||
|
||||
leverage = self.ledger.get_setting_float("leverage", get_settings().leverage)
|
||||
perp_margin = (
|
||||
abs(float(pf_px) * float(perp_qty)) / float(leverage)
|
||||
if leverage and float(leverage) > 0
|
||||
else None
|
||||
)
|
||||
return OpenResult(
|
||||
ok=True,
|
||||
group_id=group_id,
|
||||
@@ -379,9 +385,21 @@ class BinanceLiveExecutor(Matcher):
|
||||
"group_id": group_id,
|
||||
"exec_mode": "LIVE",
|
||||
"exchange": "binance",
|
||||
"bias": bias,
|
||||
"option_side": option_side,
|
||||
"perp_side": perp_side,
|
||||
"option_inst_id": option_inst_id,
|
||||
"strike": strike,
|
||||
"expiry_ymd": expiry_ymd,
|
||||
"option_ord": opt_fill.ord_id,
|
||||
"perp_ord": perp_fill_live.ord_id,
|
||||
"perp_qty_eth": float(perp_qty),
|
||||
"option_qty_eth": float(opt_qty),
|
||||
"perp_entry_px": float(pf_px),
|
||||
"option_entry_px": float(of_px),
|
||||
"initial_premium": initial_premium,
|
||||
"perp_margin": perp_margin,
|
||||
"leverage": float(leverage) if leverage else None,
|
||||
"fees": of_fee + pf_fee,
|
||||
},
|
||||
)
|
||||
@@ -1101,13 +1119,31 @@ class BinanceLiveExecutor(Matcher):
|
||||
local_net=float(net) if net is not None else None,
|
||||
)
|
||||
|
||||
fills_summary = None
|
||||
try:
|
||||
from ..sim.pnl import summarize_fills_pnl
|
||||
|
||||
fill_rows = self.db.fetchall(
|
||||
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
|
||||
)
|
||||
fills_summary = summarize_fills_pnl(list(fill_rows))
|
||||
except Exception:
|
||||
fills_summary = None
|
||||
return CloseResult(
|
||||
ok=True,
|
||||
detail="closed_live_binance",
|
||||
data={
|
||||
"group_id": group_id,
|
||||
"reason": reason,
|
||||
"perp_pnl": (
|
||||
fills_summary.get("perp_pnl") if fills_summary else None
|
||||
),
|
||||
"option_pnl": (
|
||||
fills_summary.get("option_pnl") if fills_summary else None
|
||||
),
|
||||
"net": net,
|
||||
"net_pnl": net,
|
||||
"fees": fills_summary.get("fees_total") if fills_summary else None,
|
||||
"exec_mode": "LIVE",
|
||||
"pnl_source": "live_exchange",
|
||||
},
|
||||
@@ -1306,5 +1342,16 @@ class BinanceLiveExecutor(Matcher):
|
||||
return CloseResult(
|
||||
ok=True,
|
||||
detail="perp_closed_option_residual_live_binance",
|
||||
data={"group_id": group_id, "reason": reason, "mode": "target_perp_only", "exec_mode": "LIVE"},
|
||||
data={
|
||||
"group_id": group_id,
|
||||
"reason": reason,
|
||||
"mode": "target_perp_only",
|
||||
"perp_pnl": perp_pnl,
|
||||
"option_pnl": None,
|
||||
"interim_net": interim_net,
|
||||
"net": interim_net,
|
||||
"net_pnl": interim_net,
|
||||
"option_abandoned": True,
|
||||
"exec_mode": "LIVE",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -389,6 +389,12 @@ class OkxLiveExecutor(Matcher):
|
||||
except Exception:
|
||||
logger.exception("lock exit target failed group=%s", group_id)
|
||||
|
||||
leverage = self.ledger.get_setting_float("leverage", get_settings().leverage)
|
||||
perp_margin = (
|
||||
abs(float(pf_px) * float(perp_qty)) / float(leverage)
|
||||
if leverage and float(leverage) > 0
|
||||
else None
|
||||
)
|
||||
return OpenResult(
|
||||
ok=True,
|
||||
group_id=group_id,
|
||||
@@ -396,9 +402,21 @@ class OkxLiveExecutor(Matcher):
|
||||
data={
|
||||
"group_id": group_id,
|
||||
"exec_mode": "LIVE",
|
||||
"bias": bias,
|
||||
"option_side": option_side,
|
||||
"perp_side": perp_side,
|
||||
"option_inst_id": option_inst_id,
|
||||
"strike": strike,
|
||||
"expiry_ymd": expiry_ymd,
|
||||
"option_ord": opt_fill.ord_id,
|
||||
"perp_ord": perp_fill_live.ord_id,
|
||||
"perp_qty_eth": float(perp_qty),
|
||||
"option_qty_eth": float(opt_qty),
|
||||
"perp_entry_px": float(pf_px),
|
||||
"option_entry_px": float(of_px),
|
||||
"initial_premium": initial_premium,
|
||||
"perp_margin": perp_margin,
|
||||
"leverage": float(leverage) if leverage else None,
|
||||
"fees": of_fee + pf_fee,
|
||||
},
|
||||
)
|
||||
@@ -1140,13 +1158,35 @@ class OkxLiveExecutor(Matcher):
|
||||
local_net=float(net) if net is not None else None,
|
||||
)
|
||||
|
||||
fills_summary = None
|
||||
try:
|
||||
from ..sim.pnl import summarize_fills_pnl
|
||||
|
||||
fill_rows = self.db.fetchall(
|
||||
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
|
||||
)
|
||||
fills_summary = summarize_fills_pnl(list(fill_rows))
|
||||
except Exception:
|
||||
fills_summary = None
|
||||
return CloseResult(
|
||||
ok=True,
|
||||
detail="closed_live",
|
||||
data={
|
||||
"group_id": group_id,
|
||||
"reason": reason,
|
||||
"perp_pnl": (
|
||||
fills_summary.get("perp_pnl")
|
||||
if fills_summary
|
||||
else None
|
||||
),
|
||||
"option_pnl": (
|
||||
fills_summary.get("option_pnl")
|
||||
if fills_summary
|
||||
else None
|
||||
),
|
||||
"net": net,
|
||||
"net_pnl": net,
|
||||
"fees": fills_summary.get("fees_total") if fills_summary else None,
|
||||
"exec_mode": "LIVE",
|
||||
"pnl_source": "live_exchange",
|
||||
},
|
||||
@@ -1330,7 +1370,18 @@ class OkxLiveExecutor(Matcher):
|
||||
return CloseResult(
|
||||
ok=True,
|
||||
detail="perp_closed_option_residual_live",
|
||||
data={"group_id": group_id, "reason": reason, "mode": "target_perp_only", "exec_mode": "LIVE"},
|
||||
data={
|
||||
"group_id": group_id,
|
||||
"reason": reason,
|
||||
"mode": "target_perp_only",
|
||||
"perp_pnl": perp_pnl,
|
||||
"option_pnl": None,
|
||||
"interim_net": interim_net,
|
||||
"net": interim_net,
|
||||
"net_pnl": interim_net,
|
||||
"option_abandoned": True,
|
||||
"exec_mode": "LIVE",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
+158
-25
@@ -27,6 +27,30 @@ _last_fault_key: str | None = None
|
||||
_last_fault_ms: float = 0.0
|
||||
_FAULT_DEDUP_SEC = 300.0
|
||||
|
||||
CLOSE_REASON_ZH: dict[str, str] = {
|
||||
"expiry": "到期结算全平",
|
||||
"target_perp_only": "净盈利达标·只平永续(期权归档到期)",
|
||||
"fixed_usdt": "固定净盈利达标·双腿全平",
|
||||
"premium_multiple": "权利金倍数达标·双腿全平",
|
||||
"emergency": "紧急全平",
|
||||
"emergency_perp": "紧急·只平永续",
|
||||
"manual": "手动全平",
|
||||
"perp_pending_retry": "续平永续",
|
||||
"liquidity_retry": "等待流动性后全平",
|
||||
"unknown": "未知原因",
|
||||
}
|
||||
|
||||
BIAS_ZH: dict[str, str] = {
|
||||
"call_ask_gt_put": "买Call + 永续空",
|
||||
"put_ask_gt_call": "买Put + 永续多",
|
||||
"strike_below_spot": "买Call + 永续空",
|
||||
"strike_above_spot": "买Put + 永续多",
|
||||
"fixed_long_put": "固定方向·买Put + 永续多",
|
||||
"fixed_short_call": "固定方向·买Call + 永续空",
|
||||
"manual_call": "手动·买Call + 永续空",
|
||||
"manual_put": "手动·买Put + 永续多",
|
||||
}
|
||||
|
||||
|
||||
def _as_bool(raw: str | None, default: bool = False) -> bool:
|
||||
if raw is None or raw == "":
|
||||
@@ -84,6 +108,64 @@ def venue_label() -> str | None:
|
||||
return "实盘·OKX"
|
||||
|
||||
|
||||
def close_reason_zh(reason: str | None) -> str:
|
||||
r = str(reason or "").strip()
|
||||
if not r:
|
||||
return "未知原因"
|
||||
return CLOSE_REASON_ZH.get(r, r)
|
||||
|
||||
|
||||
def direction_zh(extra: dict[str, Any]) -> str:
|
||||
bias = str(extra.get("bias") or "").strip()
|
||||
if bias in BIAS_ZH:
|
||||
return BIAS_ZH[bias]
|
||||
opt = str(extra.get("option_side") or "").strip().lower()
|
||||
perp = str(extra.get("perp_side") or "").strip().lower()
|
||||
if opt == "put" and perp == "long":
|
||||
return "买Put + 永续多"
|
||||
if opt == "call" and perp == "short":
|
||||
return "买Call + 永续空"
|
||||
if opt == "put":
|
||||
return "买Put"
|
||||
if opt == "call":
|
||||
return "买Call"
|
||||
if bias:
|
||||
return bias
|
||||
return "—"
|
||||
|
||||
|
||||
def _fmt_num(x: Any, digits: int = 2) -> str:
|
||||
try:
|
||||
if x is None or x == "":
|
||||
return "—"
|
||||
return f"{float(x):.{digits}f}"
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
|
||||
|
||||
def _fmt_money(x: Any, *, signed: bool = False) -> str:
|
||||
try:
|
||||
if x is None or x == "":
|
||||
return "—"
|
||||
v = float(x)
|
||||
if signed:
|
||||
return f"{v:+.2f}U"
|
||||
return f"{v:.2f}U"
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
|
||||
|
||||
def _pick_float(data: dict[str, Any], *keys: str) -> float | None:
|
||||
for k in keys:
|
||||
if k not in data or data[k] is None or data[k] == "":
|
||||
continue
|
||||
try:
|
||||
return float(data[k])
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def build_markdown(*, tag: str, title: str, lines: list[str] | None = None) -> str:
|
||||
body = "\n".join(f"> {ln}" if not ln.startswith(">") else ln for ln in (lines or []))
|
||||
machine = wecom_machine_name()
|
||||
@@ -175,15 +257,48 @@ def notify_pause() -> None:
|
||||
|
||||
|
||||
def notify_open(*, group_id: str, detail: str = "", extra: dict[str, Any] | None = None) -> None:
|
||||
extra = extra or {}
|
||||
extra = dict(extra or {})
|
||||
perp = extra.get("perp") if isinstance(extra.get("perp"), dict) else {}
|
||||
option = extra.get("option") if isinstance(extra.get("option"), dict) else {}
|
||||
|
||||
perp_qty = _pick_float(extra, "perp_qty_eth") or _pick_float(perp, "qty_eth")
|
||||
opt_qty = _pick_float(extra, "option_qty_eth") or _pick_float(option, "qty_eth")
|
||||
premium = _pick_float(extra, "initial_premium", "premium")
|
||||
margin = _pick_float(extra, "perp_margin", "margin")
|
||||
leverage = _pick_float(extra, "leverage")
|
||||
perp_px = _pick_float(extra, "perp_entry_px") or _pick_float(perp, "fill_px")
|
||||
opt_px = _pick_float(extra, "option_entry_px") or _pick_float(option, "fill_px")
|
||||
|
||||
# 缺保证金时用成交价×数量÷杠杆估算
|
||||
if margin is None and perp_px is not None and perp_qty is not None:
|
||||
try:
|
||||
from ..sim.ledger import Ledger
|
||||
|
||||
s = get_settings()
|
||||
lev = float(leverage) if leverage and leverage > 0 else float(
|
||||
Ledger().get_setting_float("leverage", s.leverage) or s.leverage or 1
|
||||
)
|
||||
if lev > 0:
|
||||
margin = abs(perp_px * perp_qty) / lev
|
||||
leverage = lev
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
lines = [
|
||||
f"**组**: `{group_id}`",
|
||||
f"**方向**: {extra.get('bias') or extra.get('option_side') or '—'}",
|
||||
f"**期权**: `{extra.get('option_inst_id') or '—'}`",
|
||||
f"**行权/到期**: {extra.get('strike') or '—'} / {extra.get('expiry_ymd') or '—'}",
|
||||
f"**组号**: `{group_id}`",
|
||||
f"**方向**: {direction_zh(extra)}",
|
||||
f"**期权合约**: `{extra.get('option_inst_id') or '—'}`",
|
||||
f"**行权价 / 到期**: {_fmt_num(extra.get('strike'), 0)} / {extra.get('expiry_ymd') or '—'}",
|
||||
f"**开仓数量**: 永续 {_fmt_num(perp_qty, 4)} ETH · 期权 {_fmt_num(opt_qty, 4)} ETH",
|
||||
f"**成交均价**: 永续 {_fmt_num(perp_px, 4)} · 期权 {_fmt_num(opt_px, 4)}",
|
||||
f"**权利金占用**: {_fmt_money(premium)}",
|
||||
f"**保证金占用**: {_fmt_money(margin)}"
|
||||
+ (f"(杠杆 {_fmt_num(leverage, 0)}x)" if leverage else ""),
|
||||
]
|
||||
if detail:
|
||||
lines.append(f"**说明**: {detail}")
|
||||
# 说明仅在非模板英文码时展示
|
||||
d = str(detail or "").strip()
|
||||
if d and d not in ("opened", "opened_live", "ok"):
|
||||
lines.append(f"**说明**: {d}")
|
||||
notify_async(build_markdown(tag=TAG_OPEN, title="开仓成功", lines=lines))
|
||||
|
||||
|
||||
@@ -194,26 +309,44 @@ def notify_close(
|
||||
group_id: str | None = None,
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
data = data or {}
|
||||
reason_zh = {
|
||||
"expiry": "到期平仓",
|
||||
"target_perp_only": "目标平仓·只平永续",
|
||||
"fixed_usdt": "目标平仓·双腿",
|
||||
"premium_multiple": "目标平仓·双腿",
|
||||
"emergency": "紧急全平",
|
||||
"emergency_perp": "紧急·只平永续",
|
||||
"manual": "手动全平",
|
||||
"perp_pending_retry": "续平永续",
|
||||
}.get(reason, reason)
|
||||
data = dict(data or {})
|
||||
reason_zh = close_reason_zh(reason)
|
||||
gid = group_id or data.get("group_id") or "—"
|
||||
|
||||
perp_pnl = _pick_float(data, "perp_pnl")
|
||||
opt_pnl = _pick_float(data, "option_pnl", "opt_pnl")
|
||||
net = _pick_float(data, "net", "net_pnl", "interim_net", "realized_pnl")
|
||||
# 只平永续时 interim_net 可能是净利口径
|
||||
if data.get("option_abandoned") and opt_pnl is None:
|
||||
opt_note = "期权已归档,待到期结算(本组未计入期权最终盈亏)"
|
||||
else:
|
||||
opt_note = None
|
||||
|
||||
lines = [
|
||||
f"**原因**: {reason_zh} (`{reason}`)",
|
||||
f"**组**: `{group_id or data.get('group_id') or '—'}`",
|
||||
f"**组号**: `{gid}`",
|
||||
f"**平仓方式**: {reason_zh}",
|
||||
f"**永续盈亏**: {_fmt_money(perp_pnl, signed=True)}",
|
||||
f"**期权盈亏**: {_fmt_money(opt_pnl, signed=True)}",
|
||||
f"**净利润**: {_fmt_money(net, signed=True)}",
|
||||
]
|
||||
if detail:
|
||||
lines.append(f"**说明**: {detail}")
|
||||
net = data.get("net_pnl")
|
||||
if net is not None:
|
||||
lines.append(f"**净盈亏**: {net}")
|
||||
if opt_note:
|
||||
lines.append(f"**备注**: {opt_note}")
|
||||
fees = _pick_float(data, "fees", "fees_total")
|
||||
if fees is None:
|
||||
fo = _pick_float(data, "fees_open")
|
||||
fc = _pick_float(data, "fees_close")
|
||||
if fo is not None or fc is not None:
|
||||
fees = (fo or 0.0) + (fc or 0.0)
|
||||
if fees is not None:
|
||||
lines.append(f"**手续费合计**: {_fmt_money(fees)}")
|
||||
d = str(detail or "").strip()
|
||||
if d and d not in (
|
||||
"closed",
|
||||
"perp_closed_option_residual",
|
||||
"ok",
|
||||
"manual",
|
||||
):
|
||||
lines.append(f"**说明**: {d}")
|
||||
notify_async(build_markdown(tag=TAG_CLOSE, title=f"平仓 · {reason_zh}", lines=lines))
|
||||
|
||||
|
||||
|
||||
@@ -349,15 +349,33 @@ class Matcher:
|
||||
except Exception:
|
||||
logger.exception("lock exit target failed group=%s", group_id)
|
||||
|
||||
leverage = self.ledger.get_setting_float("leverage", s.leverage)
|
||||
perp_margin = (
|
||||
abs(float(pf.fill_px) * float(perp_qty)) / float(leverage)
|
||||
if leverage and float(leverage) > 0
|
||||
else None
|
||||
)
|
||||
return OpenResult(
|
||||
ok=True,
|
||||
group_id=group_id,
|
||||
detail="opened",
|
||||
data={
|
||||
"group_id": group_id,
|
||||
"bias": bias,
|
||||
"option_side": option_side,
|
||||
"perp_side": perp_side,
|
||||
"option_inst_id": option_inst_id,
|
||||
"strike": strike,
|
||||
"expiry_ymd": expiry_ymd,
|
||||
"perp": pf.to_dict(),
|
||||
"option": of.to_dict(),
|
||||
"perp_qty_eth": float(perp_qty),
|
||||
"option_qty_eth": float(opt_qty),
|
||||
"perp_entry_px": float(pf.fill_px),
|
||||
"option_entry_px": float(of.fill_px),
|
||||
"initial_premium": initial_premium,
|
||||
"perp_margin": perp_margin,
|
||||
"leverage": float(leverage) if leverage else None,
|
||||
"fees": pf.fee + of.fee,
|
||||
"open_sequence": ["option", "perp"],
|
||||
},
|
||||
@@ -617,8 +635,10 @@ class Matcher:
|
||||
"perp_pnl": perp_pnl,
|
||||
"option_pnl": opt_pnl,
|
||||
"net": net_after_all_fees,
|
||||
"net_pnl": net_after_all_fees,
|
||||
"fees_open": open_fees,
|
||||
"fees_close": pf.fee + of.fee,
|
||||
"fees": float(open_fees) + float(pf.fee) + float(of.fee),
|
||||
"close_sequence": ["option", "perp"],
|
||||
"cash_delta": opt_cash + perp_pnl - pf.fee,
|
||||
"option_close_bid": float(close_bid),
|
||||
@@ -788,7 +808,10 @@ class Matcher:
|
||||
"reason": reason,
|
||||
"mode": "target_perp_only",
|
||||
"perp_pnl": perp_pnl,
|
||||
"option_pnl": None,
|
||||
"interim_net": interim_net,
|
||||
"net": interim_net,
|
||||
"net_pnl": interim_net,
|
||||
"option_abandoned": True,
|
||||
"strike": float(strike),
|
||||
"spot": float(spot),
|
||||
|
||||
@@ -953,9 +953,11 @@ class StrategyEngine:
|
||||
extra={
|
||||
"bias": pick.bias,
|
||||
"option_side": pick.option_side,
|
||||
"perp_side": pick.perp_side,
|
||||
"option_inst_id": option_inst,
|
||||
"strike": pick.pair.strike,
|
||||
"expiry_ymd": pick.pair.expiry_ymd,
|
||||
**(r.data or {}),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.notify.wecom import TAG_OPEN, build_markdown, venue_label
|
||||
from app.notify.wecom import (
|
||||
TAG_CLOSE,
|
||||
TAG_OPEN,
|
||||
build_markdown,
|
||||
close_reason_zh,
|
||||
direction_zh,
|
||||
notify_close,
|
||||
notify_open,
|
||||
venue_label,
|
||||
)
|
||||
|
||||
|
||||
def test_build_markdown_has_tag_and_title(monkeypatch):
|
||||
monkeypatch.setattr("app.notify.wecom.venue_label", lambda: "模拟盘")
|
||||
monkeypatch.setattr("app.notify.wecom.venue_label", lambda: "实盘·OKX")
|
||||
md = build_markdown(tag=TAG_OPEN, title="开仓成功", lines=["组: G-1"])
|
||||
assert "【模拟盘】开仓成功" in md
|
||||
assert "【实盘·OKX】开仓成功" in md
|
||||
assert "`OPEN`" in md
|
||||
assert "组: G-1" in md
|
||||
|
||||
@@ -17,4 +26,78 @@ def test_venue_label_sim(monkeypatch):
|
||||
exchange = "okx"
|
||||
|
||||
monkeypatch.setattr("app.notify.wecom.get_settings", lambda: S())
|
||||
assert venue_label() == "模拟盘"
|
||||
assert venue_label() is None
|
||||
|
||||
|
||||
def test_direction_and_close_reason_zh() -> None:
|
||||
assert "Put" in direction_zh({"option_side": "put", "perp_side": "long"})
|
||||
assert close_reason_zh("liquidity_retry") == "等待流动性后全平"
|
||||
assert close_reason_zh("fixed_usdt") == "固定净盈利达标·双腿全平"
|
||||
|
||||
|
||||
def test_notify_open_close_markdown(monkeypatch) -> None:
|
||||
captured: list[str] = []
|
||||
|
||||
monkeypatch.setattr("app.notify.wecom.wecom_enabled", lambda: True)
|
||||
monkeypatch.setattr("app.notify.wecom.wecom_webhook_url", lambda: "http://example.test")
|
||||
monkeypatch.setattr("app.notify.wecom.venue_label", lambda: None)
|
||||
monkeypatch.setattr("app.notify.wecom.wecom_machine_name", lambda: "")
|
||||
|
||||
def _capture(content: str):
|
||||
captured.append(content)
|
||||
return True, "ok"
|
||||
|
||||
monkeypatch.setattr("app.notify.wecom._post_markdown_sync", _capture)
|
||||
monkeypatch.setattr(
|
||||
"app.notify.wecom.notify_async",
|
||||
lambda content: captured.append(content),
|
||||
)
|
||||
|
||||
notify_open(
|
||||
group_id="G-20260802-01",
|
||||
detail="opened",
|
||||
extra={
|
||||
"bias": "put_ask_gt_call",
|
||||
"option_side": "put",
|
||||
"perp_side": "long",
|
||||
"option_inst_id": "ETH-USD-260802-1850-P",
|
||||
"strike": 1850,
|
||||
"expiry_ymd": "260802",
|
||||
"perp_qty_eth": 1.0,
|
||||
"option_qty_eth": 2.0,
|
||||
"perp_entry_px": 1860.5,
|
||||
"option_entry_px": 12.3,
|
||||
"initial_premium": 24.6,
|
||||
"perp_margin": 620.0,
|
||||
"leverage": 3,
|
||||
},
|
||||
)
|
||||
assert captured
|
||||
open_md = captured[-1]
|
||||
assert "开仓成功" in open_md
|
||||
assert "`OPEN`" in open_md or TAG_OPEN in open_md
|
||||
assert "权利金占用" in open_md
|
||||
assert "保证金占用" in open_md
|
||||
assert "开仓数量" in open_md
|
||||
assert "买Put" in open_md
|
||||
|
||||
captured.clear()
|
||||
notify_close(
|
||||
reason="liquidity_retry",
|
||||
detail="closed",
|
||||
data={
|
||||
"group_id": "G-20260802-01",
|
||||
"perp_pnl": -10.5,
|
||||
"option_pnl": 40.2,
|
||||
"net": 25.0,
|
||||
"fees": 4.5,
|
||||
},
|
||||
)
|
||||
close_md = captured[-1]
|
||||
assert "平仓" in close_md
|
||||
assert TAG_CLOSE in close_md or "`CLOSE`" in close_md
|
||||
assert "等待流动性后全平" in close_md
|
||||
assert "永续盈亏" in close_md
|
||||
assert "期权盈亏" in close_md
|
||||
assert "净利润" in close_md
|
||||
assert "+25.00U" in close_md or "25.00U" in close_md
|
||||
|
||||
@@ -30,7 +30,7 @@ const CLOSE_REASON_ZH: Record<string, string> = {
|
||||
expiry: "到期结算",
|
||||
emergency: "紧急全平",
|
||||
manual: "手动平仓",
|
||||
liquidity_retry: "流动性等待后续平仓",
|
||||
liquidity_retry: "等待流动性后全平",
|
||||
unknown: "未知",
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user