简介:这份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(symbol="600519", start="20180101", end="20231231"): df = ak.stock_zh_a_hist(symbol=symbol, period="daily", start_date=start, end_date=end, adjust="qfq") df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume" }) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) # 剔除停牌和异常值 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 的停牌日,避免模型学到无效样本;最后过滤掉价格异常的行。参数上,adjust="qfq"是关键,如果你用不复权数据,除权除息那天会出现巨大跳空,模型会把它当成真实波动。symbol换成你关注的标的即可,但注意不同数据源的代码格式不一样,akshare 用的是纯数字代码。
2.2 标签怎么造:分类、回归还是三分类
标签构造决定了你的模型在学什么。最常见的三种做法:一是预测未来 N 天收益率,做回归;二是预测未来 N 天涨跌方向,做二分类;三是把涨跌分成「涨、平、跌」三分类。我一般推荐二分类入门,因为它的评估指标最直观,而且和交易动作直接对应——预测涨就买,预测跌就空仓或做空。
但这里有个坑:标签的阈值怎么定。如果你用「未来 5 天收益率 > 0 就算涨」,那在震荡市里会引入大量噪声样本。更稳的做法是用「未来 5 天收益率 > 手续费+滑点」作为正样本阈值,这样模型学到的信号才有交易价值。下面是对应的标签构造代码。
def make_label(df, horizon=5, cost=0.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(drop=True) return df data = make_label(data, horizon=5, cost=0.002) print(data["label"].value_counts())逻辑说明:shift(-horizon)把未来价格挪到当前行,这是构造标签的标准操作,但也是未来函数的高发区——你只能在训练时用,预测时绝对不能有这一列。cost=0.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(drop=True) 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_years=3, test_months=6): df = df.sort_values("date").reset_index(drop=True) results = [] start = df["date"].min() end = df["date"].max() train_start = start while True: train_end = train_start + pd.DateOffset(years=train_years) test_end = train_end + pd.DateOffset(months=test_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(months=test_months) continue model = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42, 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(months=test_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_depth=5是刻意限制复杂度,防止过拟合;n_estimators=100是树的数量,再多收益递减。roc_auc_score比准确率更适合评估这类不平衡标签,因为涨跌样本比例往往不是 1:1。如果你看到某几轮 AUC 突然掉到 0.5 以下,说明那段行情和训练期分布差异太大,模型失效了,这是正常现象,不是代码 bug。
3.2 模型选型:随机森林、XGBoost 还是 LSTM
这是被问最多的问题。我的血泪经验是:在日线级别的量化投资里,树模型(随机森林、XGBoost、LightGBM)通常比深度学习更稳。原因有三个:一是样本量小,A 股单只股票 10 年也就 2400 个交易日,LSTM 根本喂不饱;二是树模型对特征尺度不敏感,你不用花大力气做标准化;三是树模型的可解释性强,能看特征重要性,知道模型在学什么。
| 模型 | 适合场景 | 主要风险 | 调参重点 |
|---|---|---|---|
| 随机森林 | 入门、特征少 | 容易欠拟合 | max_depth、n_estimators |
| XGBoost | 特征中等、追求精度 | 容易过拟合 | learning_rate、subsample |
| LightGBM | 特征多、数据量大 | 小样本易过拟合 | num_leaves、min_child_samples |
| LSTM | 高频、数据量大 | 训练慢、难调 | 序列长度、隐藏层维度 |
如果你刚开始做,我建议从随机森林起步,跑通整条链路再换 XGBoost。换模型的时候,特征工程和回测框架都不用动,只换训练那几行,这是模块化设计的好处。
3.3 特征重要性:模型到底在靠什么做决策
训练完模型,一定要看特征重要性。如果某个特征重要性高得离谱,比如占了 80%,那大概率是未来函数或者数据泄漏。正常的量化策略,特征重要性应该相对分散,前三个加起来不超过 60%。
import matplotlib.pyplot as plt model = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42) model.fit(data[feature_cols], data["label"]) importance = pd.Series(model.feature_importances_, index=feature_cols) importance = importance.sort_values(ascending=False) print(importance) importance.plot(kind="bar", title="Feature Importance") plt.tight_layout() plt.show()这段代码输出每个特征的重要性。如果ret_1重要性异常高,你要警惕——它可能和标签有隐藏的时序重叠。正常的动量类特征重要性在 0.1 到 0.2 之间比较健康。如果所有特征重要性都差不多,说明模型没学到东西,可能是特征太弱或者标签噪声太大。
注意:特征重要性是全局的,不代表每个时间段都有效。建议在滚动验证的每一轮都记录重要性,看它是否稳定。如果某特征这轮重要、下轮不重要,说明它捕捉的是噪声。
4. 回测与实盘对接:量化投资策略源码从研究到落地的最后一公里
4.1 回测框架的四个必设参数
回测不是把预测信号乘以收益率就完事。一份靠谱的量化投资策略源码,回测部分必须包含四个参数:手续费、滑点、仓位管理、信号延迟。少一个,回测结果都会虚高。
def backtest(df, signal_col, cost=0.002, slippage=0.001, position=1.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算仓位变化,每次调仓扣一次成本。cost=0.002和slippage=0.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, threshold=0.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)threshold=0.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_estimators=100, max_depth=5, random_state=42), XGBClassifier(n_estimators=100, max_depth=3, learning_rate=0.1, use_label_encoder=False, eval_metric="logloss"), LogisticRegression(max_iter=1000) ] def ensemble_predict(models, X): probs = [m.predict_proba(X)[:, 1] for m in models] return np.mean(probs, axis=0) 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_encoder=False是 XGBoost 新版本的参数,不加会警告。注意,集成不是万能的,如果三个模型都过拟合,平均后还是过拟合,所以每个模型都要单独做滚动验证。
第二个技巧是动态仓位。不要永远满仓,根据模型置信度调整仓位。置信度 0.6 时半仓,0.8 时满仓。这样在模型不确定的时候自动降低风险。实现上就是把position从固定值改成(prob - 0.5) * 2,再截断到 0 到 1 之间。
第三个技巧是定期重新训练。市场风格会变,模型会失效。我一般每季度重新训练一次,同时监控滚动验证的 AUC,如果连续两轮低于 0.55,就暂停策略,检查特征和标签是否需要调整。这不是一劳永逸的事,量化投资策略源码只是起点,持续维护才是常态。
最后说个我自己的习惯:每次改完策略,先拿最近一年的数据做「盲测」——假装不知道结果,跑一遍,看和回测差多少。差得少,才敢上模拟盘。这个习惯帮我躲过了好几次「回测很美、实盘很惨」的翻车。希望帮到你。
本文还有配套的精品资源,点击获取