☰
机器学习股票价格预测全流程:从特征工程到回测验证
2026/9/26 21:56:56 网站建设 项目流程

简介:这是一份面向股票价格预测与量化交易入门者的机器学习算法资源包,能够帮助初学者系统梳理从数据准备到模型评估的完整流程。资源整合了LSTM、Prophet、AutoARIMA、朴素贝叶斯、SVM、随机森林等多种模型的预测实现,并配有基础回测系统,便于读者横向对比不同算法在股价时序数据上的表现,也可借此理解LSTM门控机制、Prophet趋势分解等核心概念。压缩包共386个文件,主体为357个csv历史行情数据文件,另有18个Python脚本用于模型训练与回测,以及html结果展示页、png图表、js辅助资源等,整体仅1.14MB,轻量易用,目录划分清晰。目前已有306人学习下载。通过该资源,读者可获得一套完整可运行的股价预测实验框架,既能借助csv数据快速验证各模型效果,也能通过Python脚本学习特征处理、模型调用与回测评估的关键细节,尤其适合作为课程设计或量化研究的基础模板。

1. 为什么这类项目注定赚不到钱,但值得你照着跑一遍

拿到「基于机器学习的股票价格预测算法」这类项目,第一反应别是「我要发财了」——任何做过实盘的工程师都会告诉你:测试集上再漂亮的模型,放进真实市场里也经常翻车。这套项目的价值,是把机器学习算法建模和回测验证这条链路完整串起来:数据清洗、特征工程、多模型训练、回测检验,一气呵成。适合刚入门机器学习的开发者,以及想验证自己想法、不指望靠预测暴富的研究者。看完你能知道每个模块怎么落地、参数怎么设,以及为什么你的回测结果不能直接拿去实盘。

2. 数据与特征工程:所有预测算法的地基,也是最容易泄漏的一环

2.1 数据获取与复权处理:先用能免费拿到的日线数据

这个项目名里没有写数据从哪来,但任何预测算法都要先喂数据。常见做法是用免费接口拿日线行情,我一般用 akshare,它不需要 token,适合做入门验证。先用沪深300指数日线做演示,代码长这样:

import akshare as ak import pandas as pd # 获取沪深300日线数据,默认前复权 df = ak.stock_zh_a_hist( symbol="000300", period="daily", start_date="20150101", end_date="20231231", adjust="qfq" ) # 接口返回的是中文列名,先重命名并抽出需要的列 df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume" }) df = df[["date", "open", "close", "high", "low", "volume"]] df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) print(df.head())

这段代码有三个关键点。第一,adjust="qfq"表示前复权,如果不做复权处理,遇到分红送股会在价格序列上留下跳空,模型会把这种「假信号」当成学习目标。第二,接口返回的列名是中文,必须重命名,否则后面所有特征计算都要跟着改。第三,sort_values保证时间升序,很多数据源返回的顺序并不稳定,回测里顺序错了,收益曲线就是乱的。免费接口的局限也说明一下:它适合日线级别的策略验证,不保证盘中实时更新,做不了分钟级和高频回测。

数据拿到后,先看一眼有没有停牌导致的 NaN 或全天空行。常见做法是直接用df.dropna()处理,如果行情源本身干净,这一步能省掉。

2.2 技术指标特征:把裸价格变成模型能消化的输入

只有 OHLCV 五列,模型基本学不出什么东西。工程上最常用的做法是叠技术指标,让特征带上趋势、动量、波动率的信息。这里用 pandas 手写几个最常见指标,不依赖 ta-lib 这类需要编译的库:

# 简单移动均线:短期与长期趋势 df["sma_5"] = df["close"].rolling(5).mean() df["sma_20"] = df["close"].rolling(20).mean() # RSI(14):相对强弱指标,衡量近期涨跌动量 delta = df["close"].diff() gain = delta.clip(lower=0).rolling(14).mean() loss = (-delta.clip(upper=0)).rolling(14).mean() rs = gain / loss df["rsi_14"] = 100 - 100 / (1 + rs) # MACD:快慢指数均线的差离及信号线 ema_12 = df["close"].ewm(span=12, adjust=False).mean() ema_26 = df["close"].ewm(span=26, adjust=False).mean() df["macd_dif"] = ema_12 - ema_26 df["macd_dea"] = df["macd_dif"].ewm(span=9, adjust=False).mean() df["macd_hist"] = 2 * (df["macd_dif"] - df["macd_dea"]) df = df.dropna().reset_index(drop=True)

滚动窗口的选取有讲究:sma_5捕捉短期趋势,sma_20捕捉中期趋势,两者差值其实就是动量的一种表达;RSI 的 14 是 Wilder 提出的经典参数,短周期交易者会改成 7,但做日线预测 14 更稳;MACD 用 12/26/9 三组参数是查漏补缺的默认值。注意ewm(adjust=False)是标准 MACD 算法,如果写成默认的adjust=True,算出来的 DIF 曲线会整体偏移,回测里不容易察觉,但换到其他数据集上表现差异很大。对这些参数,我的习惯是全部按行业默认值先跑一轮,不要一上来就在历史数据上调参,那是过拟合的第一张多米诺骨牌。

特征不是越多越好。很多入门项目会把几十个技术指标一股脑堆进去,模型在训练集上表现得像神,测试集上立刻现原形。金融特征之间高度共线,后面用树模型时特征重要度会非常分散,线性模型则直接出现系数不稳定。我一般控制在一二十个特征以内,先做一轮相关性筛选,把相关系数超过 0.9 的同类指标只留一个。

2.3 标签与数据切分:时序数据不能随机打乱

特征之后是标签设计。做股票价格预测,标签有两种常见定义:回归任务用未来 N 日收益率,分类任务用未来 N 日是否上涨。分类任务对新手更友好,评估指标直观。预测周期 N 选多少,决定了整个项目的「性格」:N=1 做的是次日涨跌,噪音极大;N=20 偏向中长线,信号更慢但更稳。我先用 N=5 演示:

from sklearn.preprocessing import StandardScaler horizon = 5 df["future_ret"] = df["close"].shift(-horizon) / df["close"] - 1 df["label"] = (df["future_ret"] > 0).astype(int) df = df.iloc[:-horizon] # 去掉末尾没有未来收益的样本 feature_cols = ["sma_5", "sma_20", "rsi_14", "macd_dif", "macd_dea", "macd_hist"] X_raw = df[feature_cols].values.astype("float32") y_raw = df["label"].values # 按时间顺序切分,前 70% 训练,后 30% 测试 train_end = int(len(X_raw) * 0.7) # 只用训练集拟合标准化参数,这是防止数据泄漏的第一道闸 scaler = StandardScaler() scaler.fit(X_raw[:train_end]) X_train = scaler.transform(X_raw[:train_end]) X_test = scaler.transform(X_raw[train_end:]) y_train = y_raw[:train_end] y_test = y_raw[train_end:]

标签构造里shift(-horizon)是最容易出错的一行。它把未来第 5 天的收盘价相对今天的涨幅取出来,如果忘记df.iloc[:-horizon]这一步,最后几行样本的future_ret会是 NaN,dropna 会把训练尾部的数据无声删除,导致训练集和测试集的边界悄悄偏移。切分方式上,train_test_split默认的shuffle=True在这里绝不能直接用——随机打乱等于让模型在训练时看到了未来,金融时序数据必须按时间顺序切分。标准化也必须先 fit 训练集再 transform 测试集,这是整套机器学习应用流程里最容易被新手忽略的泄漏点,后面避坑章节还会展开。

到这里,数据、特征、标签、切分四件事都齐了。这套流程是所有算法的公共前置,无论你后面用线性回归还是 LSTM,喂进去的必须是这种「按时间排好、无泄漏」的矩阵。

3. 从线性回归到 LSTM:四种机器学习算法在同一份数据上的对比落地

3.1 线性回归与逻辑回归:先建立一个不丢人的基线

任何预测项目,第一步都该跑一个最朴素模型当基线,否则后面所有「效果好」都没有参照物。对二分类标签,最朴素的基线是逻辑回归,它在金融领域实际就是线性概率模型。代码非常简单:

from sklearn.linear_model import LogisticRegression clf = LogisticRegression(max_iter=1000, C=1.0) clf.fit(X_train, y_train) # 预测上涨概率 pred_prob = clf.predict_proba(X_test)[:, 1] pred_label = (pred_prob > 0.5).astype(int) # 方向准确率:预测方向与真实涨跌是否一致 acc = (pred_label == y_test).mean() print(f"方向准确率: {acc:.3f}")

max_iter=1000是给求解器一个宽松的收敛上限,数据标准化后通常几百次迭代就收敛;C=1.0是正则化强度的倒数,C 越小正则越强。注意这里评估用的是「方向准确率」而不是分类准确率——在涨跌基本五五开的数据里,你猜全部上涨也能有 50% 准确率,所以方向准确率要明显高于 0.5 才算有意义。线性模型的强项是稳定、可解释,系数能直接看出来哪个特征对预测贡献大;弱项是它假设特征与标签是对数线性关系,而市场里这种关系非常弱。跑出来的结果一般就在 0.5 附近小幅波动,这是正常的,别慌。

3.2 树模型:XGBoost 与 LightGBM 在表格特征上的优势

数据是表格型的、特征是技术指标,这种场景下梯度提升树通常是机器学习模型里表现最好的那一档。XGBoost 的代码结构如下:

from xgboost import XGBClassifier clf = XGBClassifier( n_estimators=500, learning_rate=0.05, max_depth=4, subsample=0.8, colsample_bytree=0.8, eval_metric="logloss", early_stopping_rounds=50, random_state=42 ) clf.fit( X_train, y_train, eval_set=[(X_test, y_test)], verbose=False ) # 训练结束后,early_stopping 会自动使用最优迭代次数 pred_prob = clf.predict_proba(X_test)[:, 1]

四个参数直接决定模型会不会过拟合。max_depth=4是树深,金融数据里的信号非常弱,树太深就是在硬记噪声,4 到 6 之间比较安全;subsample=0.8表示每棵树随机抽 80% 的行,colsample_bytree=0.8表示每棵树随机抽 80% 的特征列,这两招是让每棵树「长得不一样」,本质和随机森林的思路一致。early_stopping_rounds=50是关键后悔药:模型在验证集上连续 50 轮没有变好就提前停,不用手动数着迭代次数挑模型。用eval_metric="logloss"而不是准确率,因为早期轮次准确率可能震荡,logloss 对概率预测的惩罚更平滑。

LightGBM 是另一个常见选择,代码几乎一样,把XGBClassifier换成LGBMClassifier即可。两者在日线级数据上的精度差异通常不大,XGBoost 对小数据集更稳健,LightGBM 在数据量大时更快。我的建议是:样本量少于几万行,优先 XGBoost;数据量大、特征多,优先 LightGBM。

3.3 时序模型 LSTM:什么时候值得上深度模型

看到时间序列,很多人第一反应是上 LSTM。深度模型的优势是能自动学习特征之间的时序依赖,但它在这个场景里有两个绕不过去的坑:数据量太小、特征太短。几千行日线数据对 LSTM 来说就是「吃不饱」,它动辄需要十万级样本才能发挥能力。如果一定要试,代码框架是这样:

import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout window = 20 def make_windows(X, y, win): Xs, ys = [], [] for i in range(win, len(X)): Xs.append(X[i - win:i]) ys.append(y[i]) return np.array(Xs), np.array(ys) X_train_w, y_train_w = make_windows(X_train, y_train, window) X_test_w, y_test_w = make_windows(X_test, y_test, window) model = Sequential([ LSTM(64, input_shape=(window, X_train_w.shape[2]), return_sequences=True), Dropout(0.2), LSTM(32), Dense(1, activation="sigmoid") ]) model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"]) model.fit(X_train_w, y_train_w, epochs=50, batch_size=32, validation_split=0.1, verbose=0)

窗口是 20 个交易日,等于用过去约一个月的特征预测未来 5 天的涨跌,特征和标签的时间尺度不同,这是允许的。第一层 LSTM 设return_sequences=True是为了把完整的时序信息传给第二层,如果你只有单层 LSTM,这里要改成 False。Dropout(0.2)是深度模型防过拟合的唯一盾牌,金融数据噪声大,这个比例建议只增不减。训练时用validation_split=0.1从训练集尾部切出一部分做验证,注意它按顺序切,不会打乱。

实际跑下来你会发现,LSTM 在方向准确率上通常打不过调好超参的 XGBoost,训练时间却长一个数量级。这不是 LSTM 没用,而是它更适合海量高频数据。如果你只有日线级别几千条样本,我建议把它当对照组跑一次,然后安心用树模型。

3.4 模型对比:不要只看准确率,看这四个维度

把几个算法都跑完后,一个表格把所有模型摆在一起比。这里不能只看方向准确率,我一般做四列:方向准确率、IC 均值、年化收益、最大回撤。IC(信息系数)是预测概率与未来实际收益之间的秩相关,用 Spearman 相关系数计算,大于 0.03 就认为模型有一点预测力:

import scipy.stats as st ic = st.spearmanr(pred_prob, df["future_ret"].iloc[train_end:].values).correlation print(f"IC: {ic:.4f}")

对比时你会发现一个规律:逻辑回归的 IC 往往接近 0,XGBoost 的 IC 可能到 0.03 到 0.05,LSTM 的表现则不稳定,换一次随机种子可能就从 0.04 掉到 0。IC 只是预测力的一种度量,真正的裁判是回测系统,因为收益曲线还要经过交易成本和仓位管理的修正。模型对比这块,我的习惯是每次只改一个变量,比如固定同一份特征,只换算法,对比才有意义;很多人同时换特征又换算法,最后说不清到底是谁起了作用。

4. 回测系统搭建:用向量化循环把策略交给历史检验

4.1 从预测概率到持仓信号:先过一道阈值

模型输出的是「未来 5 日上涨的概率」,不能直接拿 0.5 一刀切。原因在于:预测概率集中在 0.45 到 0.55 之间,阈值设 0.5 会导致频繁开平仓,手续费吃掉大量收益。常见做法是设置一个不对称的阈值,比如 0.55 才开仓,0.45 才平仓,中间区域空仓等待:

pred_prob = clf.predict_proba(X_test)[:, 1] threshold_open = 0.55 threshold_close = 0.45 signal = np.zeros_like(pred_prob) signal[pred_prob > threshold_open] = 1 signal[pred_prob < threshold_close] = 0 # 中间区域维持前一日仓位:用前向填充处理 for i in range(1, len(signal)): if signal[i] == 0 and pred_prob[i] > threshold_close: signal[i] = signal[i - 1]

阈值不是玄学,它直接控制交易频率。阈值越高,交易越少,单笔质量要求越高;阈值越低,交易越频繁,成本越重。我一般先用 0.5 跑通全流程,然后看回测里的换手率,再决定抬不抬阈值。很多入门项目跳过了信号到仓位的这一步,直接拿 0.5 做判断,这是回测和实盘差距大的一个重要原因。

4.2 向量化回测:单循环计算净值、回撤与夏普

回测系统的作用,是把「策略信号」翻译成「收益曲线」。基本回测系统不需要事件驱动的复杂框架,一个按时间顺序迭代的循环就够了。核心在于信号与收益的对齐:预测在第 t 天收盘后生成,最早只能在第 t+1 天执行,这是回测绝对不能逾越的红线。

import numpy as np # 测试集的日收益率 close = df["close"].iloc[train_end:].values daily_ret = np.diff(close) / close[:-1] daily_ret = np.append(daily_ret, 0) # 末尾补 0,对齐长度 # 循环回测 equity = 1.0 # 初始资金 peak = 1.0 # 历史最高净值 max_drawdown = 0.0 # 最大回撤 equity_curve = [] for t in range(1, len(signal)): # t 日持仓由 t-1 日收盘后的信号决定,避免前视偏差 pos_today = signal[t - 1] equity *= (1 + pos_today * daily_ret[t]) peak = max(peak, equity) max_drawdown = max(max_drawdown, (peak - equity) / peak) equity_curve.append(equity) total_return = equity / 1.0 - 1 years = len(signal) / 250 # 一年约 250 个交易日 annual_return = (equity ** (1 / years) - 1) if years > 0 else 0 print(f"累计收益: {total_return:.2%}, 年化收益: {annual_return:.2%}, 最大回撤: {max_drawdown:.2%}")

这个循环里最容易理解错的是signal[t - 1]这一行。它表示你在第 t-1 天收盘后根据信号决定「明天持有还是空仓」,第 t 天的涨跌才计入你的仓位收益。如果直接写signal[t] * daily_ret[t],等于你在当天开盘前就知道当天涨跌,这就是前视偏差,回测收益会虚高得离谱。年化收益按 250 个交易日折算,最大回撤衡量的是从净值最高点跌到下一个低点的最大幅度,这个数字比年化收益更值得盯着,因为实盘中 50% 的回撤需要 100% 的涨幅才能回本。

4.3 交易成本与滑点:不放成本的回测是自欺欺人

上面这段代码的收益是「毛收益」,没扣任何交易成本。实际交易里,每次调仓要付佣金、印花税和冲击成本,三者的合计通常在 0.1% 到 0.2% 之间。日线策略一年调仓几十次,成本会显著侵蚀收益。加上成本后的回测才勉强接近真实:

# 双边成本估算:佣金 0.015% + 印花税 0.1%(卖出时收,这里简化双边) cost_rate = 0.001 + 0.0015 # 信号改变一次,代表一次调仓 turnover = np.abs(np.diff(signal)).sum() total_cost = cost_rate * turnover net_annual_return = annual_return - cost_rate * (turnover / years) print(f"调仓次数: {turnover:.0f}, 总成本: {total_cost:.2%}, 净年化: {net_annual_return:.2%}")

这里的印花税和佣金比例是按国内股票交易的常见水平估算的,具体数值随券商和政策浮动,回测时把它当参数留出来,不要写死。滑点更难量化,日线级别流动性好的标的可以忽略,但如果你在回测里用了开盘价成交,而实盘开盘瞬间价格往往往不利方向偏,这就是滑点。我的做法是在 cost_rate 里额外加 0.05% 到 0.1% 的滑点缓冲,宁可把回测做得难看一点。

回测系统绝不能只输出一个「累计收益」就收工。我每次回测都至少看四个数:年化收益、最大回撤、夏普比率、调仓次数。夏普比率是年化收益超出无风险利率的部分除以收益波动率,代码里可以用annual_return / np.std(daily_ret * signal_temp) * np.sqrt(250)近似计算,它衡量的是「每承担一单位波动能换多少收益」。这四个数凑在一起,才能判断一个策略是不是「收益高风险低」的良性策略,而不是单看一条净值曲线自我陶醉。

5. 五个常见问题:让预测和回测结果毁于一旦的坑

5.1 训练集准确率 98%,回测却一路亏损:数据泄漏

这是整个项目里最常见的翻车现场。现象是你在训练集上跑出「完美」的准确率,测试集一测就像换了个市场,回测曲线更是一路向南。

原因大概率出在数据切分或特征构造这两个环节。第一,你在整个数据集上做了标准化再切分,测试集的均值和方差已经被模型「看见」了;第二,你用了shuffle=True,把时序打乱,模型在训练时偷看了未来的数据。

解决方法是把顺序彻底拧过来:先按时间切分,再对训练集 fit 标准化器,最后 transform 测试集。切分时固定shuffle=False,或者干脆手动按索引切片。这是机器学习应用流程里最基本的一条纪律,但做过几个项目之后你会发现,它也是被违反得最频繁的一条。

5.2 回测每天赚钱,实盘一买就亏:前视偏差

现象很有迷惑性:回测净值曲线漂亮得像教科书,实盘第一周就开始亏钱。

原因几乎都出在「同一根 K 线既被用来计算信号,又被用来计算收益」。典型表现是用当天的收盘价计算出当天信号,却按当天的收盘价或开盘价成交。收盘价在收盘那一刻才知道,信号最早只能用于第二天的交易。另一个隐蔽版本是使用 MACD 之类需要未来数据的指标:如果指标窗口跨越了你预测的标签区间,它就提前看到了答案。

解决方法就是 4.2 节里那个对齐逻辑:第 t 天的持仓,必须由第 t-1 天收盘后生成的信号决定。逐行检查你回测循环里signal[t]和daily_ret[t]是否用了同一个 t,这是排查前视偏差最直接的办法。我的原则是:任何一个特征,如果在 t 时刻收盘后拿不到,一律按拿不到处理,宁可错过一天行情也不偷看未来。

5.3 换一组参数就崩:过拟合与超参抖动

现象是同一套数据,你把max_depth从 4 改成 6,测试集准确率就从 55% 掉到 50%,换一个时间段数据又完全反过来。

原因有两个层面。一是树模型对超参数天然敏感,而金融数据里的有效信号极其微薄,模型很容易去拟合噪声;二是你在调参时反复「试错」,每一次试错都参考了测试集结果,这等于用测试集做了训练,测试集的评估信息已经被污染。

解决方法是把评估分成两层:训练集内做时间序列交叉验证用于选参,最后一小段「从没碰过」的数据作为最终验证。训练时要开early_stopping_rounds,让模型自己决定迭代轮数。不要执着于某个参数的精确最优值,固定一组合理的默认值比反复微调更可靠。血泪经验是:在金融数据上调参,跑得越精细,过拟合越严重。

5.4 2015 年好用,2018 年失灵:市场非平稳性

现象是同一个模型在不同的市场阶段表现差异极大,牛市里收益翻倍,熊市里回撤 40%。

原因是市场本身不是一个稳定的统计过程,波动率、相关性、投资者行为都在变,模型学到的规律在下一个阶段可能完全失效。这不是 bug,而是这类预测任务的结构性困难。任何时序预测模型都逃不开这个问题,区别只在于谁更早发现。

解决方法是做滚动重训:每隔一段时间用最近数据重新训练,而不是拿一个模型无限期跑下去。回测时要把「模型每 3 个月重训一次」这个动作写进循环里,才能得到一个接近未来的真实评估。详细做法在第 6 章展开。另外一个实用技巧是加波动率过滤,市场波动率过高时强制空仓,这类规则常被证明比模型本身更赚钱。

5.5 同一份代码两次跑出不同结果:随机种子没固定

现象是代码没改,换一台机器或者重跑一次,收益数字就变了,有时差好几个百分点。

原因在于机器学习算法内部有随机性:XGBoost 的列采样、LSTM 的权重初始化、训练时的 dropout,每一步都可能引入随机扰动。如果你不在代码里固定种子,实验结果根本不具备可复现性,后面所有对比结论都会失真。

解决方法是在每个可能引入随机性的地方显式设置种子:

import os import random import numpy as np os.environ["PYTHONHASHSEED"] = "0" random.seed(42) np.random.seed(42)

同时给每个模型的random_state参数赋值,比如 XGBoost 的random_state=42,深度学习模型在keras.utils.set_random_seed(42)。固定种子不是为了追求某个具体数值,而是让你能确定「结果差异是数据或参数引起的,而不是随机波动」。我习惯把种子写在脚本开头,所有模型统一用一个值,这样对比实验才站得住。

6. 进阶验证:滚动重训与基准对比,判断模型是否真的有效

前面把流程跑通了,模型也回测了,但你还不知道「这套东西到底有没有用」。最后一个验证手段,是用滚动重训模拟真实部署环境。

真实环境里,你不可能永远用 2015 年训练好的模型预测 2023 年的行情。正确做法是每经过一段固定时间就重新训练一次。代码框架如下:

retrain_window = 750 # 每次用 750 个交易日(约 3 年)训练 trade_window = 250 # 每次验证 250 个交易日(约 1 年) ic_list = [] equity_list = [] for start in range(0, len(X_raw) - retrain_window, trade_window): X_train_roll = X_raw[start:start + retrain_window] y_train_roll = y_raw[start:start + retrain_window] X_test_roll = X_raw[start + retrain_window:start + retrain_window + trade_window] y_test_roll = y_raw[start + retrain_window:start + retrain_window + trade_window] # 每次滚动都重新标准化,避免旧统计量干扰 scaler_roll = StandardScaler().fit(X_train_roll) clf_roll = XGBClassifier(n_estimators=200, max_depth=3, random_state=42) clf_roll.fit(scaler_roll.transform(X_train_roll), y_train_roll) pred_prob_roll = clf_roll.predict_proba( scaler_roll.transform(X_test_roll))[:, 1] ic_roll = st.spearmanr( pred_prob_roll, df["future_ret"].iloc[start + retrain_window: start + retrain_window + trade_window].values ).correlation ic_list.append(ic_roll) print(f"滚动 IC 均值: {np.mean(ic_list):.4f}")

滚动重训的价值在于它看的是「模型在时间推进中的平均表现」,而不是某一段特定行情下的偶然发挥。IC 均值如果能稳定在 0.03 以上,说明模型确实有一部分预测力;如果每次滚动 IC 都在 0 附近摆动,说明前面测试集上的好表现只是运气。

最后一步,也是最容易被跳过的一步:拿策略和「买入持有」对比。买入持有就是第一天全仓买入一直不卖,它是这个市场里最朴素的基准。你的机器学习策略如果跑不赢买入持有,那无论模型多复杂、回测多漂亮,都没有实际价值。对比时同时看年化收益和最大回撤,一个收益高但回撤大一倍不止的策略,实盘很难拿得住。

套路走完,说点个人习惯。我最早做这类项目时,在数据切分上偷懒,直接train_test_split(shuffle=True),回测曲线好看到极点,实盘一测就露馅,排查了整整一周才找到泄漏源头。现在我的习惯是在写第一行特征代码前,先问自己一句「这个特征在 t 时刻真的拿得到吗」,然后把所有拿不准的统统当作拿不到处理。这套「先泄漏排查、再滚动验证、最后对比基准」的流程,能拦下九成不靠谱的策略想法。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询