简介:这份资源是一套面向金融量化初学者与投资者的A股走势预测实战项目,围绕机器学习算法与历史行情数据展开,帮助读者用数据驱动方式理解股票趋势判断。压缩包共12个文件,约2.41MB,包含6个ipynb交互式笔记、3个csv行情数据集、2个py脚本和1份md运行说明,覆盖数据获取、特征工程、模型训练与回测等环节。内容涉及K线数据入库、小市值结合盈利指标过滤策略、FFT滤波、单层与改进LSTM建模,以及基于sklearn的单票回测,并针对A股T+1与涨跌停等特性做了处理。已有521人学习下载,适合希望把机器学习落地到金融场景、需要完整代码与数据对照练习的读者,可据此复现预测流程、理解特征提取与模型评估思路,并在此基础上调整策略与优化模型。
1. 从一份带数据集和运行说明的 A 股走势预测源码说起
很多人第一次拿到「基于机器学习算法实现对A股股票走势预测系统源码+数据集+运行说明.zip」这类工程包,第一反应是解压、装依赖、跑main.py,然后盯着准确率数字发愣。我当年也这样,直到发现模型在训练集上 92%、一到实盘就接近抛硬币,才意识到问题不在算法,而在数据对齐和标签定义。这个标题真正要解决的是:用可复现的机器学习流程,把 A 股日线行情转成监督学习样本,训练一个能给出涨跌方向的分类器,并配一套能跑通的源码、数据集和运行说明。它适合想入门量化、做课程设计、或者想验证某个特征工程思路的开发者,不适合指望它直接下单赚钱的人。下面我按自己踩过的路径,把这份工程从数据到推理拆开讲。
2. 数据准备:把 A 股日线变成模型能吃的样本
2.1 数据集里通常有什么,以及你必须先检查的三列
这类工程包附带的数据集,常见格式是 CSV,字段一般包括date、code、open、high、low、close、volume、amount,有的还会带pct_chg、turnover_rate。拿到手先别急着喂模型,用 pandas 做三件事:看日期范围是否连续、看股票代码是否被混在一起、看复权状态是否一致。A 股有除权除息,如果一份数据里前复权和不复权混着,模型学到的就是跳空假信号。
import pandas as pd df = pd.read_csv("data/stock_daily.csv", parse_dates=["date"]) # 检查每只股票的交易日数量,少于 250 天的直接剔除 counts = df.groupby("code")["date"].count() valid_codes = counts[counts >= 250].index df = df[df["code"].isin(valid_codes)].copy() # 按股票和日期排序,避免后续 shift 错位 df.sort_values(["code", "date"], inplace=True) df["ret"] = df.groupby("code")["close"].pct_change() print(df.groupby("code")["ret"].describe().head())逻辑说明:groupby("code")保证每只股票独立计算收益率,pct_change()得到日收益率。参数上,250 天是我一般会设的最低门槛,对应一年交易日,太短的股票样本噪声大。如果数据集里没有复权字段,至少要用close的收益率而不是价格绝对值,这样能部分抵消除权影响。
2.2 标签怎么定义:未来 N 日涨跌比当日涨跌更稳
很多源码默认用「明天收盘比今天收盘涨」作为标签,这在 A 股 T+1 和涨跌停制度下会引入大量不可交易样本。更稳的做法是定义未来 5 日收益率是否超过某个阈值,比如 2%。这样标签更接近实际持仓周期,也能过滤掉微小波动。
HORIZON = 5 THRESH = 0.02 df["future_ret"] = df.groupby("code")["close"].shift(-HORIZON) / df["close"] - 1 df["label"] = (df["future_ret"] > THRESH).astype(int) # 最后 HORIZON 行没有未来数据,必须删掉 df = df.dropna(subset=["future_ret"]) print(df["label"].value_counts(normalize=True))参数说明:HORIZON=5对应一周持仓,THRESH=0.02是 2% 的收益门槛。如果标签比例严重失衡,比如正样本低于 20%,可以调低阈值或改用三分类。注意shift(-HORIZON)是按股票分组后的未来值,不能直接对整表 shift,否则会跨股票串行。
2.3 特征工程:别把未来函数混进来
常见特征包括过去 5/10/20 日收益率、成交量变化率、均线偏离度、RSI、MACD。关键是所有特征只能用到当日及之前的数据。我见过有人用df["close"].rolling(20).mean()算均线,但没加shift(1),结果当日收盘价参与了当日均线计算,模型在训练集上虚高。
df["ma5"] = df.groupby("code")["close"].transform(lambda x: x.rolling(5).mean()) df["ma20"] = df.groupby("code")["close"].transform(lambda x: x.rolling(20).mean()) df["ma_bias"] = df["close"] / df["ma20"] - 1 df["vol_chg"] = df.groupby("code")["volume"].pct_change() # 所有特征再整体 shift(1),确保预测时只用到昨天及以前 feature_cols = ["ma_bias", "vol_chg", "ret"] df[feature_cols] = df.groupby("code")[feature_cols].shift(1) df = df.dropna(subset=feature_cols)逻辑说明:transform保证按股票分组计算,shift(1)把特征整体后移一天,避免用当日信息预测当日标签。参数上,均线窗口 5 和 20 是常见短中期组合,成交量变化用pct_change比绝对量更稳定。这一步做完,数据才真正能进模型。
3. 模型训练:从逻辑回归到 LightGBM 的选型与调参
3.1 为什么我优先用树模型而不是 LSTM
A 股日线数据信噪比低,样本量通常几万到几十万条,LSTM 这类序列模型容易过拟合,训练慢且调参玄学。LightGBM 或 XGBoost 在表格特征上更稳,训练快,特征重要性可解释。如果工程包里用的是 LSTM,你可以先跑通,但建议同时用 LightGBM 做基线对比。常见做法是:逻辑回归看线性可分性,随机森林看非线性,LightGBM 做最终模型。
import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit # 按时间切分,不能随机打乱 train = df[df["date"] < "2022-01-01"] test = df[df["date"] >= "2022-01-01"] X_train, y_train = train[feature_cols], train["label"] X_test, y_test = test[feature_cols], test["label"] model = lgb.LGBMClassifier( n_estimators=300, learning_rate=0.05, max_depth=5, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(X_train, y_train) print(model.score(X_test, y_test))参数说明:n_estimators=300配合learning_rate=0.05是保守组合,防止过拟合;max_depth=5限制树深,A 股特征少,深树容易记住噪声;subsample和colsample_bytree做行和列采样。时间切分必须用TimeSeriesSplit或固定日期切,随机切分会导致未来数据泄露,准确率虚高。
3.2 类别不平衡与阈值调整
如果正样本比例只有 30% 左右,模型会偏向预测跌。可以用scale_pos_weight或class_weight="balanced",但更直接的是调整预测阈值。默认 0.5 不一定最优,用验证集画 PR 曲线找最佳阈值。
from sklearn.metrics import precision_recall_curve proba = model.predict_proba(X_test)[:, 1] precision, recall, thresholds = precision_recall_curve(y_test, proba) f1 = 2 * precision * recall / (precision + recall + 1e-8) best_thresh = thresholds[f1.argmax()] print("best threshold:", best_thresh) pred = (proba > best_thresh).astype(int)逻辑说明:predict_proba输出正类概率,precision_recall_curve给出不同阈值下的精确率和召回率,取 F1 最大对应的阈值。参数上,如果更看重召回可以手动调低阈值,但要注意交易成本。这一步能明显改善实盘表现,比单纯调模型参数更有效。
3.3 回测验证:别只看准确率
准确率 55% 不代表能赚钱,还要看盈亏比和最大回撤。简单做法是:当模型预测涨且概率超过阈值时买入,持有 HORIZON 天后卖出,统计累计收益。用backtrader可以做多股回测,但工程包通常只给信号,你可以先用 pandas 算净值曲线。
test = test.copy() test["signal"] = (proba > best_thresh).astype(int) test["strategy_ret"] = test["signal"] * test["future_ret"] nav = (1 + test.groupby("date")["strategy_ret"].mean()).cumprod() print(nav.iloc[-1])参数说明:future_ret是之前定义的未来 5 日收益,signal为 1 时持有。这里用等权平均模拟组合,实际还要考虑手续费和滑点。如果净值曲线在 2022 年单边下跌时回撤超过 30%,说明模型没有择时能力,需要加止损或市场状态过滤。
4. 避坑与排查:源码跑不通时先看这五条
4.1 现象:训练集准确率 95%,测试集 50%
原因:特征里混入了未来数据,最常见的是均线没 shift,或者标签用了当日收盘后信息。解决:逐列检查特征计算是否只用到date及之前的数据,把所有滚动计算加shift(1),并用时间切分验证。
4.2 现象:KeyError: 'code'或分组后行数对不上
原因:数据集里股票代码列名可能是stock_code、symbol或ts_code,不同来源不统一。解决:先print(df.columns),统一重命名为code,再做groupby。如果代码列有空格或大小写不一致,用.str.strip().str.upper()清洗。
4.3 现象:LightGBM 报错Do not support special JSON characters in feature name
原因:特征名里有中文或特殊符号。解决:训练前把feature_cols重命名为英文,比如ma_bias、vol_chg,或者用model.fit(X.values, y)传数组而不是 DataFrame。
4.4 现象:回测净值曲线一路向上,实盘却亏
原因:回测用了未来函数,或者没有考虑涨跌停无法成交。解决:检查signal是否在收盘后生成、次日开盘才能买入;涨停日买入信号要剔除。另外,手续费按万分之三、印花税千分之一估算,滑点按 0.1% 加进去。
4.5 现象:运行说明里的命令执行后缺包
原因:requirements.txt没锁版本,或者用了pip install -r但网络源不对。解决:先pip install pandas numpy scikit-learn lightgbm,再按报错逐个补。如果说明里写python main.py但实际入口是train.py,直接看目录结构找if __name__ == "__main__"。
5. 进阶技巧:用滚动训练和特征筛选把模型稳住
5.1 滚动窗口训练比一次性训练更贴近实盘
A 股风格切换快,用 2015 到 2020 年训练、2021 年测试,模型可能学到的是旧规律。我一般用滚动窗口:每次用过去 3 年数据训练,预测未来 1 个月,然后窗口前移。这样模型能跟上市场变化,但计算量会大几倍。
dates = sorted(df["date"].unique()) start_idx = 750 # 约 3 年交易日 step = 20 # 每月滚动一次 results = [] for i in range(start_idx, len(dates) - step, step): train_end = dates[i] test_end = dates[min(i + step, len(dates) - 1)] train = df[df["date"] <= train_end] test = df[(df["date"] > train_end) & (df["date"] <= test_end)] if len(test) == 0: continue model = lgb.LGBMClassifier(n_estimators=200, learning_rate=0.05, max_depth=5) model.fit(train[feature_cols], train["label"]) proba = model.predict_proba(test[feature_cols])[:, 1] test = test.copy() test["proba"] = proba results.append(test) rolling_result = pd.concat(results) print(rolling_result.groupby("date")["proba"].mean().tail())逻辑说明:start_idx=750保证首次训练有足够样本,step=20控制滚动频率。每次训练都重新拟合模型,避免用未来数据。参数上,如果股票数量多,可以按股票分别滚动,但计算时间会线性增加。这个技巧能显著降低过拟合,代价是代码复杂度上升。
5.2 特征重要性筛选:砍掉噪声特征
LightGBM 训练后可以输出feature_importances_,把重要性低于阈值的特征删掉再训练。我一般保留前 10 到 15 个特征,太多特征反而让模型学到噪声。下面是一个简单的筛选循环。
import numpy as np importance = model.feature_importances_ idx = np.argsort(importance)[::-1] selected = [feature_cols[i] for i in idx if importance[i] > 10] print("selected features:", selected) # 用筛选后的特征重新训练 model2 = lgb.LGBMClassifier(n_estimators=300, learning_rate=0.05, max_depth=5) model2.fit(X_train[selected], y_train) print(model2.score(X_test[selected], y_test))参数说明:importance[i] > 10是经验阈值,具体看特征数量,如果只有 5 个特征可以设 0。筛选后如果准确率下降超过 2 个百分点,说明砍多了,要放宽阈值。这一步配合滚动训练,能把模型从「训练集好看」拉到「测试集可用」。
5.3 一个我常犯的错误:用准确率选模型
早期我总挑测试集准确率最高的模型,后来发现准确率高的模型往往偏向预测多数类,实盘收益反而差。现在我会同时看三个指标:测试集 F1、滚动回测夏普、最大回撤。如果 F1 高但回撤大,说明模型在极端行情下失效。希望帮到你,少走我当年那段只看准确率的弯路。
本文还有配套的精品资源,点击获取