☰
基于机器学习算法的A股股票走势预测系统源码实战:从数据到回测
2026/10/12 18:54:06 网站建设 项目流程

简介:这份资源是面向金融量化初学者与机器学习实践者的A股走势预测系统源码包,围绕数据获取、特征工程、模型训练与回测验证的完整链路展开,帮助读者理解如何用算法从历史行情中挖掘价格变动规律。包内共12个文件,以6个ipynb交互式笔记为核心,配合3个csv行情数据样本、2个py脚本及1个gitignore配置,压缩包约2.53MB,结构轻量便于快速上手。内容覆盖K线数据入库、小市值结合盈利指标过滤策略验证、FFT滤波、单票LSTM建模与sklearn回测等典型环节,涉及线性回归、随机森林、SVM及LSTM等算法的落地思路。目前已有307人学习下载,适合希望打通金融数据预处理、模型调参与回测评估全流程的读者参考,可据此复现实验并迁移到自己的选股或择时场景中。

1. 从一份 A 股日线数据说起:机器学习预测股票走势到底靠不靠谱

很多人第一次听到「基于机器学习算法实现对A股股票走势预测系统源码」,脑子里浮现的是训练一个模型、输入代码、输出涨跌,然后坐等收益。我当初也这么想过,直到把一份 2018 到 2023 年的全 A 日线数据丢进 LightGBM,回测出来的年化还不如沪深300 定投,才明白问题不在模型,而在「预测什么」和「怎么验证」。这个方向真正能落地的价值,不是给你一个稳赚信号,而是把「特征工程 + 标签构造 + 滚动回测」这套工程链路跑通,让你能快速证伪一个想法,而不是靠感觉下单。它适合有 Python 基础、懂一点 pandas、想认真做量化研究的从业者,不适合指望复制源码就能躺赚的人。下面我按自己实际搭过的一版流程,把数据、特征、模型、回测和踩坑讲清楚。

2. 数据从哪来、标签怎么打:预测系统的地基

2.1 A股日线数据的获取与清洗

做 A 股预测,数据源决定了你后面所有工作的上限。常见做法有三类:一是用 akshare、tushare 这类开源接口拉日线,二是本地已有券商导出或 Wind/聚宽导出的 CSV,三是自己爬东方财富、同花顺。我一般优先用 akshare,因为它免费、字段稳定,缺点是接口偶尔抽风,需要加重试。注意,东方财富的页面结构经常变,自己写爬虫维护成本高,热搜里提到的「东方股吧反爬」就是这个坑,非必要不自己爬。

下面是我常用的拉取与清洗脚本,以 akshare 为例:

import akshare as ak import pandas as pd import time def fetch_daily(symbol: str, start: str, end: str, retry: int = 3) -> pd.DataFrame: """拉取单只股票日线,带重试""" for i in range(retry): try: df = ak.stock_zh_a_hist( symbol=symbol, # 6位代码,如 "000001" period="daily", start_date=start, # "20180101" end_date=end, adjust="qfq" # 前复权,回测必须用复权价 ) if df.empty: return pd.DataFrame() df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume", "成交额": "amount", "换手率": "turnover" }) df["date"] = pd.to_datetime(df["date"]) df["symbol"] = symbol return df.sort_values("date").reset_index(drop=True) except Exception as e: print(f"{symbol} 第{i+1}次失败: {e}") time.sleep(2) return pd.DataFrame() # 批量拉取,注意加 sleep 避免被限流 symbols = ["000001", "600519", "300750"] all_df = [] for s in symbols: d = fetch_daily(s, "20180101", "20231231") if not d.empty: all_df.append(d) time.sleep(0.5) data = pd.concat(all_df, ignore_index=True)

逻辑说明:adjust="qfq"是关键参数,前复权保证价格连续,否则除权日会出现假跳空,模型会学到噪声。retry配合sleep是为了应对接口限流,实测 0.5 秒间隔比较稳。清洗阶段还要做三件事:剔除上市不足 60 个交易日的次新股、剔除停牌日(成交量长期为 0)、把涨跌停日单独标记,因为涨跌停当天特征失真严重。

2.2 标签构造:预测未来 N 日收益还是涨跌方向

标签决定了模型在学什么。常见两种:分类标签(未来 5 日涨/跌)和回归标签(未来 5 日收益率)。我倾向回归,因为分类会丢掉幅度信息,而且阈值一改结果全变。标签公式:

# 未来5日收益率,作为回归标签 data = data.sort_values(["symbol", "date"]) data["future_ret"] = data.groupby("symbol")["close"].transform( lambda x: x.shift(-5) / x - 1 ) # 二分类标签:未来5日是否上涨 data["label"] = (data["future_ret"] > 0).astype(int) # 去掉尾部无法计算标签的行 data = data.dropna(subset=["future_ret"])

参数说明:shift(-5)表示用未来第 5 天收盘价对比今天,预测周期 5 日是 A 股短线研究的常用窗口,太长噪声大,太短交易成本吃光收益。注意标签必须按股票分组计算,否则会跨股票串行。这一步最容易翻车的地方是用了未来数据——比如用当日收盘价算标签又用当日收盘价做特征,回测漂亮,实盘必亏。

2.3 特征工程:量价、波动率与资金流

特征不在多,在有没有信息增量。我一般分四组:动量类(过去 5/10/20 日收益率)、波动类(20 日收益率标准差、ATR)、量能类(换手率、量比、成交额环比)、位置类(收盘价在 20 日高低点中的分位)。下面是一个特征生成函数:

import numpy as np def add_features(df: pd.DataFrame) -> pd.DataFrame: df = df.sort_values(["symbol", "date"]).copy() g = df.groupby("symbol")["close"] # 动量 for n in [5, 10, 20]: df[f"ret_{n}"] = g.transform(lambda x: x / x.shift(n) - 1) # 波动率 df["vol_20"] = df.groupby("symbol")["ret_1"].transform( lambda x: x.rolling(20).std() ) if "ret_1" in df else np.nan df["ret_1"] = g.transform(lambda x: x.pct_change()) df["vol_20"] = df.groupby("symbol")["ret_1"].transform( lambda x: x.rolling(20).std() ) # 量能 df["vol_ratio"] = df["volume"] / df.groupby("symbol")["volume"].transform( lambda x: x.rolling(20).mean() ) # 位置 df["high_20"] = df.groupby("symbol")["high"].transform( lambda x: x.rolling(20).max() ) df["low_20"] = df.groupby("symbol")["low"].transform( lambda x: x.rolling(20).min() ) df["pos_20"] = (df["close"] - df["low_20"]) / (df["high_20"] - df["low_20"] + 1e-9) return df data = add_features(data)

逻辑说明:所有滚动计算都必须groupby("symbol"),否则会把不同股票的数据混在一起。vol_ratio用 20 日均量做分母,衡量当天放量程度。pos_20是 0 到 1 的分位,接近 1 说明在近期高位。这些特征都是当日收盘后可知的,不存在未来函数。特征做完后要检查缺失值,前 20 行必然有 NaN,直接 drop 或填充都要按股票分组处理。

3. 模型选型与训练:LightGBM 为什么比 LSTM 更适合起步

3.1 树模型和深度模型的取舍

热搜里「机器学习算法」「机器学习模型」「机器学习和深度学习」反复出现,很多人一上来就想上 LSTM、Transformer。我的血泪经验是:A 股日频数据信噪比极低,样本量按股票算也就几千行,深度模型很容易过拟合,训练还慢。LightGBM 这类梯度提升树对表格特征友好、训练快、可解释性强,特征重要性直接告诉你哪些因子在起作用。常见做法是先用 LightGBM 跑通基线,再考虑要不要上深度模型。

选型对比:

模型训练速度小样本表现可解释性适合场景
LightGBM快好强表格特征、基线
XGBoost中好强同上,参数更多
LSTM慢差弱序列建模、数据量大
线性回归极快一般极强因子有效性验证

3.2 时序切分与滚动训练

股票数据不能随机切分,必须按时间切。我一般用「训练 3 年、验证 1 年、测试 1 年」的滚动窗口,或者用 walk-forward。下面是最小训练脚本:

import lightgbm as lgb from sklearn.metrics import mean_squared_error # 按时间切分,禁止 shuffle train = data[data["date"] < "2022-01-01"] valid = data[(data["date"] >= "2022-01-01") & (data["date"] < "2023-01-01")] test = data[data["date"] >= "2023-01-01"] feat_cols = [c for c in data.columns if c.startswith(("ret_", "vol_", "pos_"))] params = { "objective": "regression", "metric": "rmse", "learning_rate": 0.05, "num_leaves": 31, "min_data_in_leaf": 100, # 防止过拟合的关键参数 "feature_fraction": 0.8, "bagging_fraction": 0.8, "bagging_freq": 5, "verbose": -1 } dtrain = lgb.Dataset(train[feat_cols], label=train["future_ret"]) dvalid = lgb.Dataset(valid[feat_cols], label=valid["future_ret"]) model = lgb.train( params, dtrain, num_boost_round=500, valid_sets=[dvalid], callbacks=[lgb.early_stopping(50)] ) pred = model.predict(test[feat_cols]) print("Test RMSE:", mean_squared_error(test["future_ret"], pred, squared=False))

参数说明:min_data_in_leaf=100是防过拟合最重要的参数,A 股噪声大,叶子节点样本太少会记住噪声。early_stopping(50)表示验证集 50 轮不提升就停。learning_rate=0.05配合 500 轮是比较稳的组合。注意feat_cols的筛选要排除future_ret、label这些标签列,否则就是数据泄露。

3.3 特征重要性与因子筛选

训练完一定要看特征重要性,不然你不知道模型在靠什么预测:

importance = pd.DataFrame({ "feature": feat_cols, "gain": model.feature_importance("gain") }).sort_values("gain", ascending=False) print(importance.head(15))

如果发现某个特征 gain 异常高,先怀疑是不是未来函数。我踩过一次坑:把amount(成交额)和future_ret一起算,结果amount排第一,查了半天才发现是数据里混入了未来某天的成交额。特征重要性是排查数据泄露的第一道防线。

4. 回测验证:backtrader 多股回测怎么接预测信号

4.1 从预测值到交易信号

模型输出的是未来 5 日收益率预测,要转成买卖信号。常见做法是设定阈值:预测收益大于 1% 买入,小于 -1% 卖出,中间持有。注意 A 股 T+1 和涨跌停限制,回测必须模拟。

def signal_from_pred(pred_ret: float, buy_th: float = 0.01, sell_th: float = -0.01) -> int: if pred_ret > buy_th: return 1 # 买入 elif pred_ret < sell_th: return -1 # 卖出 return 0 # 持有

参数说明:阈值不能拍脑袋,要用验证集调。阈值太高信号少,太低交易成本吃光收益。我一般会画一张阈值-收益曲线,选夏普最高的点。

4.2 backtrader 多股回测的最小骨架

热搜里「backtrader多股回测」是真实需求,单股回测容易过拟合,多股才能看出策略稳健性。下面是一个多股回测骨架:

import backtrader as bt class MLSignalStrategy(bt.Strategy): params = (("signal_dict", {}),) # {date: {symbol: signal}} def __init__(self): self.order_dict = {} def next(self): dt = self.datas[0].datetime.date(0) signals = self.params.signal_dict.get(dt, {}) for d in self.datas: sig = signals.get(d._name, 0) pos = self.getposition(d).size if sig == 1 and pos == 0: self.buy(data=d, size=100) elif sig == -1 and pos > 0: self.close(data=d) cerebro = bt.Cerebro() cerebro.broker.setcash(1000000) cerebro.broker.setcommission(commission=0.0003) # 万三佣金 # 逐个 adddata,注意用复权后的数据 # cerebro.addstrategy(MLSignalStrategy, signal_dict=signal_dict) # cerebro.run()

逻辑说明:signal_dict是提前算好的「日期-股票-信号」映射,回测时按日查表。setcommission(0.0003)是佣金,别忘了印花税(卖出千一),A 股实际成本比这高。多股回测要注意资金分配,简单等权容易在股票多时每只仓位过小,我一般限制同时持仓不超过 10 只。

4.3 回测指标怎么看

别只看年化收益。我一般看四个:夏普比率、最大回撤、胜率、盈亏比。夏普低于 1 基本不用看,最大回撤超过 30% 要检查是不是集中持仓。还有一个容易被忽略的:换手率。预测周期 5 日、每天调仓,换手率会高得吓人,实盘成本远超回测。

5. 避坑与排查:这套系统最容易翻车的 5 个地方

5.1 未来函数:回测漂亮实盘必亏

现象:回测年化 50%,实盘一上就亏。原因:特征里混入了未来信息,比如用当日收盘价算了标签又当特征,或者用了未来才公布的财务数据。解决:所有特征必须用shift(1)确保只用历史数据,标签单独算,训练前用「特征日期 < 标签日期」做一次断言检查。

5.2 幸存者偏差:退市股票没进样本

现象:模型在历史数据上表现很好,但实盘选出的股票经常暴雷。原因:拉数据时只拉了当前还在上市的股票,退市的没进来,样本天然偏向活下来的公司。解决:用包含退市股票的完整成分股列表,或者至少用指数成分股历史变更数据。akshare 有退市股票接口,拉全量。

5.3 复权方式选错:除权日出现假信号

现象:某天特征突然异常,模型给出错误信号。原因:用了不复权价格,除权日价格跳空被当成真实下跌。解决:回测统一用前复权qfq,如果做长周期研究可以用后复权。注意前复权在每次除权后会变,历史数据要重新拉。

5.4 过拟合:验证集调参调到天上去

现象:验证集夏普 2.0,测试集 0.3。原因:在验证集上反复调参,把验证集也「训练」了。解决:留一个从未看过的测试集,只在最后跑一次。参数搜索用滚动窗口,不要用固定验证集。

5.5 交易成本低估:换手率吃光收益

现象:回测年化 20%,扣掉手续费和印花税只剩 5%。原因:预测周期短、调仓频繁,成本被低估。解决:回测里把佣金设成万三、印花税千一,滑点按 0.1% 算。如果策略换手率超过每年 20 倍,基本可以放弃。

6. 进阶技巧:用滚动训练和集成提升稳健性

跑通基线后,想再进一步,我一般做两件事:滚动训练和模型集成。滚动训练是每隔一段时间用最新数据重新训练,避免模型老化。下面是一个简化实现:

def walk_forward(data, feat_cols, train_years=3, retrain_freq="6M"): """滚动训练,每6个月重训一次""" dates = sorted(data["date"].unique()) results = [] for i, split_date in enumerate(pd.date_range(dates[0], dates[-1], freq=retrain_freq)): train_end = split_date train_start = train_end - pd.DateOffset(years=train_years) test_end = train_end + pd.DateOffset(months=6) train = data[(data["date"] >= train_start) & (data["date"] < train_end)] test = data[(data["date"] >= train_end) & (data["date"] < test_end)] if len(train) < 1000 or len(test) < 100: continue model = lgb.train(params, lgb.Dataset(train[feat_cols], train["future_ret"]), num_boost_round=300) pred = model.predict(test[feat_cols]) results.append({"split": split_date, "rmse": mean_squared_error( test["future_ret"], pred, squared=False)}) return pd.DataFrame(results)

参数说明:train_years=3是训练窗口,太短学不到规律,太长包含过时市场结构。retrain_freq="6M"是重训频率,A 股风格切换快,半年一次比较合理。集成则是把 LightGBM、XGBoost、线性回归的预测值做加权平均,权重用验证集 IC 确定。实测集成后 IC 能提升 0.01 到 0.02,别小看这点,在低信噪比场景里很值钱。

最后说个习惯:我每次改完特征或参数,第一件事不是看收益,而是看特征重要性和 IC 稳定性。收益可以骗人,IC 不会。这套系统值不值得做,取决于你愿不愿意花时间在数据和验证上,而不是模型本身。希望帮到你。

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

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

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

立即咨询