
简介这是一套面向Python入门者、希望快速接触量化投资与机器学习交叉领域的教学型代码包围绕LightGBM模型构建证券投资策略并完成历史回测。包内共20个文件以5个py脚本、6张png图表、3个zbak备份、2个txt清单及docx文档、md说明为主压缩包约5.59MB涵盖数据采集、特征工程、模型训练与回测评估的完整链路。已有104人学习关注。读者可参照脚本依次完成行情获取、衍生特征生成、LightGBM训练与回测输出每日建议持仓及收益记录并得到累计收益率、最大回撤、夏普比率三项核心指标与可视化图表直观理解机器学习选股流程。代码结构清晰、注释友好适合作为课程设计或自学练手项目帮助建立从数据到策略评估的量化分析思维。1. 用 LightGBM 做量化策略从因子到回测这套源码把链路跑通了很多人做量化卡在同一个地方因子挖了一堆模型也训了但一到回测就发现收益曲线跟训练集对不上实盘更是直接翻车。问题往往不在模型本身而在特征工程、标签构造、时序切分和回测撮合这几段链路没对齐。这份资源给了一套基于 LightGBM 的量化投资策略完整实现从数据预处理、因子构建、模型训练到回测分析代码是能直接跑通的那种不是只给个 notebook 片段。它适合两类人一是刚接触机器学习选股、想搞明白 LightGBM 在金融时序里到底怎么用的新手二是已经会调模型、但回测环节总是踩坑、想找一份结构清晰的参考实现来对照的熟手。下面我按自己拆包的顺序把关键环节和参数逐个讲清楚。2. 因子工程与标签构造LightGBM 吃得下什么数据2.1 为什么量化场景偏爱 LightGBM金融数据有几个特点样本量不算大、特征维度高、噪声重、非线性关系多。深度学习在这类数据上容易过拟合线性模型又抓不住交互项。LightGBM 作为梯度提升树天然适合处理表格型特征对缺失值不敏感训练速度快还能输出特征重要性方便做因子筛选。它的直方图算法和 leaf-wise 生长策略在几万到几十万样本的股票面板数据上单机就能跑完不需要 GPU。这也是为什么近两年量化圈子里 LightGBM 回归模型成了基线方案——不是它多先进而是性价比高、可解释性够用、调参空间可控。常见做法是把 LightGBM 当回归模型用预测未来 N 日的收益率再按预测值排序选股。也有人做分类预测涨跌方向。这份资源走的是回归路线标签是未来收益率这样能保留更多信息量排序时也更细腻。2.2 因子构建的代码骨架因子部分通常包括动量、波动率、估值、流动性几大类。下面这段是资源里因子计算的典型写法我加了注释说明每一步在干什么import pandas as pd import numpy as np def build_features(df): df 需包含: date, code, close, volume, amount, high, low, open 返回带因子的 DataFrame df df.sort_values([code, date]).copy() g df.groupby(code) # 动量因子过去 20 日收益率 df[ret_20] g[close].pct_change(20) # 波动率因子过去 20 日收益率标准差 df[vol_20] g[close].pct_change().rolling(20).std().reset_index(0, dropTrue) # 换手率代理成交量 / 20 日均量 df[vol_ratio] df[volume] / g[volume].rolling(20).mean().reset_index(0, dropTrue) # 振幅因子 df[amplitude] (df[high] - df[low]) / df[close].shift(1) # 标签未来 5 日收益率 df[label] g[close].shift(-5) / df[close] - 1 return df.dropna()逻辑上groupby(code)保证每个因子都在单只股票内部按时间计算避免跨股票串数据。pct_change(20)算的是 20 日累计收益rolling(20).std()算波动shift(-5)构造未来收益标签。参数上20 和 5 是常见起点但要注意窗口越长因子越平滑但滞后越严重标签周期要和调仓频率匹配周频调仓就用 5 日月频就用 20 日。注意rolling之后接reset_index(0, dropTrue)是为了对齐索引不同 pandas 版本行为有差异跑之前先确认索引没错位。2.3 标签构造里最容易忽略的细节标签是未来收益率但这里有个坑如果你用close算标签又用close算因子那当天收盘价同时出现在特征和标签里会造成信息泄露。正确做法是因子用 T 日及之前的数据标签用 T1 到 TN 的收益。资源里标签用的是shift(-5) / close - 1隐含假设是 T 日收盘买入、T5 日收盘卖出。如果你改成 T1 开盘买入标签就要相应调整否则回测收益会虚高。另外停牌、涨跌停的样本要处理。停牌期间因子不变但标签可能缺失涨跌停日买入不进去回测时会失真。常见做法是标记这些日期在训练时剔除或降权。3. 模型训练与时序切分别用随机切分毁掉整个回测3.1 时序切分为何不能用 train_test_split这是血泪经验很多人习惯性用train_test_split随机切分结果模型在测试集上表现很好一回测就崩。原因是随机切分让未来数据泄露到了训练集模型“见过”未来了。金融时序必须按时间切分训练集在前验证集在后测试集再往后。资源里的做法是滚动窗口或扩展窗口。滚动窗口是固定训练长度比如用过去 3 年训练、预测下 1 个月扩展窗口是训练集不断累加。两种各有适用场景滚动窗口对市场风格切换更敏感扩展窗口样本更多但可能引入过时数据。def time_series_split(df, train_end, valid_end): train_end: 训练集截止日期 valid_end: 验证集截止日期 测试集为 valid_end 之后 train df[df[date] train_end] valid df[(df[date] train_end) (df[date] valid_end)] test df[df[date] valid_end] return train, valid, test参数上train_end和valid_end要根据数据跨度定。如果数据从 2018 到 2024可以设 2022-12-31 为训练截止、2023-06-30 为验证截止剩下做测试。注意验证集要留够至少一个完整市场周期否则早停不可靠。3.2 LightGBM 关键参数怎么设资源里的训练代码大致如下import lightgbm as lgb params { objective: regression, metric: mse, boosting_type: gbdt, num_leaves: 31, learning_rate: 0.05, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, min_data_in_leaf: 50, lambda_l2: 1.0, verbose: -1 } train_data lgb.Dataset(X_train, labely_train) valid_data lgb.Dataset(X_valid, labely_valid, referencetrain_data) model lgb.train( params, train_data, num_boost_round1000, valid_sets[valid_data], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] )逐个说num_leaves31控制树复杂度金融数据噪声大叶子数别太高否则过拟合learning_rate0.05配合num_boost_round1000和早停是稳妥组合feature_fraction0.8和bagging_fraction0.8引入随机性降低方差min_data_in_leaf50防止叶子节点样本太少lambda_l21.0是 L2 正则。这些值不是最优但作为起点不容易翻车。调参时优先动num_leaves和min_data_in_leaf这两个对过拟合影响最大。提示early_stopping(50)表示验证集指标 50 轮不提升就停。金融数据验证集噪声大早停轮数可以设大一点比如 100避免过早停止。3.3 特征重要性与因子筛选训练完可以输出特征重要性importance pd.DataFrame({ feature: model.feature_name(), gain: model.feature_importance(gain) }).sort_values(gain, ascendingFalse) print(importance.head(20))gain比split更能反映特征贡献因为它累计了分裂带来的增益。如果某些因子重要性长期垫底可以考虑剔除减少维度。但别只看一次训练结果不同时间段重要性会变最好滚动多期观察稳定性。4. 回测分析从预测值到净值曲线要过几道关4.1 回测框架的核心逻辑回测不是简单地把预测收益累加。资源里的回测模块大致做这几件事按调仓日取预测值、排序选股、计算持仓收益、扣除交易成本、记录净值。下面是一个简化版def backtest(df, pred_col, top_n50, cost0.001): df: 含 date, code, close, pred_col top_n: 每期选股数 cost: 单边交易成本 dates sorted(df[date].unique()) nav [1.0] prev_holdings set() for i in range(len(dates) - 1): today, tomorrow dates[i], dates[i 1] cross df[df[date] today].copy() cross cross.sort_values(pred_col, ascendingFalse) holdings set(cross.head(top_n)[code]) # 计算次日收益 next_ret df[df[date] tomorrow].set_index(code)[close] / \ df[df[date] today].set_index(code)[close] - 1 port_ret next_ret[list(holdings)].mean() # 换手成本 turnover len(holdings - prev_holdings) / top_n port_ret - turnover * cost nav.append(nav[-1] * (1 port_ret)) prev_holdings holdings return pd.Series(nav, indexdates)逻辑上每个调仓日按预测值排序取前top_n等权持有到下一日换手部分扣成本。参数top_n影响分散度和收益50 是常见值太小波动大太大趋近指数。cost0.001是单边千一实际要按佣金加印花税加冲击成本估。4.2 回测指标怎么看净值曲线只是表象关键指标包括年化收益、最大回撤、夏普比率、换手率、胜率。资源里应该带了指标计算我一般会补一个def performance(nav, freq252): ret nav.pct_change().dropna() ann_ret (nav.iloc[-1]) ** (freq / len(nav)) - 1 ann_vol ret.std() * np.sqrt(freq) sharpe ann_ret / ann_vol if ann_vol 0 else 0 drawdown (nav / nav.cummax() - 1).min() return {年化收益: ann_ret, 年化波动: ann_vol, 夏普: sharpe, 最大回撤: drawdown}夏普高不一定好如果换手率极高扣完成本可能就没了。最大回撤要结合恢复时间看回撤深且恢复慢的策略实盘很难拿住。4.3 常见回测失真来源一是前视偏差用了未来数据二是幸存者偏差股票池只含现在还在的股票三是成本低估没算冲击成本四是流动性假设小盘股按收盘价成交不现实。资源里如果没处理这些回测收益会偏乐观。我一般会做敏感性测试把成本翻倍、把top_n减半、把调仓日错开一天看净值变化大不大。变化剧烈说明策略脆弱。5. 避坑与排查这几处翻车点我替你踩过了5.1 现象验证集 loss 很低但回测亏钱原因通常是验证集和测试集分布不一致或者验证集本身有泄露。解决检查时序切分是否严格按时间验证集是否包含了训练集见过的股票或日期。另外验证集 loss 是 MSE回测看的是排序收益两者目标不一致MSE 低不代表排序好。可以改用 IC 或 rank IC 作为验证指标。5.2 现象特征重要性里某因子极高但去掉后回测没变化原因可能是该因子和标签有泄露关系比如用了未来数据构造。解决逐个因子做时间对齐检查确认因子计算只用到 T 日及之前数据。常见泄露点是shift方向写反、rolling默认包含当前值、groupby后索引错位。5.3 现象训练正常预测时全为 NaN原因通常是预测数据的特征列和训练时不一致或者有缺失值未处理。LightGBM 对缺失值容忍但列名和顺序要对齐。解决保存训练时的特征列表预测前用reindex(columnsfeature_cols)对齐缺失值填充或保留 NaN 让模型处理。5.4 现象回测净值曲线过于平滑夏普高得离谱原因多半是用了收盘价成交且没有滑点或者选股数太少导致偶然性大。解决改成次日开盘成交加入滑点扩大选股数做多组随机种子测试。如果夏普仍然异常高检查是否有未来函数。5.5 现象LightGBM 训练报错Cannot convert NaN to int原因通常是标签列有 NaN或者类别特征未编码。解决训练前dropna(subset[label])类别特征用astype(category)或提前做编码。LightGBM 支持类别特征但需要显式声明categorical_feature。6. 进阶技巧用滚动训练和 IC 加权把策略再推一步基础版跑通后我一般会做两件事滚动训练和预测值后处理。滚动训练是每隔一段时间重新训练模型用最新数据更新参数适应市场变化。实现上可以用扩展窗口每次加入新数据后重新lgb.train但要注意训练频率别太高否则过拟合近期噪声。常见做法是每月或每季度重训一次。def rolling_train(df, feature_cols, label_col, train_months36, retrain_freqM): 滚动训练每次用过去 train_months 个月数据训练 返回每个调仓日的预测值 df[month] pd.to_datetime(df[date]).dt.to_period(M) months sorted(df[month].unique()) preds [] for i, m in enumerate(months): if i train_months: continue train_start months[i - train_months] train_df df[(df[month] train_start) (df[month] m)] test_df df[df[month] m] model lgb.train(params, lgb.Dataset(train_df[feature_cols], train_df[label_col]), num_boost_round500) test_df test_df.copy() test_df[pred] model.predict(test_df[feature_cols]) preds.append(test_df[[date, code, pred]]) return pd.concat(preds)参数train_months36是训练窗口长度太长会引入过时数据太短样本不够。retrain_freq控制重训频率月频调仓就按月重训。这段代码没做早停因为滚动训练里验证集不好固定可以留最后一个月做验证。第二件事是预测值后处理。LightGBM 输出的预测值绝对值没意义排序才有意义。我一般会做横截面标准化或排名再按排名选股。另外可以计算 IC信息系数即预测值和实际收益的横截面相关系数用 IC 均值除以 IC 标准差得到 ICIR衡量因子稳定性。如果 ICIR 低于 0.3策略可靠性存疑。def calc_ic(df, pred_col, label_col): ic df.groupby(date).apply( lambda x: x[pred_col].corr(x[label_col], methodspearman) ) return ic.mean(), ic.std(), ic.mean() / ic.std()spearman秩相关比 pearson 更适合金融数据因为关系非线性且异常值多。IC 均值 0.05 以上、ICIR 0.5 以上算不错但也要看市场环境。最后说个习惯从那以后我每次跑完回测都会强制走一遍「成本翻倍 调仓错开一天 随机选股对照」这三项检查任何一项让策略收益腰斩就说明策略不够稳健得回去改因子或调参。希望帮到你。本文还有配套的精品资源点击获取