首页 / 资讯中心 / 文章详情

Python量化策略实现:从环境搭建到双均线回测的完整指南

Python量化策略实现:从环境搭建到双均线回测的完整指南 ★ FEATURED ARTICLE
1. 先把环境盘明白数据、工具链与最小工作台很多人一提到“python量化策略实现”第一反应是去找选股公式、抄一段金叉死叉代码。我刚开始做的时候也是这样结果折腾了两周策略没跑起来光装环境、找数据就劝退了一半热情。后来才明白量化研究里真正拉开差距的不是那个交易信号而是把环境、数据、回测逻辑和成本模型这四件事做对。这篇文章我不讲玄乎的“量化圣杯”只把我从零到一跑通策略的经验拆开给同样在这条路上摸索的人一个可以直接落地的参考。先说结论一个能用的量化策略工作台至少由三部分组成——干净的编程环境、可靠的历史行情数据、以及一个不会自欺欺人的回测引擎。这三个基础没打牢后面策略再 fancy 都是空中楼阁。1.1 语言环境与依赖库怎么选才不给自己挖坑Python 版本我建议直接用 3.9 或 3.10不用追最新很多第三方库对最新版本的支持有滞后。这里有个很实际的建议永远不要在你系统自带的 Python 里直接 pip install。我见过太多人把系统 Python 搞坏最后不得不重装系统。正确做法是用 venv 或 conda 管理一个独立环境。# 创建虚拟环境 python -m venv quant_env # 激活环境Windows quant_env\Scripts\activate # 激活环境macOS/Linux source quant_env/bin/activate # 安装基础依赖 pip install pandas numpy matplotlib akshare backtrader如果你是第一次配环境大概率会遇到下载慢、超时的问题。这时候把 pip 源换成国内镜像能省下大量时间pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pandas numpy matplotlib在 IDE 的选择上VSCode 加 Python 插件就够用关键是配置好解释器路径。很多人配置 VSCode 的 Python 环境时明明装了库却提示找不到模块十有八九是解释器没指到刚才创建的虚拟环境。在 VSCode 里按CtrlShiftP输入Python: Select Interpreter选quant_env里的那个 Python问题就解决了。至于库的选型核心其实是三个层级我把它们分开讲因为很多新手分不清层级库名用途我的评价数据处理pandas / numpy行情清洗、指标计算、向量化运算必装量化开发的地基数据获取akshare / tushare / baostock拉取股票、基金、期货历史行情免费方案足够学习和回测回测框架自写简易回测 / backtrader / vectorbt验证策略、计算绩效新手建议先自写再上框架这里重点说说回测框架的选择。网上铺天盖地推荐 backtrader它确实功能全支持多标的事件驱动回测但学习曲线比较陡。vectorbt 用向量化计算跑参数扫描飞快但它的思维方式是“对整个数据矩阵做运算”对新手不太友好。我的建议是第一个策略用手写回测搞清楚原理等逻辑跑通了再切框架。原因很简单——如果你不亲自写过一次信号偏移和资金曲线计算你永远不知道框架底层在帮你做什么出了问题也无从排查。1.2 行情数据从哪来复权到底怎么选数据是量化策略的“原材料”。数据错了策略再好也是白搭。免费的数据源里面我用得比较多的是 akshare 和 baostock先给个直观对比数据源免费程度数据覆盖接口稳定性上手难度akshare免费A股、港股、美股、期货、基金偶尔改接口需要跟进版本简单文档全tushare部分免费积分越高权限越大A股为主也有期货等稳定简单需要注册 tokenbaostock完全免费A股日线、分钟线、财务数据稳定但更新较慢简单我个人入门时用的是 akshare因为它不需要注册 token几行代码就能拉到数据。对于学习目的来说完全够用。但要注意akshare 的接口有时会随上游数据源调整而变化代码里要做好异常处理或者直接 pip 升级到最新版。然后是复权。这可能是新手最容易忽略但又最关键的概念。股票会分红、送股导致价格出现跳空缺口。前复权是保持最新价格不变调整历史价格后复权是保持上市首日价格不变调整后续价格。回测时最怕的是用不复权数据因为历史上的价格缺口会被策略误判成信号产生一堆假交易。我的习惯是回测统一用前复权数据因为它的价格与当前市价接近便于对比策略结果和实际行情。如果你用ak.stock_zh_a_hist(symbol000001, perioddaily, adjustqfq)qfq就是前复权。这里有个细节复权因子会根据最新公告变化所以历史回测结果每次拉取可能会有细微差异这是正常的。数据拿到之后还要做最基本的清洗统一日期格式、按时间升序排序、去掉停牌日没有成交量的空行。这些步骤看似繁琐但能让后面所有策略代码稳定运行花十分钟值得。1.3 最小工作台的目录与脚本设计我见过很多人把所有代码塞进一个test.py数据文件乱丢在桌面回测结果靠眼睛记。这样做的后果是策略一旦多起来你自己都搞不清哪个结果对应哪个参数组合。建议从一开始就建立一个简单清晰的项目结构quant_study/ ├── data/ # 存放历史行情数据 │ └── stocks/ ├── strategy/ # 策略逻辑代码 │ └── double_ma.py ├── backtest/ # 回测引擎与绩效统计 │ └── engine.py ├── results/ # 回测结果、图片、日志 └── main.py # 入口脚本这个结构不复杂但能在三个月后帮你省下大量“找文件”的时间。说白了量化策略实现是一个反复试验的过程你的工作台应该是“用完就能快速继续下一次试验”的状态而不是每次都要从头搭起。2. 第一条策略落地双均线系统的完整代码拆解2.1 为什么第一个策略一定是双均线我见过不少新手一上来就挑战“多因子模型”或“LSTM 预测股价”结果数据预处理就卡了一周。我的看法很直接第一个策略一定选逻辑最简单、代码最短的双均线就是最典型的一个。它只有两条均线短期均线上穿长期均线就做多下穿就空仓没有任何复杂参数。选它的核心原因有三个可解释性强。每一笔交易、每一次持仓变化都能在图上直观看到方便你验证回测引擎是否算对了。方便排查错误。当策略结果不对劲时你可以从信号、持仓、收益、成本一步步拆不会被复杂逻辑绕晕。适合做框架的“冒烟测试”。就像新买一台机器先空跑一遍双均线能快速检验你的数据、回测、绩效统计整条链路是否通畅。双均线背后的逻辑是趋势跟踪——短期均线代表近期的平均持仓成本长期均线代表更长时间的市场平均成本。当短期均线从下方穿越长期均线时说明近期买入力量占优趋势可能向上反之则可能向下。这个逻辑并不神秘但它能赚钱的关键在于趋势一旦形成往往有一定惯性策略赚的就是这段惯性的钱。2.2 数据获取与预处理代码我们以平安银行000001为例拉取 2018 年到 2024 年的日线数据做演示。注意这里只是用历史数据做技术研究不构成投资建议。import akshare as ak import pandas as pd import numpy as np # 拉取前复权日线数据 df ak.stock_zh_a_hist( symbol000001, perioddaily, start_date20180101, end_date20241231, adjustqfq ) # 统一列名只要核心字段 df.columns [ date, open, close, high, low, volume, amount, amplitude, change_pct, change, turnover ] df df[[date, open, close, high, low, volume]] # 日期清洗与索引 df[date] pd.to_datetime(df[date]) df df.set_index(date).sort_index() # 检查缺失值 print(df.isnull().sum()) print(df.head())跑完之后你会得到一份干净的日线数据表。这里有个小细节ak.stock_zh_a_hist返回的列名是中文我在代码里直接改了列名后面所有代码用英文列名避免编码问题来回折腾。2.3 信号生成与未来函数双均线的信号生成只有五行代码# 计算短期和长期均线窗口分别为5日和20日 df[ma_short] df[close].rolling(window5).mean() df[ma_long] df[close].rolling(window20).mean() # 生成原始信号短期均线在长期均线上方为1否则为0 df[signal] (df[ma_short] df[ma_long]).astype(int) # 真正可执行的持仓信号向后偏移一天 df[position] df[signal].shift(1).fillna(0)这之间的区别如果没想明白回测会出大问题。signal是用当天收盘价计算出来的但你在收盘那一刻只能看到收盘价并不能立刻按收盘价成交。现实交易中最快的成交也要等到下一根 K 线开盘。如果你用当天的signal乘以当天的收益就用了“当天收盘才知道的信息”去赚“当天开盘到收盘的收益”这在专业领域被称为未来函数是回测中最致命、最隐蔽的错误之一。shift(1)的作用就是把信号整体向后平移一天保证你用的信息严格早于收益发生的时间。这个细节我当年反复栽跟头每次回测收益看起来很好实盘就一塌糊涂。现在我把这个检查当作回测代码的第一条铁律持仓状态必须比收益计算晚一个周期。2.4 一个足够实用的回测函数手写一个简单但不算简陋的回测函数能帮你在不依赖框架的情况下跑通整个流程。def run_backtest(df, short_win5, long_win20, commission_rate0.00025, slippage_rate0.0005): 双均线策略回测 - commission_rate: A股佣金万2.5 - slippage_rate: 滑点按0.05%估算 df df.copy() df[ma_short] df[close].rolling(short_win).mean() df[ma_long] df[close].rolling(long_win).mean() # 信号与持仓shfit(1)避免未来函数 df[signal] (df[ma_short] df[ma_long]).astype(int) df[position] df[signal].shift(1).fillna(0) # 策略收益 持仓状态 × 当日收益率 df[ret] df[close].pct_change().fillna(0) df[strategy_ret] df[position] * df[ret] # 交易成本只在仓位变化时计算 df[trade] df[position].diff().abs().fillna(df[position].iloc[0]) df[cost] df[trade] * (commission_rate slippage_rate) df[strategy_net] df[strategy_ret] - df[cost] # 策略净值与基准净值 df[nav] (1 df[strategy_net]).cumprod() df[benchmark_nav] (1 df[ret]).cumprod() return df成本模型我在这里直接放进了回测函数。别小看手续费和滑点在短周期策略里这两项被忽略的成本足以把盈利变成亏损。A股佣金通常在万1.5到万2.5之间卖出时还要交印花税我这里为了简化为单向费用的估算实际做策略时需要按你真实交易成本填准确一些。跑完回测后用下面这段代码计算核心绩效指标def performance_stats(nav, benchmark_nav, risk_free_rate0.02, periods_per_year252): 计算年化收益、最大回撤、夏普比率等指标 total_return nav.iloc[-1] - 1 years len(nav) / periods_per_year annual_return nav.iloc[-1] ** (1 / years) - 1 # 年化波动率 daily_vol nav.pct_change().dropna().std() annual_vol daily_vol * np.sqrt(periods_per_year) # 夏普比率 sharpe (annual_return - risk_free_rate) / annual_vol if annual_vol ! 0 else 0 # 最大回撤 rolling_max nav.cummax() drawdown (nav - rolling_max) / rolling_max max_drawdown drawdown.min() return { 累计收益率: round(total_return, 4), 年化收益率: round(annual_return, 4), 年化波动率: round(annual_vol, 4), 夏普比率: round(sharpe, 4), 最大回撤: round(max_drawdown, 4), } # 使用示例 df_result run_backtest(df, short_win5, long_win20) stats performance_stats(df_result[nav], df_result[benchmark_nav]) print(stats)这个函数输出的五类指标是我判断一个策略值不值得继续研究的基础。累计收益和年化收益看赚钱能力最大回撤看承受的风险夏普比率看风险调整后的收益这比单纯看收益要靠谱得多。别忘了画图不然你很难直观感受策略的表现import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False plt.figure(figsize(12, 6)) plt.plot(df_result[nav], label策略净值) plt.plot(df_result[benchmark_nav], label基准净值) plt.title(双均线策略净值曲线) plt.legend() plt.show()3. 回测里最隐蔽的四个坑我替你验证过的失败路径回测代码能跑通和回测结果能相信中间隔着四个大坑。这四个坑我全都踩过每个都让我对自己的策略产生了深深的怀疑。提前看清楚你就不用再走一遍弯路。3.1 未来函数回测收益的作弊器我已经在上文提到了shift(1)但这里值得再展开一次因为它太重要了。未来函数是所有回测错误中最隐蔽、危害最大的一种它让你在回测里看到根本不存在的利润。举个例子有段时间我写了一个“突破策略”逻辑是“价格突破昨日最高价就收盘买入”。代码写出来回测收益惊人年化百分之几百。后来我冷静下来一查发现我的代码在当天收盘时刻触发了买入却按当天收盘价和当天最高价之间的差额计算了收益——这等于你穿越回去告诉昨天的自己今天会突破然后趁低价买入。现实里根本不可能。验证自己有没有未来函数有个笨但有效的方法把手里的数据往前推 20 根 K 线然后打印出最后 5 天的signal、position、ret三列一笔一笔核对。如果发现某天position1且ret是当天的涨幅而这天的signal也是当天算出来的那就中招了。记住一个原则任何成交量信号永远比价格收益慢一步。3.2 手续费与滑点不扣成本都是纸面富贵一个策略回测年化 20%看起来不错。但如果你忽略了手续费、印花税和滑点真实的净收益可能只有 8%甚至更低。对于高频或短线策略交易成本几乎决定了策略的生死。我在回测中配置成本时踩过的具体场景是这样的双均线在震荡行情里会频繁产生金叉死叉每产生一次信号就要支付一次买卖成本。假设一年交易 80 次来回共 160 笔单边交易每次单边成本 0.1%佣金加滑点一年的交易成本就是 16%。如果策略本身的毛收益只有 20%扣完成本就剩 4%基本白忙活。所以回测函数里的成本参数一定不能设成 0甚至要保守一点。我做策略时习惯把滑点设得比真实情况稍大如果在这个偏保守的成本假设下策略还能盈利才算初步过关。3.3 幸存者偏差与股票池选择幸存者偏差在股票回测里是个经典陷阱——你用“现在还在上市的股票”去做历史回测等于剔除了那些退市的公司策略在过去看来就会异常美好。我一度只用上证 50 成分股回测策略发现收益不错。后来我把股票池扩大到全市场包括退市股结果策略收益大幅缩水。原因就在于历史上的绩差股、退市股在筛选成分股时已经被剔除了。同样是 2018 年上证 50 的成分股大多活到了今天而这些股票本身就是相对优质的。合理的做法有两种一是用固定一段时间的历史成分股列表做回测而不是用当下的列表二是扩大股票池范围比如用全市场所有 A 股或者用宽基指数的全部成分股让结果更接近真实可投资的域。对个人研究者来说最现实的办法是——用指数本身的数据来模拟策略指数已经内含了成分股定期调整的规则能在很大程度上缓解幸存者偏差。3.4 过拟合与参数平原最后一个坑也是最磨人的过拟合。双均线有两个参数5 日和 20 日。如果你把短参数从 1 到 50 遍历一遍把长参数从 10 到 200 遍历一遍一定能找到一组“历史表现最优”的参数。但这组参数往往只是精确地拟合了过去的噪音在未来毫无预测能力。怎么判断是否过拟合看参数敏感性。如果参数从 5 调到 6年化收益就从 25% 掉到 2%那这组参数就是“孤峰”不可靠。真正稳健的策略应该有“参数平原”——在某个参数区间内绩效变化是平缓的而不是陡升陡降。我处理这个问题的方法很简单做一次网格搜索把结果画成热力图观察高收益区域是不是连成片的。如果是说明参数稳健如果只是零星几个亮点那策略本身就值得怀疑不是你参数没调好。4. 实盘前的最后一道关成本、持仓、风险三位一体回测结果看着不错之后不要急着投真金白银。我见过太多人拿着回测曲线就冲进市场结果被现实教育得很惨。在实盘前有几个问题必须先用代码和逻辑验证清楚。4.1 参数稳健性检验怎么做前面提到网格搜索和参数平原这一步是检验策略“真身”的关键环节。实际操作上我会把参数组合的输出整理成一张透视表横轴是短窗口纵轴是长窗口值放夏普比率或最大回撤然后看热力图的形态。results [] for short_win in range(3, 21): for long_win in range(20, 121): if long_win short_win: continue df_tmp run_backtest(df, short_winshort_win, long_winlong_win) stats performance_stats(df_tmp[nav], df_tmp[benchmark_nav]) results.append({ short_win: short_win, long_win: long_win, annual_return: stats[年化收益率], max_drawdown: stats[最大回撤], sharpe: stats[夏普比率], }) result_df pd.DataFrame(results) pivot result_df.pivot_table(indexlong_win, columnsshort_win, valuessharpe) print(pivot)这一跑可能几秒钟就能出结果。热力图要不要画取决于你是否想更直观地看出“平原区”。但无论画不画原则是选参数中心落在平原区的组合而不是孤峰顶点的组合。另外更严格的做法是把数据分成样本内和样本外两个区间在样本内选参数在样本外验证。样本外的表现可能比样本内稍差但如果差得离谱那策略就不值得实盘。4.2 模拟撮合与交易约束回测是理想化的现实交易有一堆约束。A股最典型的就是 T1当天买入的股票当天不能卖出。如果你在策略里做了“当天开仓当天平仓”的操作回测会发生实际交易中根本不可能发生的交易。T1 在回测里怎么模拟简单的方法是记录每一笔买入日期约束卖出日期必须晚于买入日期至少一个自然日。更复杂的情况还有涨跌停限制涨停板的股票你买不进去跌停板的股票你卖不出去。真实盘手知道涨停时排单买进难如登天跌停时挂单卖出同样困难。如果你的策略频繁在涨跌停位置触发信号说明信号逻辑本身对极端行情缺乏应对回测收益会明显虚高。我给自己的要求是回测收益至少要打八折才是心理上可预期的实盘收益。这个不严谨但能从预期管理上避免“回测猛如虎实盘亏成狗”的落差。4.3 仓位管理与止损很多人的第一个策略都是“全仓进出”——信号来了满仓干信号走了清仓走人。这样做在回测里很爽但实盘一个 20% 的回撤就足以让你心态崩掉然后做出一连串错误操作。仓位管理不是玄学有几类简单可落地的方案方案逻辑适用场景固定比例仓位每次按总资金的固定比例下注如10%初学者最推荐波动率目标仓位根据标的近期波动率动态调整仓位有一定经验的用户凯利公式仓位根据胜率与盈亏比计算最优下注比例需要较长样本支撑我个人的偏好是“固定比例仓位 单笔最大亏损限制”。比如总资金 100 万单笔投资不超过 20%单笔亏损达到总资金的 2% 就无条件止损。止损的逻辑不是为了让你赚钱而是为了让你在这个市场里活得更久。回测里可以反复试错实盘里一次大亏就可能让你失去继续参与的资格。止损策略要写进回测代码不要在心里止损。比如双均线策略可以在持仓期间设置一个“移动止损”如果价格从持仓以来的最高点回撤超过 8%就强制平仓哪怕均线还没有发出卖出信号。这个逻辑可以用几行代码实现关键是要让每一次决策都可被检验。4.4 读懂绩效指标不被单一年化收益迷惑我见过有人发策略第一句话就是“年化收益率 300%”。这种话只能说明他对风险毫无概念。评价策略好坏要看一组指标的组合而不是单独某一个数字。指标含义我的参考经验年化收益率每年平均复利收益越高越好但要结合回撤看最大回撤历史最高点到最低点的跌幅越小越好超过30%容易拿不住夏普比率每承担一单位风险换来的超额收益大于1可接受大于2算优秀胜率盈利交易占比趋势策略胜率往往不高但赚大亏小盈亏比平均盈利/平均亏损与胜率此消彼长需要平衡对于大多数稳健策略我会要求夏普比率大于 1最大回撤控制在 20% 以内盈亏比至少 1.5 以上。如果这三个指标不达标即使年化收益看起来不错实盘也很难拿住。5. 从单标的到策略体系把代码变成可持续迭代的研究工具单只股票、单一参数组合的回测只是起点。当你确认一个策略逻辑扎实之后下一个问题就是它在更多品种、更长周期上是否依然有效只有经历过多标的、多周期的检验你才敢说这个策略有“一般性”而不是某个股票的偶然巧合。5.1 多标的批量回测与汇总把单标的回测包装成函数之后批量跑多个标的就非常简单了。核心逻辑是遍历股票列表分别回测再把每次的绩效指标汇总到一张大表里。symbols [000001, 600000, 000002, 600519, 000858] all_stats [] for symbol in symbols: single_df fetch_stock_data(symbol) # 复用之前的拉数函数 bt_df run_backtest(single_df, short_win5, long_win20) s performance_stats(bt_df[nav], bt_df[benchmark_nav]) s[symbol] symbol all_stats.append(s) summary pd.DataFrame(all_stats).set_index(symbol) print(summary)如果跑下来 5 只股票里 4 只亏损只有 1 只靠运气赚钱那说明策略的普适性很差不建议继续投入精力。如果多数股票都能跑出正收益且回撤水平基本可控策略才具备继续迭代的价值。我在这个阶段会特别关注“最差标的”的表现。均值好看没有意义最差的那个标的如果在回撤和收益上都不像话实盘中你很可能刚好选中它。所以批量回测的价值不只是证明策略有效更是在帮你看清楚策略的脆弱边界在哪里。5.2 框架选择的进阶逻辑自写还是背靠轮子自己手写回测引擎跑到这一步你已经对回测的每个环节都有了直观理解。接下来要不要换 backtrader 或 vectorbt取决于你接下来要做多复杂的策略。backtrader 的优点是事件驱动模型更接近真实交易逻辑社区生态完善支持多标的、多策略组合缺点是学习成本高数据格式有自己的一套规范调试起来不如手写直观。vectorbt 的优点是用 numpy 做向量化计算大规模参数扫描非常快缺点是它的编程思维和传统回测差异较大不适合复杂的事件逻辑。我的个人经验是分三步走第一步用手写回测跑通单标的策略理解所有细节第二步当策略扩展到持仓管理、止损、组合等复杂逻辑时迁移到 backtrader利用它的订单管理能力第三步当需要大量参数寻优时用 vectorbt 做快速扫描扫描结果回到手写框架里做深度验证。框架是工具不是目的。用最顺手的工具解决问题比追随流行的框架重要得多。5.3 策略上线的最小闭环很多教程停留在回测阶段但我知道大多数人的目标是想实盘跑起来。策略上线不需要一上来就接券商的交易接口最小闭环可以很简单每日更新数据定时任务拉取当日行情追加到本地数据库。运行策略脚本计算当天的信号输出“建议持仓状态”。人工复核与执行根据策略输出的信号手动下单记录实际成交价。记录日志把每次信号、成交、持仓变化都记录下来方便复盘。实盘初始阶段我强烈建议用小资金测试同时把策略输出当成“参考信号”而不是“交易指令”。这个过程能让你积累真实市场中的执行经验——你会发现信号在开盘那一瞬间的价格冲击、网络延迟、滑点都是回测里无法完全模拟的。如果后面想接入程序化交易就要研究券商的官方 API申请相应的接口权限并且做好异常处理、断线重连、风控熔断这些工程层面的东西。这部分更接近软件开发而不是策略研究但如果你想长期做这件事是绕不开的。5.4 后面还能玩什么策略迭代的边界与方向双均线只是一个起点它解决的是“如何把策略实现出来”的问题。当你掌握了全流程可以尝试的方向包括但不限于加入过滤条件比如用 RSI、成交量、波动率来过滤虚假信号减少震荡市的无效交易。多周期共振日线信号和周线信号方向一致时再入场提高胜率。多因子打分把动量、价值、波动率等多个因子合成一个综合评分按评分排序选股。风险平价与组合管理不押注单一标的而是通过多标的、低相关性组合来降低整体风险。机器学习模型的“数理层面应用”用模型做特征筛选或收益预测但这需要更长样本和更严谨的统计检验不是套几个函数就能上实盘的。这里要提醒一点不要把策略越做越复杂回测效果就会越好。复杂模型过拟合的概率远高于简单模型我在实际研究中对“复杂模型”的一贯态度是——先用简单模型把问题定义清楚再逐步增加复杂度每一步都要看在样本外是否真正带来了改进。最后分享一条我自己的实操习惯每研究一个新策略我都在本地建一个experiment_日期的目录里面只放三个文件——策略代码、回测结果表、一张净值图。三个月后回来看哪些策略值得继续迭代、哪些只是过拟合的产物一目了然。量化策略实现这条路比的不是谁短期收益高而是谁的研究流程更严谨、更能持续复现。把这套流程跑顺了你才算真正入了量化研究的大门。
阅读完成 · 觉得有帮助?
咨询建站