☰
Python股票价格走势预测:从数据清洗到回测的完整实践指南
2026/10/1 17:50:45 网站建设 项目流程

简介:面向Python入门后想尝试量化交易的开发者,这份资源是一套基于TensorFlow框架的股票价格走势预测小项目,目标是用已有行情数据推测次日开盘价。项目实际代码借助tushare获取股票数据,pandas负责清洗与特征构造,matplotlib在同一张图中绘制真实价格与预测价格曲线,方便直观评估回归效果;虽然当前仅覆盖单日开盘价预测,但作者表示后续会继续完善,整体结构简洁,适合作为学习LSTM或时序回归的起点。压缩包共5个文件,包含两个Python脚本、一个TensorFlow checkpoint以及配套的data与index数据文件,整包仅456KB,下载后即可快速加载模型并运行测试。目前已有2142人学习浏览,适合想在股票数据上动手实践深度学习的初学者。运行test.py可完成模型加载、预测与可视化,配合自带权重可直接跳过训练过程,快速看到当前模型的预测表现,并对特征工程、网络结构及预测步长做进一步调优。

1. 用Python做股票价格走势预测:拆开这个zip,先看四块硬骨头

拿到一个"基于python实现股票价格走势预测.zip",多数人第一反应是找里面有没有LSTM模型,其实真正让这个方向翻车的从来不是模型,而是数据、标签和回测口径。Python做价格走势预测的完整链路,是把历史行情、特征工程、预测模型、回测评估四块串起来,缺一块都跑不出可信结果。这个方向适合三类人:想入门量化交易的开发者、做数据分析或毕业设计的学生、以及想验证"技术指标到底有没有用"的投资者。先泼一盆冷水:单靠历史价格预测未来价格,准确率天花板就在那里,但把这条研究管道搭好,你手里就多了一套可反复验证的策略实验台,这个价值比"预测准一天"大得多。

2. 数据是第一个黑匣子:行情取数、标签对齐和复权边界

价格预测模型的本质是"用历史和当前的信息,预测未来某个时点的价格或方向"。这句话落地到实现,就是数据准备阶段必须回答三个问题:数据从哪来、标签怎么打、复权和停牌怎么处理。很多新手在这一步就把顺序搞错——先去找模型代码,结果模型跑通后怎么验都不对,回头查才发现是标签错位或者复权不一致。数据阶段不像建模那么"性感",但它决定了后面所有步骤能不能成立。

2.1 用akshare把日线拉回本地:三行代码和参数说明

常见做法是通过akshare从东方财富接口取A股日线,这个库不需要注册token,适合本地实验。先把行情拉到DataFrame里,再做字段标准化和排序:

import pandas as pd import akshare as ak df = ak.stock_zh_a_hist( symbol="000001", period="daily", start_date="20180101", end_date="20231231", adjust="qfq" ) df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume" }) df = df[["date", "open", "high", "low", "close", "volume"]] df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) print(df.head())

这段代码的逻辑是:akshare返回的中文列名先映射成英文标准字段,再按日期升序排列并重置索引。symbol传的是不带交易所前缀的代码,000001是平安银行;period只取日线,避免分钟线带来的数据量和噪声翻倍;adjust="qfq"表示前复权,这是计算技术指标时最常用的口径,因为前复权价格在历史区间内是连续的,均线、MACD这类指标不会因为分红除权出现虚假跳空。

如果akshare接口偶尔抽风或者返回空值,备用路径是baostock:

import baostock as bs lg = bs.login() rs = bs.query_history_k_data_plus("sz.000001", "date,open,high,low,close,volume", start_date="2018-01-01", end_date="2023-12-31", frequency="d", adjustflag="2") bs.logout()

这里adjustflag="2"对应前复权,1是后复权,3是不复权。数据拉到本地后,第一件事不是建模,而是检查有没有停牌导致的空行、重复日期、成交量异常为零的情况。我一般会先打印df.isnull().sum()和df["date"].duplicated().sum(),这两行排查能省掉后面大量莫名其妙的报错。

2.2 构造标签:预测涨跌方向还是预测收益率

数据清洗完,接下来是打标签。预测"涨还是跌"是分类问题,预测"涨多少"是回归问题。两者都可以做,但标签构造的方式直接决定模型学什么。为了防止信息泄漏,标签必须用shift(-1)也就是"未来的值"来构建,同时特征里不能携带未来信息:

df["ret"] = df["close"].pct_change() df["label_up"] = (df["ret"].shift(-1) > 0).astype(int) # 明日是否上涨 df["label_ret"] = df["ret"].shift(-1) # 明日收益率 df = df.dropna().reset_index(drop=True)

逻辑说明:pct_change()计算的是当日相对前一日的收益率,shift(-1)把明天的收益率搬到今天这一行,这样模型用今天的特征去预测明天的涨跌方向或收益率。这里最大的坑是把ret直接当成标签——ret是今天的收益率,预测今天没有任何决策价值,必须取shift(-1)。label_up适合做分类模型和准确率统计,label_ret适合做回归模型和排序类指标IC,两者可以都保留,模型选型时再决定用哪个。

2.3 停牌、复权和涨跌停样本的取舍

复权问题是回测翻车的第一大来源。如果做技术指标和特征工程,用前复权,因为指标要求价格连续;如果做收益计算和资金回测,用后复权更准,因为后复权价格包含了分红再投资的收益,计算账户净值时不会因为除权产生虚假亏损。我见过不少人研究代码没问题,净值曲线却莫名向下跳,一查就是回测用了前复权数据,分红日被当成了下跌。

停牌样本的处理要看场景:停牌日成交量通常为0,有的数据源会直接留空行。这类样本要删除,否则pct_change()会把复牌日的收益率算成从0涨到开盘价,产生一个离谱的异常值。涨跌停样本更微妙——涨停板次日不一定买得进,跌停板次日不一定卖得出,如果不标记这类样本,回测里会出现大量"完美抄底逃顶"的假交易。我一般会给每行打一个is_limit标记,规则是涨幅超过9.8%记为涨停、跌幅超过9.8%记为跌停,回测阶段单独处理。

3. 特征工程:用Python算出MA、RSI、MACD,并防好信息泄漏

特征工程阶段的目标不是堆指标,而是构造出"今天收盘后就能算出来、且对未来收益有预测能力"的变量。均线、RSI、MACD这类技术指标是数据从业者最常用的起点,因为它们基于收盘价就能算,天然满足"不用未来数据"的约束。这一章的难点不在于公式本身,而在于实现时容易把rolling窗口用错、把归一化的range算错,导致泄漏混进模型。

3.1 用pandas算均线、RSI和MACD:rolling与ewm的坑

用pandas计算技术指标的代码很简洁,但有一处特别容易错——rolling(window)默认是对当前值以及之前的window-1个值求窗口统计,不包含未来,这是它适合做特征的根本原因。常见的均线、RSI、MACD代码如下:

df["ma5"] = df["close"].rolling(5).mean() df["ma20"] = df["close"].rolling(20).mean() delta = df["close"].diff() up = delta.clip(lower=0) down = -delta.clip(upper=0) roll_up = up.rolling(14).mean() roll_down = down.rolling(14).mean() df["rsi14"] = 100 - 100 / (1 + roll_up / roll_down) ema12 = df["close"].ewm(span=12, adjust=False).mean() ema26 = df["close"].ewm(span=26, adjust=False).mean() df["macd_dif"] = ema12 - ema26 df["macd_dea"] = df["macd_dif"].ewm(span=9, adjust=False).mean() df["macd_hist"] = df["macd_dif"] - df["macd_dea"]

逻辑说明:MA5和MA20分别是5日和20日收盘均线,反映短期和中期趋势;RSI14用14日涨跌幅均值之比衡量超买超卖;MACD是经典的12日EMA减26日EMA得到DIF,再对DIF做9日EMA得到DEA。ewm(span=..., adjust=False)是很多新手容易踩的点——adjust=False保证EMA从第一行开始就递推计算,而不是用前一段数据的加权平均做初始化,否则特征的前几十行会出现莫名的大幅波动。参数上,这些窗口值都是行业常用默认值,不要为了拟合历史而频繁改动,否则就滑向了过拟合。

3.2 归一化不要在整段数据上做:泄漏从这开始

特征算好之后,LSTM或神经网络类模型需要做归一化。这里的泄漏陷阱非常隐蔽:如果先对整段数据fit_transform,测试集的均值和最大值已经参与了训练数据的缩放计算,等于模型在训练时偷看了测试集分布。正确做法是只在训练段上fit,再用同一个scaler去transform测试段:

from sklearn.preprocessing import MinMaxScaler features = ["ma5", "ma20", "rsi14", "macd_dif", "macd_dea", "macd_hist"] train_size = int(len(df_scaled) * 0.8) scaler = MinMaxScaler(feature_range=(0, 1)) df_scaled = df.copy() df_scaled.loc[:train_size - 1, features] = scaler.fit_transform( df_scaled.loc[:train_size - 1, features] ) df_scaled.loc[train_size:, features] = scaler.transform( df_scaled.loc[train_size:, features] )

逻辑说明:第一行fit_transform只用了训练段的数据去计算min和max,第二行transform把同样的min和max套到测试段上。feature_range=(0, 1)是LSTM的常规输入区间,如果你用ARIMA或XGBoost这类模型,树模型对量纲不敏感,不做归一化也没关系。判断有没有泄漏的方法很简单:训练段的min/max和解压出的scaler属性打印出来,如果测试段出现了超出(0,1)区间范围的值,那就是正常的,因为测试段的极值本来就不该参与训练。

3.3 特征相关性排查:热力图之外看互信息

特征算完不是直接灌进模型,要先做一轮相关性排查。常见做法是画seaborn热力图看皮尔逊相关系数,但相关性矩阵只能抓线性关系,价格序列里很多有效信号是非线性的。互信息可以弥补这一点:

from sklearn.feature_selection import mutual_info_classif mi = mutual_info_classif( df_scaled[features], df_scaled["label_up"], random_state=42 ) print(dict(zip(features, mi)))

逻辑说明:mutual_info_classif计算每个特征与标签"明日是否上涨"的互信息值,数值越大说明特征与标签的非线性关联越强。random_state=42是为了结果可复现。这一轮排查的价值在于:互信息接近0的特征可以直接丢弃,相关性极高的一对特征(比如MA5和MA20相关系数超过0.95)保留一个即可,否则回归类模型会把多重共线性放大,预测值变得忽高忽低。

4. 三套模型最少代码:ARIMA、LSTM、XGBoost怎么落地

模型选型是这个zip里最容易被神化的部分。价格走势预测在从业者手里通常有三条路:ARIMA这类统计模型、LSTM这类深度学习模型、XGBoost这类梯度提升树模型。它们不是竞争关系,而是三种不同假设——ARIMA假设线性自相关,LSTM假设长短期时序依赖,XGBoost假设特征与标签存在可学习的非线性映射。选型看数据量和任务:样本少于500条先跑ARIMA和XGBoost,样本充足再上LSTM。

4.1 ARIMA:statsmodels最小可跑代码

ARIMA是老牌时间序列模型,核心是order=(p, d, q)三个参数。它只吃单变量序列,所以这里用收盘价本身来建模,不掺特征。代码如下:

from statsmodels.tsa.arima.model import ARIMA series_close = df_scaled["close"].iloc[:train_size] model_arima = ARIMA(series_close, order=(5, 1, 0)) model_arima_fit = model_arima.fit() forecast = model_arima_fit.forecast(steps=5) print(forecast)

逻辑说明:series_close是训练段的收盘价序列,order=(5, 1, 0)表示5阶自回归、1阶差分、0阶移动平均。d=1是因为原始收盘价通常非平稳,一阶差分后序列才会稳定;p=5是粗定的自回归阶数,准确做法是先画plot_acf和plot_pacf两张图,看截尾和拖尾特征来定阶。forecast(steps=5)预测未来5个交易日。这段代码的价值是当baseline——如果后面LSTM和XGBoost连ARIMA都跑不赢,说明特征或标签构造有问题,而不是模型不够高级。

4.2 LSTM:keras里最容易写错的三行

LSTM做价格预测的标准姿势是"滑动窗口监督学习":用过去20个交易日的特征预测下一个交易日的收益。最容易写错的地方集中在三处:input_shape不带batch维度、第一层return_sequences=True、训练时shuffle=False。最小代码如下:

import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense seq_len = 20 X, y = [], [] for i in range(seq_len, train_size): X.append(df_scaled[features].iloc[i - seq_len:i].values) y.append(df_scaled["label_ret"].iloc[i]) X = np.array(X) y = np.array(y) model = Sequential([ LSTM(50, return_sequences=True, input_shape=(seq_len, len(features))), LSTM(50), Dense(1) ]) model.compile(optimizer="adam", loss="mse") model.fit(X, y, epochs=30, batch_size=32, validation_split=0.1, shuffle=False)

逻辑说明:X的形状是(样本数, 20, 特征数),每个样本是连续20天的特征矩阵,y是对应第21天的收益率。input_shape=(seq_len, len(features))不需要写batch维度。第一层return_sequences=True是因为后面还要接第二层LSTM,如果写成False会直接报维度错误。shuffle=False是必须的——价格序列有强时序性,随机打乱样本等于把未来样本混进训练集,模型会在测试集上表现得忽好忽坏。epochs=30、batch_size=32是入门参数,实际调参看验证集loss曲线,如果验证loss先降后升,就该减少epochs或调小学习率。

4.3 XGBoost:特征矩阵直接喂,效果往往最稳

XGBoost在这个任务上经常被低估。它不需要滑动窗口构造三维输入,直接拿"今天的特征预测明天的收益",训练速度快、不需要归一化,而且对特征中的非线性关系拟合能力很强。最小代码如下:

from xgboost import XGBRegressor X_train = df_scaled[features].iloc[:train_size] y_train = df_scaled["label_ret"].iloc[:train_size] X_test = df_scaled[features].iloc[train_size:] model_xgb = XGBRegressor( n_estimators=200, learning_rate=0.03, max_depth=4, subsample=0.8, random_state=42 ) model_xgb.fit(X_train, y_train) pred_xgb = model_xgb.predict(X_test)

逻辑说明:X_train是训练段的特征矩阵,y_train是训练段的明日收益率,X_test是测试段特征。n_estimators=200控制树的数量,learning_rate=0.03是收缩步长,max_depth=4限制单棵树深度来防过拟合,subsample=0.8让每棵树只用80%的样本训练,增加随机性。这组参数对日频价格预测来说是一个稳定起点。XGBoost最实用的点在于它能输出feature_importances_,跑完直接看哪些特征在起作用,而不是把模型当黑匣子。

4.4 三者对比:选型看数据量、任务类型、可解释性

三套模型各有边界,下面这张表整理了最关键的区别:

模型输入形式适合数据量是否吃多特征可解释性常见翻车点
ARIMA单变量序列500条以下否高非平稳、阶数定错
LSTM滑动窗口三维张量2000条以上是极低归一化泄漏、shuffle错开
XGBoost特征矩阵500条以上是中学习率过大、特征泄漏

选型建议很直接:数据量不足500个交易日,ARIMA做baseline、XGBoost做主力,LSTM基本不用碰;数据量超过2000个交易日、你有GPU或耐心调参,再让LSTM上场。另外,把预测当成回归任务还是分类任务也影响选型——预测收益率用XGBRegressor或LSTM回归头,预测方向用XGBClassifier输出概率。实操里我见过最稳的组合是XGBoost回归预测收益率,再用阈值过滤信号,这个思路在下一章回测环节会体现价值。

5. 回测里的常见问题:为什么预测准了却亏钱

预测模型在测试集上表现不错,放到回测里却亏钱,这是价格走势预测项目最经典的"玄学时刻"。原因通常不在模型本身,而在回测口径和交易约束。这一章集中写回测阶段最常见的5个坑,每条都是"现象、原因、解决"三步,覆盖了新手90%的翻车现场。

5.1 回测口径:预测对了不代表能按这个价成交

回测的第一原则是"成交价不能偷看未来"。常见错误是:模型预测明天上涨,回测里用今天的收盘价买入,第二天用预测日的收盘价卖出。这等于提前知道明天的收盘价去成交,回测收益天然虚高。正确口径是:今天收盘后产生预测信号,明天开盘价买入,持有若干天后用卖出日的开盘价或收盘价卖出。用开盘价成交已经是最乐观的假设了,更保守的做法是用"次日开盘价加一个滑点"来模拟冲击成本。

回测的净值计算也要统一复权口径。用后复权数据算累计收益最稳妥,因为后复权价格天然包含了分红再投资,不会因为除权造成净值跳变。代码上,净值曲线的计算方式是:

df_strategy["position"] = df_strategy["signal"].shift(1) df_strategy["strategy_ret"] = df_strategy["position"] * df_strategy["ret"] df_strategy["nav"] = (1 + df_strategy["strategy_ret"]).cumprod()

逻辑说明:signal是当日收盘后产生的信号(1表示持仓、0表示空仓),shift(1)是为了让信号在第二天才生效,对应"次日开盘执行"的规则。strategy_ret是持仓状态下能拿到的收益率,nav是策略净值曲线。如果省略shift(1),信号当天就成交,等于提前使用了收盘后的信息,这就是未来函数的一种。

5.2 高频踩坑清单

5.2.1 未来函数:回测完美、实盘崩盘

现象:训练loss很低,回测净值曲线漂亮得像教科书,一上实盘就亏。原因:归一化用了全样本的min和max,或者特征里混入了当日的未来统计量,模型在测试阶段偷看了不该看的信息。解决:归一化只fit训练段;所有特征在构造时做shift(1),确保第T天的特征值在第T天收盘后才能算出来;回测信号必须shift(1)再用于次日成交。

5.2.2 时序样本被随机打乱

现象:LSTM训练过程正常,但预测曲线是锯齿状,完全不像连续的价格序列。原因:model.fit里shuffle=True,随机打乱了时间顺序,模型学到的是"乱序样本之间的统计关系",失去了时序依赖。解决:LSTM训练必须设置shuffle=False;如果用了validation_split,要确认验证集是训练集末尾的连续切片而不是随机抽取。

5.2.3 涨停买不进、跌停卖不出

现象:回测里盈利最多的几笔交易都发生在涨停板上。原因:没有处理涨跌停约束,模型预测涨停次日还大涨,回测默认按开盘价成交,但真实情况是涨停封单巨大根本买不进。解决:在数据里标记涨跌停样本,回测中对一字涨停样本禁止买入、一字跌停样本禁止卖出,或者直接把这类样本从交易时段中剔除。

5.2.4 前复权与后复权混用

现象:同一策略换一个数据源跑,净值曲线差异巨大。原因:技术指标用前复权计算,收益回测却用前复权数据算资产变动,分红除权日被误判为下跌。解决:特征计算用前复权,资金回测改用后复权数据,两者分开维护,混用时至少要做收益率的对齐校验。

5.2.5 用整段数据做特征筛选

现象:测试集IC高得离谱,但滚动回测收益为负。原因:先用全样本算互信息或相关性,筛完特征再切训练测试集,特征选择过程已经偷看了测试集。解决:特征筛选只在训练段内完成,或者使用下一章的walk-forward方式,每一段重训时重新选特征。

5.3 评估指标怎么选:准确率、IC与最大回撤

评价预测模型和评价策略是两件事。预测层面,方向准确率要超过53%才有统计意义上的优势,随机猜测是50%;收益率预测可以用IC来衡量,也就是预测值与真实收益率的秩相关。策略层面,只看累计收益不够,最大回撤和夏普比率更关键。计算IC的最小代码:

from scipy.stats import spearmanr y_test = df_scaled["label_ret"].iloc[train_size:].values ic, p_value = spearmanr(pred_xgb, y_test) print(f"IC: {ic:.4f}, p-value: {p_value:.4f}")

逻辑说明:spearmanr计算的是秩相关系数,不要求预测值和真实值呈线性关系,IC绝对值超过0.05就说明预测有微弱但有效的排序能力;p_value用来判断显著性,小于0.05才值得继续往下做。如果IC接近0,别浪费时间调参,回头查特征构造和标签对齐。

6. 进阶验证:用walk-forward滚动回测把预测驯化成信号

静态切分训练集和测试集只能证明模型"在历史数据上没翻车",不能证明模型"在未知未来上能持续工作"。价格预测项目最接近实盘环境的验证方式是walk-forward,也叫滚动训练:每过一段时间就用最近一段数据重新训练模型,然后用新模型预测下一段,模拟"边更新边交易"的真实节奏。这里以XGBoost为例,因为它训练快、不需要滑窗,最适合滚动验证:

import xgboost as xgb retrain_window = 250 retrain_step = 5 preds = [] test_dates = [] for end in range(retrain_window, len(df_scaled) - retrain_step, retrain_step): train_df = df_scaled.iloc[end - retrain_window:end] test_df = df_scaled.iloc[end:end + retrain_step] model = XGBRegressor( n_estimators=200, learning_rate=0.03, max_depth=4, subsample=0.8, random_state=42 ) model.fit(train_df[features], train_df["label_ret"]) preds.append(model.predict(test_df[features])) test_dates.extend(test_df["date"].tolist())

逻辑说明:retrain_window=250表示每次用最近250个交易日(约一年)的数据训练,retrain_step=5表示每5个交易日滚动一次。循环内部重新训练模型并预测接下来5天,预测结果拼成一条完整的预测序列。这样得到的预测曲线每个点都是用"当时能拿到的最新数据"算出来的,不存在任何未来信息,评估结果才可信。

滚动预测跑通之后,下一步是把预测值转成可执行的信号。不要每天交易,而是设阈值过滤:预测收益率大于0.5%才开仓,预测收益率低于负阈值才平仓或做空。这样做的本质是牺牲交易频率换取胜率,模型预测不准的日子直接空仓。阈值的选择看回测结果,一般取预测分布的上分位数,比如只交易预测收益排在前20%的日子。

我早期在这个项目上最大的教训就是急着上线LSTM,花了大量时间调参,回测却跑不过一个简单的XGBoost加阈值方案。后来我才意识到,价格走势预测项目的难点从不在单一模型的准确率,而在于数据是否干净、回测是否诚实、信号是否有过滤机制。把这三个问题解决好,哪怕用最简单的模型,也能搭出一个逻辑自洽、可验证的策略框架。希望帮到你。

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

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

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

立即咨询