概述

策略上线后,最朴素的诉求就是"出事的时候手机能响一下"。邮件方案在 邮件推送 里讲过了,但邮件有个问题:到达手机的即时性依赖邮箱 App 的推送策略,有时会延迟几分钟甚至几十分钟。而微信几乎是国人打开频率最高的 App,把告警直接送到微信,体验会好很多。

本文整理 QMT 策略中可用的几条微信推送通道——Server酱、企业微信群机器人、WxPusher、PushPlus,给出统一封装的工具类和 QMT 集成示例,并和 运行状态监控与心跳报警 配合,构成完整的告警链路。

一、方案选型

方案触达位置注册门槛频率限制适合场景
Server酱个人微信「Server酱Turbo」应用扫码登录拿 SendKey免费版 5 条/天,Turbo 200 条/天个人单策略、低频告警
企业微信群机器人企业微信群建群+添加机器人20 条/分钟多策略集中告警、团队协作
WxPusher个人微信「WxPusher」公众号注册+关注公众号免费版较宽松一对多推送、带 UI 管理
PushPlus个人微信「PushPlus推送加」公众号注册+关注公众号免费版 200 条/天个人推送、支持模板

选型建议:个人单策略选 Server酱或 PushPlus,多策略/团队选企业微信群机器人。企业微信群机器人不需要个人开通企业微信会员,建个只有自己的群也能用。

二、Server酱:最简单的方案

Server酱(sct.ftqq.com)走的是「一个 HTTP 请求 = 一条微信消息」的模式,整个接入只需要一个 SendKey。

步骤

  1. 访问 sct.ftqq.com,微信扫码登录
  2. 在「SendKey」页面拿到 SCT**** 开头的 key
  3. 在「微信推送」页面扫码绑定要接收消息的微信号
  4. 调用接口

代码

# wechat_utils.py
# -*- coding: utf-8 -*-
import requests

SERVERCHAN_KEY = "SCT****your_send_key****"

def send_serverchan(title, content=""):
    """
    通过 Server酱 推送到微信
    :param title: 消息标题(必填,最长 32)
    :param content: 消息内容,支持 Markdown
    :return: 是否发送成功
    """
    url = f"https://sctapi.ftqq.com/{SERVERCHAN_KEY}.send"
    try:
        r = requests.post(url, data={"title": title, "desp": content}, timeout=10)
        return r.json().get("code") == 0
    except Exception as e:
        print(f"Server酱推送失败: {e}")
        return False

Server酱免费版每天 5 条,对监控告警来说偏少。要么升级 Turbo 版,要么配合下面的节流策略只用它发关键告警。

三、企业微信群机器人:多策略群告警

企业微信群机器人不需要企业认证,建一个群、加个机器人就能拿到 Webhook URL,最适合把多个策略的告警集中到一个群里。

步骤

  1. 在企业微信里建一个群(自己一个人的群也行)
  2. 群设置 → 群机器人 → 添加机器人 → 起个名字
  3. 复制 Webhook 地址,形如 https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx

文本消息

import requests

WECOM_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key"

def send_wecom_text(content, mentioned_list=None):
    """
    发送文本消息
    :param content: 文本内容
    :param mentioned_list: 需要@的用户ID列表,['@all'] 表示@所有人
    """
    data = {
        "msgtype": "text",
        "text": {
            "content": content,
            "mentioned_list": mentioned_list or [],
            "mentioned_mobile_list": []
        }
    }
    try:
        r = requests.post(WECOM_WEBHOOK, json=data, timeout=10)
        return r.json().get("errcode") == 0
    except Exception as e:
        print(f"企业微信推送失败: {e}")
        return False

Markdown 消息

告警内容用 Markdown 排版会清晰很多:

def send_wecom_markdown(content):
    """
    发送 Markdown 消息(企业微信群机器人支持有限 Markdown 语法)
    """
    data = {
        "msgtype": "markdown",
        "markdown": {"content": content}
    }
    try:
        r = requests.post(WECOM_WEBHOOK, json=data, timeout=10)
        return r.json().get("errcode") == 0
    except Exception as e:
        print(f"企业微信推送失败: {e}")
        return False

# 使用示例
msg = """## QMT 告警
> **策略**: 双均线
> **账号**: 600000
> **时间**: 2026-08-04 14:30

**事件**: 账号掉线
<font color="warning">请及时检查</font>"""
send_wecom_markdown(msg)

企业微信群机器人支持 <font color="info|comment|warning"> 给文字上色,warning 是橙红色,比较醒目。

四、WxPusher / PushPlus

两者都是「关注公众号 → 注册拿 token → HTTP 调接口」的模式,用法和 Server酱 类似,只是接口参数不同。

WxPusher

WXPUSHER_TOKEN = "your_app_token"

def send_wxpusher(title, content):
    url = "https://wxpusher.????.com/api/send/message"
    data = {
        "appToken": WXPUSHER_TOKEN,
        "content": content,
        "summary": title,             # 消息摘要(必填,否则推送列表里显示空白)
        "contentType": 1,             # 1=文本, 2=html, 3=markdown
        "topicIds": [],               # 主题ID,可留空
        "uids": ["your_uid"]          # 目标用户UID,在公众号「我的」里查
    }
    try:
        r = requests.post(url, json=data, timeout=10)
        return r.json().get("success")
    except Exception as e:
        print(f"WxPusher 推送失败: {e}")
        return False

PushPlus

PUSHPLUS_TOKEN = "your_token"

def send_pushplus(title, content, template="html"):
    url = "http://www.pushplus.plus/send"
    data = {
        "token": PUSHPLUS_TOKEN,
        "title": title,
        "content": content,
        "template": template           # html / json / markdown
    }
    try:
        r = requests.post(url, json=data, timeout=10)
        return r.json().get("code") == 200
    except Exception as e:
        print(f"PushPlus 推送失败: {e}")
        return False

五、统一封装:notify.py

不同渠道接口不同,但策略代码只想关心「发条消息」。封装一个统一的 notify,支持渠道切换、节流、容错:

# notify.py
# -*- coding: utf-8 -*-
import time
import threading

class Notifier:
    def __init__(self, default_channel="serverchan"):
        self.default_channel = default_channel
        self._last = {}                # (channel, key) -> ts
        self._lock = threading.Lock()

    def _throttle(self, channel, key, cooldown=600):
        """同一类消息 cooldown 秒内只发一次,避免刷屏"""
        k = (channel, key)
        now = time.time()
        with self._lock:
            if now - self._last.get(k, 0) < cooldown:
                return False
            self._last[k] = now
            return True

    def send(self, title, content="", channel=None, key=None, cooldown=600):
        """
        发送消息
        :param title: 标题
        :param content: 内容
        :param channel: 渠道,默认 self.default_channel
        :param key: 节流键,默认用 title;同一 key 在 cooldown 内只发一次
        :param cooldown: 节流秒数,默认 600 秒
        """
        channel = channel or self.default_channel
        key = key or title
        if not self._throttle(channel, key, cooldown):
            print(f"[notify] 节流命中,跳过: {title}")
            return False

        try:
            if channel == "serverchan":
                return send_serverchan(title, content)
            elif channel == "wecom":
                return send_wecom_text(f"{title}\n{content}")
            elif channel == "wecom_md":
                return send_wecom_markdown(f"## {title}\n{content}")
            elif channel == "wxpusher":
                return send_wxpusher(title, content)
            elif channel == "pushplus":
                return send_pushplus(title, content)
            else:
                print(f"[notify] 未知渠道: {channel}")
                return False
        except Exception as e:
            # 告警本身失败不能影响策略
            print(f"[notify] 发送异常: {e}")
            return False

notifier = Notifier(default_channel="serverchan")

def notify(title, content="", **kwargs):
    """供策略调用的统一入口"""
    return notifier.send(title, content, **kwargs)

把 notify.py 放到 QMT 安装目录的 python 文件夹下,所有策略都能引用,路径配置见 公共代码的编写与引用。

六、在 QMT 策略中集成

示例 1:策略启动通知

from notify import notify

def init(C):
    C.set_account(account)
    # 开盘前推送一条,确认策略已加载
    notify("QMT 策略启动", f"账号 {account} 策略已启动")

示例 2:成交回调通知

成交是发通知的最佳时机——不是"下单了"就发,而是"真成交了"才发,避免被废单打扰。成交回调用法详见 成交回调:

from notify import notify

def deal_callback(C, dealInfo):
    code = dealInfo.m_strInstrumentID
    if code.startswith('6'):
        code += '.SH'
    else:
        code += '.SZ'
    side = '买入' if dealInfo.m_nOffset == 48 else '卖出'
    msg = f"{side} {code}\n价格 {dealInfo.m_dPrice}\n数量 {dealInfo.m_nVolume}"
    notify("成交回报", msg)

示例 3:异常巡检告警

和 心跳报警 的巡检结合,发现异常立刻推微信:

def patrol(C):
    problems = []
    # ... 巡检逻辑,发现异常追加到 problems ...
    if problems:
        notify("QMT巡检异常", "\n".join(problems),
               channel="wecom_md", key="patrol", cooldown=600)

这里 key="patrol" 把所有巡检异常归并到同一个节流桶,10 分钟内不会因为同一个巡检任务刷屏。不同问题想分开告警,就给不同的 key。

示例 4:收盘日报

def init(C):
    # 每个交易日 15:05 推送日报
    C.run_time('daily_report', '1nDay', '20260101 15:05:00')

def daily_report(C):
    acc = get_trade_detail_data(account, accountType, 'account')[0]
    holdings = get_trade_detail_data(account, accountType, 'position')
    md = f"## 收盘报告\n> 账号 {account}\n\n"
    md += f"- 可用资金: {acc.m_dAvailable:.2f}\n"
    md += f"- 持仓数: {len(holdings)}\n\n"
    for h in holdings:
        md += f"- {h.m_strInstrumentID}: {h.m_nVolume}股 浮盈{h.m_dProfit:.2f}\n"
    notify("收盘报告", md, channel="wecom_md", cooldown=0)  # 日报不节流

run_time 定时任务的写法见 每天代码重置与定时任务。

七、常见坑

  1. QMT 内置 Python 缺 requests:QMT 自带的 Python 环境通常已包含 requests,但版本可能较旧。若提示 No module named 'requests',按 安装第三方库 装一下,或用标准库 urllib 改写。
  2. 超时阻塞策略:HTTP 请求默认会阻塞,务必传 timeout=10,并放在 run_time 任务或回调里发,别放 handlebar 高频路径。
  3. 频率超限:Server酱免费版 5 条/天,企业微信 20 条/分钟。监控类告警务必节流,否则一天的额度几分钟就刷光。
  4. 中文编码:JSON body 用 json= 传参会自动处理;用 data= 传 form 时注意 requests 会按 utf-8 编码,一般没问题,但日志里打印 content 别用 gbk 控制台,会报错。
  5. 告警本身失败:所有推送函数都要 try/except 兜底,告警挂了不能拖垮策略。notify 已经做了这层保护。
  6. 关键告警走多通道:账号掉线、重复下单风险这种关键告警,建议同时发 Server酱(个人微信)和企业微信群,避免单通道故障漏报。

八、和邮件方案的关系

邮件(邮件推送)和微信不是二选一,而是互补:

  • 微信:即时性强、阅读率高,适合异常告警、成交回报、关键事件。
  • 邮件:留档性好、可带附件、可发长报告,适合日报、周报、详细日志。

推荐组合:实时事件走微信,日报兜底走邮件。notify 封装里再加一个 email 渠道,调用 email_utils.send_qmt_email 即可统一入口。

总结

把消息推到微信,QMT 策略就有了"会叫的watchdog":

  • 个人单策略用 Server酱,多策略/团队用 企业微信群机器人,需要一对多用 WxPusher/PushPlus。
  • 统一封装到 notify.py,支持渠道切换、节流、容错。
  • 集成到 init(启动通知)、deal_callback(成交回报)、patrol(巡检告警)、daily_report(收盘日报)四个时机。
  • 关键告警走多通道,实时走微信、留档走邮件。

配合 运行状态监控与心跳报警 的心跳/巡检/watchdog体系,整套 QMT 运维闭环就齐了——出事手机能响,没事每天一报,安心睡觉。


说明:本文包含 AI 创作内容,请自行判断是否适用。文中包含的代码片段,切勿直接使用,需要根据实际情况修改。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐