Enrich WeCom open/close notifies with Chinese qty, margin, and PnL.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+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))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user