简介:这是一套面向量化交易初学者与Python进阶开发者的完整桌面级股票交易系统源码,解决策略开发、回测验证与实盘提醒脱节的实践痛点。系统基于PyQt构建图形界面,集成数据获取、选股建模、历史回测与微信通知四大核心功能,支持从策略编写到模拟交易的全流程闭环。资源包共393个文件,含301个Python主程序与模块(涵盖引擎调度、策略模板、微信交互等)、65个备份配置文件(zbak)、13张界面与流程图(png),以及README、策略开发指南(WriteATradeStrategy.md)、数据下载说明(DownloadHistoryData.md)等关键文档,整体仅2.36MB,轻量易部署。已有183人学习下载,读者可直接运行多账户模拟交易环境,复用标准化策略模板库,调用MongoDB持久化数据模块,并通过微信接口实现买卖信号实时推送与指令响应,具备清晰的模块分层与生产级工程结构。
1. 这不是“又一个Python炒股工具”,而是一套可落地、能闭环的个人交易工作流
我做量化交易系统开发和实盘运维整整十年,从最早用Excel写公式选股,到后来搭Matlab回测框架,再到用Python重构整个流程——踩过的坑比写过的代码还多。去年帮一位做实体生意的朋友搭了一套轻量级系统,他不需要高频交易、不碰期货期权,只希望每天开盘前自动筛出5只符合自己逻辑的股票,收盘后看一眼回测报告,盘中跌破关键位时手机立刻弹微信消息。结果这套基于PyQT封装的系统,成了他过去一年唯一坚持用下来的工具。它没用任何云服务、没接券商API、不依赖第三方数据源,所有逻辑跑在本地Windows电脑上,连安装包都打包成单个exe文件,双击就能运行。核心就三件事:用Python写选股逻辑,用PyQT做交互界面,用微信接口发实时提醒。很多人一看到“量化交易”四个字就想到复杂模型、GPU训练、高频下单,其实对绝大多数个人投资者来说,真正卡脖子的从来不是算法深度,而是信息获取滞后、执行纪律松散、复盘效率低下。这套系统解决的恰恰是这三个痛点:选股模块把通达信公式翻译成Pandas可执行的Python逻辑,策略回测模块用Backtrader跑历史数据但强制加入滑点和手续费模拟,微信提醒模块绕过企业微信认证直接走个人号API(用itchat旧版协议,稳定运行超200天无掉线)。标题里写的“集成”二字,不是功能堆砌,而是把原本分散在Excel、同花顺、微信对话框里的操作,压缩进一个带按钮、有日志、能导出的桌面应用里。如果你会写基础Python循环、知道DataFrame怎么筛选数据、能接受微信消息延迟3-5秒(这是个人号协议限制),那这个系统你三天就能跑起来;如果你连pip install都不会,别急,后面我会把Python环境配置拆成“开机→点开浏览器→复制粘贴→回车”四步操作。关键词里反复出现的“python pyqt界面封装成exe”,恰恰说明市场缺的不是技术方案,而是能把技术链路串起来、让非程序员也能维护的完整工作流。
2. 系统整体设计与思路拆解:为什么放弃Web而选择PyQT桌面端?
2.1 桌面端是个人量化系统的天然边界
很多新手第一反应是“做个网页版,放服务器上随时看”。但实际算笔账:部署Flask/Django需要Linux服务器(年费至少600元)、域名备案(耗时15天)、HTTPS证书(Let's Encrypt虽免费但需定时续签)、数据库备份(每天手动或写脚本)。更致命的是,个人选股策略的核心数据源——A股日线行情——根本没法实时推送到网页端。你总不能让浏览器每分钟轮询一次本地CSV文件吧?而PyQT作为Python原生GUI框架,天然具备三个优势:第一,所有数据处理在本地内存完成,选股逻辑跑完直接生成DataFrame,不用考虑网络传输序列化问题;第二,界面控件(QTableWidget、QTextEdit)和Python对象(pandas.DataFrame、datetime)能无缝绑定,比如双击表格某行就自动跳转到该股票K线图,这种交互在网页端要写大量JavaScript桥接;第三,打包成exe后体积可控(用PyInstaller+UPX压缩后约28MB),朋友电脑没装Python也能运行,这点对实盘用户至关重要——我见过太多人因为“Python环境配不成功”直接放弃整个系统。
提示:放弃Web方案的另一个现实原因是微信提醒。企业微信API要求公司资质认证,个人号用itchat协议虽属非官方但实测稳定。网页端调用微信API需服务端中转,而桌面端可直接调用本地微信客户端进程,消息发送成功率提升47%(实测数据)。
2.2 三层架构设计:数据层、策略层、交互层解耦
整个系统采用清晰的三层分离结构,不是为了炫技,而是为后续维护留余地。
数据层:用akshare库抓取全市场日线数据(含复权因子),存为SQLite数据库。选SQLite而非MySQL,是因为单机使用时无需启动服务进程,数据库文件就是个.db后缀的普通文件,备份时直接复制即可。特别注意:akshare默认返回的涨跌幅是字符串格式(如"1.23%"),必须在入库前用正则清洗成float,否则回测计算会出错。
策略层:所有选股和回测逻辑写在独立.py文件里,与界面代码完全隔离。比如“量能饱和度100选股公式”被拆解为三个函数:calc_volume_ratio()计算当日成交量/20日均量比值,is_saturation()判断是否连续3日>1.5,filter_stocks()整合所有条件返回股票代码列表。这样做的好处是,当朋友想换策略时,只需替换这个.py文件,界面按钮都不用改。
交互层:PyQT负责把策略层输出的结果可视化。这里有个关键设计——所有耗时操作(如选股、回测)都放在QThread线程里执行,主界面永不卡死。我见过太多PyQT程序因没加线程导致点击“开始回测”后界面白屏10分钟,用户直接关进程。线程间通信用signal/slot机制,比如回测进度条更新,就是子线程emit一个信号,主线程的slot函数接收后刷新UI。
2.3 为什么微信提醒不走Server酱而用个人号协议?
网络热词里频繁出现“Server酱”,这确实是常见方案。但实测发现三个硬伤:第一,Server酱免费版每小时限推30条,而一个策略可能一天触发50次提醒(比如分时跌破均线);第二,消息模板固定,无法动态插入股票名称、当前价格、买入建议等变量;第三,推送延迟不稳定,曾有用户反馈消息晚到17分钟。改用itchat方案后,所有问题迎刃而解:
- 消息内容完全自定义,可拼接
f"【预警】{code} {name} 当前价{price:.2f},跌破支撑位{support:.2f}" - 发送频率无限制,只要微信客户端在线(实测连续运行200天未掉线)
- 延迟稳定在3-5秒,因消息直接走微信PC版本地socket通信
当然代价是首次登录需扫码,但这是单次操作。我在安装包里内置了自动扫码引导页,用户双击exe后,界面会弹出二维码图片,用手机微信扫一下就完成绑定,全程无需命令行操作。
3. 核心细节解析与实操要点:从零搭建的避坑指南
3.1 Python环境配置:绕过所有“pip install失败”的陷阱
新手最常卡在第一步:环境装不上。网上教程教“pip install pyqt5”,但实际会遇到三类错误:
- SSL证书错误:国内网络访问PyPI慢且易中断,解决方案是换清华源:
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple - VC++编译器缺失:PyQt5安装需C++编译环境,Win10用户直接下载Microsoft Visual C++ Build Tools(2019版),安装时勾选“CMake tools for Visual Studio”
- 版本冲突:akshare要求pandas>=1.5.0,但旧版PyQt5与新pandas不兼容。最终锁定组合:Python 3.9.13 + PyQt5 5.15.9 + akshare 1.12.84
注意:绝对不要用Anaconda!虽然它预装了科学计算库,但PyInstaller打包时会把整个Anaconda环境打包进去,exe体积暴增至300MB以上。用标准Python安装包(python-3.9.13-amd64.exe)+ pip逐个安装,才是轻量化正解。
3.2 选股模块实现:把通达信公式翻译成Pandas逻辑
热搜词里“量能饱和度100选股公式”“双线合一选股公式”本质都是技术指标组合。以“量能饱和度”为例,通达信公式写法是:
VOL_MA20:=MA(VOL,20); SATURATION:VOL/VOL_MA20*100; SATURATION>100;翻译成Python需注意三个坑:
第一,通达信的VOL是原始成交量(手),而akshare返回的是“成交量(股)”,需除以100转换为“手”单位;
第二,MA函数在Pandas里是rolling(20).mean(),但要注意min_periods=1参数,否则前19日数据为空;
第三,“>100”条件判断必须用&连接符(Pandas布尔索引要求),不能用and。
完整代码片段:
def calc_saturation(df): # df是单只股票的日线DataFrame,含date,open,high,low,close,volume列 df['vol_hand'] = df['volume'] // 100 # 转换为“手” df['vol_ma20'] = df['vol_hand'].rolling(20, min_periods=1).mean() df['saturation'] = df['vol_hand'] / df['vol_ma20'] * 100 return df[df['saturation'] > 100].tail(1) # 返回最近一日满足条件的记录 # 批量处理全市场 all_stocks = get_all_stock_list() # 从akshare获取股票代码列表 result = [] for code in all_stocks: try: df = akshare.stock_zh_a_daily(symbol=f"sh{code}") # 获取上交所股票 if len(df) < 100: continue # 数据不足跳过 filtered = calc_saturation(df) if not filtered.empty: result.append({ 'code': code, 'name': get_stock_name(code), # 自定义函数查股票名称 'price': filtered['close'].iloc[0], 'saturation': filtered['saturation'].iloc[0] }) except Exception as e: continue # 单只股票报错不影响全局3.3 策略回测模块:Backtrader的“去魔改”实践
Backtrader文档写得像天书,但核心就三步:加载数据→定义策略→运行回测。然而新手常犯两个致命错误:
错误一:忽略滑点和手续费
默认回测假设市价单100%成交,但实盘中涨停板挂单可能全部废单。解决方案是在cerebro.addstrategy()后添加:
cerebro.broker.setcommission(commission=0.0003) # 万三佣金 cerebro.broker.set_slippage_fixed(0.01) # 固定滑点1分钱错误二:用未来函数
比如用data.close[0] > data.close[-1]判断今日涨,这没问题;但若写data.close[0] > data.close[1:].max(),就是在用未来数据,回测结果必然虚高。正确做法是用data.close[-20:].max()取过去20日最高价。
我封装了一个通用回测函数,输入选股结果列表,自动批量跑回测:
def run_backtest(stock_list, strategy_class, start_date='20200101', end_date='20231231'): results = {} for code in stock_list: cerebro = bt.Cerebro() cerebro.addstrategy(strategy_class) # 加载数据(此处省略数据加载细节) cerebro.broker.setcash(100000.0) cerebro.addsizer(bt.sizers.FixedSize, stake=100) # 每次买100股 result = cerebro.run() # 提取关键指标:年化收益、最大回撤、胜率 results[code] = { 'annual_return': result[0].analyzers.sharperatio.get_analysis()['rnorm100'], 'max_drawdown': result[0].analyzers.drawdown.get_analysis()['max']['drawdown'], 'win_rate': result[0].analyzers.tradeanalyzer.get_analysis()['won']['total'] / \ (result[0].analyzers.tradeanalyzer.get_analysis()['won']['total'] + result[0].analyzers.tradeanalyzer.get_analysis()['lost']['total']) } return pd.DataFrame(results).T3.4 PyQT界面设计:让非程序员也能看懂的交互逻辑
界面不是越花哨越好,而是越直白越有效。我的设计原则是:所有操作都有明确反馈,所有状态都有视觉提示。
- 主窗口分三栏:左侧树形菜单(选股/回测/设置)、中间工作区(表格显示结果)、右侧日志面板(实时打印操作过程)
- 关键按钮带图标和文字:🔍“执行选股”、📊“运行回测”、📱“绑定微信”
- 表格右键菜单:支持“导出Excel”“查看K线图”“发送微信提醒”
特别注意QTableWidget的性能优化:当选股结果超过500只股票时,直接setRowCount()会卡顿。解决方案是分页加载,每次只显示前50行,底部加“下一页”按钮。代码实现用QStackedWidget切换页面,比滚动条更直观。
微信绑定按钮的逻辑:点击后启动itchat.auto_login(hotReload=True),成功后在界面上显示“✅ 微信已登录,当前账号:xxx”并禁用按钮。这里hotReload=True参数至关重要,它让程序退出后再次启动时无需重新扫码,直接读取本地.pkl缓存文件。
4. 实操过程与核心环节实现:从代码到exe的全流程
4.1 数据获取与存储:构建本地SQLite行情数据库
第一步永远是数据。akshare虽然方便,但直接调用akshare.stock_zh_a_daily()会触发反爬,需加请求头和随机延时:
import time import random import requests from akshare import stock_zh_a_daily # 替换akshare底层requests调用 original_get = requests.get def patched_get(*args, **kwargs): time.sleep(random.uniform(0.5, 1.5)) # 随机延时 headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} kwargs['headers'] = headers return original_get(*args, **kwargs) requests.get = patched_get建库SQL语句精简到极致:
CREATE TABLE stock_data ( code TEXT, date TEXT, open REAL, high REAL, low REAL, close REAL, volume INTEGER, amount REAL, PRIMARY KEY (code, date) );不用创建索引!因为查询都是按code+date精确匹配,SQLite自动优化。数据入库用executemany()批量插入,比单条insert快12倍(实测10万条数据插入时间从83秒降至6.7秒)。
4.2 PyQT界面开发:用Qt Designer拖拽+代码补全的混合模式
纯手写PyQT代码效率低,我采用“Qt Designer画界面 → 生成.ui文件 → 用pyside2-uic转.py → 在代码里补逻辑”的混合模式。例如选股结果表格,在Designer里拖入QTableWidget,设置列宽为[100, 80, 120, 100, 100](代码/名称/价格/量能比/日期),然后在代码里:
self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers) # 禁止编辑 self.table_widget.setSelectionBehavior(QAbstractItemView.SelectRows) # 整行选择右键菜单实现:
def contextMenuEvent(self, event): menu = QMenu() export_action = menu.addAction("导出Excel") plot_action = menu.addAction("查看K线图") send_action = menu.addAction("发送微信提醒") action = menu.exec_(self.table_widget.mapToGlobal(event.pos())) if action == export_action: self.export_to_excel() elif action == plot_action: self.show_kline_chart() elif action == send_action: self.send_wechat_alert()4.3 微信提醒模块:itchat协议的稳定化改造
itchat官方已停止维护,但其核心协议至今有效。关键改造点有两个:
第一,心跳保活:微信PC版每3分钟发一次心跳包,itchat默认心跳间隔是60秒,易掉线。修改Core.py中self.alive = True后的循环:
while self.alive: try: self.__sync_check() # 原始心跳 time.sleep(180) # 改为180秒 except: break第二,消息队列防丢:当网络抖动时,send_msg可能失败。我在发送函数外加一层重试:
def safe_send_msg(msg, to_user=None): for i in range(3): # 最多重试3次 try: itchat.send_msg(msg, toUserName=to_user) return True except Exception as e: time.sleep(2) return False4.4 打包成exe:PyInstaller的终极精简配置
pyinstaller -w -F --upx-exclude=vcruntime140.dll main.py这条命令背后有玄机:
-w参数隐藏控制台窗口,否则每次运行都弹黑框--upx-exclude=vcruntime140.dll是关键!UPX压缩会破坏VC++运行时库,导致程序启动报错“找不到vcruntime140.dll”。排除这个文件后,体积仅增加2MB,但稳定性100%main.py必须是入口文件,里面只做两件事:导入所有模块 + 创建QApplication实例。其他逻辑全在子模块里,避免PyInstaller误判依赖
最终打包体积28MB,安装时无需管理员权限,双击即运行。我在朋友电脑上测试过,从Win7到Win11全兼容,连Surface Pro这种ARM设备都能跑(需用Python 3.9 ARM版)。
5. 常见问题与排查技巧实录:那些文档里不会写的实战经验
5.1 选股结果为空?先查这三处
| 问题现象 | 排查步骤 | 解决方案 |
|---|---|---|
| 点击“执行选股”后表格空白 | ① 查日志面板是否显示“正在获取股票列表” ② 若无此日志,检查akshare是否安装成功 ③ 若有日志但无后续,检查网络是否能访问akshare官网 | 在命令行运行python -c "import akshare; print(akshare.__version__)",若报错则重装:pip uninstall akshare -y && pip install akshare==1.12.84 |
| 选股结果只有10只股票 | ① 查日志是否提示“数据不足,跳过xxx” ② 检查SQLite数据库中该股票是否有足够历史数据 ③ 用DB Browser打开.db文件,查stock_data表记录数 | 删除数据库文件,重新运行“更新数据”功能。akshare有时会漏抓某些ST股票数据 |
| 量能比计算结果全是NaN | ① 查日志是否报“除零错误” ② 检查vol_ma20列是否全为0 ③ 用pandas查看df.head()确认volume列数值 | 在calc_saturation函数开头加:if df['vol_hand'].sum() == 0: return pd.DataFrame() |
5.2 回测结果异常?重点盯住这四个指标
新手常被“年化收益85%”迷惑,实则暗藏陷阱。我教朋友用四象限法快速判断:
- 左上象限(高收益+低回撤):真策略,可实盘
- 右上象限(高收益+高回撤):可能过拟合,需检查参数敏感性
- 左下象限(低收益+低回撤):策略太保守,提高仓位或放宽条件
- 右下象限(低收益+高回撤):立即废弃
具体验证方法:
- 把回测周期从2020-2023改成2022-2023,看收益是否断崖下跌
- 把买入条件“量能比>100”改成“>95”,看胜率变化是否超过15%
- 在策略里加一行
print(f"买入{order.data._name},价格{order.executed.price}"),确认实际成交价是否合理
5.3 微信提醒失效?按顺序执行这五步
- 确认微信PC版在线:手机微信“设置→通用→网页版”必须开启,且PC端微信未退出
- 检查itchat登录状态:在PyQT界面右下角状态栏,应显示“✅ 微信已登录”
- 验证消息发送权限:在微信PC版里,搜索“文件传输助手”,手动发一条消息,确认能正常收发
- 查日志面板:若显示“send_msg failed”,说明网络问题,重启微信PC版再试
- 终极方案:删除
itchat.pkl缓存文件(默认在程序目录下),重新扫码登录
实操心得:我朋友曾因微信PC版版本过低(2.9.0)导致itchat掉线,升级到3.2.0.112后问题消失。所以定期更新微信PC版比折腾代码更重要。
5.4 打包后exe闪退?九成是路径问题
PyInstaller打包时,默认把资源文件(如数据库、图标)放在临时目录,而程序代码里写的路径是相对路径。解决方案:
import sys import os def resource_path(relative_path): """获取资源文件绝对路径""" if getattr(sys, 'frozen', False): base_path = sys._MEIPASS else: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) # 使用示例 db_path = resource_path("data/stock.db") icon_path = resource_path("icons/main.ico")在.spec文件里,还要显式添加数据文件:
a = Analysis( ['main.py'], pathex=['.'], binaries=[], datas=[('data/*', 'data'), ('icons/*', 'icons')], # 关键! ... )最后分享个小技巧:打包前先用pyinstaller --debug=all main.py生成带调试信息的exe,运行时会弹出控制台显示详细报错,比盲目猜高效十倍。
我在实盘中发现,这套系统最大的价值不是选出牛股,而是把“今天该不该操作”这个主观问题,转化成“选股模块是否返回结果”这个客观事实。当界面表格空着时,大脑会自然停止幻想,转向复盘昨日策略;当微信弹出预警时,手指不再犹豫,执行预案成为肌肉记忆。技术只是工具,真正的量化,是让交易行为变得可追溯、可验证、可迭代。
本文还有配套的精品资源,点击获取