简介:面向金融时序预测与机器学习实践的完整算法包,整合了股票价格预测中常用的多种建模方法,包括LSTM、Prophet、AutoARIMA、SVM、随机森林等,并自带基础回测系统,便于读者在同一框架下对比不同模型的预测表现。压缩包共386个文件,以357个csv历史行情与训练数据为主,配合18个Python算法与回测脚本、4个HTML可视化页面,以及少量JS、Excel、Markdown辅助文件,整体大小仅1.14MB,结构紧凑、数据齐全,适合用于课程设计、量化入门或个人研究。目前已有306人学习下载。除算法代码外,包内还保留了数据清洗、模型调用与回测结果组织方式,可帮助读者快速搭建自己的股价预测流程,省去从零收集数据和编写框架的时间。对于想深入理解LSTM门控机制或对比传统机器学习与深度学习方法差异的读者,这是一份可直接运行的参考资料。
1. 基于机器学习的股票价格预测:一份自带回测系统的算法集合
拿到这份资源的时候,我第一反应是“又一份调包预测脚本”,但拆开看完才发现,它把 LSTM、Prophet、AutoARIMA、朴素贝叶斯、SVM、随机森林六类算法全塞进了一个项目里,还配了一套基础回测系统。对做量化入门的人来说,最头疼的不是跑通单个模型,而是不知道同一份数据在不同算法下表现差多少、回测怎么搭才不算自欺欺人。这份资源恰好把这两件事焊在了一起。它适合两类人:一是想对比各类机器学习算法在股价预测上效果的从业者,二是刚开始搭个人量化框架、需要一套可复现基线的新手。下面我按自己拆项目的习惯,从数据、预处理、单模型、LSTM 细节、到回测踩坑,一层层给你过一遍。
2. 把 CSV 变成模型能吃的时间序列:数据规范与预处理
2.1 数据文件长什么样
资源里放了一堆 CSV,文件名像data2.csv、data3.csv、2020年08月16日.csv这种。日期命名的文件通常是按天导出的行情快照,而 data2/data3 这类更可能是连续日线或分钟线。我当时先打开了data2.csv看表头,发现列基本逃不开date/open/high/low/close/volume这五六个字段。预测任务做的是下一日收盘价或未来几日的趋势方向,所以预处理的第一步就是把日期列转成datetime索引,并按时间升序排好。
import pandas as pd df = pd.read_csv('data2.csv', parse_dates=['date'], index_col='date') df = df.sort_index() # 去掉停牌或空值行 df = df[['open', 'high', 'low', 'close', 'volume']].dropna() print(df.head())这里parse_dates可以直接把中文字段名转成标准时间索引。sort_index很容易被忽略,但原始 CSV 可能不是严格按时间排序,尤其从多个文件拼接时,漏掉这一步后续切 train/test 会直接造成数据泄漏。dropna()是底线操作,停牌日或者数据源缺行时,后面模型会报维度错误,提前清掉能省一堆调试时间。
2.2 特征构造与归一化的选择
原始行情只有 OHLCV,直接丢给 LSTM 或 SVM 效果很糙。我一般会再算几列常见技术特征:涨跌幅、5 日均线、20 日均线、成交量变化率。注意,这些特征必须在每个时间步用“当时已知”的数据计算,不能用未来数据。
df['return'] = df['close'].pct_change() df['ma5'] = df['close'].rolling(5).mean() df['ma20'] = df['close'].rolling(20).mean() df['vol_ratio'] = df['volume'] / df['volume'].rolling(5).mean() df = df.dropna()pct_change()计算的是相对前一日的收益率,rolling(5).mean()是过去 5 天收盘均值,vol_ratio用于捕捉放量缩量。这些特征对后面所有模型共用,LSTM 用归一化后的数值,树模型用原始数值也行,但 SVM 对尺度敏感,必须缩放。
归一化我建议用MinMaxScaler或StandardScaler。对股票序列,MinMaxScaler会把价格压缩到 [0,1],避免 LSTM 激活函数饱和。这里有一个关键细节:scaler 只能用训练集的统计量拟合,再分别 transform 训练集和测试集,千万别在划分之前对全量数据 fit。
from sklearn.preprocessing import MinMaxScaler feature_cols = ['open', 'high', 'low', 'close', 'volume', 'return', 'ma5', 'ma20', 'vol_ratio'] scaler = MinMaxScaler() scaled = scaler.fit_transform(df[feature_cols])参数说明:MinMaxScaler默认feature_range=(0,1),股价波动大时需要注意是否有极端异常值,如果有,建议先做 winsorize 截断,不然某个涨停日会把其他所有数据压到一坨,模型直接废掉。
2.3 滑窗样本生成
LSTM 和 Prophet 这类模型需要把连续序列切成固定长度样本。我一般用lookback=30,意思是拿过去 30 天预测第 31 天收盘价。滑窗步长取 1,样本会有重叠,但对时间序列来说重叠是正常的,不会像交叉验证那样造成严重泄漏。
import numpy as np def make_sequences(data, lookback=30): X, y = [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i]) y.append(data[i, 3]) # close 所在列索引,按 feature_cols 顺序 return np.array(X), np.array(y) X, y = make_sequences(scaled, lookback=30)这里的data[i, 3]对应的就是close列。如果你把feature_cols顺序改了,索引要同步改。常见错误是把整个scaled数组拿去划分,结果 model 的输入维度从(样本数, 时间步, 特征数)变成了(样本数, 特征数),维度对不上就报错。生成样本后,按时间顺序前 80% 做训练,后 20% 做测试,不要随机打乱。
3. 传统时序模型先上场:Prophet、AutoARIMA 与朴素基准
3.1 为什么要先跑一遍简单模型
遇到算法集合,我习惯先跑一个“朴素基准”——拿昨天的收盘价当今天的预测。这个基准的意义是:任何模型如果连这个都跑不过,说明它在当前数据上没有学习到有效规律。很多新手上来直接调 LSTM,最后效果差得怀疑人生,回头一对比,不如拿昨天价格直接复制一份。
# 朴素基线:预测值 = 当前收盘价 pred_naive = close_test.shift(1)用 RMSE(均方根误差)和 MAE(平均绝对误差)评估,shift(1)的含义是取前一天的真实值作为后一天的预测。逻辑很简单,但它是后面所有模型的天花板参照系。如果 LSTM 的 RMSE 和它差不多,那说明 LSTM 其实没学到东西,只是记住了上一个价格。
3.2 Prophet 的用法与坑
资源里包含 Prophet 算法,Facebook 开源的时序预测工具,对节假日和趋势的处理非常强,但它默认是加法趋势。股票数据通常有周期性波动,我一般会把yearly_seasonality关掉,因为股票没有固定年度周期,开了反而会拟合出假周期。
from prophet import Prophet train_df = df.reset_index()[['date', 'close']].rename(columns={'date': 'ds', 'close': 'y'}) model = Prophet(daily_seasonality=False, weekly_seasonality=False, yearly_seasonality=False) model.fit(train_df) future = model.make_future_dataframe(periods=len(test_df), freq='D') forecast = model.predict(future)daily_seasonality=False是因为日频交易数据在一天内没有多尺度周期,weekly_seasonality对股票也不稳定,A 股没有周末竞价,周内效应不明显。make_future_dataframe必须传入与训练数据相同的日期频率,不然预测会错位。Prophet 对缺失日期会自动填充,但如果你用了交易日历,直接按自然日扩展会引入非交易日,导致预测结果看起来漂移很大。
3.3 AutoARIMA:让模型自己选差分阶数
AutoARIMA 是自动搜索(p,d,q)参数的工具,来自pmdarima库。它对股票这种非平稳序列,会自动做差分。我在用的时候会限制max_d=2,不要让它过度差分,不然会把原本的趋势信息差分成噪声。
from pmdarima import auto_arima model = auto_arima( train_series, start_p=1, max_p=5, start_q=0, max_q=3, max_d=2, seasonal=False, stepwise=True, trace=True ) pred = model.predict(n_periods=len(test_series))参数含义:start_p和max_p是自回归项搜索范围,max_q是移动平均项,stepwise=True使用逐步搜索代替穷举,速度能快几倍。trace=True会把每一轮搜索的 AIC 打印出来,方便你确认它选了哪个模型。AutoARIMA 的坑是输入的train_series必须是pd.Series,索引要是日期,否则它内部的时间索引会错乱,预测长度可能与测试集对不上。
3.4 三个模型的对比策略
我一般会把 Prophet、AutoARIMA、朴素基线的预测结果画在同一张图上,看拐点和滞后性。朴素基线天然滞后一天,AutoARIMA 本质也是线性模型,对突变的跳空缺口反应慢。Prophet 的预测曲线非常平滑,经常预测出接近直线的水平线。对比的意义在于判断数据到底有没有可预测性。如果这三个模型全部打平,那就别挣扎了,LSTM 大概率也救不回来。
4. 核心戏肉:LSTM 的构建、训练与预测细节
4.1 LSTM 为何适合股价预测
股价序列有长期依赖关系,比如某个支撑位形成的逻辑可能依赖过去几十天的价格轨迹。LSTM 通过输入门、遗忘门、输出门控制信息保留和丢弃,理论上能在长序列里保持梯度流通,这是 RNN 不具备的优势。但要注意,LSTM 只是“更不容易忘”,不是“一定记得住”。对股票这种信噪比极低的序列,它学到的往往是短期动量。我在用 LSTM 时,输入特征除了 OHLCV,还会把上面构造的 return、ma5、ma20 一起塞进去,让模型更容易发现趋势变化。
4.2 搭建一个能跑的 LSTM 预测模型
我用 Keras 搭一个最简单但有效的双层 LSTM,第一层返回完整序列,第二层只返回最后时间步,然后接全连接层回归。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential() model.add(LSTM(64, return_sequences=True, input_shape=(30, X.shape[2]))) model.add(Dropout(0.2)) model.add(LSTM(32, return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse')参数说明:第一层return_sequences=True是因为下一层还是 LSTM,需要保留时间步输出;第二层False只输出最后一个隐藏状态。Dropout(0.2)是时序 Dropout,随机丢弃部分神经元输出,防止过拟合,千万别用普通 Dropout 在 LSTM 层之间,效果会差很多。损失函数用mse而不是mae,因为 MSE 对大偏差惩罚更大,能逼模型拟合突变点。训练时我用validation_split=0.1从训练集尾部切一串当验证,不能用随机切分。
4.3 训练与早停
训练 LSTM 最怕两件事:过拟合和学到了噪声。我一般加EarlyStopping,监控验证集损失,如果连续 10 轮不下降就停。
from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) history = model.fit(X_train, y_train, epochs=100, batch_size=32, validation_split=0.1, callbacks=[early_stop], verbose=1)patience=10表示允许验证损失连续 10 轮不创新低,再强行停掉。restore_best_weights=True会把模型权重恢复到验证损失最低的那一轮,不然你拿到的是最后 10 轮里已经过拟合的权重。batch_size=32对日线数据来说够用,如果你的机器内存小,可以降到 16。训练完必须检查history里有过有一个先降后升的拐点,如果有,说明早停没拦住,还得调低学习率或者增大 Dropout。
4.4 预测与反归一化
LSTM 输出的是归一化后的值,要看真实价格必须反变换回来。因为close在feature_cols的索引是 3,所以只取 scaler 的对应列做 inverse。
pred_scaled = model.predict(X_test) pred_close = scaler.inverse_transform( np.concatenate([np.zeros((len(pred_scaled), 3)), pred_scaled, np.zeros((len(pred_scaled), 5))], axis=1) )[:, 3]这段代码有点绕,思路是把预测的 close 列放回原来的特征矩阵位置,其余列补零,再用inverse_transform,最后只取第 3 列。注意scaler是在全特征上拟合的,所以补零的位置不在乎,只要列数和顺序对就行。更稳妥的办法是单独为close做一个 MinMaxScaler,就不用来回拼接了。
4.5 LSTM 的常见翻车点
我拆过不少股票 LSTM 项目,发现翻车集中在三处。第一处是忘记对测试集做transform,用了全局 scaler 导致信息泄漏,测试 RMSE 低得离谱,实盘却拉胯。第二处是把lookback设得太短比如 5 天,模型只能看到非常局部的波动,交易逻辑稍长一点就失效。第三处是不做差分,直接把价格序列丢给 LSTM,模型容易学成“预测值约等于上一个价格”,此时 RMSE 看着比朴素基线好一点,但其实是对趋势的延迟复制,不是真预测。
5. 机器学习家族集合:随机森林、SVM 与朴素贝叶斯
5.1 从序列到二维特征:把时序转成监督学习
随机森林和 SVM 不能直接吃三维序列,需要把每个时间步的特征展平成一条样本。常见做法是用前 30 天的所有特征拼接成一个长向量,作为该样本的输入。
def flatten_sequences(X): n_samples, n_timesteps, n_features = X.shape return X.reshape(n_samples, n_timesteps * n_features) X_flat = flatten_sequences(X_train) X_test_flat = flatten_sequences(X_test)展平后每个样本有 30×9=270 个特征。这样随机森林和 SVM 就能跑了。代价是特征维数膨胀,对 SVM 来说可能引发维数灾难,所以我会先对展平后的特征做 PCA 降维,保留 95% 方差。
5.2 随机森林:逆天的 Baseline
随机森林是集成树模型,对非线性关系拟合能力强,对异常值不敏感,而且几乎不需要调参。用它做股价预测经常比 LSTM 效果还好,因为股价噪声大,树模型天然抗噪。
from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor( n_estimators=200, max_depth=10, min_samples_leaf=5, random_state=42 ) rf.fit(X_flat, y_train) pred_rf = rf.predict(X_test_flat)n_estimators=200表示 200 棵树,再多对性能提升不大,训练时间却会线性增长。max_depth=10限制树深度,防止单棵树记住样本噪声。min_samples_leaf=5每个叶节点至少 5 个样本,也是正则化。随机森林最大的优势是能输出特征重要性,我跑完会看rf.feature_importances_,如果最有效的特征是最近 1~2 天的return,说明市场确实只有短期动量。
5.3 SVM:对尺度极其敏感
SVM 在时序预测里通常是配角,因为它是对偶求解,样本量稍大就慢得不行。但资源里既然给了,我一般会用来做涨跌分类而不是回归。做回归时,必须先把目标值缩放,而且核函数选rbf时要小心gamma值。
from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler scaler_y = MinMaxScaler() y_svm = scaler_y.fit_transform(y_train.values.reshape(-1, 1)).ravel() X_svm = StandardScaler().fit_transform(X_flat) # 注意已经展平 svr = SVR(kernel='rbf', C=100, epsilon=0.01) svr.fit(X_svm, y_svm) pred_svm = scaler_y.inverse_transform(svr.predict(X_test_svm).reshape(-1, 1)).ravel()StandardScaler把每个特征变成均值 0、方差 1,这一步对 SVM 是必须的,因为 RBF 核计算的是样本距离,不同特征的量纲不一致会导致距离被价格主导。C=100是惩罚参数,越大越强调分类对所有样本的拟合;epsilon=0.01是回归管道的宽度,允许预测值偏离真实值 0.01 时不计算损失。SVM 训练数据量一上去就很折磨人,我用前 500 个样本训练时速度还能接受,超过 2000 个就想去换模型了。
5.4 朴素贝叶斯:硬凑还是有用?
朴素贝叶斯是生成模型,特征独立假设很严格,股价特征几乎不独立。但把它用在“明天涨还是跌”的二分类上,倒也能跑出一个准确率底数。它在资源里出现的意义,更多是当对照实验,证明特征相关性对模型影响有多大。
from sklearn.naive_bayes import GaussianNB # 把 y 转成分类:1 表示下一日收盘价高于今日,0 表示跌 def to_label(close_series): return (close_series.shift(-1) > close_series).astype(int)[:-1] y_cls = to_label(df['close']) X_cls = df[['return', 'ma5', 'ma20']].fillna(0)[:-1] gnb = GaussianNB() gnb.fit(X_cls, y_cls)to_label这一步用了未来一天的 close 与当前比较,所以构造样本时要去掉最后一行,避免标签里混入 NaN。高斯朴素贝叶斯会把每个特征独立拟合高斯分布,对分类的高斯概率取对数求和。实际准确率通常就在 0.5 左右晃,因为涨跌接近随机。如果它能稳定超过 0.55,说明构造的特征有信息量,这时候再去看随机森林和 LSTM 才有意义。
5.5 对比:什么时候用哪个
我的习惯是,先跑随机森林和朴素贝叶斯分类器,快速判断数据中是否有可学习的信号。如果信号弱,SVM 和 LSTM 大概率也救不回来。如果随机森林的回归 RMSE 显著低于朴素基线,再投入时间调 LSTM。SVM 适合小样本数据集,比如只有几百条日线时表现稳定;大数据量下树提升更吃香。而朴素贝叶斯基本看作是随机预测的对照线,不是实际可用的预测模型。
6. 回测系统怎么搭:避免自欺欺人的四个关键
6.1 回测的基本框架
资源里的回测系统本质是一条资金曲线模拟器。核心逻辑是:预测模型给出每天的预测信号,交易规则决定什么时候买入卖出,最后统计累计收益和最大回撤。我拆到的这个版本里,是按信号在每日收盘价买入,次日收盘价卖出的 T+1 模式。
class Backtester: def __init__(self, initial_cash=100000): self.cash = initial_cash self.position = 0 self.equity_curve = [] def run(self, signals, prices): for i in range(1, len(signals)): if signals[i] == 1 and self.cash >= prices[i]: self.position = self.cash / prices[i] self.cash = 0 elif signals[i] == -1 and self.position > 0: self.cash = self.position * prices[i] self.position = 0 self.equity_curve.append(self.cash + self.position * prices[i]) return self.equity_curvesignals为 1 表示买入,-1 表示卖出,0 表示持仓不动。这里忽略手续费和滑点,如果用于实盘评估,必须加入成本模型。参数initial_cash是初始资金,prices[i]是当日收盘价。整个回测不涉及未来数据,因为信号是用预测模型当天的输出生成的,价格也是当前已收盘的价格。
6.2 关键坑一:未来函数泄漏
最常见的问题是,在预测今天信号时用了今天收盘后才知道的数据。比如用当日close计算 5 日均线并产生买入信号,这在收盘后没问题,但如果你的特征里包含当日最高价、最低价的一些未来统计,比如“当日最高价比昨日高 5%”这种,实盘时当天盘中根本不知道会不会更高。我检查数据里的rolling(5)等操作是否在切割样本前就做完了,如果做完了会包含未来窗口的信息,必须重新按时间顺序逐日滚动计算。
6.3 关键坑二:训练集包含测试集时间
很多人在做模型迭代时先把全量数据喂给模型训练,再拿同一段数据的区间来回测。这等于考试先给答案,回测曲线当然漂亮。正确做法是把数据按时间线切成三段:最前面 60% 训练,中间 20% 作为验证调参与早停,最后 20% 回测,期间模型权重不能再更新。
split_idx = int(len(df) * 0.6) val_idx = int(len(df) * 0.8) train_df = df.iloc[:split_idx] val_df = df.iloc[split_idx:val_idx] test_df = df.iloc[val_idx:]回测只能基于test_df来跑。如果你看到别人的项目里用同一批数据同时做训练和回测,直接拉黑。
6.4 关键坑三:只盯着累计收益率
单看累计收益,一个在尾部暴涨 50% 的模型会让前 300 天的亏损显得无关紧要。但真实交易中,最大回撤决定了你能否撑到收益来的时候。回测必须同时输出年化收益、夏普比率、最大回撤三项。
import numpy as np def max_drawdown(equity_curve): peak = np.maximum.accumulate(equity_curve) drawdown = (equity_curve - peak) / peak return drawdown.min() def sharpe_ratio(returns, rf=0.02): return (np.mean(returns) - rf / 252) / np.std(returns) * np.sqrt(252)max_drawdown用累积峰值减去当前值再除峰值,得到最惨时刻的亏损比例。sharpe_ratio里rf=0.02是 2% 年化无风险利率,/252换算到日收益率。如果模型收益曲线上涨很猛但最大回撤超 30%,短线资金根本扛不住波动。
6.5 关键坑四:信号与实际交易不同步
我踩过一个坑:模型在 T 日收盘后预测,T+1 日开盘才执行买卖,但回测代码里却用了 T 日收盘价成交。这会造成一种“买了就涨”的幻觉,原因是预测的强势延续在 T 日收盘后已经透支了部分收益。正确做法是预测 T+1 日收益率,信号产生后按 T+1 日收盘价成交,或者至少按 T+1 日开盘价成交。我后来统一改成“预测当日信号,次日开盘价成交”,这样更接近实盘。
从那以后,我每次把一份新资源跑起来,都会强制走一遍这个流程:先看数据有没有未来函数,再构造朴素基线,然后训练模型并检查测试集是否严格滞后,最后用买入信号与成交价错位一天的方式回测。即使某个算法被吹得天花乱坠,我也只信经过这四层拷打的结果。这套资源和它的回测系统,能帮你把以上每一个坑都踩一遍并留好防坑代码,省掉最折磨人的前期重复劳动。希望帮到你。
本文还有配套的精品资源,点击获取