你有没有过这样的经历——想学量化交易,但一看到那些动辄几千上万的付费数据源、复杂的回测框架和云服务器账单,就觉得门槛太高,还没开始就想放弃了?或者,你已经用Excel手动算过几次策略,但每次想调整参数、测试不同周期,都得重新整理数据、重写公式,过程繁琐到让人失去耐心?
我最初接触量化时,也卡在这个阶段。直到后来想明白一件事:量化回测的核心,不是寻找“圣杯”策略,而是搭建一个能让你快速验证想法、迭代策略的“实验环境”。这个环境必须足够轻量、免费、可控,让你能把精力集中在策略逻辑本身,而不是浪费在配置环境和解决依赖上。
今天要聊的,就是如何用完全免费的工具和开源库,在你自己电脑上搭建一套“双击即看”的量化回测系统。这套系统的所有数据都来自免费且稳定的接口,所有代码都是可运行、可修改的Python源码。它不追求覆盖所有复杂场景,但足以让你跑通从数据获取、策略编写、回测分析到可视化展示的完整链路。更重要的是,它能帮你建立起对量化工作流最直观的认知:数据怎么来、策略怎么写、回测怎么跑、结果怎么看。
1. 为什么“免费”和“双击即看”对量化入门如此重要?
在深入代码之前,我们需要先达成一个共识:对于量化入门者而言,第一个系统的核心价值是“降低启动摩擦力”,而不是“提供生产级性能”。
很多人一开始就陷入误区,去研究那些需要复杂配置、商业许可、高频数据处理的专业框架。结果往往是环境还没配好,热情就已经耗尽了。这就像学开车,你应该先找辆能开动的教练车熟悉方向盘和油门,而不是直接去研究F1赛车的空气动力学。
一个理想的入门级回测系统,应该满足三个最低要求:
- 零成本启动:不需要为数据、软件或云资源付费。
- 环境简单:最好能用最普及的工具(如Python)和最常见的库,避免冷门依赖。
- 流程闭环:从数据到策略再到图表,整个链路能在一个脚本或简单工程里跑通,形成正反馈。
“双击即看”正是这种理念的体现。它意味着你拿到源码后,只需要最基础的Python环境,运行一个脚本,就能看到从数据下载、回测计算到图表输出的全过程。这种即时反馈,是保持学习动力的关键。
那么,数据从哪里来?这就是我们接下来要解决的核心问题。
2. 数据源选型:为什么是akshare和baostock?
数据是量化的基石。对于免费系统,数据源的选择直接决定了系统的可行性和稳定性。我们的核心诉求是:免费、稳定、易获取、覆盖A股主要数据。
经过对比和长期使用,我选择了两个开源库作为数据支柱:akshare和baostock。这不是说它们完美无缺,而是在免费、中文友好和功能覆盖上达到了一个很好的平衡。
2.1 akshare:你的“量化数据瑞士军刀”
如果你只能用一句话记住akshare,那就是:它试图用统一的接口,聚合大量散落在各处的免费金融数据源。
它的设计思路很直接——为每一个数据需求(如股票日线、财务报表、宏观经济、新闻舆情)提供一个对应的函数。你不需要关心数据具体来自哪个网站,也不需要写爬虫去解析HTML,调用函数就能拿到结构化的DataFrame。
它的核心优势:
- 覆盖面极广:从股票、基金、期货、期权,到宏观经济、行业分类、新闻事件,甚至社交媒体情绪数据,几乎涵盖了量化研究可能用到的所有免费数据维度。
- 接口统一:无论什么数据,返回的基本都是Pandas DataFrame,大大降低了后续数据处理的复杂度。
- 社区活跃:作为一个开源项目,它持续更新,能较快地适应一些数据源接口的变化。
需要注意的边界:
- 稳定性依赖上游:akshare本身不生产数据,它是数据的“搬运工”。如果某个原始网站改版或限制访问,对应的接口可能会暂时失效。这是所有免费数据源都要面对的风险。
- 不适合高频场景:它的数据更新频率通常是日级或分钟级(有限),且调用时有网络请求,无法用于需要Tick级数据或极低延迟的高频交易研究。
- 需要处理网络环境:由于需要访问外部网站,在某些网络环境下可能需要配置。
对于入门回测,akshare在日线数据、基本面数据、板块数据的获取上,完全够用。它的价值在于让你摆脱“找数据”的琐碎,快速进入“用数据”的阶段。
2.2 baostock:专注而稳定的A股历史数据服务
如果说akshare是“广”,那么baostock就是“专”。它由深圳证券交易所旗下公司运营,专注于提供A股历史行情数据。
它的核心优势:
- 数据质量相对规整:作为官方背景的服务,其提供的股票日/周/月线、复权因子等数据格式统一,历史数据完整度较好。
- 接口稳定:采用登录-查询-退出的客户端模式,连接相对稳定,提供了Python、R等多种语言的API。
- 免费提供复权数据:计算回测收益时,复权价格至关重要。baostock直接提供了前复权、后复权价格,省去了自己计算的麻烦。
需要注意的边界:
- 数据范围聚焦A股:主要就是沪深京市的股票、指数数据,不覆盖港股、美股或其他资产。
- 非实时数据:同样适用于历史回测,而非实盘交易。
- 需要登录:使用前需要调用
login()函数进行登录(免费注册即可)。
在实际搭建中,我通常会以baostock作为日线行情数据的主数据源,因为它稳定、规整。同时用akshare作为补充,获取基本面指标、板块分类、资金流向等维度数据,用于丰富策略逻辑。
2.3 数据获取的通用流程与避坑指南
无论使用哪个库,数据获取的代码模式是相似的,但有几个关键点决定了后续流程是否顺畅:
- 先定义时间范围和标的:不要一上来就下载全部数据。先明确你要测试的策略时间段和股票池(例如,2020-2023年的沪深300成分股)。
- 处理复权:这是回测准确性的生命线。股价会因为分红、送股而产生缺口,必须使用复权价格进行计算,否则收益计算会严重失真。baostock的
query_history_k_data函数可以直接指定adjustflag参数(2为前复权,3为后复权)。如果使用其他未提供复权价的数据源,务必获取复权因子并自行计算。 - 统一数据格式:确保从不同接口获取的数据,其日期列名(如
‘date’)、价格列名(如‘close’)是统一的,并转换为datetime类型和float类型,方便后续合并与计算。 - 本地化存储:不要每次回测都重新下载数据。将下载好的数据以
CSV或Parquet格式保存到本地,并建立简单的版本管理(如按下载日期命名文件夹)。这能极大提升回测效率,也是对数据源不稳定的一种缓冲。
# 示例:使用baostock获取单只股票前复权日线数据并保存 import baostock as bs import pandas as pd # 登录 lg = bs.login() # 显示登录返回信息 print('login respond error_code:'+lg.error_code) print('login respond error_msg:'+lg.error_msg) # 获取沪深300成分股列表(示例,实际可从akshare获取更全面的列表) stock_list = ['sh.600000', 'sz.000001'] # 示例股票代码 all_data = [] for code in stock_list: rs = bs.query_history_k_data_plus(code, "date,code,open,high,low,close,preclose,volume,amount,adjustflag,turn,tradestatus,pctChg,peTTM,pbMRQ,psTTM,pcfNcfTTM,isST", start_date='2020-01-01', end_date='2023-12-31', frequency="d", adjustflag="2") # adjustflag="2" 前复权 # 将数据转换为DataFrame data_list = [] while (rs.error_code == '0') & rs.next(): data_list.append(rs.get_row_data()) df = pd.DataFrame(data_list, columns=rs.fields) df['date'] = pd.to_datetime(df['date']) # 将数值列转换为float for col in ['open', 'high', 'low', 'close', 'volume', 'pctChg']: df[col] = pd.to_numeric(df[col], errors='coerce') all_data.append(df) # 退出 bs.logout() # 合并并保存 if all_data: combined_df = pd.concat(all_data, ignore_index=True) combined_df.to_csv('./data/stock_daily_data.csv', index=False) print(f"数据已保存,共{len(combined_df)}行")注意:在实际项目中,你需要添加更完善的错误处理(如网络重试)、进度提示,以及将不同股票的数据分开存储,以便增量更新。
3. 构建核心回测引擎:从简单的“向量化回测”开始
有了数据,下一步就是让策略在历史数据上“跑”起来,这就是回测。对于入门者,我强烈建议从向量化回测开始,而不是一上来就模仿实盘的事件驱动回测。
为什么?向量化回测的核心思想是:将整个时间序列数据(如每日收盘价)看作一个向量(数组),策略信号和交易逻辑也通过向量(数组)运算来表达。它一次性对整个历史周期进行计算,思路更贴近数据分析。
事件驱动回测则模拟真实交易,逐个处理“时间点”上的事件(如开盘、收盘、订单),更贴近实盘,但复杂度也高得多。
对于大多数基于日线数据的初级策略(如均线交叉、动量、突破),向量化回测完全够用,且代码直观,易于调试和理解。它能快速回答“这个想法历史上有没有效”这个核心问题。
3.1 一个最简单的双均线策略回测示例
让我们用代码实现一个经典的双均线策略(金叉买入,死叉卖出),并计算其收益。假设我们已经有了包含date,code,close列的DataFramedf,并且已经按date排序。
import pandas as pd import numpy as np def vectorized_backtest(df, short_window=5, long_window=20): """ 简单的向量化双均线策略回测 :param df: 包含日期、代码、收盘价的数据框,单只股票 :param short_window: 短期均线周期 :param long_window: 长期均线周期 :return: 添加了信号和持仓的df,以及累计收益率 """ # 计算均线 df['SMA_short'] = df['close'].rolling(window=short_window).mean() df['SMA_long'] = df['close'].rolling(window=long_window).mean() # 生成交易信号:1为买入(金叉),-1为卖出(死叉),0为持有 # 金叉:短期均线上穿长期均线 df['signal'] = 0 df.loc[df['SMA_short'] > df['SMA_long'], 'signal'] = 1 df.loc[df['SMA_short'] < df['SMA_long'], 'signal'] = -1 # 为了得到准确的交叉点,我们计算信号的变化点(从1变-1或从-1变1) df['position'] = df['signal'].replace(0, method='ffill').shift(1).fillna(0) # position=1表示持有股票,position=0表示持有现金 # 计算每日收益率(股价涨跌幅) df['returns'] = df['close'].pct_change() # 计算策略每日收益率:当持仓为1时,获得市场收益;为0时,收益为0(忽略利息) df['strategy_returns'] = df['position'] * df['returns'] # 计算累计收益 df['cumulative_market_returns'] = (1 + df['returns']).cumprod() df['cumulative_strategy_returns'] = (1 + df['strategy_returns']).cumprod() return df # 假设df是某只股票的数据 # df = pd.read_csv('your_stock_data.csv') # df['date'] = pd.to_datetime(df['date']) # df = df.sort_values('date').reset_index(drop=True) # 运行回测 # result_df = vectorized_backtest(df) # print(result_df[['date', 'close', 'SMA_short', 'SMA_long', 'position', 'cumulative_strategy_returns']].tail())这段代码虽然简单,但包含了向量化回测的所有关键要素:
- 数据准备:确保数据按时间排序。
- 指标计算:在整列数据上滚动计算均线。
- 信号生成:通过向量比较产生买卖信号。
- 持仓管理:将信号转化为持仓状态(这里用了前向填充和滞后一期,避免未来函数)。
- 收益计算:根据持仓计算策略每日和累计收益。
3.2 必须警惕的“未来函数”陷阱
在编写信号逻辑时,未来函数是最大的坑。它指的是在时间点t,使用了t时刻之后才能获得的信息。在回测中,这会导致结果严重虚高,因为策略“预知”了未来。
如何避免?
- 严格使用
.shift():当你用t时刻的数据生成信号,并决定在t+1时刻交易时,在计算收益时,一定要用df['position'].shift(1)来对齐。上面的示例已经体现了这一点。 - 仔细检查指标计算:例如,计算
t日的20日均线,理论上应该使用t-19日到t日的数据。pandas的rolling().mean()默认是包含当前行的,这在回测中是允许的,因为收盘后即可计算。但要确保没有用到t+1日的数据。 - 回测时点模拟:在脑海中模拟:在每个交易日收盘后,我根据截至当天的所有历史数据,能做出什么交易决策?这个决策只能在下一个交易日执行。
3.3 从单只股票到股票池:向量化思维的延伸
上面的例子是针对单只股票的。对于一个多股票的策略(如行业轮动、选股),向量化回测依然高效。核心思路是:
- 将数据构造成一个以
日期为索引、股票代码为列的多维DataFrame或Panel(虽然Panel已弃用,但可以用多层索引的DataFrame)。 - 对所有股票同时计算指标、生成信号。
- 在每一天,根据信号对所有股票进行排序、筛选,决定持仓组合。
- 计算整个股票池的日度组合收益。
这部分的代码会复杂一些,需要熟练使用pandas的groupby、pivot、unstack等操作。但逻辑内核不变:避免循环,用数组(向量)运算代替。
4. 绩效分析与可视化:让结果自己“说话”
回测跑出曲线只是第一步,更重要的是解读。一个策略光看最终收益率是远远不够的,必须从多个维度评估其表现和风险。
4.1 必须计算的几个核心绩效指标
年化收益率:将总收益率换算成年均水平,便于比较。
total_days = len(result_df) years = total_days / 252.0 # 假设一年252个交易日 cum_ret = result_df['cumulative_strategy_returns'].iloc[-1] annual_return = (cum_ret ** (1/years)) - 1年化波动率:衡量策略收益的波动程度,即风险。
daily_returns = result_df['strategy_returns'] annual_volatility = daily_returns.std() * np.sqrt(252)夏普比率:最常用的风险调整后收益指标。表示每承受一单位风险,能获得多少超额回报。通常假设无风险利率为0。
sharpe_ratio = annual_return / annual_volatility最大回撤:策略从前期高点跌到最低点的最大跌幅。这是衡量策略下行风险和投资者心理承受能力的关键指标。
cumulative = result_df['cumulative_strategy_returns'] running_max = cumulative.expanding().max() drawdown = (cumulative - running_max) / running_max max_drawdown = drawdown.min()胜率:盈利交易次数占总交易次数的比例。
盈亏比:平均盈利与平均亏损的比值。
4.2 用可视化建立直观认知
数字是抽象的,图表是直观的。至少生成三张图:
策略净值 vs 基准净值曲线:这是最重要的图。将你的策略累计收益和某个基准(如沪深300指数)的累计收益画在一起,直观对比。
import matplotlib.pyplot as plt plt.figure(figsize=(12,6)) plt.plot(result_df['date'], result_df['cumulative_strategy_returns'], label='Strategy') plt.plot(result_df['date'], result_df['cumulative_market_returns'], label='Benchmark (Buy & Hold)') plt.title('Strategy vs Benchmark Cumulative Returns') plt.xlabel('Date') plt.ylabel('Cumulative Returns') plt.legend() plt.grid(True) plt.show()回撤曲线图:展示策略历史上每一次回撤的深度和持续时间。
plt.figure(figsize=(12,4)) plt.fill_between(result_df['date'], drawdown, 0, drawdown < 0, color='red', alpha=0.3) plt.plot(result_df['date'], drawdown, color='red', linewidth=1) plt.title('Strategy Drawdown') plt.xlabel('Date') plt.ylabel('Drawdown') plt.grid(True) plt.show()月度收益热力图:分析策略收益的季节性或月度效应。
# 将日收益数据重采样为月收益 result_df['year_month'] = result_df['date'].dt.to_period('M') monthly_returns = result_df.groupby('year_month')['strategy_returns'].apply(lambda x: (1+x).prod()-1) # 将月度收益转换为数据透视表格式,用于热力图 # ... (此处略去数据重塑代码) # 使用seaborn绘制热力图
4.3 可视化不是装饰,是分析工具
不要为了好看而画图。每张图都应该能回答一个具体问题:
- 净值曲线:我的策略长期是否跑赢了简单持有?它在牛熊市的表现如何?
- 回撤图:我最大可能亏多少钱?回撤持续了多久?我能否承受?
- 月度热力图:我的策略在某些月份是否持续失效?是否有季节性规律?
通过图表发现的问题,要回头去检查策略逻辑、参数,或者思考市场环境的变化。这是一个不断迭代的过程。
5. 从“能跑”到“好用”:工程化与迭代的思考
当你成功运行了第一个回测,看到了第一条净值曲线后,这个“双击即看”的系统就完成了它的启蒙使命。接下来,你需要思考如何让它从一个脚本进化成一个“好用”的工具,以支持更复杂的策略迭代。
5.1 模块化设计:让代码可维护
最初的脚本可能把所有功能都写在一个文件里。随着策略变复杂,你需要拆分:
data_fetcher.py:负责从akshare、baostock获取数据,并保存到本地数据库或文件。strategy.py:定义策略类,包含信号生成的核心逻辑。backtest_engine.py:包含回测引擎,负责运行策略、计算持仓和收益。performance.py:负责计算夏普比率、最大回撤等指标。plotting.py:负责所有可视化功能。config.py:集中管理参数,如股票池、回测周期、策略参数等。main.py:主程序,串联整个流程。
这样,当你需要测试一个新策略时,只需要修改或新建一个strategy.py,其他模块可以复用。
5.2 参数优化与过拟合陷阱
当你发现一个策略有效时,很自然地会想:“如果调整一下均线周期,结果会不会更好?”于是开始尝试不同的参数组合。这就是参数优化。
一个简单的网格搜索示例:
def optimize_parameters(df): results = [] for short in range(5, 20, 5): for long in range(20, 100, 20): if short >= long: continue result_df = vectorized_backtest(df, short, long) # 计算绩效指标,如年化夏普 # ... sharpe = calculate_sharpe(result_df) results.append({'short': short, 'long': long, 'sharpe': sharpe}) return pd.DataFrame(results)但是,必须极度警惕过拟合!过拟合是指策略在历史数据上表现极好,仅仅是因为它恰好“记住”了那段历史的噪声,而非抓住了有效规律,导致在未来实盘中失效。
如何规避?
- 样本外测试:将历史数据分为两段:训练集(用于优化参数)和测试集(用于验证结果)。绝对不能用测试集的数据做任何参数优化。
- 交叉验证:将数据分成多段,轮流用其中一部分测试,观察策略在不同时段是否稳定。
- 保持参数简单:不要使用过多的参数或过于复杂的规则。一个需要几十个参数才能“工作”的策略,大概率是过拟合的。
- 逻辑优先于曲线:策略的核心理念应该有经济学或行为金融学的逻辑支撑,而不是纯粹的数据挖掘。问问自己:这个策略为什么应该赚钱?它的逻辑在未来还会成立吗?
5.3 理解免费系统的边界,规划下一步
我们搭建的这套免费系统,其边界非常清晰:
- 适用:个人学习、策略思路验证、基于日/周线数据的低频策略研究。
- 不适用:高频交易、实盘交易(缺乏实时数据、订单执行、风控模块)、需要极端精确的计算(如考虑滑点、手续费、冲击成本)。
当你用这个系统验证了一个有潜力的想法后,下一步的进阶路径可能是:
- 引入更精细的成本模型:在回测中加入手续费、印花税、滑点(假设成交价与预期价的偏差)。
- 探索更专业的回测框架:如
Zipline,Backtrader,PyAlgoTrade等。它们提供了更严谨的事件驱动模型、更丰富的订单类型和风险控制模块。但学习成本也更高。 - 连接模拟交易:使用一些券商提供的模拟交易API,在实时的模拟环境中测试策略,感受市场的微观结构。
- 学习风险管理:如何设置止损、如何控制仓位、如何分散投资,这些是比寻找“圣杯”策略更重要的事。
最终,这套免费系统的最大价值,不是给你一个赚钱的策略,而是给你一个完整的、可触摸的量化认知框架。你知道了数据从哪来、怎么处理;知道了策略怎么写、怎么回测;知道了绩效怎么看、怎么评价。你亲手走通了从想法到验证的完整闭环。这个过程中培养的数据处理能力、逻辑思维能力和对市场的理解,远比某一段漂亮的回测曲线更有价值。
所以,不要停留在仅仅运行一遍源码。去修改它,用你自己的策略想法替换掉双均线;去扩展它,尝试加入基本面指标;去质疑它,思考这个策略的弱点在哪里。这个系统是你的实验室,而最好的学习,永远发生在你亲自动手实验的时候。