news 2026/10/12 14:24:05

机器学习量化投资实战:从数据到回测的完整策略搭建指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
机器学习量化投资实战:从数据到回测的完整策略搭建指南

简介:一套帮助初学者快速入门的机器学习量化投资演示源码,以A股市场为例,面向已有一定Python基础、但对炒股和量化投资了解不深的读者。量化投资的核心是通过历史数据分析发现并利用市场规律,在风险可控前提下追求超额收益;机器学习模型具备自我学习与适应能力,可提升预测准确性,策略本质是跟随趋势而非创造趋势。代码完整覆盖数据获取、缺失值清洗、特征工程、模型训练与回测评估等环节,并融入止损设置、仓位控制等风险管理思想。压缩包共14个文件,含5个Python脚本(分别承担数据读取、特征构建、模型建模、回测执行和主流程串联)、6张可视化图表(K线、决策树预测、最大回撤曲线等)、2个文本说明文档及依赖清单,整体仅727KB,轻量易读。已有156人学习下载;通过运行示例,读者可直观学会处理金融时间序列数据、应用常见机器学习算法预测市场走势,并通过图表对照回测效果,是一份实操性很强的入门参考。

1. 机器学习加量化投资:这可能是你最快拿到一份完整策略的方式

把「基于机器学习的量化投资策略」做成一个 demo,最大的价值不是帮你躺赚,而是用最小的成本把「数据 → 特征 → 模型 → 回测」这条链路完整走一遍。很多人卡在第一步是因为资料太散,看了一堆模型原理却不知道从哪下手;而这份 demo 源码的意义在于,它给了一个可以直接改的骨架:换数据源、调特征、换模型、改回测规则,每一步都是透明的。适合三类人:想入门量化但没写过完整策略的工程师、手里有数据但不知道怎么跟机器学习结合的从业者,以及想快速验证一个想法再决定是否投入的独立开发者。这篇笔记就按我实际搭建这类 demo 的顺序,从数据、特征、模型到回测逐个拆开讲,最后把坑也一并交代清楚。

2. 从数据到特征:搭一个能跑通的机器学习量化 demo 骨架

2.1 数据源选型:免费数据也有讲究,别一上来就搞高频

做 demo 第一个要解决的问题是数据从哪来。常见做法是用 akshare 或 tushare 这类免费接口拉日线行情,没必要一开始就上高频或付费数据。akshare 的好处是免注册、接口多,拉 A 股日线数据几行代码就能完成,适合做原型;tushare 需要 token,但数据规整度更好,如果后续要加大规模,可以再迁过去。

我一般会用 akshare 先拉一只股票的历史日线,把数据存成 parquet 格式,避免每次跑 demo 都重新请求接口。这里有个细节:接口返回的列名是中文,比如「收盘」「开盘」「成交量」这些,做特征工程前最好先统一改成英文字段名,后面写代码能少很多麻烦。

import akshare as ak import pandas as pd # 拉取平安银行 2018 年至今的日线数据,前复权 df = ak.stock_zh_a_hist( symbol="000001", period="daily", start_date="20180101", end_date="20241231", adjust="qfq", ) # 统一改成英文字段名,方便后续处理 df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume", }) df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date").sort_index() # 只保留需要的列 df = df[["open", "high", "low", "close", "volume"]] df.to_parquet("stock_000001.parquet") print(df.head())

这段代码里,adjust="qfq"表示前复权,这是做历史回测必须的,否则分红送股会让价格出现跳空,模型学到的全是假信号。sort_index()保证时间序列从早到晚排列,后面做滚动切分时才不会乱。存成 parquet 而不是 csv,是因为 parquet 带类型信息、读取快,而且文件体积小。

需要注意:akshare 接口偶尔会变,返回的列名可能调整,跑之前先print(df.columns)看一眼。另外免费接口有频率限制,demo 阶段拉几十只票没问题,要是拉全市场,就得考虑本地缓存或者换付费源了。

2.2 特征工程:先把技术指标和目标变量之间的逻辑理顺

有了原始行情,下一步是造特征。常见做法是把价格和成交量加工成三类信号:动量类(过去 N 日涨跌幅)、波动类(过去 N 日收益标准差)、量价类(成交量与均量的比值)。这三类特征基本覆盖了趋势跟踪和均值回归两类主流策略的信息需求。

import numpy as np df["ret_1"] = df["close"].pct_change(1) # 最近 1 日收益 df["ret_5"] = df["close"].pct_change(5) # 最近 5 日收益 df["ret_10"] = df["close"].pct_change(10) # 最近 10 日收益 df["ret_20"] = df["close"].pct_change(20) # 最近 20 日收益 df["vol_10"] = df["ret_1"].rolling(10).std() # 10 日波动率 df["vol_20"] = df["ret_1"].rolling(20).std() # 20 日波动率 df["vol_ratio"] = df["volume"] / df["volume"].rolling(20).mean() # 量比 df["high_low_range"] = (df["high"] - df["low"]) / df["close"] # 日内振幅 df["close_open_range"] = (df["close"] - df["open"]) / df["open"] # 开盘收盘相对位置 # 删除 NaN 行 df = df.dropna()

pct_change()算的是收益率而非价格差,这样对价格绝对水平不敏感,不同股票之间的特征才能横向对比。rolling().std()是滚动标准差,用来刻画波动;vol_ratio大于 1 说明放量,小于 1 说明缩量,这是量价配合的核心指标。

特征这块有个容易被忽略的坑:所有特征计算只能用 t 时刻及之前的数据,不能用未来信息。比如计算 5 日动量,用的必须是close在 t-4 到 t 的数据,而不能包含 t+1 之后的价格。代码里pct_change(5)在 t 时刻的值天然只用到过去 5 天,这是安全的。但如果你手写特征,比如用shift(-n)把未来数据挪进来,那训练出来的模型就是典型的「未来函数」,回测净值曲线会漂亮得骗人。

特征维度上,demo 阶段 8 到 10 个特征足够了,不需要堆上百个。特征太多会让模型过拟合历史噪音,而且训练时间变长。先把这些基础特征跑通,后面要加量价相关性、RSI、MACD 这些指标,都是在df上加一列的事。

3. 模型选型与训练:为什么 demo 首选 LightGBM 而不是 LSTM

3.1 先定预测目标:预测涨跌方向,比预测价格更靠谱

量化策略里机器学习通常做两件事:预测未来价格(回归)或者预测涨跌方向(分类)。做 demo 我强烈建议先做分类——预测未来 5 个交易日收益是否大于某个阈值。原因很简单:价格预测容易过拟合,而且哪怕预测准了,买卖点也难落地;分类问题不仅好评估,还能直接映射成买入/不买入的决策。

标签怎么打?用「未来收益 > 阈值」来标。注意一定是「未来」,也就是要用 t+5 的收盘价相对于 t 时刻的收益,这需要把未来数据往前挪,但要保证挪完之后和特征严格对齐。

# 定义标签:未来 5 日收益 > 2% 记为 1,否则为 0 df["future_ret"] = df["close"].shift(-5) / df["close"] - 1 df["label"] = (df["future_ret"] > 0.02).astype(int)

shift(-5)是把未来第 5 天的数据拿到当前行。这里要特别注意:打完标签后,最后 5 行会因为未来数据缺失而变成 NaN,需要dropna()删掉。阈值 2% 不是定死的,它决定正负样本的比例——如果设置太高,样本里 1 太少,模型学不到东西;设置太低,预测准确率虚高但没有实际交易价值。A 股日线级别上,5 日涨 2% 大约对应 40% 左右的正样本比例,是个比较均衡的起点。

3.2 时序切分而不是随机切分:这是训练里最重要的一步

训练集和测试集的划分,股票数据和常规机器学习有本质区别。随机切分会把未来数据混进训练集,模型在测试集上表现好,是因为它「见过」测试集时间段附近的走势。正确的做法是按时间点切分:早段训练,晚段测试,中间绝对不能有重叠。

split_date = "2023-01-01" feature_cols = ["ret_1", "ret_5", "ret_10", "ret_20", "vol_10", "vol_20", "vol_ratio", "high_low_range", "close_open_range"] X = df[feature_cols] y = df["label"] train_mask = df.index < split_date test_mask = df.index >= split_date X_train, X_test = X[train_mask], X[test_mask] y_train, y_test = y[train_mask], y[test_mask]

我一般会把切分日期放在数据总量后 20% 到 30% 的位置,比如 2018 到 2024 的数据,切分点设在 2023 年,正好留下近两年的样本做验证。这样训练集覆盖牛熊周期,测试集则是模型「没见过」的最新行情,评估结果更有说服力。另外切分时最好用日期篝索引直接过滤,不要用iloc的整数位置,因为后期如果删除了中间的行,位置就全错位了。

3.3 LightGBM 参数初始化:从一组稳健的默认值出发

树模型是结构化数据的首选,LightGBM 训练快、内存占用小,而且对特征缩放不敏感,不需要做标准化,特别适合量化这种表格特征场景。LSTM 这类神经网络不是不行,而是 demo 阶段数据量小、调参成本高,收益不成正比。

import lightgbm as lgb model = lgb.LGBMClassifier( n_estimators=300, # 树的数量 learning_rate=0.05, # 学习率 num_leaves=31, # 每棵树的叶子数 max_depth=5, # 树的最大深度 subsample=0.8, # 样本采样比例,防过拟合 subsample_freq=1, # 每个迭代都采样 colsample_bytree=0.8, # 特征采样比例 min_child_samples=50, # 叶子最少样本数 reg_alpha=0.1, # L1 正则 reg_lambda=0.1, # L2 正则 random_state=42, ) model.fit( X_train, y_train, eval_set=[(X_test, y_test)], eval_metric="auc", callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)], )

上面这组参数是一套比较稳的起点。subsample和colsample_bytree都是 0.8,相当于每次迭代只用 80% 的样本和 80% 的特征,能显著降低过拟合。min_child_samples=50是防止叶子节点落到太少的样本上,在金融数据这种噪音很大的场景里很重要。early_stopping(50)的意思是测试集 AUC 连续 50 轮不提升就停,省时间的同时也能防止训练过头。

这里有个值得说的点:eval_set传的是测试集,但它的作用只是用来监控过拟合、触发早停,模型并不「学习」测试集。实际做生产级模型时,应该再切一份验证集出来做早停监控,测试集留着最终评估。demo 阶段为了简单可以只用训练/测试两段,但心里要清楚这个简化会带来轻微乐观偏差。

4. 回测与评估:别让净值曲线骗了你

4.1 回测引擎:用向量化操作模拟完整交易流程

模型训练完,真正的考验是回测。最简单的回测不是写循环模拟每天买卖,而是向量化计算——用 Pandas 的shift来对齐信号和收益,一次性算出整个测试期的每日收益。这么做的好处是代码短、速度快,而且不容易在循环里埋 bug。

# 获取模型对测试期的预测概率 test_prob = model.predict_proba(X_test)[:, 1] # 构建测试期 DataFrame test_df = df.loc[test_mask].copy() test_df["pred_prob"] = test_prob test_df["daily_ret"] = test_df["close"].pct_change(1) # 信号:预测概率大于 0.5 就持有,否则空仓 test_df["position"] = (test_df["pred_prob"] > 0.5).astype(int) # 关键:用 shift(1) 实现 T+1 成交,避免用当日信号买当日收益 test_df["strategy_ret"] = test_df["position"].shift(1) * test_df["daily_ret"] test_df["strategy_ret"] = test_df["strategy_ret"].fillna(0) # 计算净值曲线 test_df["net_value"] = (1 + test_df["strategy_ret"]).cumprod() print("最终净值:", test_df["net_value"].iloc[-1])

position.shift(1)是回测里最重要的一行:它把信号滞后一天,模拟的是「今天收盘后产生信号,明天开盘才能买入」的真实约束。如果不做 shift,就等于用当天的信号买当天的收益,这是典型的幸存者偏差,回测收益会被人为放大。A 股是 T+1 交易,当天买的股票最早第二天才能卖,所以信号天然要滞后一拍。

0.5 的阈值也不是必须的。如果预测概率集中在 0.4 到 0.6 之间,可以画一下概率分布图,然后选择一个能让交易次数和胜率平衡的阈值。常见做法是取训练集上预测概率的 60 分位或者 70 分位作为阈值,这样信号更「稀缺」,交易频率下降,但每笔交易的胜率可能更高。

4.2 评估指标:夏普比率、最大回撤和年化收益要一起看

净值曲线给出了一个整体观感,但不能只看最终净值上去还是下来,要看几个硬指标。年化收益衡量赚钱能力,最大回撤衡量风险,夏普比率衡量风险调整后的收益——这三个缺一不可。一个策略年化收益 30% 但回撤 40%,和年化 15% 回撤 8%,后者在实际执行中反而更容易拿住。

# 年化收益 days = len(test_df) strategy_annual_ret = (test_df["net_value"].iloc[-1]) ** (252 / days) - 1 # 最大回撤 rolling_max = test_df["net_value"].cummax() drawdown = test_df["net_value"] / rolling_max - 1 max_drawdown = drawdown.min() # 夏普比率:年化收益 / 年化波动 daily_std = test_df["strategy_ret"].std() sharpe = (test_df["strategy_ret"].mean() - 0) / daily_std * np.sqrt(252) print(f"年化收益: {strategy_annual_ret:.2%}") print(f"最大回撤: {max_drawdown:.2%}") print(f"夏普比率: {sharpe:.2f}")

夏普比率里的0是无风险利率,demo 阶段可以简化为 0,但心里要明白这意味着没有考虑资金成本。当策略的年化收益在 10% 左右波动时,无风险利率的影响就开始变得不可忽视了。另外回撤指标用cummax()算的是「从历史最高点到当前的最低点」,它衡量的是心理和资金链的承受极值——很多策略不是亏没的,而是回撤大到让人拿不住、提前止损出局的。

回测到这里算是「能跑」了,但距离「可信」还有两个额外检查点。第一个是将策略收益和同期沪深 300 指数对比,看超额收益是正的还是负的;第二个是检查交易次数——如果整个测试期只交易了不到 20 次,统计上这个结果没有意义,任何一次大盈亏都会让评估指标失真。这两个检查不需要写复杂代码,打印几行数据就能看出来。

5. 避坑指南:量化 demo 最常见的 5 个翻车现场

5.1 未来函数偷跑了数据:净值曲线好看到不真实

做回测时发现策略净值曲线一路向上、回撤几乎为零,先别高兴。这个现象八成是未来函数导致的。最典型的写法是信号计算用到了当天的收盘价,而持仓也在当天生效——相当于你收盘时看到了结果,再决定这天买不买,这在真实世界是不可能的。排查方法是检查策略收益那行有没有用shift(1)把持仓滞后一天;另一个隐蔽来源是特征计算里用了未来数据,比如用 t+1 的成交量算量比。解决的方式就是我的代码里那行position.shift(1),以及所有特征必须保证只用截至 t 时刻的数据。

5.2 随机切分训练集:模型「见过」未来行情

训练时如果把数据随机打乱再切分,模型会看到测试集时间段附近的行情片段,测试集上的 AUC 会偏高,但实盘完全不是这样。现象是训练时 AUC 0.65,随机切分后测试 AUC 也是 0.65 左右,但一换到按时间切分,测试 AUC 直接掉到 0.52。原因是金融数据是时间序列,相邻日期的特征高度相关,随机切分等于把相似的样本同时放进训练和测试里。解决方式很简单:严格按日期切分训练、测试集,甚至最好在训练集和测试集之间留出几天的空隙,防止滚动窗口特征把信息串过去。

5.3 标签不平衡被忽略:预测全 0 也能拿到高准确率

如果阈值设得过高,比如未来 5 日收益 > 5%,正样本比例可能只有 10%。这时代码里光是model.fit、predict跑通,准确率轻松超过 90%——因为模型只要全部预测 0 就对了九成。现象是准确率很好看,但策略净值基本是平的,几乎没有买入信号。原因是分类阈值没有结合样本分布。解决方式是用confusion_matrix看预测分布,或者直接计算预测概率的分位数,把阈值定在训练集预测概率的 30 到 70 分位之间,也可以给少数类加权重,比如class_weight="balanced"。

5.4 手续费和滑点设为零:回测收益和实盘差出一个数量级

demo 里经常有人忘记加交易成本,结果回测年化 30%,实盘跑下来只有 8%。现象是策略看起来每笔都在赚钱,但交易频率越高,实盘和回测的差距越大。原因是 A 股双边手续费加印花税大约千分之二到千分之三,高频交易还有滑点和冲击成本。解决方式是在回测收益计算时扣除成本——每次信号从 0 变 1 或者从 1 变 0,都扣掉一个固定比例的费率。常见做法是单边扣0.001到0.002,然后重新评估年化收益是否还能看。

trade_flags = test_df["position"].diff().abs().fillna(0) cost = trade_flags * 0.001 # 单边千分之一成本 test_df["strategy_ret_net"] = test_df["strategy_ret"] - cost

5.5 只用一只股票做回测:结论没有任何代表性

训练和回测都在同一只股票上做,换一只股票立刻亏损。这个现象说明模型学到的不是规律,而是那只股票特有的历史路径。原因是单只股票的行情受个股事件影响极大,模型很容易记住这些特殊情况。解决方式是多选几只不同行业、不同市值的股票,分别训练、分别回测,看看策略在多数标的上是否还能保持正收益。做 demo 至少要跑 3 到 5 只票再下结论。如果只在某一只票上有效,那大概率是过拟合,不是策略本身有优势。

6. 让 demo 往前走一步:滚动训练与信号稳定性验证

模型做完回测并不算完,demo 要真正有价值,还得解决一个关键问题:模型会不会「过期」?金融市场的统计特征会缓慢漂移,一年前有效的特征权重现在可能已经失效。常见做法是把一个 4 年的数据集按时间切成多段,每段都训练一个新模型,然后用紧接着的下一年做验证。这样能测出模型在不同市场状态下的表现,而不只是撞上一个牛市窗口。

滚动训练的实现不复杂:设定训练窗口长度和步长,循环里反复切数据、训练、预测。

train_window = 700 # 用大约 700 个交易日做训练 step = 200 # 每 200 个交易日滚动一次 for start in range(0, len(df) - train_window - 200, step): train_data = df.iloc[start:start + train_window] test_data = df.iloc[start + train_window: start + train_window + 200] X_tr = train_data[feature_cols] y_tr = train_data["label"] X_te = test_data[feature_cols] model = lgb.LGBMClassifier(n_estimators=200, learning_rate=0.05, num_leaves=31, min_child_samples=50, random_state=42) model.fit(X_tr, y_tr) pred = model.predict_proba(X_te)[:, 1] # 在这里记录每段的 AUC、收益、交易次数

滚动训练得到的结果可以画一张分段的年化收益柱状图,观察策略在每段的稳定性。如果某一段收益明显为负,不要急着否定策略,先看那段市场是否处于单边下行——负收益在熊市不是 bug,但回撤比指数小就说明有一定防守能力。

实操中还有一个具体的验证技巧值得养成习惯:每次训练完,把特征重要性排序打印出来,看看排在前三的特征是否和经济直觉一致。如果 20 日波动率排名第一,说明模型在捕捉低波动上涨的规律;如果某个随机噪音特征排第一,就说明特征工程里有问题。

做这个 demo 的过程里我自己栽过最大的跟头,就是一开始只盯着回测净值曲线,忘了加手续费和做时间切分,当时差点把一个纯属未来函数的策略当成「圣杯」上线。后来养成一个习惯:任何策略结果出来,先在三张表上交叉验证——按时间切分的测试集、扣费后的收益、滚动窗口分段表现。这三张表过不了,再多花哨的模型都只是自我感动。这也是我一直对新手工程师重复的建议。希望这篇笔记能帮你在量化这条路上少走几步弯路,让第一版 demo 既能跑通、也经得起质疑。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 14:21:02

Python课程设计:田径运动会管理系统源码与数据库实战解析

简介&#xff1a;面向Python课程设计及数据库初学者的田径运动会管理系统完整代码与数据库包。系统覆盖运动员信息管理、比赛项目配置、成绩录入与查询、排名展示等典型功能&#xff0c;将Python后端逻辑、SQLite数据库操作与GUI界面开发整合于一体&#xff0c;适合作为课程设计…

作者头像 李华
网站建设 2026/10/12 14:20:57

iOS 14 Web Clip封装实战:绿标Web App合规落地

简介&#xff1a;本资源是一套面向iOS开发者与企业内测人员的绿标免签封装实战方案&#xff0c;聚焦iOS 14系统下Web App全屏化分发痛点&#xff0c;解决Safari Web Clip中顶部URL栏暴露、意外跳转等影响用户体验的关键问题。压缩包含7022个文件&#xff0c;主体为5123个smali&…

作者头像 李华
网站建设 2026/10/12 14:18:41

WOA-CNN-BiLSTM时间序列预测:自动调参实战与避坑指南

简介&#xff1a;这份资源面向具备一定编程基础的数据分析师与机器学习工程师&#xff0c;聚焦时间序列预测场景&#xff0c;提供一套完整的WOA-CNN-BiLSTM集成模型实现方案。模型以鲸鱼优化算法自动搜索超参数&#xff0c;借助卷积网络提取时序特征&#xff0c;再由双向长短期…

作者头像 李华
网站建设 2026/10/12 14:18:33

AI日报制作全攻略:从信息采集到知识库的工程实践

1. 一份AI日报的定位与内容框架设计 1.1 为什么选择日报这种形式 做AI日报这件事&#xff0c;我坚持了快两年。最开始只是自己每天刷各种信息源&#xff0c;看到有价值的东西就随手丢进备忘录&#xff0c;后来发现积累的碎片越来越多&#xff0c;但真正需要回顾的时候根本找不…

作者头像 李华
网站建设 2026/10/12 14:15:06

成功解决 pymssql Cursor object at 0x... 打印 keys 与 dict 的调试实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华