十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python股票量化系统源码解析:搭建个人回测框架实战

Python股票量化系统源码解析:搭建个人回测框架实战 简介一份面向Python量化交易入门者的完整源码与配套教程包适合希望从零搭建股票量化系统、学习策略开发与回测流程的开发者。资源包含244个文件压缩包仅3.53MB以Python源码py/pyc、界面文件ui/css/html、JSON数据配置、可视化图片和说明文档为主目录结构清晰便于按功能模块查找。已有38人浏览学习。内容除系统主体代码外还提供了环境准备与运行说明覆盖MySQL数据库配置推荐root/88888888、依赖批量安装以及win_main.py启动方式随附教程有助于理解数据获取、策略编写、回测执行和风险控制等核心环节。通过阅读和修改源码能够掌握量化系统开发的完整思路为后续扩展自己的交易策略打下基础。从环境搭建到策略回测形成一套可实操的闭环路径。该资源来自网络分享若涉及版权问题可联系制作者删除。1. 为什么 Python 是个人做股票量化系统的“最短路径”最近三五年天天有人在群里问“我想搞股票量化从哪开始”而我的回复几乎永远是同一句话如果你不是科班出身、不是高频交易团队那就老老实实用 Python。这不是情怀是现实。市面上一大堆能用 Python 直接调用的数据接口、回测框架、技术指标库、可视化工具组合起来就是一套能跑通的闭环。你不需要从零去写 K 线计算也不需要自己维护数据库集群更不需要懂 C 的指针和内存管理就能把手里的交易想法变成可以回测、可以出图的程序。这篇文章要讲的就是一套我自用的 Python 股票量化系统源码结构以及配套的教程思路。它不是某个不可告人的“圣杯”而是把数据获取、策略编写、回测、参数调优、风险控制这几层拆开揉碎了给你一条能自己复现的落地路径。你会看到我是怎么组织目录的、关键模块怎么写的、参数怎么调、踩过哪些坑。无论你是刚装好 Python 的新手还是已经写了几个爬虫但没碰过回测的老手这套系统的骨架都适合你拿去做底子然后往里面填你自己的交易逻辑。先声明一句股市里没有任何一套源码能保证赚钱但一套结构清晰、能让你随时改策略的代码能帮你避开绝大多数“拍脑袋下单”的坑。本文准备用六章的篇幅把这套系统的源码结构和教程要点讲透对着做你的第一个可回测量化框架基本就在手里了。2. 先搞懂量化系统的骨架一个最小可运行的目录结构2.1 别一上来就写策略先把四个模块分开我在带人入门时发现新手最爱犯的错就是把行情获取、指标计算、下单信号、结果显示全部揉在一个脚本里。改一个参数要翻几百行跑一次回测要重新下载数据最后自己都看不懂输出在干嘛。这绝对不是好习惯。一个能持续迭代的量化系统核心就是分工明确。我这里说的“最小骨架”包含四层数据层、策略层、回测层、绩效层。每层各司其职再加上一个config.py来放全局参数整个工程不过 10 个文件左右。quant_system/ ├── config.py # 全局参数股票池、初始资金、手续费率、回测区间 ├── data_fetcher.py # 数据层从网络接口获取日线/分钟线统一格式 ├── indicators.py # 策略指标MA、MACD、RSI 等计算逻辑 ├── strategy.py # 策略层根据指标生成买卖信号 ├── backtest.py # 回测层按时间顺序撮合计算持仓和资金曲线 ├── performance.py # 绩效层年化、回撤、夏普、超额收益 ├── plot_results.py # 可视化资金曲线 回撤图 ├── run_backtest.py # 主入口一键跑完整流程 ├── requirements.txt └── README.md上面这个目录结构是我实际使用过好多年的版本简化的。它未必花哨但胜在每改一处逻辑并不需要牵动其它文件。比如你想换一个数据源只需改data_fetcher.py想换策略只需重写strategy.py里的信号函数。新手照着这个搭后面读任何开源量化项目都不会发懵。config.py是我最建议你认真写的文件。初始资金定多少、手续费单边算多少、滑点按多少个 tick 估算这些直接决定回测结果是不是失真。你后面做参数优化时如果发现结果异常漂亮先别高兴去查 config 里的成本参数是不是设成了零。2.2 数据获取统一 DataFrame 格式是第一要务先看data_fetcher.py。常见做法是用akshare或baostock这类免费接口拿行情但我更推荐你先封装一层把从任何源拿到的数据统一成“日期升序、包含 open/high/low/close/volume 字段”的 DataFrame。因为不同源的字段命名不一样有的叫date有的叫trade_date不统一的话策略层会写得非常痛苦。# -*- coding: utf-8 -*- import pandas as pd import akshare as ak def fetch_stock_data(symbol: str, start_date: str, end_date: str) - pd.DataFrame: 获取A股日线数据并统一格式。 输入股票代码如 600519起止日期如 2020-01-01。 输出按日期升序排列的 DataFrame列名为 date/open/high/low/close/volume。 raw_df ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart_date, end_dateend_date, adjustqfq ) df pd.DataFrame({ date: pd.to_datetime(raw_df[日期]), open: raw_df[开盘].astype(float), high: raw_df[最高].astype(float), low: raw_df[最低].astype(float), close: raw_df[收盘].astype(float), volume: raw_df[成交量].astype(float), }) df.sort_values(date, inplaceTrue) df.reset_index(dropTrue, inplaceTrue) return df这段代码的逻辑不复杂但我故意保留了中文日期列的处理因为 akshare 返回的就是中文表头。如果你用 tushare pro 或其他接口表头又不一样所以统一的动作必须放在这里。函数最后保证日期升序、索引从 0 开始后面的指标计算和回测才能老实工作。参数说明symbol传纯数字代码即可接口内部会拼成 600519 和 sh600519 的差异adjustqfq是前复权的意思回测必须用前复权价格否则除权除息当天价格会跳空导致你的策略产生虚假亏损或盈利。这是新手最容易忽略的一个点。2.3 技术指标模块自己写还是用 TA-Lib技术指标这块有现成的TA-Lib库但它安装起来在 Windows 上偶尔会翻车。我个人的建议是均线、MACD、RSI、布林带这些常见指标干脆自己用 pandas 写一遍。既避开了 TA-Lib 的安装麻烦又能让你清楚每个指标内部是怎么滚动的后面调参时心里有底。# -*- coding: utf-8 -*- import pandas as pd def add_moving_average(df: pd.DataFrame, window: int 20) - pd.DataFrame: 在 df 中新增 ma_window 列例如 ma_20。 df[fma_{window}] df[close].rolling(windowwindow).mean() return df def add_macd(df: pd.DataFrame, fast: int 12, slow: int 26, signal: int 9) - pd.DataFrame: 计算 MACD 的 DIF、DEA 与 MACD 柱。 ema_fast df[close].ewm(spanfast, adjustFalse).mean() ema_slow df[close].ewm(spanslow, adjustFalse).mean() df[dif] ema_fast - ema_slow df[dea] df[dif].ewm(spansignal, adjustFalse).mean() df[macd] (df[dif] - df[dea]) * 2 return df def add_rsi(df: pd.DataFrame, window: int 14) - pd.DataFrame: Wilder 原始 RSI 计算避免 TA-Lib 依赖。 delta df[close].diff() gain delta.clip(lower0) loss -delta.clip(upper0) avg_gain gain.ewm(alpha1/window, adjustFalse).mean() avg_loss loss.ewm(alpha1/window, adjustFalse).mean() rs avg_gain / avg_loss df[frsi_{window}] 100 - (100 / (1 rs)) return df这段代码里MA 用的rolling是普通窗口平均MACD 和 RSI 用的ewm是指数加权平均。注意adjustFalse这个参数它让 ewm 按递推方式计算结果和国内股票软件上看到的 MACD 数值一致。如果你省掉这个参数默认的 adjustTrue 会让前几个值偏差很大。add_macd里最后乘以 2是国内通达信等软件的常规做法而海外很多平台不乘 2。所以你自己写的好处又来了可以完全对齐你熟悉的看盘软件的指标口径。后面策略生成信号时直接用df[dif]、df[dea]、df[macd]这些列就行。3. 用交易信号把策略写出来从想法到可回测的代码3.1 策略与指标分离策略只管买什么、卖什么策略层是整个系统里最灵活、也最容易写烂的地方。新手常犯的错误是在策略文件里又调用数据接口又计算指标结果换来换去很麻烦。我这里推荐的做法是策略函数接收一个“已经算好指标”的 DataFrame然后输出一个包含 target_position 列的新 DataFrame。买多少、卖多少由仓位管理逻辑单独决定策略本身只回答“该持有多大的仓位”。# -*- coding: utf-8 -*- def ma_cross_signal(df: pd.DataFrame, short_window: int 5, long_window: int 20) - pd.DataFrame: 双均线策略信号。 当 short 均线上穿 long 均线时目标仓位设为 1.0满仓 当 short 均线下穿 long 均线时目标仓位设为 0.0空仓。 其余情况保持前一天的 target_position 不变。 df df.copy() df[fma_{short_window}] df[close].rolling(short_window).mean() df[fma_{long_window}] df[close].rolling(long_window).mean() df[target_position] 0.0 # 判断上穿与下穿 golden_cross (df[fma_{short_window}] df[fma_{long_window}]) \ (df[fma_{short_window}].shift(1) df[fma_{long_window}].shift(1)) death_cross (df[fma_{short_window}] df[fma_{long_window}]) \ (df[fma_{short_window}].shift(1) df[fma_{long_window}].shift(1)) df.loc[golden_cross, target_position] 1.0 df.loc[death_cross, target_position] 0.0 df[target_position] df[target_position].replace(0.0, methodffill) df.loc[df.index[:long_window], target_position] 0.0 # 前 long_window 根 K 线无信号 return df这段代码的实现逻辑是先算两条均线然后用shift(1)和当前值比较找到交叉点。replace(0.0, methodffill)是向前填充意思是只有交叉当天信号才变化其它时间沿用上一时刻的仓位这样把“持币”和“持股”的状态延续了下来。参数说明short_window和long_window是策略的核心参数。5/20 是一个比较敏感的日线组合来回交叉多交易频繁20/60 则更平滑交易少适合趋势行情。你可以在config.py里把它们设成可传参方便后面做参数扫描。需要留意的是前long_window根 K 线均线还没形成有意义的趋势所以强制仓位为 0避免回测初期出现虚假信号。3.2 信号与执行之间还有一个“仓位管理”拦路虎信号只告诉你要不要买但没告诉你能不能一把梭。真实的量化系统里通常还有风控和仓位管理模块。不过这套最小系统里我倾向于把“最大仓位比例”和“最大单笔亏损”放在回测层里做检查而不是塞进策略层。这样策略层保持纯净你换策略时不用反复改风控代码。在strategy.py里可以再加一个fetch_current_signal函数用来给实盘模拟或后续接入交易接口时调用。回测阶段主要使用上一节的target_position列。你把策略产出理解成一个“目标仓位序列”后面回测引擎按这个序列去撮合而不是在策略里直接下单。这是回测和实盘逻辑能够共用一套代码的关键。4. 回测引擎把信号变成资金曲线的关键一环4.1 为什么不能简单地“每天按收盘价买卖”新手写回测时最容易写错的就是撮合规则。拿日线数据举例如果你在当日收盘价出现信号后就在当日收盘价成交那是典型的“未来函数”加“偷价”。因为信号本身是用收盘价算出来的你不能再认为能以收盘价买到。正确的做法是当日收盘后产生信号次日开盘价成交。这才符合T1和盘中决策的实际情况。# -*- coding: utf-8 -*- import pandas as pd def run_backtest(df: pd.DataFrame, init_cash: float 100000.0, fee_rate: float 0.0003, slippage: float 0.0002) - pd.DataFrame: 极简向量化回测引擎。 使用 target_position 列默认次日开盘价成交。 返回带现金、持仓市值、总资产、当日盈亏的 DataFrame。 df df.copy() df[signal] df[target_position].shift(1) # 今天用昨天的信号 df[trade_price] df[open] * (1 slippage) # 买入滑点卖出时再调 # 计算每日目标持仓市值 df[target_value] init_cash * df[signal] df[prev_total] init_cash cash init_cash stock_value 0.0 total_list [] cash_list [] stock_list [] for idx, row in df.iterrows(): # 根据昨日信号调整仓位这里简化计算实际应处理手数和费用 target_stock_value row[target_value] diff target_stock_value - stock_value if diff 0: # 买入增加持仓扣除手续费滑点成本 buy_value diff fee buy_value * fee_rate cash - buy_value fee stock_value buy_value else: # 卖出减少持仓扣除手续费卖出价已含滑点 sell_value -diff fee sell_value * fee_rate cash sell_value - fee stock_value - sell_value total cash stock_value total_list.append(total) cash_list.append(cash) stock_list.append(stock_value) df[cash] cash_list df[stock_value] stock_list df[total] total_list df[daily_return] df[total].pct_change().fillna(0) return df这段回测代码表面上很简单但它抓住了几个核心signal列用shift(1)做了延迟避免未来函数买入时把滑点加在开盘价上卖出时同样按开盘价减滑点手续费按双边收取。如果你是做高频或分钟级别这会过于粗暴但对日线策略的初步评估已经足够。需要注意这里没有处理“整手”一手 100 股也没有处理涨跌停不能成交的问题。真实场景里股票只能买 100 股整数倍而且一字涨停根本买不进。处理这些需要引入订单簿级别的撮合代码复杂度会上好几倍。我的建议是第一版回测先不管手数只看策略的信号逻辑是否有效等策略定型了再加整手约束看损耗多少。4.2 绩效指标别只看总收益率回测跑完你会得到一条资金曲线。最外行的做法是把最终资金除以初始资金然后惊呼“一年翻倍”。但真正评估策略至少要看年化收益率、最大回撤、夏普比率、胜率和盈亏比。这几个指标在performance.py里通过几行代码就能算出来。指标计算公式说明年化收益率(最终资金/初始资金)^(245/交易天数) - 1A 股每年约 245 个交易日最大回撤max(1 - 资金曲线/累计峰值)衡量策略最惨时亏多少夏普比率(日收益均值 / 日收益标准差) * sqrt(245)每承受一单位风险的超额回报胜率盈利交易次数 / 总交易次数必须结合盈亏比一起看这里有一个血泪经验很多人看到某套源码回测收益极高就直接拿实盘资金去试最后亏得不明不白。你去看它的绩效报告大概率最大回撤超过 30%或者夏普比率还不到 1。所以拿到任何量化源码我都会先在回测阶段跑出这三个指标不满足自己风险偏好的策略直接pass。5. 参数调优与避坑为什么你的回测曲线一改参数就翻车5.1 参数扫描的正确姿势网格搜索与可视化策略写好后参数怎么设就成新问题。通常的做法是网格搜索把short_window在 5 到 50 之间long_window在 20 到 120 之间两两组合跑一遍观察哪个组合的夏普比率最高。注意网格搜索阶段用的是同一段历史数据非常容易过拟合所以我会再留一段最近两年的数据做样本外验证。# -*- coding: utf-8 -*- import itertools from data_fetcher import fetch_stock_data from strategy import ma_cross_signal from backtest import run_backtest from performance import annual_return, max_drawdown, sharpe_ratio symbol 600519 start 2020-01-01 end 2023-12-31 df_raw fetch_stock_data(symbol, start, end) results [] for short_win, long_win in itertools.product([5, 10, 15, 20], [30, 60, 90, 120]): if short_win long_win: continue df_signal ma_cross_signal(df_raw, short_win, long_win) df_backtest run_backtest(df_signal) sharpe sharpe_ratio(df_backtest) mdd max_drawdown(df_backtest) results.append((short_win, long_win, sharpe, mdd)) results_df pd.DataFrame(results, columns[short, long, sharpe, mdd]) best results_df.loc[results_df[sharpe].idxmax()] print(best)参数说明网格搜索的间距越大速度越快但容易漏掉最优参数。上面示例中 4x4 的组合只有 16 种跑得非常快。如果你想更细致可以先把short和long都放在一个更窄的区间里比如 short 在 5~20 每隔 5 取一个long 在 20~60 每隔 5 取一个这样 4 组 x 9 组也有 36 种依然在一分钟内能完成。不要一上来就搞 1000 组结果不仅是慢而且更容易过拟合。注意网格搜索的目标函数不能只看总收益我会至少同时看夏普比率和最大回撤。有时候最优夏普组合的最大回撤会高达 25%这需要你自己权衡。一股脑追求收益后面实盘心态很容易崩。5.2 常见避坑清单3 个我已经替你踩过的雷量化系统里坑实在太多这里挑三个最高频的按“现象 → 原因 → 解决”给你列一下。第一个坑前视偏差导致的“藤月收益”。现象是回测资金曲线非常丝滑几乎没有回撤。原因往往是代码里用了df[close]同时做信号和成交价或者用了未来数据做归一化。解决方法是仔细检查每一个用到shift的地方并把成交价一律改为次日开盘价。我一直建议回测引擎里必须加一个断言如果信号列和成交价有重叠直接抛异常。第二个坑手续费和滑点设成 0 时的“完美幻觉”。现象是同一套策略在设置成本为 0 时年化 50%加上万三手续费后只剩下 20%。原因是双均线策略交易次数频繁每笔的磨损都在蚕食利润。解决方法是 config 里至少按万三手续费加一个滑点估算。想更真实还可以按成交额的千分之一额外计入冲击成本尤其对小市值股票更明显。第三个坑参数过拟合导致样本外失效。现象是 2018-2021 年表现最好的一组参数到 2022 年突然变成亏损。原因是你在同一批数据上反复调参导致参数被历史噪声“记住”了。解决方法就是把数据切分为训练段和测试段先在训练段做网格搜索再在测试段做一次性验证。如果测试段结果很平庸那这个策略大概率没有实际价值。5.3 可视化资金曲线和回撤图一眼看透风险plot_results.py是最后为你提供直观感受的模块。用matplotlib画出资金曲线、每日回撤和基准指数对比你会立刻看出策略在哪个时间段失效回撤最大的一段是不是你能承受的。这里提供一个最小画图脚本# -*- coding: utf-8 -*- import matplotlib.pyplot as plt def plot_backtest_result(df: pd.DataFrame, benchmark: pd.Series None) - None: df 是回测引擎返回的 DataFrame包含 date 和 total 列。 benchmark 可传入同期沪深300净值序列进行对比。 plt.figure(figsize(12, 8)) plt.plot(df[date], df[total] / df[total].iloc[0], labelStrategy) if benchmark is not None: plt.plot(benchmark.index, benchmark / benchmark.iloc[0], labelBenchmark) plt.title(Backtest Equity Curve) plt.xlabel(Date) plt.ylabel(Normalized Value) plt.legend() plt.grid(True, alpha0.3) plt.show()这个函数会把策略净值曲线和基准指数放在同一张图上归一化到起始日为 1这样谁跑赢谁一目了然。注意基准必须是同期的指数点位如果日期对不齐最好先做一次reindex对齐。到此一个从数据到策略到回测到可视化的完整闭环就已经跑通。6. 让系统走入实战前我在参数、仓位和心态上的几个技巧如果你照着上面的代码把最小系统跑通了下一步就是让它“更真实”。这里我想分享三个已经在我自己系统里落地的技巧全部围绕“不要被回测欺骗”这一点展开。技巧一一定要把你自己的交易成本模型写进回测。除了手续费A 股还有印花税目前是卖出时单边收取千分之零点五。别看这只是一小条对高频调仓的策略影响极大。我的config.py里会有专门的tax_rate参数卖出时自动乘以成交额。此外如果你做的是小盘股滑点按千分之二计算都不为过。把这些都加上后回测才会收敛到一个相对真实的结果。技巧二用“参数平原”代替“最优参数”。网格搜索找到的全局最优往往在一个非常陡峭的孤峰上周围参数表现极差。这种策略就是过拟合的典型。我会看最优参数周围区域的平均表现如果整体都还不错而不是只有孤零零一个点突出我才认为这个参数组合有稳健性。具体操作是把网格搜索结果按 short 和 long 分组画出夏普比率的热力图如果连成片的红色区域多就说明策略稳定性高如果只看到一个红点其余全是绿那就直接放弃。技巧三样本外验证不能只做一次。最稳妥的做法是把历史数据切成三段第一段训练参数第二段验证参数第三段留到最后模拟“未来”。每次调整代码或参数都在训练段反复进行直到确定最终模型才在第三段跑一次。如果这一步的收益仍然能接受我才会考虑用最小仓位去实盘模拟。我个人的习惯是样本外至少连续三个月跑出正向收益才敢把真金白银放进去。最后说一个心态上的坎回测曲线再漂亮实盘也会让你措手不及。连续止损、策略失效、平台卡顿、数据延迟这些都是家常便饭。我自己的做法是给系统装上“熔断开关”当实盘账户回撤超过 15% 时无条件停止策略并复盘。这个“后悔药”不是代码能给的而是你在编写系统时就要提前设计好的风控边界。整套系统到这里你已经拥有了自己的数据层、策略层、回测层、绩效层和参数调优工具。剩下的就是把你的交易想法翻译成target_position函数然后用真实数据反复验证。源码的价值在于给你一条可靠的骨架而交易逻辑的思考永远需要你亲自完成。希望这些踩过坑的经验能帮你少走一截弯路。本文还有配套的精品资源点击获取
返回列表