基于机器学习的量化投资策略Python源码:从回测到实盘的避坑指南 简介这份Python源码面向具备一定编程基础的量化投资学习者与金融数据分析从业者围绕机器学习预测金融市场价格走势这一核心问题提供了一套可运行的策略实现方案。资源包共15个文件以5个py脚本为主体涵盖数据获取、特征工程、模型训练与回测等模块另含6张png图表用于展示K线、回撤与预测效果以及txt、md等说明文件压缩包约737KB结构紧凑便于快速上手。源码采用LightGBM梯度提升决策树模型结合K线图中的开盘价、收盘价、最高价、最低价等价格信息构建特征完整覆盖数据收集、特征提取、模型训练与测试四个阶段读者可据此理解量化策略从数据到决策的全流程。目前已有222人学习下载适合希望将机器学习方法落地于金融预测场景、并借助现成代码框架进行二次开发与实验的读者参考。1. 从一份「基于机器学习的量化投资策略Python源码」说起它到底能帮你赚到钱还是帮你少亏钱很多人第一次搜「基于机器学习的量化投资策略Python源码」脑子里想的是一份下载下来就能自动下单、躺着数钱的脚本。我当年也这么想过结果第一次跑通回测年化收益 40%实盘一上两周回撤 18%。问题不在代码在于我把「回测漂亮」当成了「策略有效」。这份源码真正值钱的地方不是那几行预测涨跌的模型而是它把数据清洗、特征工程、标签构造、滚动训练、回测撮合这一整条链路串起来了——它是一套可复现的研究框架不是一个印钞机。这篇文章面向两类人一类是会用 Python、懂点 pandas但不知道怎么把机器学习塞进交易逻辑里的开发者另一类是做过传统技术指标策略想升级到机器学习模型却总在过拟合和未来函数上翻车的交易者。我会按「数据怎么来、特征怎么造、模型怎么训、回测怎么防作弊、实盘怎么接」的顺序把一份能跑的量化投资策略源码拆开讲清楚参数怎么设、坑在哪、哪些地方是玄学都摊开说。你照着做至少能少走我踩过的那些弯路。2. 数据与标签量化投资策略源码里最容易被低估的地基2.1 为什么说 80% 的翻车都发生在数据层一份量化投资策略 Python 源码模型部分往往只有一两百行但数据处理部分能占一半以上。原因很简单金融时间序列和图像、文本不一样它有三个致命特性——非平稳、低信噪比、存在未来函数。非平稳意味着你 2018 年训练的模型2020 年可能完全失效低信噪比意味着你随便加个特征回测收益就变了但那是噪声不是信号未来函数意味着你不小心用了当天收盘价去预测当天开盘回测能做出 90% 胜率实盘一分钱赚不到。常见做法是先用 akshare 或 tushare 拉日线数据做前复权处理然后构造特征和标签。我一般会把数据分成三段训练集、验证集、测试集但注意——金融数据不能随机打乱必须按时间切分否则就是变相的未来函数。下面这段代码是数据加载和基础清洗的最小骨架。import pandas as pd import numpy as np import akshare as ak # 拉取单只股票日线前复权 def load_stock_data(symbol600519, start20180101, end20231231): df ak.stock_zh_a_hist(symbolsymbol, perioddaily, start_datestart, end_dateend, adjustqfq) df df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume }) df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 剔除停牌和异常值 df df[df[volume] 0] df df[(df[close] 0) (df[open] 0)] return df[[date, open, close, high, low, volume]] data load_stock_data() print(data.tail())这段代码的逻辑是先按前复权拉数据保证价格连续再剔除成交量为 0 的停牌日避免模型学到无效样本最后过滤掉价格异常的行。参数上adjustqfq是关键如果你用不复权数据除权除息那天会出现巨大跳空模型会把它当成真实波动。symbol换成你关注的标的即可但注意不同数据源的代码格式不一样akshare 用的是纯数字代码。2.2 标签怎么造分类、回归还是三分类标签构造决定了你的模型在学什么。最常见的三种做法一是预测未来 N 天收益率做回归二是预测未来 N 天涨跌方向做二分类三是把涨跌分成「涨、平、跌」三分类。我一般推荐二分类入门因为它的评估指标最直观而且和交易动作直接对应——预测涨就买预测跌就空仓或做空。但这里有个坑标签的阈值怎么定。如果你用「未来 5 天收益率 0 就算涨」那在震荡市里会引入大量噪声样本。更稳的做法是用「未来 5 天收益率 手续费滑点」作为正样本阈值这样模型学到的信号才有交易价值。下面是对应的标签构造代码。def make_label(df, horizon5, cost0.002): # 未来 horizon 天的收益率 df[future_ret] df[close].shift(-horizon) / df[close] - 1 # 扣除交易成本后才算正样本 df[label] (df[future_ret] cost).astype(int) # 最后 horizon 行没有未来数据必须丢掉 df df.dropna(subset[future_ret]).reset_index(dropTrue) return df data make_label(data, horizon5, cost0.002) print(data[label].value_counts())逻辑说明shift(-horizon)把未来价格挪到当前行这是构造标签的标准操作但也是未来函数的高发区——你只能在训练时用预测时绝对不能有这一列。cost0.002代表双边交易成本约千分之二你可以按自己券商的费率调。dropna那一步不能省否则最后几行的标签是 NaN模型训练会报错或者学到脏数据。2.3 特征工程别一上来就堆几百个因子新手最容易犯的错是把能算的因子全算一遍然后扔给模型。结果就是过拟合回测曲线漂亮得像假的。我一般会控制特征数量在 10 到 30 个之间分四类动量类过去 N 天收益率、波动类过去 N 天标准差、量能类成交量变化率、均线类价格与均线偏离度。每类选几个有代表性的就行。def build_features(df): df[ret_1] df[close].pct_change(1) df[ret_5] df[close].pct_change(5) df[ret_20] df[close].pct_change(20) df[vol_5] df[ret_1].rolling(5).std() df[vol_20] df[ret_1].rolling(20).std() df[vol_ratio] df[volume] / df[volume].rolling(20).mean() df[ma_5] df[close].rolling(5).mean() df[ma_20] df[close].rolling(20).mean() df[bias_5] (df[close] - df[ma_5]) / df[ma_5] df[bias_20] (df[close] - df[ma_20]) / df[ma_20] df df.dropna().reset_index(dropTrue) return df data build_features(data) print(data.columns.tolist())这段代码里pct_change算收益率rolling().std()算波动率vol_ratio是量比bias是乖离率。参数上5 和 20 是两个常用周期分别代表短线和中线。你可以加 10、60但别加太多否则特征之间高度相关模型权重会不稳定。dropna之后数据量会减少这是正常的因为前 20 行算不出 20 日均线。提示特征构造完一定要检查有没有无穷大值除以 0 会产生 inf用np.isfinite过滤一遍否则模型训练会直接报错。3. 模型训练与滚动验证让量化投资策略源码经得起时间考验3.1 为什么随机切分训练集是自欺欺人我见过太多人用train_test_split随机切分金融数据然后得到一个 0.95 的 AUC兴奋得不行。这是典型的自欺欺人。金融数据有时间顺序你用 2023 年的数据训练去预测 2020 年这在实盘里根本不可能发生。正确的做法是滚动训练用前 N 年数据训练预测下一年然后窗口往前滚。这样每一轮预测用的都是「过去」的信息才接近真实交易环境。from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, roc_auc_score def walk_forward_train(df, feature_cols, train_years3, test_months6): df df.sort_values(date).reset_index(dropTrue) results [] start df[date].min() end df[date].max() train_start start while True: train_end train_start pd.DateOffset(yearstrain_years) test_end train_end pd.DateOffset(monthstest_months) if test_end end: break train df[(df[date] train_start) (df[date] train_end)] test df[(df[date] train_end) (df[date] test_end)] if len(train) 200 or len(test) 20: train_start pd.DateOffset(monthstest_months) continue model RandomForestClassifier(n_estimators100, max_depth5, random_state42, n_jobs-1) model.fit(train[feature_cols], train[label]) pred model.predict_proba(test[feature_cols])[:, 1] results.append({ train_start: train_start, train_end: train_end, test_end: test_end, auc: roc_auc_score(test[label], pred), acc: accuracy_score(test[label], (pred 0.5).astype(int)) }) train_start pd.DateOffset(monthstest_months) return pd.DataFrame(results) feature_cols [ret_1, ret_5, ret_20, vol_5, vol_20, vol_ratio, bias_5, bias_20] wf walk_forward_train(data, feature_cols) print(wf)逻辑说明walk_forward_train每次用 3 年数据训练预测未来 6 个月然后窗口往前滚 6 个月。RandomForestClassifier的max_depth5是刻意限制复杂度防止过拟合n_estimators100是树的数量再多收益递减。roc_auc_score比准确率更适合评估这类不平衡标签因为涨跌样本比例往往不是 1:1。如果你看到某几轮 AUC 突然掉到 0.5 以下说明那段行情和训练期分布差异太大模型失效了这是正常现象不是代码 bug。3.2 模型选型随机森林、XGBoost 还是 LSTM这是被问最多的问题。我的血泪经验是在日线级别的量化投资里树模型随机森林、XGBoost、LightGBM通常比深度学习更稳。原因有三个一是样本量小A 股单只股票 10 年也就 2400 个交易日LSTM 根本喂不饱二是树模型对特征尺度不敏感你不用花大力气做标准化三是树模型的可解释性强能看特征重要性知道模型在学什么。模型适合场景主要风险调参重点随机森林入门、特征少容易欠拟合max_depth、n_estimatorsXGBoost特征中等、追求精度容易过拟合learning_rate、subsampleLightGBM特征多、数据量大小样本易过拟合num_leaves、min_child_samplesLSTM高频、数据量大训练慢、难调序列长度、隐藏层维度如果你刚开始做我建议从随机森林起步跑通整条链路再换 XGBoost。换模型的时候特征工程和回测框架都不用动只换训练那几行这是模块化设计的好处。3.3 特征重要性模型到底在靠什么做决策训练完模型一定要看特征重要性。如果某个特征重要性高得离谱比如占了 80%那大概率是未来函数或者数据泄漏。正常的量化策略特征重要性应该相对分散前三个加起来不超过 60%。import matplotlib.pyplot as plt model RandomForestClassifier(n_estimators100, max_depth5, random_state42) model.fit(data[feature_cols], data[label]) importance pd.Series(model.feature_importances_, indexfeature_cols) importance importance.sort_values(ascendingFalse) print(importance) importance.plot(kindbar, titleFeature Importance) plt.tight_layout() plt.show()这段代码输出每个特征的重要性。如果ret_1重要性异常高你要警惕——它可能和标签有隐藏的时序重叠。正常的动量类特征重要性在 0.1 到 0.2 之间比较健康。如果所有特征重要性都差不多说明模型没学到东西可能是特征太弱或者标签噪声太大。注意特征重要性是全局的不代表每个时间段都有效。建议在滚动验证的每一轮都记录重要性看它是否稳定。如果某特征这轮重要、下轮不重要说明它捕捉的是噪声。4. 回测与实盘对接量化投资策略源码从研究到落地的最后一公里4.1 回测框架的四个必设参数回测不是把预测信号乘以收益率就完事。一份靠谱的量化投资策略源码回测部分必须包含四个参数手续费、滑点、仓位管理、信号延迟。少一个回测结果都会虚高。def backtest(df, signal_col, cost0.002, slippage0.001, position1.0): df df.copy() # 信号延迟一天避免用当天收盘信号当天成交 df[position] df[signal_col].shift(1).fillna(0) * position # 策略收益 仓位 * 当日收益率 df[strategy_ret] df[position] * df[close].pct_change(1) # 扣除交易成本仓位变化时产生 df[turnover] df[position].diff().abs().fillna(0) df[strategy_ret] - df[turnover] * (cost slippage) df[equity] (1 df[strategy_ret]).cumprod() return df data[signal] (data[ret_5] 0).astype(int) # 示例信号 bt backtest(data, signal) print(bt[[date, equity]].tail())逻辑说明shift(1)是信号延迟确保你今天收盘后生成的信号明天开盘才执行这是实盘的真实节奏。turnover算仓位变化每次调仓扣一次成本。cost0.002和slippage0.001加起来是千分之三这是 A 股比较保守的估计。如果你做的是高频滑点要设得更高。equity是净值曲线用它算最大回撤和夏普比率。4.2 评估指标别只看年化收益年化收益是最容易骗人的指标。一个策略年化 30%但最大回撤 50%你根本拿不住。我一般看四个指标年化收益、最大回撤、夏普比率、胜率。夏普比率低于 1 的策略实盘基本不用考虑最大回撤超过 30% 的仓位要砍半。def evaluate(bt): ret bt[strategy_ret].dropna() annual (1 ret.mean()) ** 252 - 1 sharpe ret.mean() / ret.std() * np.sqrt(252) equity bt[equity] drawdown (equity / equity.cummax() - 1).min() win_rate (ret 0).mean() return { 年化收益: round(annual, 4), 夏普比率: round(sharpe, 2), 最大回撤: round(drawdown, 4), 胜率: round(win_rate, 4) } print(evaluate(bt))这段代码里252是一年交易日数cummax算历史最高净值用来算回撤。注意这里的年化收益是算术平均年化不是几何年化两者在波动大时差异明显。如果你想更严谨用(equity.iloc[-1]) ** (252/len(equity)) - 1算几何年化。4.3 实盘对接模拟盘先跑三个月回测再好也别直接上实盘。我一般会先用模拟盘跑三个月对比模拟盘和回测的信号差异。如果差异超过 20%说明回测里有没考虑到的因素比如涨跌停无法成交、流动性不足。实盘对接一般用券商的 API把模型预测信号输出成买卖指令。这部分每家的接口不一样但核心逻辑是一样的每天收盘后拉数据、算特征、跑模型、生成信号、第二天开盘执行。def generate_order(df, model, feature_cols, threshold0.6): latest df.iloc[-1:].copy() prob model.predict_proba(latest[feature_cols])[:, 1][0] if prob threshold: return {action: buy, prob: round(prob, 4)} elif prob 1 - threshold: return {action: sell, prob: round(prob, 4)} else: return {action: hold, prob: round(prob, 4)} order generate_order(data, model, feature_cols) print(order)threshold0.6是置信度阈值只有模型足够确定时才交易这样能过滤掉大量噪声信号。你可以根据回测结果调这个值阈值越高交易越少胜率越高但收益可能下降。generate_order只输出信号不直接下单这是为了安全——下单逻辑单独写加人工确认或风控规则。5. 避坑与排查量化投资策略源码里那些没人告诉你的暗礁5.1 回测年化 50%实盘亏 20%现象回测曲线一路向上实盘一上就回撤。原因回测用了未来函数或者没扣够交易成本。解决检查标签构造里有没有shift(-N)泄漏到特征里把手续费和滑点调高一倍再回测如果收益腰斩说明策略对成本极度敏感实盘不可用。5.2 模型 AUC 很高但交易不赚钱现象验证集 AUC 0.7 以上但按信号交易就是亏。原因AUC 衡量的是排序能力不是收益能力。模型可能把大涨和大跌都预测成高概率但方向错了。解决不要只看 AUC要看按概率分组后的平均收益。把预测概率分成 5 组看最高组和最低组的收益差差越大策略越有效。5.3 特征重要性每次跑都不一样现象同样的数据换个随机种子特征重要性排名大变。原因特征之间高度相关模型随机选一个。解决做特征聚类相关性超过 0.8 的只留一个或者用 PCA 降维后再训练。另外random_state固定住方便复现。5.4 滚动训练越跑越慢现象滚动窗口多了以后训练时间线性增长。原因每轮都重新实例化模型没有复用。解决把数据预处理提前做好滚动时只切分不重复计算用 LightGBM 替代随机森林训练速度快一个数量级或者减少滚动频率从每 6 个月改成每 12 个月。5.5 实盘信号和回测信号对不上现象同一天回测说买实盘说卖。原因数据源不同、复权方式不同、或者实盘用了盘中数据而回测用收盘数据。解决固定数据源和复权方式实盘信号生成时间统一在收盘后把实盘信号和回测信号逐日对比找出第一处差异顺藤摸瓜。提示这五条里第一条和第五条最致命。未来函数让你以为能赚钱数据不一致让你实际赚不到钱。每次改完代码先跑一遍「成本翻倍测试」和「信号对齐测试」能省下大量真金白银。6. 把源码用活三个让策略更稳的进阶技巧第一个技巧是集成多个模型的信号。不要只用一个随机森林可以同时训随机森林、XGBoost、逻辑回归三个模型投票两个以上看多才买。这样能降低单模型过拟合的风险。代码上就是把三个模型的预测概率取平均再和阈值比较。from sklearn.linear_model import LogisticRegression from xgboost import XGBClassifier models [ RandomForestClassifier(n_estimators100, max_depth5, random_state42), XGBClassifier(n_estimators100, max_depth3, learning_rate0.1, use_label_encoderFalse, eval_metriclogloss), LogisticRegression(max_iter1000) ] def ensemble_predict(models, X): probs [m.predict_proba(X)[:, 1] for m in models] return np.mean(probs, axis0) for m in models: m.fit(data[feature_cols], data[label]) ens_prob ensemble_predict(models, data[feature_cols]) print(ens_prob[:10])逻辑说明三个模型各有所长随机森林稳XGBoost 准逻辑回归简单不易过拟合。取平均后极端预测会被平滑掉。use_label_encoderFalse是 XGBoost 新版本的参数不加会警告。注意集成不是万能的如果三个模型都过拟合平均后还是过拟合所以每个模型都要单独做滚动验证。第二个技巧是动态仓位。不要永远满仓根据模型置信度调整仓位。置信度 0.6 时半仓0.8 时满仓。这样在模型不确定的时候自动降低风险。实现上就是把position从固定值改成(prob - 0.5) * 2再截断到 0 到 1 之间。第三个技巧是定期重新训练。市场风格会变模型会失效。我一般每季度重新训练一次同时监控滚动验证的 AUC如果连续两轮低于 0.55就暂停策略检查特征和标签是否需要调整。这不是一劳永逸的事量化投资策略源码只是起点持续维护才是常态。最后说个我自己的习惯每次改完策略先拿最近一年的数据做「盲测」——假装不知道结果跑一遍看和回测差多少。差得少才敢上模拟盘。这个习惯帮我躲过了好几次「回测很美、实盘很惨」的翻车。希望帮到你。本文还有配套的精品资源点击获取