接 Telegram 时查出 Guard.realized() 定义了但全仓库没有调用点——pnl_day 恒为 0,MAX_DAY_LOSS 完全不生效。三条硬约束里最重要的一条是死的。 根因是止损与止盈都挂在交易所侧成交,本进程收不到通知,而 sweep 只在 48 分钟到点才查持仓。连带第二个后果:execs 条目不清,MAX_OPEN 把已出场的 仓位继续算在场,新信号被白挡到截止时刻。 补 watch() 循环(10s):持仓消失即判出场,去 history-position 取 netProfit(= pnl + 资金费 + 开平手续费)记回闸并释放名额。盈亏取交易所的 数而不自己按标记价估——估会漏掉费用且方向总偏乐观。历史未落库时留到下轮, 不会漏记。字段名按文档与官方 TS 类型的差异同时兼容 ctime/cTime。 Telegram(live/tg.py,stdlib + aiohttp):推开仓、平仓带已实现盈亏、被硬 约束挡住、报错、对账平仓、跨日结算、启动与停机。不推信号过期跳过(常态, 搬运重连会重放旧信号)与心跳,否则真事会被淹掉。启动那条兼作通道自检。 研究侧 tg_notify.send 改为复用生产的传输层,方向与 signal_bus 一致。 status.sh 增加一条判读:开过仓但 pnl 仍为 0 就是 watch() 出了问题。 实测:8 类消息渲染、_match_hist 的过早/方向不符/币不符/驼峰字段/取最近 五种情形、100 USDT 下 SOL 与 ADA 的端到端空跑(两腿等量,50/50 精确)。 Co-authored-by: Cursor <cursoragent@cursor.com>
113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""生产侧 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')}")
|