1. 这套系统到底在解决什么问题散户做量化最头疼的从来不是策略本身而是“策略跑出来了信号怎么及时送到手上”。我身边不少朋友用 Python 写完回测收益曲线画得漂漂亮亮结果实盘的时候还在手动刷新行情软件、手动记买卖点甚至用手机闹钟提醒自己看盘。这种操作方式策略再好也白搭因为执行环节完全脱节。这套“LLM 企微量化推送系统”要干的事情很明确让 Python 策略脚本在本地或服务器上定时跑把计算出来的信号交给大语言模型做一层“人话翻译”和风险提示再通过企业微信的机器人接口推送到手机。整个过程不需要你盯着屏幕也不需要你手动转发消息。它解决的是从策略信号到人工决策之间的最后一公里问题。适合谁来参考如果你已经会写一点 Python能看懂 pandas 的 DataFrame 操作知道什么是 API 调用那这套东西你花一个周末就能搭起来。如果你完全没碰过代码也没关系我会把每一步拆到“复制粘贴就能跑”的程度但你需要有耐心跟着配置环境。这套系统不承诺帮你赚钱它只是一个信息管道和辅助判断工具最终的买卖决策仍然在你手里。提示本文涉及的所有代码和配置均为本地化部署方案不涉及任何自动化下单接口。推送系统只负责“告诉你发生了什么”不负责“替你操作账户”。2. 整体架构设计与技术选型思路2.1 为什么是“LLM 企微”这个组合先拆解一下这个组合的逻辑。量化策略的输出通常是一堆冷冰冰的数字和指标比如“MACD 金叉”“RSI 超卖”“成交量突破 20 日均量”。这些信号直接推送到手机对于非全职交易者来说阅读成本很高。你在地铁上收到一条“000001.SZ 触发买入信号”第一反应可能是“为什么现在什么价位大盘什么情况”——这些上下文信息如果缺失推送就等于噪音。LLM 在这里的角色是信号解释器和上下文补充器。它不参与策略计算只负责把策略输出的结构化数据翻译成一段带逻辑的简短文字并且根据你预设的规则补充风险提示。比如策略输出“RSI 30”LLM 可以生成“当前 RSI 为 28.3处于超卖区间过去 5 次出现该信号后有 3 次在 3 个交易日内出现反弹但当前大盘成交量萎缩需注意流动性风险。”这段话的信息密度比单纯一个数字高得多。企业微信机器人则是推送通道里最稳妥的选择之一。它的 Webhook 接口极其简单一个 POST 请求就能发消息不需要申请复杂的公众号资质也不需要用户安装额外 App。对于个人开发者来说企微机器人的稳定性和到达率远超邮件和短信而且支持 Markdown 格式消息排版清晰。2.2 系统模块拆解整个系统我把它分成四个模块每个模块的职责边界要划清楚不然后期维护会很痛苦。模块名称核心职责技术选型运行位置数据采集层获取行情数据、财务数据AKShare / Tushare本地或云服务器策略计算层指标计算、信号生成pandas / numpy本地或云服务器LLM 解释层信号翻译、风险提示生成本地 LLM 或 API 调用本地优先推送触达层消息格式化、企微推送requests / 企微 Webhook本地或云服务器数据采集层我推荐 AKShare原因是它免费、接口稳定、覆盖 A 股全市场数据而且不需要 token。Tushare 虽然数据质量更高但积分门槛对新手不太友好。策略计算层用 pandas 就够了没必要上 backtrader 或 vnpy 这种重型框架因为我们的目标不是做高频交易而是做日线级别的信号推送。LLM 解释层是这套系统的特色。你可以选择调用云端 API也可以用 Ollama 在本地跑一个 7B 参数的小模型。本地跑的好处是数据不出门坏处是需要一台至少有 8GB 显存的机器。如果只是做文本翻译和简单推理7B 模型完全够用量化信号解释不需要太强的推理能力。推送触达层最简单企微机器人的 Webhook 就是一个 URL用 requests.post 发 JSON 就行。但这里有个细节企微机器人对消息频率有限制每分钟最多 20 条所以推送前要做去重和合并。2.3 为什么强调“开源”和“本地化”这套系统的所有组件都是开源或免费可用的。AKShare 是开源库pandas 是开源库Ollama 是开源项目企微机器人接口是免费开放的。整个链路不依赖任何付费服务你唯一需要投入的是一台能跑 Python 的电脑和一点电费。本地化部署的另一个好处是数据隐私。你的持仓信息、策略逻辑、交易信号这些都属于敏感数据。如果全部走云端 API理论上存在泄露风险。用 Ollama 本地跑 LLM数据全程在你自己机器上流转心里踏实得多。注意即使使用本地 LLM企微推送的内容仍然会经过腾讯的服务器。所以推送内容里不要包含具体的账户号码、持仓数量等敏感信息只推送信号和公开市场数据即可。3. 环境搭建与核心依赖安装3.1 Python 环境配置的避坑指南Python 安装本身没什么难度但版本选择有讲究。我实测下来Python 3.10 或 3.11是最稳的版本。3.12 虽然新但部分量化库的 wheel 包还没跟上安装时容易报编译错误。3.9 及以下版本对新的类型注解支持不好写代码时会有很多不便。安装 Python 时务必勾选“Add Python to PATH”这个选项如果不勾后面在命令行里敲 python 会提示找不到命令。Windows 用户建议直接去 python.org 下载官方安装包不要用微软商店的版本因为商店版本的权限管理比较特殊后期安装某些库时会遇到莫名其妙的权限错误。虚拟环境是必须的。我见过太多人把所有库都装在全局环境里结果不同项目之间依赖冲突最后只能重装系统。用 venv 创建独立环境命令如下python -m venv quant_env # Windows 激活 quant_env\Scripts\activate # macOS / Linux 激活 source quant_env/bin/activate激活后命令行前面会出现(quant_env)标识说明你已经进入虚拟环境。之后所有 pip 安装都只影响这个环境不会污染全局。3.2 核心库安装与版本锁定这套系统需要的核心库不多但版本要锁死避免自动升级导致接口不兼容。我整理了一份经过实测的依赖清单pip install akshare1.12.0 pip install pandas2.1.4 pip install numpy1.26.2 pip install requests2.31.0 pip install schedule1.2.1AKShare 的版本更新非常频繁不同版本之间的接口返回值可能有细微差异。1.12.0 是我测试下来比较稳定的版本获取日线数据和实时行情都没问题。pandas 2.1.4 对 AKShare 返回的 DataFrame 兼容性最好不会出现 SettingWithCopyWarning 满天飞的情况。如果你打算用 Ollama 跑本地 LLM还需要额外安装 ollama 的 Python 客户端pip install ollama0.1.6安装完成后用pip list检查一下所有库的版本是否与上面一致。如果某个库版本不对用pip install 库名版本号强制指定。3.3 Ollama 本地 LLM 部署要点Ollama 的安装很简单去官网下载对应系统的安装包一路下一步就行。安装完成后在命令行里执行ollama pull qwen2.5:7b拉取模型。这里我推荐通义千问的 7B 模型原因是它对中文的理解能力在同等参数规模里属于第一梯队而且对金融文本的处理也比较靠谱。拉取完成后用ollama list确认模型已经存在。然后启动服务ollama serve默认情况下 Ollama 监听 11434 端口。你可以在 Python 里这样测试连接import ollama response ollama.chat( modelqwen2.5:7b, messages[{role: user, content: 用一句话解释什么是RSI指标}] ) print(response[message][content])如果这段代码能正常输出说明本地 LLM 环境已经就绪。如果报连接错误检查一下 Ollama 服务是否在运行以及防火墙是否放行了 11434 端口。提示7B 模型在 16GB 内存的机器上跑起来比较吃力建议至少 32GB 内存。如果机器配置不够可以考虑用 3B 或 1.5B 的小模型虽然解释质量会下降但基本功能不受影响。4. 企微机器人配置与消息通道打通4.1 创建企微群机器人的完整流程企业微信的群机器人创建入口藏得比较深第一次用的人容易找不到。具体路径是打开企业微信客户端进入一个群聊可以自己建一个只有自己的群点击右上角的“...”菜单选择“群机器人”然后点击“添加机器人”。给机器人起个名字比如“量化信号助手”创建完成后会得到一个 Webhook 地址。这个 Webhook 地址的格式是这样的https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx关键的是key后面的那串字符这是机器人的唯一标识。千万不要把这个地址泄露出去任何拿到这个地址的人都可以往你的群里发消息。建议把它存在环境变量里不要硬编码在代码中。import os WEBHOOK_URL os.environ.get(QYWX_WEBHOOK_URL)在 Windows 上设置环境变量可以用setx QYWX_WEBHOOK_URL 你的地址macOS 或 Linux 用export QYWX_WEBHOOK_URL你的地址。设置完成后重启终端生效。4.2 消息格式设计与 Markdown 排版技巧企微机器人支持 text、markdown、image、news 等多种消息类型。对于量化信号推送我强烈推荐用markdown 类型因为它的排版能力最强可以加粗关键信息、用颜色标注涨跌、用列表组织多条信号。企微的 markdown 语法和标准 markdown 略有差异它支持font colorinfo、font colorwarning、font colorcomment三种颜色标签。info 是绿色warning 是橙红色comment 是灰色。利用这三个颜色可以把买入信号标绿、卖出信号标红、中性信息标灰。一条典型的推送消息长这样def build_markdown_message(signals): lines [## 量化信号推送, ] for sig in signals: color info if sig[action] BUY else warning lines.append(f**{sig[code]} {sig[name]}**) lines.append(ffont color\{color}\信号{sig[action]}/font) lines.append(f价格{sig[price]} 涨跌{sig[change]}%) lines.append(f {sig[llm_comment]}) lines.append() return \n.join(lines)这里有个细节企微 markdown 不支持表格语法所以不要试图用表格排版。用列表和引用块组合视觉效果反而更清爽。4.3 推送频率控制与去重逻辑企微机器人每分钟最多接受 20 条消息超过会被限流。对于日线级别的策略一天最多推送几次完全不会触发限制。但如果你做的是分钟级监控就必须做频率控制。我的做法是在推送前加一个去重层用 Redis 或本地 SQLite 记录最近推送过的信号相同标的相同信号在 4 小时内不重复推送。如果没有 Redis用 Python 的字典加时间戳也能实现import time push_cache {} def should_push(signal_key, cooldown14400): now time.time() if signal_key in push_cache: if now - push_cache[signal_key] cooldown: return False push_cache[signal_key] now return True这个简单的缓存机制能过滤掉 90% 的重复推送。实测下来没有去重层的时候手机一天能收到几十条重复消息加了之后清爽很多。5. 策略信号生成与 LLM 解释层实现5.1 用 AKShare 获取行情数据的实操细节AKShare 获取 A 股日线数据的接口是ak.stock_zh_a_hist参数包括股票代码、周期、开始日期、结束日期、复权方式。这里有几个坑要注意第一股票代码不要带前缀。stock_zh_a_hist接受的代码格式是纯数字比如000001而不是sz000001。如果你从其他数据源拿到的代码带前缀需要先做字符串切片。第二复权方式选择qfq前复权。回测和信号计算一定要用前复权数据否则除权除息那天会出现巨大的价格跳空导致指标失真。第三返回的 DataFrame 列名是中文的比如“日期”“开盘”“收盘”“最高”“最低”“成交量”。后续计算前建议重命名为英文避免编码问题import akshare as ak df ak.stock_zh_a_hist( symbol000001, perioddaily, start_date20240101, end_date20241231, adjustqfq ) df df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume }) df[date] pd.to_datetime(df[date]) df df.set_index(date).sort_index()这段代码跑通后你就有了一个干净的日线数据 DataFrame索引是日期列是 OHLCV。5.2 双均线 RSI 策略的信号计算我选一个最简单的策略做示例5 日均线上穿 20 日均线且 RSI 小于 70 时产生买入信号5 日均线下穿 20 日均线且 RSI 大于 30 时产生卖出信号。这个策略逻辑简单不容易过拟合适合做推送系统的演示。import pandas as pd import numpy as np def calculate_signals(df): df df.copy() df[ma5] df[close].rolling(5).mean() df[ma20] df[close].rolling(20).mean() delta df[close].diff() gain delta.where(delta 0, 0).rolling(14).mean() loss (-delta.where(delta 0, 0)).rolling(14).mean() rs gain / loss df[rsi] 100 - (100 / (1 rs)) df[signal] 0 buy_condition (df[ma5] df[ma20]) (df[ma5].shift(1) df[ma20].shift(1)) (df[rsi] 70) sell_condition (df[ma5] df[ma20]) (df[ma5].shift(1) df[ma20].shift(1)) (df[rsi] 30) df.loc[buy_condition, signal] 1 df.loc[sell_condition, signal] -1 return df计算完成后取最后一行看 signal 是否非零。如果是 1说明当天产生了买入信号如果是 -1说明产生了卖出信号。这里要注意信号是基于收盘价计算的所以推送时间应该放在收盘后比如下午 3 点半。5.3 LLM 解释层的 Prompt 设计与输出控制LLM 解释层的输入是策略输出的结构化信号输出是一段人话解释。Prompt 的设计直接决定了输出质量。我试过很多版本最终稳定下来的 Prompt 模板是这样的PROMPT_TEMPLATE 你是一个A股量化信号解释助手。请根据以下信号数据生成一段不超过100字的简短解释。 股票代码{code} 股票名称{name} 当前价格{price} 涨跌幅{change}% 技术指标MA5{ma5}, MA20{ma20}, RSI{rsi} 信号类型{signal_type} 要求 1. 用一句话说明信号含义 2. 用一句话提示当前风险 3. 不要给出买卖建议 4. 不要使用“建议”“应该”等词汇 5. 输出纯文本不要加任何前缀 这个 Prompt 的关键约束是“不要给出买卖建议”。LLM 很容易越界生成“建议买入”这种话。虽然最终决策在人但推送内容里出现明确的买卖建议心理上会有暗示作用。加上这条约束后输出就变成了客观描述。调用 Ollama 的代码import ollama def explain_signal(signal_data): prompt PROMPT_TEMPLATE.format(**signal_data) response ollama.chat( modelqwen2.5:7b, messages[{role: user, content: prompt}], options{temperature: 0.3} ) return response[message][content].strip()temperature 设为 0.3 是为了让输出更稳定不要每次都不一样。实测下来这个温度下生成的解释文本既不会太死板也不会太发散。6. 定时调度与完整推送流程串联6.1 用 schedule 库做定时任务schedule 是一个轻量级的 Python 定时任务库比 cron 更容易上手而且跨平台。安装后你可以这样设置每个交易日收盘后的推送任务import schedule import time from datetime import datetime def job(): now datetime.now() if now.weekday() 5: return run_daily_push() schedule.every().day.at(15:35).do(job) while True: schedule.run_pending() time.sleep(30)这里加了周末判断因为 A 股周末不开盘。15:35 这个时间点是收盘后 5 分钟此时当日行情数据已经稳定AKShare 也能获取到完整的日线数据。如果你不想让脚本一直挂着可以用系统的任务计划程序Windows或 crontabLinux/macOS来触发。Windows 的任务计划程序配置起来稍微麻烦一点但设置一次就不用管了。6.2 完整推送流程的代码串联把前面所有模块串起来主流程是这样的def run_daily_push(): stock_list [000001, 600519, 300750] signals_to_push [] for code in stock_list: df fetch_data(code) df calculate_signals(df) last_row df.iloc[-1] if last_row[signal] ! 0: signal_type 买入信号 if last_row[signal] 1 else 卖出信号 signal_data { code: code, name: get_stock_name(code), price: round(last_row[close], 2), change: round(last_row[pct_change], 2), ma5: round(last_row[ma5], 2), ma20: round(last_row[ma20], 2), rsi: round(last_row[rsi], 2), signal_type: signal_type } signal_data[llm_comment] explain_signal(signal_data) signals_to_push.append(signal_data) if signals_to_push: message build_markdown_message(signals_to_push) send_to_qywx(message)这段代码每天跑一次有信号就推送没信号就静默结束。实测下来一个 10 只股票的池子每天平均产生 1 到 2 条信号推送频率很舒服。6.3 推送效果验证与日志记录推送发出去之后你需要知道到底发成功了没有。企微机器人的接口会返回一个 JSONerrcode为 0 表示成功其他值表示失败。建议把每次推送的结果记录到日志文件里import logging logging.basicConfig( filenamepush.log, levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s ) def send_to_qywx(message): resp requests.post(WEBHOOK_URL, json{ msgtype: markdown, markdown: {content: message} }) result resp.json() if result.get(errcode) 0: logging.info(推送成功) else: logging.error(f推送失败{result})日志文件按天切割保留最近 30 天就够了。这样出问题的时候可以回溯看看是数据获取失败还是推送接口报错。7. 常见问题排查与实战避坑经验7.1 数据获取失败的几种典型情况AKShare 的数据接口偶尔会抽风常见报错包括连接超时、返回空 DataFrame、字段缺失等。我的处理原则是重试三次每次间隔 5 秒三次都失败就跳过这只股票并记录日志。def fetch_data_with_retry(code, retries3): for i in range(retries): try: df ak.stock_zh_a_hist(symbolcode, perioddaily, adjustqfq) if df.empty: raise ValueError(返回空数据) return df except Exception as e: logging.warning(f{code} 第{i1}次获取失败{e}) time.sleep(5) logging.error(f{code} 获取数据失败跳过) return None另外AKShare 的接口在盘后刚收盘那几分钟可能返回不完整的数据所以推送时间不要设得太早。15:35 是一个比较稳妥的时间点我实测下来这个时间数据已经完整了。7.2 LLM 输出不稳定的调优方法本地小模型有时候会“胡言乱语”比如把 RSI 28 说成“RSI 处于高位”或者生成一段和信号无关的废话。遇到这种情况先从三个方面排查第一检查 Prompt 是否足够明确。模糊的指令会导致模糊的输出。把“请解释这个信号”改成“请用一句话说明 MA5 上穿 MA20 的含义”输出质量会明显提升。第二降低 temperature。0.3 还是不稳定的话降到 0.1 甚至 0。温度越低输出越确定。第三换模型。7B 模型在中文金融文本上的表现参差不齐如果 qwen2.5 效果不好可以试试 glm4 或 baichuan2。Ollama 支持一键切换模型成本很低。还有一个技巧是加 few-shot 示例。在 Prompt 里给一两个输入输出示例模型会模仿示例的风格和格式。比如示例输入MA510.2, MA2010.0, RSI55, 信号买入 示例输出5日均线上穿20日均线形成金叉RSI处于中性区间短期动能转强但需关注成交量是否配合。加上示例后输出的稳定性和格式一致性会好很多。7.3 企微推送被限流或屏蔽的处理企微机器人被限流通常有两个原因频率过高或内容触发风控。频率问题前面说了加去重层就能解决。内容风控比较麻烦如果你的推送消息里包含“涨停”“跌停”“暴涨”“暴跌”等敏感词可能会被拦截。我的做法是在推送前做一次敏感词过滤把可能触发风控的词汇替换成中性表达SENSITIVE_WORDS { 暴涨: 大幅上涨, 暴跌: 大幅下跌, 涨停: 达到涨幅上限, 跌停: 达到跌幅上限 } def filter_sensitive(text): for word, replacement in SENSITIVE_WORDS.items(): text text.replace(word, replacement) return text这个过滤层加上之后推送被拦截的概率大幅下降。另外消息内容里不要包含外部链接企微对带链接的消息审核更严格。7.4 常见问题速查表问题现象可能原因排查方法解决方案推送收不到Webhook 地址错误用 curl 手动发一条测试消息重新复制 Webhook 地址数据为空AKShare 接口变更打印原始 DataFrame 看列名升级 AKShare 到最新版LLM 无响应Ollama 服务未启动执行 ollama list 检查重启 ollama serve信号重复推送去重逻辑失效检查缓存字典是否被重置改用 SQLite 持久化缓存推送内容乱码编码问题检查字符串是否 UTF-8在请求头加 charsetutf-8脚本中途崩溃未捕获异常查看日志文件最后几行加 try-except 包裹主流程这张表是我踩坑之后总结出来的基本上覆盖了 90% 的常见问题。遇到新问题的时候先看日志日志里通常有足够的线索定位原因。8. 系统扩展方向与个人实操体会这套系统跑通之后扩展空间其实很大。比如你可以把单策略扩展成多策略并行每个策略独立计算信号然后在推送层做合并。也可以把日线级别扩展到 30 分钟级别但要注意 AKShare 的分钟级数据接口不太稳定可能需要换数据源。另一个扩展方向是加一个简单的 Web 界面用 Streamlit 或 Gradio 快速搭一个看板展示历史信号和推送记录。这样你不仅能在手机上看推送还能在电脑上回溯分析。我在实际使用中最大的体会是推送系统的价值不在于信号有多准而在于让你保持对市场的感知。以前我可能一周都不看一次盘现在每天收盘后收到一两条信号推送顺便就了解了持仓股票的技术面变化。这种“轻量级触达”比强迫自己每天复盘要可持续得多。还有一个小技巧在推送消息的末尾加一行“回复任意内容可查看当前持仓状态”然后配合企微机器人的回调接口做一个简单的交互查询。虽然实现起来要多写几十行代码但用起来会方便很多。不过这个功能涉及到消息接收需要配置企微应用而不是群机器人复杂度会上升一个台阶建议先把推送跑稳了再考虑。
