"""生产侧 Telegram 通知。只用标准库 + aiohttp。 ## 推什么、不推什么 推送的价值在于**你会因此做点什么**。按这个标准筛: 推 开仓 能立刻眼看一遍方向/价位对不对 推 平仓 带已实现盈亏(含手续费与资金费),这是唯一的真账 推 闸拦截 仅 MAX_OPEN / MAX_DAY / MAX_DAY_LOSS —— 说明有 bug 或策略在流血 推 报错、对账平仓、跨日结算 不推 信号过期跳过 这是常态(重连重放会带上旧信号),推了就淹掉真事 不推 心跳 日志里有,推了每天 288 条 量级:信号 6.8 个/天、日开仓上限 15,所以最多约 30 条/天。 ## 失败一律只打日志 推送挂了不能连坐交易。所有异常在这里吞掉——上层不该因为 Telegram 抽风而 影响下单或平仓。 """ from __future__ import annotations import os import time TOKEN = os.environ.get("TG_TOKEN", "") CHAT = os.environ.get("TG_CHAT", "") ENABLED = bool(TOKEN and CHAT) # 生产机上给这条推送打个前缀,免得和采集机推的手工信号混在一个对话里分不清 TAG = os.environ.get("TG_TAG", "实盘") async def send(text: str) -> None: """推一条。任何失败都只打日志——推送挂了不能连坐交易。""" if not ENABLED: return try: import aiohttp url = f"https://api.telegram.org/bot{TOKEN}/sendMessage" async with aiohttp.ClientSession() as s: async with s.post(url, json={"chat_id": CHAT, "text": text}, timeout=aiohttp.ClientTimeout(total=10)) as r: if r.status != 200: body = (await r.text())[:200] print(f" [TG] 推送失败 HTTP {r.status} {body}", flush=True) except Exception as e: # noqa: BLE001 print(f" [TG] 推送异常 {type(e).__name__}: {e}", flush=True) def _fmt(px: float) -> str: """按量级选小数位。跨币种从 79000 到 0.087,固定位数会难读或丢精度。""" a = abs(px) if a >= 1000: return f"{px:.1f}" if a >= 10: return f"{px:.3f}" if a >= 1: return f"{px:.4f}" return f"{px:.6f}" async def opened(sym: str, side: str, entry: float, atr_pct: float, notional: float, leverage: int, stop_px: str, legs: list[dict], age_s: float) -> None: arrow = "多" if side == "LONG" else "空" lines = [f"[{TAG}] 开仓 {sym} {arrow}", f"参考价 {_fmt(entry)} · ATR {atr_pct * 1e4:.1f}bp", f"名义 {notional:.0f} USDT · {leverage}x · " f"保证金 {notional / leverage:.0f} USDT", f"止损 {stop_px}(两腿共用,不移动)"] for lg in legs: lines.append(f" {lg['tag']:<6} {lg['size']} 币 · 止盈 {lg['tp_px']}") lines.append(f"距参考价成立 {age_s:.1f}s") await send("\n".join(lines)) async def closed(sym: str, side: str, pnl: float, open_px: float, close_px: float, day_pnl: float, day_n: int, reason: str = "") -> None: arrow = "多" if side == "LONG" else "空" mark = "盈" if pnl > 0 else ("亏" if pnl < 0 else "平") tail = f" · {reason}" if reason else "" await send(f"[{TAG}] 平仓 {sym} {arrow} {mark} {pnl:+.2f} USDT{tail}\n" f"开 {_fmt(open_px)} → 平 {_fmt(close_px)}\n" f"当日 {day_n} 笔 · 累计 {day_pnl:+.2f} USDT") async def blocked(key: str, why: str) -> None: """只在被硬约束挡住时推。过期跳过属于常态,不走这里。""" await send(f"[{TAG}] ⛔ 信号被挡 {key}\n{why}") async def error(what: str, detail: str) -> None: await send(f"[{TAG}] ⛔ {what}\n{detail[:500]}") async def started(notional: float, leverage: int, syms: int, dry: bool) -> None: mode = "空跑(不下真单)" if dry else "真跑" await send(f"[{TAG}] 执行器启动 · {mode}\n" f"名义 {notional:.0f} USDT · {leverage}x · {syms} 币") async def day_rolled(day: str, n: int, pnl: float) -> None: await send(f"[{TAG}] {day} 结算 · {n} 笔 · {pnl:+.2f} USDT") async def stopping(n_open: int) -> None: await send(f"[{TAG}] 收到停机信号,平掉在场 {n_open} 笔后退出。" f"\n注意:停机后不再有超时平仓与新开仓。" f"发生时间 {time.strftime('%H:%M:%S')}")