Add hedge-plan WeChat start/end alerts and expiry settlement.
Wire idempotent notify on open/close/partial fail, settle OO at expiry, and close orphaned TP option legs without rewriting plan totals. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"""对冲计划企业微信推送(起止必发,幂等落库标记)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from lib.hedge_plan.hedge_plan_db import update_plan
|
||||
|
||||
|
||||
def _fmt(v: Any, d: int = 2) -> str:
|
||||
try:
|
||||
if v is None or v == "":
|
||||
return "—"
|
||||
return f"{float(v):.{d}f}"
|
||||
except (TypeError, ValueError):
|
||||
return str(v)
|
||||
|
||||
|
||||
def _type_label(plan_type: str) -> str:
|
||||
return "永期对冲" if (plan_type or "") == "perp_options" else "期期对冲"
|
||||
|
||||
|
||||
def _dir_label(direction: str) -> str:
|
||||
d = (direction or "").lower()
|
||||
if d == "long":
|
||||
return "做多"
|
||||
if d == "short":
|
||||
return "做空"
|
||||
return "—"
|
||||
|
||||
|
||||
def build_hedge_start_message(plan: dict[str, Any], *, legs: Optional[list[dict[str, Any]]] = None) -> str:
|
||||
pt = plan.get("plan_type") or ""
|
||||
lines = [
|
||||
f"🟢 对冲计划启动 #{plan.get('id')}",
|
||||
f"📌 类型:{_type_label(pt)}",
|
||||
f"🪙 标的:{plan.get('underlying') or '—'}",
|
||||
]
|
||||
if pt == "perp_options":
|
||||
lines.extend(
|
||||
[
|
||||
f"📈 方向:{_dir_label(plan.get('direction') or '')}",
|
||||
f"💵 开仓参考:{_fmt(plan.get('entry_mark'))}",
|
||||
f"🎯 止盈:{_fmt(plan.get('tp'))}|止损:{_fmt(plan.get('sl'))}",
|
||||
f"📦 永续张数:{_fmt(plan.get('perp_size'), 4)}|杠杆:{_fmt(plan.get('leverage'), 0)}x",
|
||||
f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC",
|
||||
]
|
||||
)
|
||||
else:
|
||||
lines.extend(
|
||||
[
|
||||
f"🎯 目标价 S*:{_fmt(plan.get('target_price'))}",
|
||||
f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC",
|
||||
]
|
||||
)
|
||||
if legs:
|
||||
for leg in legs:
|
||||
role = leg.get("leg_role") or ""
|
||||
if role == "perp":
|
||||
lines.append(f"· 永续腿 {leg.get('symbol') or ''} ×{_fmt(leg.get('size'), 4)}")
|
||||
else:
|
||||
lines.append(
|
||||
f"· {role} {(leg.get('opt_type') or '')} K{_fmt(leg.get('strike'), 0)} "
|
||||
f"×{_fmt(leg.get('size'), 0)}张 {leg.get('inst_id') or ''}"
|
||||
)
|
||||
lines.append("📎 独立模块推送,不进普通交易复盘")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_hedge_end_message(plan: dict[str, Any]) -> str:
|
||||
reason = plan.get("close_reason") or "—"
|
||||
total = plan.get("realized_pnl_total")
|
||||
try:
|
||||
tv = float(total) if total is not None else None
|
||||
except (TypeError, ValueError):
|
||||
tv = None
|
||||
head = "🔴" if (tv is not None and tv < 0) else "🟢"
|
||||
reason_map = {
|
||||
"perp_tp": "永续止盈(期权默认不平)",
|
||||
"perp_sl": "永续止损(期权强制平)",
|
||||
"target_win_leg": "期期已平盈利腿(中间态)",
|
||||
"oo_expiry_loss": "期期到期无盈利·总亏损",
|
||||
"oo_expiry_win": "期期到期仍盈利",
|
||||
"expiry": "到期收口",
|
||||
"manual": "人工结束",
|
||||
"partial_fail": "半腿失败收尾",
|
||||
"cancelled": "已取消",
|
||||
}
|
||||
lines = [
|
||||
f"{head} 对冲计划结束 #{plan.get('id')}",
|
||||
f"📌 类型:{_type_label(plan.get('plan_type') or '')}",
|
||||
f"🪙 标的:{plan.get('underlying') or '—'}",
|
||||
f"📎 原因:{reason_map.get(reason, reason)}",
|
||||
f"💰 合计≈U:{_fmt(total)}",
|
||||
f"· 永续分项:{_fmt(plan.get('realized_pnl_perp'))} USDT",
|
||||
f"· 期权分项:{_fmt(plan.get('realized_pnl_options'))} USDC(≈U 1:1)",
|
||||
f"⏱ 开仓:{plan.get('opened_at') or '—'}|结束:{plan.get('closed_at') or '—'}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_hedge_alert_message(
|
||||
*,
|
||||
title: str,
|
||||
plan_id: Any = None,
|
||||
detail: str = "",
|
||||
) -> str:
|
||||
lines = [f"⚠️ 对冲计划告警{(' #' + str(plan_id)) if plan_id else ''}", f"📌 {title}"]
|
||||
if detail:
|
||||
lines.append(str(detail)[:800])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def notify_hedge(
|
||||
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 notify_plan_start(
|
||||
cfg: dict[str, Any],
|
||||
conn: Any,
|
||||
plan: dict[str, Any],
|
||||
legs: Optional[list[dict[str, Any]]] = None,
|
||||
) -> bool:
|
||||
if int(plan.get("wechat_start_sent") or 0):
|
||||
return False
|
||||
ok = notify_hedge(cfg, build_hedge_start_message(plan, legs=legs))
|
||||
if ok and plan.get("id") is not None:
|
||||
update_plan(conn, int(plan["id"]), wechat_start_sent=1)
|
||||
plan["wechat_start_sent"] = 1
|
||||
return ok
|
||||
|
||||
|
||||
def notify_plan_end(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> bool:
|
||||
if int(plan.get("wechat_end_sent") or 0):
|
||||
return False
|
||||
# 中间态 target_win_leg 不算正式结束推送(用告警)
|
||||
if (plan.get("close_reason") or "") == "target_win_leg" and (plan.get("status") or "") != "closed":
|
||||
notify_hedge(
|
||||
cfg,
|
||||
build_hedge_alert_message(
|
||||
title="期期已平盈利腿,亏损腿继续持有至到期",
|
||||
plan_id=plan.get("id"),
|
||||
detail=f"目标价 {_fmt(plan.get('target_price'))}",
|
||||
),
|
||||
)
|
||||
return True
|
||||
ok = notify_hedge(cfg, build_hedge_end_message(plan))
|
||||
if ok and plan.get("id") is not None:
|
||||
update_plan(conn, int(plan["id"]), wechat_end_sent=1)
|
||||
plan["wechat_end_sent"] = 1
|
||||
return ok
|
||||
|
||||
|
||||
def notify_partial_fail(cfg: dict[str, Any], *, plan_type: str, msg: str, results: Any = None) -> bool:
|
||||
detail = msg
|
||||
if results:
|
||||
try:
|
||||
detail = f"{msg}\n路径结果:{results}"[:800]
|
||||
except Exception:
|
||||
pass
|
||||
return notify_hedge(
|
||||
cfg,
|
||||
build_hedge_alert_message(title=f"{_type_label(plan_type)}半腿失败", detail=detail),
|
||||
)
|
||||
Reference in New Issue
Block a user