1. 项目概述:当量化投资遇上机器学习

如果你对股票市场感兴趣,同时又对机器学习(Machine Learning)有所涉猎,那么你很可能已经听说过或者正在寻找一个能将两者结合起来的实战项目。 robertmartin8/MachineLearningStocks 正是这样一个在GitHub上备受瞩目的开源项目。它不是一个复杂的理论框架,而是一个旨在“用机器学习预测股市”的、可直接运行的Python脚本集合。简单来说,这个项目的核心目标就是:利用历史股票数据,训练机器学习模型,尝试预测未来股价的走势,并据此构建一个简单的自动化交易策略。

我第一次接触这个项目时,它吸引我的地方在于其“务实”和“透明”。它没有承诺一夜暴富的“圣杯”,而是清晰地展示了从数据获取、特征工程、模型训练到回测评估的完整流程。对于金融、计算机科学的学生,或者希望进入量化领域的开发者而言,这是一个绝佳的入门沙盒。你可以亲手触摸到量化策略开发的每一个环节,理解模型在真实(尽管是历史)市场数据上的表现,并深刻体会到理论与现实之间的鸿沟。

这个项目主要适合以下几类人:一是编程和机器学习初学者,想找一个有实际应用场景的练手项目;二是对量化金融感兴趣,但不知从何下手的探索者;三是经验丰富的开发者,希望快速验证某个数据或模型想法。通过复现和改造这个项目,你不仅能巩固Python、Pandas、Scikit-learn等工具栈,更能建立起对市场预测复杂性的直观认知——这远比任何教科书都来得深刻。

2. 核心思路与架构拆解

2.1 项目目标与核心假设

在深入代码之前,我们必须先理解这个项目的底层逻辑和基本假设。它的目标并非进行高频交易或捕捉微观的市场波动,而是着眼于中短期的趋势预测。其核心假设可以概括为: 股票市场的历史价格和交易量数据中,蕴含着可以用于预测未来价格走势的、可被机器学习模型识别的模式。

具体来说,项目通常试图预测的是未来某个时间窗口(例如5天、20天后)的股价涨跌(分类问题:涨/跌)或具体价格(回归问题)。它默认市场并非完全随机游走,而是存在一定的趋势性和惯性,并且这些特性可以通过技术指标(如移动平均线、相对强弱指数RSI等)来量化,并作为特征输入模型。

注意 :这是一个非常强且充满争议的假设。有效市场假说(EMH)认为市场价格已经反映了所有可用信息,因此基于历史数据的预测是徒劳的。这个项目的价值不在于证明或反驳EMV,而在于提供一个实践框架,让我们可以检验在特定数据集和模型下,这一假设能在多大程度上成立。理解这一点,是正确使用该项目、理性看待结果的前提。

2.2 整体技术架构与流程

项目的整体流程是一个标准的机器学习工作流,但被置于金融时间序列的背景下。我们可以将其拆解为以下几个核心阶段:

  1. 数据获取与预处理 :从免费或付费的金融数据API(如Yahoo Finance, Alpha Vantage)下载历史股价数据(OHLC:开盘价、最高价、最低价、收盘价,以及成交量)。随后进行数据清洗(处理缺失值、异常值)和格式化,确保数据可供后续分析。
  2. 特征工程 :这是量化策略的灵魂。原始的价格和成交量数据信息量有限,需要从中构造出更有预测力的特征。项目通常会计算一系列技术指标,例如:
    • 趋势指标 :简单移动平均线(SMA)、指数移动平均线(EMA)。
    • 动量指标 :相对强弱指数(RSI)、动量(MOM)。
    • 波动率指标 :布林带(Bollinger Bands)的宽度、平均真实波幅(ATR)。
    • 其他衍生特征 :前期涨跌幅、成交量变化率、价格与均线的偏离度等。
  3. 标签定义 :我们需要告诉模型要预测什么。对于分类任务,常见的标签定义是:如果未来N日的收益率超过阈值X%,则标记为“买入”(1),否则为“不操作”或“卖出”(0)。对于回归任务,标签就是未来N日的实际价格或收益率。
  4. 模型训练与验证 :将特征和标签数据按时间顺序划分为训练集和测试集( 严禁使用随机划分,必须使用前向验证或时间序列交叉验证 )。然后使用Scikit-learn中的模型(如逻辑回归、随机森林、梯度提升树)进行训练,并在测试集上评估性能。
  5. 策略回测与评估 :将模型的预测信号转化为具体的交易动作(例如,当模型预测为“涨”时,在下一个交易日开盘买入并持有固定周期)。在历史数据上模拟整个交易过程,计算策略的收益曲线、夏普比率、最大回撤等关键绩效指标,并与基准(如买入并持有指数)进行比较。

这个架构清晰地将数据流、模型流和策略评估流分开,使得每个模块都可以独立优化和调试,是设计良好的量化研究系统的雏形。

3. 环境准备与数据源配置

3.1 Python环境与依赖库搭建

要运行这个项目,一个稳定、库版本兼容的Python环境是基础。我强烈建议使用 conda venv 创建独立的虚拟环境,避免与系统或其他项目的库发生冲突。

# 使用 conda 创建环境(推荐)
conda create -n ml_stocks python=3.8
conda activate ml_stocks

# 或者使用 venv
python -m venv ml_stocks_env
source ml_stocks_env/bin/activate  # Linux/Mac
# ml_stocks_env\Scripts\activate  # Windows

接下来安装核心依赖。项目通常会用到以下库,你可以通过 pip 一次性安装:

pip install pandas numpy scikit-learn matplotlib seaborn yfinance pandas-datareader ta
  • pandas/numpy : 数据操作的基石,无需多言。
  • scikit-learn : 提供各种机器学习模型和评估工具。
  • matplotlib/seaborn : 用于数据可视化和结果绘图。
  • yfinance : 一个非常方便、免费的雅虎财经数据下载库(原 fix-yahoo-finance 的继承者)。这是本项目最常用的数据源。
  • pandas-datareader : 另一个金融数据读取库,支持多个数据源,但雅虎财经接口可能不稳定。
  • ta : 一个专门用于计算技术分析指标的库,比手动计算方便、准确得多。

实操心得 :库的版本是最大的坑之一。特别是 yfinance ,雅虎财经会不时更改其网页结构,导致库需要更新。如果遇到数据下载失败,首先尝试 pip install --upgrade yfinance 。另外, ta 库的API也可能变化,如果原项目代码中计算指标的语句报错,查阅 ta 的最新文档通常是解决问题的捷径。

3.2 数据获取的实战细节与备选方案

原项目可能使用较老的数据接口。现在最稳定、最推荐的方式是使用 yfinance

import yfinance as yf
import pandas as pd

# 下载苹果公司(AAPL)从2020年至今的日线数据
ticker = "AAPL"
start_date = "2020-01-01"
end_date = "2023-12-31"

data = yf.download(ticker, start=start_date, end=end_date)
print(data.head())

yf.download 返回的是一个多级索引的DataFrame,包含了 Open , High , Low , Close , Adj Close , Volume 等列。我们通常使用调整后收盘价( Adj Close )进行分析,因为它考虑了分红、拆股等公司行为,更能反映真实回报。

数据源备选方案

  1. Alpha Vantage : 提供免费的API,有调用频率限制,但数据质量高,还包含很多基本面数据。需要注册获取API密钥。
  2. Quandl : 拥有丰富的数据集,部分免费,部分收费。
  3. 本地数据库 : 对于严肃的研究,建议将数据下载到本地数据库(如SQLite、PostgreSQL)中管理,避免每次运行都重新下载,也便于进行更复杂的数据关联查询。

数据预处理关键步骤

  • 处理缺失值 :股票数据在非交易日是缺失的。我们需要前向填充( ffill )或者直接删除这些行,确保时间序列是连续的交易日。
  • 复权处理 :确保使用的是 Adj Close 列。
  • 数据对齐 :如果你要分析多个股票,需要确保它们的时间索引对齐,缺失的日期需要进行填充或删除。
# 示例:基础数据清洗
data = data[['Adj Close', 'Volume']] # 选取需要的列
data.columns = ['close', 'volume'] # 重命名以便操作
data = data.asfreq('B') # 设置为工作日频率,产生NaN
data = data.ffill() # 前向填充非交易日的NaN
data.dropna(inplace=True) # 删除开头可能存在的NaN

4. 特征工程:从原始数据中挖掘信号

4.1 技术指标的计算与选择

特征工程是决定模型性能的上限。对于股价预测,技术指标是经过市场长期检验的特征来源。使用 ta 库可以轻松计算数十种指标。

import ta

# 假设 df 是一个包含‘close’, ‘high’, ‘low’, ‘volume’列的DataFrame
df = data.copy()

# 趋势指标
df['sma_20'] = ta.trend.sma_indicator(df['close'], window=20)
df['ema_12'] = ta.trend.ema_indicator(df['close'], window=12)

# 动量指标
df['rsi_14'] = ta.momentum.rsi(df['close'], window=14)

# 波动率指标
bb = ta.volatility.BollingerBands(df['close'], window=20, window_dev=2)
df['bb_high'] = bb.bollinger_hband()
df['bb_low'] = bb.bollinger_lband()
df['bb_width'] = (df['bb_high'] - df['bb_low']) / df['sma_20'] # 布林带宽度标准化

# 成交量指标
df['volume_sma'] = ta.trend.sma_indicator(df['volume'], window=20)
df['volume_ratio'] = df['volume'] / df['volume_sma']

如何选择指标? 这不是一个随意堆砌的过程。你需要有一些经济学或市场学的直觉:

  • 趋势跟踪 :在趋势明显的市场,移动平均线及其交叉是很好的特征。
  • 均值回归 :在震荡市,RSI、布林带等可以识别超买超卖状态。
  • 量价关系 :价格上涨伴随放量通常更可靠,因此成交量相关特征很重要。

一个实用的方法是计算一个较全面的指标池,然后利用特征重要性分析(如随机森林的 feature_importances_ )或相关性分析进行筛选,去除冗余和噪音特征。

4.2 创建滞后特征与数据对齐

金融数据是时间序列,今天的特征可能用于预测明天的价格。因此,我们必须非常小心“数据泄露”问题——绝不能使用未来的信息预测过去。

标准的做法是创建滞后特征。例如,我们用截至 t 日的所有信息(这些信息在 t 日收盘后才知道)来预测 t+1 日或 t+N 日的价格。

# 假设我们已经有了一个包含所有计算好的技术指标的DataFrame `df_features`
# 定义预测未来多少天
forecast_horizon = 5

# 创建标签:未来5日的收益率
df_features['future_return'] = df_features['close'].pct_change(forecast_horizon).shift(-forecast_horizon)

# 将特征全部滞后一期,确保用t时刻的特征预测t+1时刻的未来
for col in df_features.columns:
    if col not in ['future_return']: # 标签列不滞后
        df_features[col] = df_features[col].shift(1)

# 删除因滞后和计算指标而产生的NaN行
df_features.dropna(inplace=True)

这个操作至关重要。 shift(1) 意味着我们用昨天的特征预测明天的收益。如果不做这个操作,模型就是在“作弊”,因为它看到了本应在未来才出现的信息,回测结果会好得不真实,但实盘必然失败。

5. 模型构建、训练与验证策略

5.1 模型选择与数据划分

对于分类问题(预测涨跌),常见的入门模型有:

  • 逻辑回归(Logistic Regression) : 基线模型,可解释性强,能看出特征的正负影响。
  • 随机森林(Random Forest) : 表现通常不错,能处理非线性关系,自带特征重要性评估。
  • 梯度提升机(如XGBoost, LightGBM) : 在结构化数据上往往有最佳性能,但需要更多调参。

对于回归问题(预测价格或收益率),则使用对应的回归模型。

时间序列数据划分 :绝对不能使用 train_test_split 进行随机划分!必须按时间顺序划分。

# 按时间顺序划分训练集和测试集(例如,前70%训练,后30%测试)
split_ratio = 0.7
split_index = int(len(df_features) * split_ratio)

train = df_features.iloc[:split_index].copy()
test = df_features.iloc[split_index:].copy()

X_train = train.drop('future_return', axis=1)
y_train = train['future_return']
X_test = test.drop('future_return', axis=1)
y_test = test['future_return']

更严谨的方法是使用 时间序列交叉验证(TimeSeriesSplit) ,它能在多个连续的时间段上评估模型的稳定性。

from sklearn.model_selection import TimeSeriesSplit
from sklearn.ensemble import RandomForestClassifier

tscv = TimeSeriesSplit(n_splits=5)
model = RandomForestClassifier(n_estimators=100, random_state=42)

for train_index, val_index in tscv.split(X_train):
    cv_X_train, cv_X_val = X_train.iloc[train_index], X_train.iloc[val_index]
    cv_y_train, cv_y_val = y_train.iloc[train_index], y_train.iloc[val_index]
    model.fit(cv_X_train, cv_y_train)
    # 在cv_X_val上评估...

5.2 处理类别不平衡与模型评估

在股市中,大涨大跌的日子是少数,大部分时间波动不大。这会导致我们定义的“涨”/“跌”标签严重不平衡。直接训练模型,它会倾向于预测占多数的类别(例如“平盘”),导致准确率高但毫无用处。

解决方法

  1. 调整样本权重 :在模型(如逻辑回归、随机森林)中设置 class_weight='balanced'
  2. 过采样/欠采样 :使用SMOTE等方法生成少数类样本,或随机删除多数类样本。
  3. 修改标签定义 :调整定义“涨”的阈值,使两类样本更平衡。

评估指标 :对于分类问题,不要只看准确率(Accuracy)。更重要的指标是:

  • 精确率(Precision) : 在所有预测为“涨”的样本中,真正上涨的比例。这关系到你买入后赚钱的概率。
  • 召回率(Recall) : 在所有实际上涨的样本中,被你成功预测到的比例。这关系到你捕捉机会的能力。
  • F1分数 : 精确率和召回率的调和平均。
  • 混淆矩阵(Confusion Matrix) : 直观展示分类结果。
  • AUC-ROC曲线 : 评估模型在不同阈值下的整体分类性能。
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score

y_pred = model.predict(X_test)
y_pred_proba = model.predict_proba(X_test)[:, 1] # 预测为“涨”的概率

print(classification_report(y_test, y_pred))
print(f"AUC-ROC: {roc_auc_score(y_test, y_pred_proba):.4f}")

6. 策略回测:从预测到交易的惊险一跃

6.1 构建简易回测框架

模型预测出信号只是第一步,将信号转化为实际的交易策略并评估其盈利能力,才是量化投资的最终目的。一个最简单的回测框架可以这样实现:

  1. 生成交易信号 :在测试集的每一天,运行模型得到预测(例如,预测概率大于0.6时发出买入信号)。
  2. 定义交易规则
    • 买入 :当出现买入信号,且当前未持仓时,在次日开盘价买入。
    • 卖出 :持有固定周期后(例如5天)卖出,或者当出现卖出信号时卖出。简单的策略可以设定为买入后持有N日,然后卖出。
  3. 计算资金曲线 :假设初始资金为1,记录每次买卖后的现金和资产总值。
# 一个极度简化的回测示例,忽略手续费、滑价等
def simple_backtest(df_test, predictions, hold_days=5):
    """
    df_test: 包含价格数据的测试集DataFrame
    predictions: 模型对测试集的预测标签(1=买入,0=不操作)
    hold_days: 买入后持有的天数
    """
    cash = 1.0 # 初始现金
    position = 0 # 持仓股数
    portfolio_value = [cash] # 资产总值记录
    buy_price = 0
    hold_counter = 0

    for i in range(len(df_test)):
        current_price = df_test.iloc[i]['close']

        # 卖出逻辑:持有期满
        if position > 0:
            hold_counter += 1
            if hold_counter >= hold_days:
                cash = position * current_price
                position = 0
                hold_counter = 0

        # 买入逻辑:有信号且空仓
        if predictions[i] == 1 and position == 0:
            position = cash / current_price
            cash = 0.0
            buy_price = current_price
            hold_counter = 0

        # 计算当日总资产
        total_asset = cash + position * current_price
        portfolio_value.append(total_asset)

    return portfolio_value

6.2 关键绩效指标解读

计算出资金曲线后,需要用专业的指标来评估策略好坏:

  • 总收益率(Total Return) : (最终资产 - 初始资产) / 初始资产
  • 年化收益率(Annualized Return) : 将总收益率折算到年维度,便于比较。
  • 年化波动率(Annualized Volatility) : 收益率的标准差,衡量风险。
  • 夏普比率(Sharpe Ratio) : (年化收益率 - 无风险利率) / 年化波动率 。衡量每承担一单位风险所获得的超额回报,是核心指标。通常大于1的策略才值得考虑。
  • 最大回撤(Max Drawdown) : 资产从峰值到谷底的最大跌幅。这是衡量策略极端风险和投资者心理承受能力的关键指标。
  • 胜率(Win Rate) : 盈利交易次数占总交易次数的比例。
  • 盈亏比(Profit Factor) : 总盈利 / 总亏损。

实操心得 :回测中最大的陷阱是“过拟合”和“未来函数”。除了之前提到的数据泄露,还要小心:

  1. 幸存者偏差 :如果你只用了今天还存在的公司数据(比如标普500当前成分股),那么你的策略里没有包含那些已经破产退市的公司,结果会过于乐观。解决方法是使用“点-in-time”数据,即在任何时间点,只使用当时存在的公司数据。
  2. 交易成本与滑价 :实盘交易有佣金和买卖价差(滑价)。在回测中忽略它们,尤其是对于高频策略,会导致结果严重失真。一个粗略的估计是每次买卖扣除0.1%的成本。
  3. 参数优化中的过拟合 :如果你在全部数据上反复测试不同参数以找到最佳值,那么这个“最佳参数”很可能只是噪声。必须将数据分为训练集、验证集和测试集,参数优化只在验证集上进行,最终用一次都未使用过的测试集来评估。

7. 常见问题、陷阱与进阶思考

7.1 模型表现不佳的排查思路

当你发现模型在测试集上表现平平甚至很差时,可以按以下顺序排查:

问题现象 可能原因 排查与解决方法
预测准确率接近50%(随机猜测) 1. 特征没有预测力。
2. 标签定义不合理(市场不可预测)。
3. 数据泄露处理不当。
1. 检查特征与标签的相关性。
2. 尝试更长的预测周期或不同的标签定义(如预测波动率而非方向)。
3. 仔细检查 shift() 操作,确保特征严格来自过去。
训练集表现好,测试集差(过拟合) 1. 模型太复杂(如树深度太大)。
2. 特征过多或存在噪音。
3. 训练数据量不足。
1. 增加正则化,简化模型(减少树深度、增加 min_samples_leaf )。
2. 进行特征选择,剔除不重要特征。
3. 收集更长时间跨度的数据。
策略回测结果远优于基准 1. 存在数据泄露(未来函数)。
2. 未考虑交易成本和滑价。
3. 过拟合了历史数据。
1. 这是首要怀疑对象! 彻底审查数据预处理和特征工程流水线。
2. 在回测中加入手续费和滑价模型。
3. 进行样本外测试或前向滚动验证。
策略夏普比率低或为负 1. 信号质量差,交易频繁但盈利微薄。
2. 市场状态发生变化(模型失效)。
1. 提高信号触发阈值,减少交易次数,追求高胜率或高盈亏比。
2. 考虑引入市场状态识别(如波动率 regime),在不同状态下使用不同模型。

7.2 从“玩具项目”到“严肃研究”的进阶方向

MachineLearningStocks 项目是一个完美的起点,但要想将其转化为有实际价值的研究,你需要深入以下几个方向:

  1. 引入更多元的数据 :股价和技术指标只是冰山一角。考虑加入:
    • 基本面数据 :市盈率、市净率、营收增长率等。
    • 宏观数据 :利率、通胀率、GDP增速。
    • 另类数据 :新闻情绪分析(使用NLP)、社交媒体热度、供应链数据等。
  2. 探索更复杂的模型 :从传统的机器学习模型转向深度学习,如LSTM、Transformer等,它们能更好地捕捉时间序列中的长期依赖关系。但要注意,深度学习模型需要更大量的数据,且更容易过拟合。
  3. 构建投资组合策略 :不要只预测单只股票。预测所有股票的收益率或排名,然后构建一个多空投资组合(买入预期表现好的,卖出预期表现差的),可以对冲市场整体风险(Beta),赚取选股能力(Alpha)的收益。
  4. 注重风险控制 :任何策略都必须包含严格的风险管理规则,例如单笔交易最大亏损、每日最大回撤止损、仓位控制等。没有风险控制的策略,就像没有刹车的汽车。
  5. 实盘模拟与心理建设 :在投入真金白银前,一定要进行长时间的模拟盘交易。这不仅能检验策略在实时市场中的表现,更能让你体验策略连续亏损时的心理压力,这是量化交易中至关重要却又常被忽视的一环。

我个人在反复折腾这类项目的过程中,最大的体会是: 市场预测的难点不在于模型的复杂度,而在于金融时间序列信噪比极低、非平稳、且存在结构性变化的特性。 一个在2010-2015年表现优异的策略,在2016-2020年可能完全失效。因此,持续学习、迭代和保持对市场的敬畏,比找到某个“神奇参数”要重要得多。这个项目最好的结果,未必是得到一个能盈利的策略,而是帮你建立起一套严谨的量化研究思维框架——如何提出问题、获取数据、构建特征、验证假设、评估风险。这套方法论,才是你在这个领域最宝贵的收获。

更多推荐