简介:这份资源面向金融、数据分析方向的学生与从业者,围绕时间序列分析在股票场景中的完整应用展开,覆盖从数据获取到策略回测的全流程,适合正在完成金融作业或希望系统练习Python量化分析的学习者。包内共9个文件,包含5个ipynb、3个py与1个md,压缩包约567KB,notebook用于分步演示与结果呈现,脚本便于直接运行复用,md则提供整体说明。内容涵盖台股近半年股价的均值、标准差、最高最低价及成交量前三名筛选,五日每日报价表与EPS条件筛选,多数据源合并与前十名涨跌幅统计,RSI超买超卖信号检测与多股绘图,ARIMA与Prophet的30天股价预测及经验总结,以及回测对标和投资组合损益计算。已有1375人学习下载,可帮助读者掌握数据抓取、指标计算、信号识别、预测建模与组合评估的完整链路,并积累可迁移的排错与调参经验。
1. 时间序列分析做股票预测:一份能直接跑通的 Python 实现路径
很多人第一次接触时间序列分析,都是在金融作业或者量化入门项目里,题目往往就一句话:用 Python 对某只股票做时间序列分析并预测。听起来简单,真动手才发现坑一个接一个——数据从哪来、平稳性怎么检验、ARIMA 的 p d q 怎么定、LSTM 又该怎么搭、预测出来的结果为什么和真实价格差一大截。这份实现说明就是冲着这些问题来的,目标是把「时间序列分析 + 股票分析 + Python 代码」这条链路完整走一遍,从数据获取、平稳性检验、模型选型到预测评估,每一步都给可复现的代码和参数解释。适合正在做金融作业的学生,也适合想用 Python 量化交易策略代码做入门验证的从业者。下面按实际动手顺序展开,不绕弯子。
2. 数据获取与预处理:把股票数据变成能喂给模型的格式
2.1 用 yfinance 或 akshare 拉取日线数据
股票时间序列分析的第一步永远是拿到干净的数据。常见做法是用yfinance拉美股,用akshare拉 A 股。两者都返回 DataFrame,列名略有差异,需要统一成date, open, high, low, close, volume这种结构。下面这段代码用 akshare 拉平安银行日线,并做基础清洗。
import akshare as ak import pandas as pd import numpy as np # 拉取平安银行日线数据,adjust="qfq" 表示前复权 df = ak.stock_zh_a_hist(symbol="000001", period="daily", start_date="20200101", end_date="20241231", adjust="qfq") # 统一列名,akshare 返回中文列名 df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume" }) # 只保留需要的列并按日期排序 df = df[["date", "open", "high", "low", "close", "volume"]].copy() df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) # 缺失值处理:股票数据一般用前向填充 df = df.ffill().dropna() print(df.shape, df.head())逻辑说明:adjust="qfq"前复权是关键参数,不复权的话除权除息日会出现价格跳空,直接破坏时间序列的连续性。ffill()前向填充适合股票数据,因为停牌日没有交易,用前一天收盘价填充比插值更合理。参数上start_date和end_date建议至少覆盖 3 年以上,否则 ARIMA 的季度周期性根本学不出来。
2.2 平稳性检验与差分:ADF 检验怎么读结果
时间序列分析的核心前提是平稳性。股票收盘价几乎一定是非平稳的,直接建模会得到虚假回归。ADF 检验(Augmented Dickey-Fuller)是判断平稳性最常用的方法,原假设是「存在单位根,序列非平稳」。
from statsmodels.tsa.stattools import adfuller def adf_test(series, name="series"): result = adfuller(series.dropna(), autolag="AIC") print(f"{name} ADF Statistic: {result[0]:.4f}") print(f"{name} p-value: {result[1]:.4f}") print(f"Critical Values: {result[4]}") return result[1] # 对收盘价做检验 p_close = adf_test(df["close"], "close") # 一阶差分后再检验 df["close_diff"] = df["close"].diff() p_diff = adf_test(df["close_diff"], "close_diff")逻辑说明:autolag="AIC"让 statsmodels 自动选择滞后阶数,比手动指定更稳。判断标准很简单——p-value 小于 0.05 就拒绝原假设,认为序列平稳。实际跑下来,收盘价的 p-value 通常接近 1,一阶差分后往往降到 0.05 以下。如果一阶差分还不平稳,就做二阶差分,但二阶差分在股票上很少需要,过度差分会丢失长期信息。这里有个血泪经验:差分次数不是越多越好,d 参数超过 2 基本说明数据本身有问题。
2.3 训练集测试集切分:时间序列不能随机打乱
这是新手最容易翻车的地方。时间序列的切分必须按时间顺序,不能像普通机器学习那样train_test_split(shuffle=True)。常见做法是留最后 20% 作为测试集。
# 按时间顺序切分,前 80% 训练,后 20% 测试 split_idx = int(len(df) * 0.8) train = df.iloc[:split_idx].copy() test = df.iloc[split_idx:].copy() print(f"训练集: {train['date'].min()} 到 {train['date'].max()}, 共 {len(train)} 条") print(f"测试集: {test['date'].min()} 到 {test['date'].max()}, 共 {len(test)} 条")逻辑说明:split_idx用整数索引切分,保证训练集时间全部早于测试集。如果打乱顺序,模型会「看到未来」,评估结果虚高,这在作业里是致命错误。测试集比例 20% 是常见选择,数据量少于 500 条时建议降到 15%,否则测试集样本太少,评估指标波动大。
3. ARIMA 建模:p d q 三个参数到底怎么定
3.1 用 ACF 和 PACF 图定阶
ARIMA 的 p 是自回归项,d 是差分次数,q 是移动平均项。d 已经通过 ADF 检验确定为 1,剩下 p 和 q 靠 ACF(自相关)和 PACF(偏自相关)图判断。规则是:PACF 截尾、ACF 拖尾用 AR;ACF 截尾、PACF 拖尾用 MA;两者都拖尾用 ARMA。
import matplotlib.pyplot as plt from statsmodels.graphics.tsaplots import plot_acf, plot_pacf fig, axes = plt.subplots(2, 1, figsize=(12, 8)) plot_acf(df["close_diff"].dropna(), lags=40, ax=axes[0]) plot_pacf(df["close_diff"].dropna(), lags=40, ax=axes[1]) plt.tight_layout() plt.savefig("acf_pacf.png", dpi=120) plt.show()逻辑说明:lags=40表示看 40 个滞后阶,日线数据一般看 20 到 40 就够。图上蓝色阴影区域是置信区间,超出阴影的滞后阶才显著。实际看股票差分序列,ACF 和 PACF 往往都在前几阶显著然后快速衰减,p 和 q 取 1 到 3 之间比较常见。这里别死磕图形,图形只是初筛,最终还要靠 AIC/BIC 网格搜索确认。
3.2 网格搜索最优 p d q 组合
图形法定阶主观性太强,工程上更可靠的做法是网格搜索,用 AIC 或 BIC 选最优。
import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings("ignore") best_aic = np.inf best_order = None best_model = None # d 固定为 1,p 和 q 在 0-4 之间搜索 for p in range(5): for q in range(5): try: model = ARIMA(train["close"], order=(p, 1, q)) fitted = model.fit() if fitted.aic < best_aic: best_aic = fitted.aic best_order = (p, 1, q) best_model = fitted except Exception as e: continue print(f"最优阶数: {best_order}, AIC: {best_aic:.2f}")逻辑说明:order=(p, 1, q)里 d 固定为 1,因为前面 ADF 已经确认一阶差分平稳。AIC 越小模型越好,但要注意 AIC 会奖励拟合优度、惩罚参数数量,所以不会无脑选高阶。warnings.filterwarnings("ignore")是因为部分阶数组合不收敛会刷屏警告,不影响搜索。跑完通常会发现最优阶数在 (1,1,1) 到 (3,1,2) 之间,如果搜出来 p 或 q 等于 4,要警惕过拟合。
3.3 预测与评估:MAPE 和 RMSE 怎么算
模型定好后,用forecast或get_forecast做样本外预测,再和测试集对比。
from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error # 用最优模型预测测试集长度 forecast = best_model.get_forecast(steps=len(test)) pred = forecast.predicted_mean.values conf_int = forecast.conf_int() # 评估指标 rmse = np.sqrt(mean_squared_error(test["close"], pred)) mape = mean_absolute_percentage_error(test["close"], pred) print(f"RMSE: {rmse:.2f}, MAPE: {mape:.4f}") # 画图对比 plt.figure(figsize=(12, 5)) plt.plot(train["date"], train["close"], label="train") plt.plot(test["date"], test["close"], label="actual") plt.plot(test["date"], pred, label="forecast") plt.fill_between(test["date"], conf_int.iloc[:, 0], conf_int.iloc[:, 1], color="gray", alpha=0.3) plt.legend() plt.savefig("arima_forecast.png", dpi=120) plt.show()逻辑说明:get_forecast(steps=len(test))做多步预测,predicted_mean是点预测,conf_int()给置信区间。RMSE 衡量绝对误差,MAPE 衡量相对误差,股票预测里 MAPE 在 2% 到 5% 算不错,超过 10% 基本说明模型没学到东西。注意 ARIMA 多步预测会快速收敛到均值,预测步数越长越平,这是模型特性不是 bug。如果作业要求预测未来 30 天,建议只信前 5 到 10 天的结果。
4. LSTM 建模:什么时候该上深度学习
4.1 用 MinMaxScaler 做归一化并构造滑动窗口
ARIMA 处理线性关系强,但股票里有非线性成分,LSTM 能捕捉。上 LSTM 前必须做归一化,否则梯度爆炸。滑动窗口是把时间序列转成监督学习样本的关键。
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) close_scaled = scaler.fit_transform(df[["close"]]) def create_sequences(data, window=60): X, y = [], [] for i in range(window, len(data)): X.append(data[i-window:i, 0]) y.append(data[i, 0]) return np.array(X), np.array(y) window = 60 X, y = create_sequences(close_scaled, window) X = X.reshape((X.shape[0], X.shape[1], 1)) # 按时间切分 split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:]逻辑说明:MinMaxScaler把价格压到 0 到 1,fit_transform只在全量数据上做一次,严格来说应该只在训练集上 fit,但作业场景下差异不大。window=60表示用过去 60 个交易日预测下一天,这个参数对应约 3 个月,是常见选择。窗口太小模型看不到趋势,太大训练慢且容易过拟合。reshape成三维是因为 LSTM 输入要求(样本数, 时间步, 特征数)。
4.2 搭一个两层 LSTM 并训练
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model = Sequential([ LSTM(50, return_sequences=True, input_shape=(window, 1)), Dropout(0.2), LSTM(50, return_sequences=False), Dropout(0.2), Dense(25, activation="relu"), Dense(1) ]) model.compile(optimizer="adam", loss="mse") 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)逻辑说明:第一层 LSTMreturn_sequences=True把序列传给第二层,第二层return_sequences=False只输出最后一步。Dropout(0.2)防过拟合,股票数据噪声大,dropout 很有必要。EarlyStopping的patience=10表示验证损失 10 轮不降就停,restore_best_weights=True恢复最优权重,避免最后一轮过拟合。batch_size=32是默认值,数据量小可以降到 16。
4.3 反归一化并对比 ARIMA
pred_scaled = model.predict(X_test) pred = scaler.inverse_transform(pred_scaled) actual = scaler.inverse_transform(y_test.reshape(-1, 1)) rmse_lstm = np.sqrt(mean_squared_error(actual, pred)) mape_lstm = mean_absolute_percentage_error(actual, pred) print(f"LSTM RMSE: {rmse_lstm:.2f}, MAPE: {mape_lstm:.4f}")逻辑说明:inverse_transform把预测值还原到原始价格尺度,否则算出来的误差没有意义。实际对比下来,LSTM 在短期预测上往往比 ARIMA 好一点,但优势没有想象中大,而且训练时间长、调参麻烦。如果作业只要求时间序列分析,ARIMA 足够;如果要求「用深度学习」,再上 LSTM。别为了炫技硬上 LSTM,调不好还不如 ARIMA。
5. 避坑与排查:股票时间序列分析最常见的 5 个翻车点
5.1 现象:模型预测是一条直线
原因:ARIMA 多步预测会收敛到均值,或者 d 参数设错导致模型学不到趋势。解决:检查差分次数,确认 d 与 ADF 检验一致;预测步数控制在 10 步以内;如果必须长步预测,改用带趋势的模型或 LSTM。
5.2 现象:MAPE 小于 1% 但预测明显不对
原因:数据泄漏。常见于归一化时用了全量数据 fit,或者切分时打乱了顺序。解决:scaler 只在训练集 fit,测试集 transform;切分严格按时间顺序,用iloc而不是sample。
5.3 现象:LSTM 训练损失不下降
原因:学习率太大、窗口太小、或者没做归一化。解决:确认输入在 0 到 1 之间;window至少 30;把adam换成adam(learning_rate=0.001)显式指定学习率;检查数据里有没有 NaN。
5.4 现象:ADF 检验 p-value 一直大于 0.05
原因:数据有季节性或者结构突变。解决:先做对数变换再差分;检查是否有除权除息未复权;用seasonal_decompose看趋势和季节性,必要时上 SARIMA。
5.5 现象:akshare 拉数据报错或返回空
原因:接口限流或股票代码格式不对。解决:A 股代码不带前缀,如000001;加time.sleep(1)避免频繁请求;换yfinance拉美股对比测试。这类接口问题没有后悔药,只能多试几个数据源。
6. 把预测结果落到交易信号:一个可验证的进阶技巧
模型跑通只是第一步,真正有价值的是把预测转成可执行的信号。我一般会用一个简单但有效的规则:预测下一日收益率大于阈值就买入,小于负阈值就卖出,否则持有。下面这段代码把 ARIMA 预测转成信号并做回测。
# 用滚动预测生成每日信号 signals = [] threshold = 0.005 # 0.5% 阈值 for i in range(len(test)): # 用训练集 + 测试集前 i 天重新拟合,模拟实盘 history = pd.concat([train["close"], test["close"].iloc[:i]]) model = ARIMA(history, order=best_order).fit() next_pred = model.forecast(steps=1).iloc[0] last_close = history.iloc[-1] ret = (next_pred - last_close) / last_close if ret > threshold: signals.append(1) elif ret < -threshold: signals.append(-1) else: signals.append(0) test = test.copy() test["signal"] = signals test["ret"] = test["close"].pct_change().shift(-1) test["strategy_ret"] = test["signal"] * test["ret"] cum_strategy = (1 + test["strategy_ret"].fillna(0)).cumprod() cum_buy_hold = (1 + test["ret"].fillna(0)).cumprod() print(f"策略累计收益: {cum_strategy.iloc[-1]:.4f}") print(f"买入持有累计收益: {cum_buy_hold.iloc[-1]:.4f}")逻辑说明:滚动预测是模拟实盘的关键,每次只用当前可见的历史数据重新拟合,避免未来函数。threshold=0.5%是过滤噪声的阈值,太小会被交易成本吃掉,太大信号太少。shift(-1)把次日收益对齐到当日信号,保证信号和收益的时间对应正确。回测结果通常会发现策略跑不赢买入持有,这很正常——股票预测模型的价值不在于稳赚,而在于理解序列结构和风险边界。
几个参数调整建议:阈值可以试 0.3% 到 1%,看策略收益和交易频率的平衡;best_order如果每次滚动都重新搜索会很慢,可以固定为前面网格搜索的结果;回测里没扣交易成本,实盘要减去手续费和滑点,否则结果虚高。我自己的习惯是,任何股票预测模型上线前,先用滚动回测跑一遍,再和买入持有对比,跑不赢就老老实实承认模型没学到 alpha,别硬吹。
希望帮到你。
本文还有配套的精品资源,点击获取