8ecc70a61c
Replace single S* with up/down targets across UI, preview, persist, monitor, and alerts so ranging breakouts can close the winner either way. Co-authored-by: Cursor <cursoragent@cursor.com>
187 lines
6.5 KiB
Python
187 lines
6.5 KiB
Python
"""对冲计划企业微信推送(起止必发,幂等落库标记)."""
|
||
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"🎯 上破:{_fmt(plan.get('target_price_up') or plan.get('target_price'))}"
|
||
f"|下破:{_fmt(plan.get('target_price_down') or 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": "期期已平盈利腿(中间态)",
|
||
"target_up_win_leg": "期期上破·已平盈利腿",
|
||
"target_down_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 "") in (
|
||
"target_win_leg",
|
||
"target_up_win_leg",
|
||
"target_down_win_leg",
|
||
) and (plan.get("status") or "") != "closed":
|
||
side = "上破" if "up" in str(plan.get("close_reason")) else (
|
||
"下破" if "down" in str(plan.get("close_reason")) else "目标价"
|
||
)
|
||
notify_hedge(
|
||
cfg,
|
||
build_hedge_alert_message(
|
||
title=f"期期{side}已平盈利腿,亏损腿继续持有至到期",
|
||
plan_id=plan.get("id"),
|
||
detail=(
|
||
f"上破 {_fmt(plan.get('target_price_up') or plan.get('target_price'))}"
|
||
f"|下破 {_fmt(plan.get('target_price_down') or 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),
|
||
)
|