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

如果你对股票市场感兴趣,同时又对Python和机器学习有些许了解,那么你很可能在GitHub上见过一个名为“MachineLearningStocks”的项目。这个由robertmartin8创建的开源项目,在过去几年里吸引了大量关注,它像一座桥梁,连接了看似高深的机器学习算法与普通人也能理解的股票投资逻辑。简单来说,这个项目的核心目标,就是尝试用历史数据训练一个机器学习模型,让它来预测股票的未来价格走势,从而辅助投资决策。

我第一次接触这个项目时,正处在从纯理论研究转向寻找实际应用场景的阶段。当时市面上充斥着各种“AI炒股”、“量化圣杯”的喧嚣,但大多数要么是黑箱系统,要么就是过于理论化,离真正的实操有十万八千里。MachineLearningStocks的出现让我眼前一亮,它没有复杂的金融工程术语堆砌,而是用清晰的代码和步骤,展示了一个完整的、从数据获取到模型预测的量化分析流程。它解决的问题非常直接:给定一只股票的历史数据(价格、成交量等),我们能否训练一个模型,学习其中的模式,并对未来的价格做出有一定参考价值的预测?这本质上是一个监督学习中的回归问题。

这个项目特别适合几类人:一是对量化投资充满好奇的编程爱好者或数据科学初学者,可以通过它理解整个分析链条;二是希望将机器学习技能应用于金融领域的开发者,可以将其作为一个坚实的起点进行扩展;三是那些不满足于简单技术指标,想探索更复杂分析方法的个人投资者。当然,我必须强调,这个项目提供的是一种方法论和工具,绝非“稳赚不赔”的秘籍。金融市场充满噪声和不确定性,任何基于历史数据的预测模型都面临巨大挑战。但正是这种挑战性,使得探索过程本身充满了学习和实践的价值。

2. 核心思路与技术栈拆解

2.1 整体架构与工作流

MachineLearningStocks项目的架构非常清晰,遵循了经典的数据科学工作流:数据采集与清洗 -> 特征工程 -> 模型训练与验证 -> 预测与回测。整个流程被封装在几个核心的Python脚本中,逻辑连贯,易于跟踪。

项目的起点是数据。它并没有使用昂贵的专业金融数据库,而是巧妙地利用了雅虎财经(Yahoo Finance)的公开接口,通过 pandas-datareader 库来获取股票的历史行情数据。这一步降低了准入门槛,让任何人都能零成本地开始实验。获取到的原始数据包括每日的开盘价、最高价、最低价、收盘价和成交量,也就是常说的OHLCV数据。

拿到数据后,项目并没有直接将这些价格扔进模型。这里体现了量化分析的一个关键思想:原始价格序列往往难以直接建模,我们需要从中提炼出更有信息量的“特征”。项目构建了一系列技术指标作为特征,例如移动平均线(MA)、相对强弱指数(RSI)、布林带(Bollinger Bands)等。这些指标是传统技术分析的核心,将它们转化为特征,相当于让机器学习模型去学习技术分析师们多年来总结出的经验模式。

接下来是定义预测目标。项目预测的不是明天的绝对价格,而是未来一段时间(例如5天后)的价格相对于当前价格的涨跌幅度,即收益率。这是一个更稳健的做法,因为预测相对变化比预测绝对价格更容易。然后,项目将数据划分为训练集和测试集,用训练集数据来训练一个回归模型(项目中默认使用了线性回归和随机森林等),并在测试集上评估模型预测未来收益率的准确率。最后,基于模型的预测信号(例如,预测收益率大于某个阈值则买入),模拟一个简单的交易策略,并计算其历史表现,这个过程称为“回测”。

2.2 关键技术选型与考量

1. 数据获取: pandas-datareader vs. 其他选择 项目选用 pandas-datareader 是出于简便和免费的考虑。在项目创建初期,雅虎财经的API稳定且免费,是个人开发者的首选。然而,雅虎财经的API后来经历了多次变动和不稳定时期,这也是该项目在实际运行中常遇到的问题之一。替代方案包括 yfinance 库(专门针对雅虎财经,更稳定)、Alpha Vantage或IEX Cloud的免费层API。选择时需权衡数据质量、稳定性、调用频率限制和费用。

注意 :使用任何公开金融数据API时,务必仔细阅读其服务条款,特别是关于数据用途和调用限制的规定。商用或高频调用可能需要购买授权。

2. 特征工程:技术指标库( ta-lib pandas 手动计算) 技术指标的计算是特征工程的核心。项目可以选择使用专业的 TA-Lib 库,它用C语言编写,计算速度快且经过广泛验证,包含了上百种技术指标。但 TA-Lib 的安装(特别是Windows下)有时会比较麻烦。因此,许多简化版本或教程中会直接用 pandas numpy 手动实现几个核心指标,如简单移动平均(SMA)和指数移动平均(EMA)。虽然速度可能稍慢,但避免了依赖问题,更利于代码的传播和复现。

3. 机器学习框架: scikit-learn 的核心地位 模型部分完全依赖于 scikit-learn ,这是一个极其明智的选择。 scikit-learn 提供了统一、简洁的API用于数据预处理、模型训练、评估和预测。项目中可能用到的模型包括:

  • 线性回归/岭回归 :作为基线模型,解释性强,能快速判断特征与目标之间是否存在线性关系。
  • 随机森林回归 :更强大的非线性模型,能捕捉特征间复杂的交互作用,且对过拟合有一定的抵抗能力,是这类问题的常用选择。
  • 支持向量机(SVR) 梯度提升树(如XGBoost) :在更复杂的版本中可能会被引入。

选择 scikit-learn 使得项目重点保持在“应用流程”上,而非纠结于模型实现细节。

4. 回测引擎:简易自定义 vs. 专业框架 项目的回测逻辑通常是自定义的,比较简单。它会遍历测试集的每一天,根据模型当天的预测生成交易信号(买/卖/持有),并跟踪一个虚拟投资组合的价值变化。这种简易回测没有考虑交易成本(佣金、滑点)、仓位管理、风险控制等复杂因素。对于严肃的策略研究,建议转向专业的回测框架如 Backtrader Zipline QuantConnect ,它们提供了更真实的市场模拟和丰富的分析工具。

3. 从零开始复现核心流程

3.1 环境搭建与数据获取实战

首先,我们需要建立一个干净的Python环境。推荐使用 conda venv 创建独立环境,避免包冲突。

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

# 安装核心依赖
pip install pandas numpy matplotlib seaborn scikit-learn yfinance

这里我直接用 yfinance 替代了可能已失效的 pandas-datareader 雅虎接口,它更稳定。接下来是数据获取脚本:

import yfinance as yf
import pandas as pd

def fetch_stock_data(ticker, start_date, end_date):
    """
    获取单只股票历史数据
    :param ticker: 股票代码,如 'AAPL'(苹果)
    :param start_date: 开始日期,'2015-01-01'
    :param end_date: 结束日期,'2022-12-31'
    :return: 包含OHLCV数据的DataFrame
    """
    try:
        # 下载数据
        stock = yf.download(ticker, start=start_date, end=end_date, progress=False)
        # 检查是否下载成功
        if stock.empty:
            print(f"警告:未获取到股票 {ticker} 的数据,请检查代码或网络。")
            return None
        # 确保索引是日期时间类型,并排序
        stock.index = pd.to_datetime(stock.index)
        stock.sort_index(inplace=True)
        print(f"成功获取 {ticker} 从 {stock.index[0].date()} 到 {stock.index[-1].date()} 的数据,共 {len(stock)} 条记录。")
        return stock
    except Exception as e:
        print(f"获取数据时发生错误:{e}")
        return None

# 示例:获取苹果公司2018年至2022年的数据
aapl_data = fetch_stock_data('AAPL', '2018-01-01', '2022-12-31')
print(aapl_data.head())

这段代码会下载苹果公司五年的日线数据。 yfinance 返回的 DataFrame 列名包括 Open , High , Low , Close , Adj Close , Volume 。我们通常使用调整后收盘价( Adj Close )进行分析,因为它考虑了分红、拆股等公司行为,更能反映真实回报。

3.2 特征工程:构建模型能理解的市场语言

原始的价格和成交量序列是模型难以直接消化的。我们需要构造特征。这里手动实现几个关键的技术指标:

import numpy as np

def calculate_technical_indicators(df):
    """
    计算技术指标并添加到DataFrame中
    :param df: 包含价格和成交量的DataFrame
    :return: 添加了技术指标列的DataFrame
    """
    # 使用副本避免修改原数据
    df = df.copy()
    # 确保有调整后收盘价和成交量
    if 'Adj Close' not in df.columns:
        df['Adj Close'] = df['Close'] # 后备方案

    price = df['Adj Close']
    volume = df['Volume']

    # 1. 简单移动平均线 (SMA)
    df['SMA_20'] = price.rolling(window=20).mean()  # 20日均线
    df['SMA_50'] = price.rolling(window=50).mean()  # 50日均线

    # 2. 指数移动平均线 (EMA) - 对近期价格赋予更高权重
    df['EMA_12'] = price.ewm(span=12, adjust=False).mean()
    df['EMA_26'] = price.ewm(span=26, adjust=False).mean()

    # 3. MACD (Moving Average Convergence Divergence)
    df['MACD'] = df['EMA_12'] - df['EMA_26']
    df['MACD_Signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
    df['MACD_Histogram'] = df['MACD'] - df['MACD_Signal']

    # 4. 相对强弱指数 (RSI)
    delta = price.diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
    rs = gain / loss
    df['RSI_14'] = 100 - (100 / (1 + rs))

    # 5. 布林带 (Bollinger Bands)
    df['BB_Middle'] = price.rolling(window=20).mean()
    bb_std = price.rolling(window=20).std()
    df['BB_Upper'] = df['BB_Middle'] + 2 * bb_std
    df['BB_Lower'] = df['BB_Middle'] - 2 * bb_std
    df['BB_Width'] = df['BB_Upper'] - df['BB_Lower'] # 布林带宽度,衡量波动率

    # 6. 价格变化率 (ROC)
    df['ROC_10'] = price.pct_change(periods=10) * 100  # 10日价格变化率

    # 7. 成交量加权平均价 (VWAP) - 日内指标,这里用日数据近似
    # VWAP = (典型价格 * 成交量)的累计和 / 成交量的累计和
    typical_price = (df['High'] + df['Low'] + df['Close']) / 3
    df['VWAP'] = (typical_price * volume).cumsum() / volume.cumsum()

    # 8. 价格与均线的距离(标准化)
    df['Price_to_SMA20_Ratio'] = price / df['SMA_20']
    df['Price_to_SMA50_Ratio'] = price / df['SMA_50']

    # 删除因为滚动计算产生的NaN行(例如前49天没有SMA_50)
    df.dropna(inplace=True)
    return df

# 应用特征计算
aapl_data_with_features = calculate_technical_indicators(aapl_data)
print(f"特征工程后数据形状:{aapl_data_with_features.shape}")
print(aapl_data_with_features[['Adj Close', 'SMA_20', 'RSI_14', 'MACD']].tail())

现在,我们的数据从原始的几列扩展到了包含二十多个技术指标的丰富特征集。每个指标都从不同角度描述了市场状态,例如趋势(均线)、动量(RSI, MACD)、波动性(布林带宽度)和量价关系(VWAP)。

3.3 定义预测目标与创建标签

我们的目标是预测未来收益。假设我们想预测5个交易日后的收益率:

def create_labels(df, forward_days=5):
    """
    创建预测标签:未来N日的收益率
    :param df: 包含‘Adj Close’的DataFrame
    :param forward_days: 向前看的交易日数
    :return: 添加了‘Label’列的DataFrame
    """
    df = df.copy()
    # 计算未来第N日的调整后收盘价
    future_price = df['Adj Close'].shift(-forward_days)
    # 计算未来收益率:(未来价 - 当前价) / 当前价
    df['Label'] = (future_price - df['Adj Close']) / df['Adj Close']
    # 再次删除因创建标签产生的NaN行(最后forward_days行)
    df.dropna(subset=['Label'], inplace=True)
    return df

aapl_data_labeled = create_labels(aapl_data_with_features, forward_days=5)
print(f"创建标签后数据形状:{aapl_data_labeled.shape}")
print(aapl_data_labeled[['Adj Close', 'Label']].tail(10))

Label 列正数表示预测上涨,负数表示预测下跌。我们也可以将其转化为分类问题,例如 Label > 0 为1(涨),否则为0(跌),但原项目更侧重于回归预测。

3.4 模型训练、评估与预测

接下来,我们将数据分为特征矩阵 X 和目标向量 y ,并按时间顺序划分训练集和测试集( 严禁随机打乱 ,否则会导致数据泄露,即用未来信息预测过去)。

from sklearn.model_selection import TimeSeriesSplit
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import matplotlib.pyplot as plt

# 准备数据
# 选择特征列,排除价格和标签本身
feature_columns = [col for col in aapl_data_labeled.columns if col not in ['Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume', 'Label']]
X = aapl_data_labeled[feature_columns].values
y = aapl_data_labeled['Label'].values
dates = aapl_data_labeled.index

# 按时间顺序划分:前80%训练,后20%测试
split_idx = int(len(X) * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
dates_train, dates_test = dates[:split_idx], dates[split_idx:]

print(f"训练集样本数:{len(X_train)}, 测试集样本数:{len(X_test)}")
print(f"训练期:{dates_train[0].date()} 至 {dates_train[-1].date()}")
print(f"测试期:{dates_test[0].date()} 至 {dates_test[-1].date()}")

# 初始化并训练随机森林回归模型
model = RandomForestRegressor(
    n_estimators=100,  # 树的数量
    max_depth=10,       # 树的最大深度,防止过拟合
    min_samples_split=10, # 内部节点再划分所需最小样本数
    random_state=42     # 固定随机种子,确保结果可复现
)
model.fit(X_train, y_train)

# 在训练集和测试集上进行预测
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)

# 评估模型性能
def evaluate_predictions(y_true, y_pred, set_name):
    mse = mean_squared_error(y_true, y_pred)
    mae = mean_absolute_error(y_true, y_pred)
    r2 = r2_score(y_true, y_pred)
    print(f"\n{set_name} 评估结果:")
    print(f"  均方误差 (MSE): {mse:.6f}")
    print(f"  平均绝对误差 (MAE): {mae:.6f}")
    print(f"  决定系数 (R²): {r2:.6f}")
    # 计算方向准确性:预测符号与实际符号一致的比例
    direction_acc = np.mean((y_pred > 0) == (y_true > 0))
    print(f"  方向准确性: {direction_acc:.4f}")
    return mse, mae, r2, direction_acc

train_metrics = evaluate_predictions(y_train, y_train_pred, "训练集")
test_metrics = evaluate_predictions(y_test, y_test_pred, "测试集")

运行这段代码,你会得到模型在训练集和测试集上的表现。 一个关键观察点 :训练集的R²可能很高,但测试集的R²可能很低甚至是负数。这强烈暗示模型存在过拟合——它记住了训练数据的噪声,而非学到了普适规律。测试集的“方向准确性”是一个更直观的指标,它表示模型预测涨跌方向的能力,如果长期稳定高于50%,则可能具有统计意义。

3.5 策略回测:将预测转化为交易信号

模型预测出未来收益率后,我们需要一个简单的规则将其转化为交易动作。例如:

  • 规则1 :如果预测收益率 > 阈值(如0.01,即1%),则在当天收盘时买入(假设次日开盘成交)。
  • 规则2 :如果持有仓位且预测收益率 < -阈值(如-0.005),则卖出。
  • 初始资金 :假设为100,000。
  • 仓位 :每次全仓买入/卖出(为简化,不考虑仓位管理)。
  • 交易成本 :暂不考虑(实际中需考虑佣金和滑点)。
def simple_backtest(predictions, prices, initial_capital=100000, buy_threshold=0.01, sell_threshold=-0.005):
    """
    简易回测引擎
    :param predictions: 模型在测试集上的预测收益率序列
    :param prices: 测试集对应的价格序列(用于计算买卖股数)
    :param initial_capital: 初始资金
    :param buy_threshold: 买入信号阈值
    :param sell_threshold: 卖出信号阈值
    :return: 资金曲线和交易记录
    """
    capital = initial_capital
    position = 0  # 持有股数
    equity_curve = [capital]  # 资产总值曲线
    trade_log = []
    in_market = False  # 是否持有仓位

    for i in range(len(predictions)):
        current_price = prices[i]
        pred = predictions[i]

        # 交易逻辑
        if not in_market and pred > buy_threshold:
            # 买入信号
            position = int(capital / current_price)  # 全仓买入
            capital -= position * current_price  # 资金减少
            in_market = True
            trade_log.append({'date': dates_test[i], 'action': 'BUY', 'price': current_price, 'shares': position})
        elif in_market and pred < sell_threshold:
            # 卖出信号
            capital += position * current_price  # 资金增加
            position = 0
            in_market = False
            trade_log.append({'date': dates_test[i], 'action': 'SELL', 'price': current_price, 'shares': position})

        # 每日计算总资产(现金 + 持仓市值)
        total_equity = capital + position * current_price
        equity_curve.append(total_equity)

    # 最后一天如果还持有仓位,强制平仓
    if in_market:
        capital += position * prices[-1]
        trade_log.append({'date': dates_test[-1], 'action': 'SELL', 'price': prices[-1], 'shares': position})
        equity_curve[-1] = capital
        position = 0

    final_capital = equity_curve[-1]
    total_return = (final_capital - initial_capital) / initial_capital * 100
    print(f"\n回测结果:")
    print(f"  初始资金: ${initial_capital:,.2f}")
    print(f"  最终资金: ${final_capital:,.2f}")
    print(f"  总收益率: {total_return:.2f}%")
    print(f"  交易次数: {len(trade_log)}")

    return np.array(equity_curve), trade_log

# 运行回测
prices_test = aapl_data_labeled.loc[dates_test, 'Adj Close'].values
equity_curve, trades = simple_backtest(y_test_pred, prices_test)

# 绘制资金曲线
plt.figure(figsize=(12, 6))
plt.plot(dates_test, equity_curve[:-1], label='Portfolio Value', linewidth=2)
plt.xlabel('Date')
plt.ylabel('Portfolio Value ($)')
plt.title('Simple Trading Strategy Equity Curve')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

这个简易回测会输出策略的最终资金和收益率,并绘制资金曲线。你可以将其与“买入并持有”(Buy and Hold)基准策略进行比较,即期初全仓买入该股票并一直持有到期末,计算其收益率。这是衡量你的主动策略是否创造了“阿尔法”(超额收益)的最基本标准。

4. 深入解析:特征重要性、过拟合与模型调优

4.1 理解模型在“看”什么:特征重要性分析

使用随机森林的一个巨大优势是它可以输出特征的重要性评分,这能帮助我们理解模型决策的依据,甚至反过来验证我们的特征工程是否合理。

# 获取特征重要性
importances = model.feature_importances_
feature_names = feature_columns
indices = np.argsort(importances)[::-1]  # 降序排列

# 打印最重要的10个特征
print("Top 10 特征重要性:")
for i in range(min(10, len(indices))):
    print(f"{i+1:2d}. {feature_names[indices[i]]:30s} : {importances[indices[i]]:.4f}")

# 可视化
plt.figure(figsize=(10, 6))
plt.title('Random Forest Feature Importances')
plt.bar(range(15), importances[indices[:15]], align='center')
plt.xticks(range(15), [feature_names[i] for i in indices[:15]], rotation=45, ha='right')
plt.xlabel('Feature')
plt.ylabel('Importance Score')
plt.tight_layout()
plt.show()

分析结果可能会显示,某些均线比率、波动率指标(如布林带宽度)或动量指标(如ROC)占据了主导地位。如果发现某个特征重要性异常高,需要思考其是否可能引入了未来数据泄露(例如,不小心使用了包含未来信息的指标)。如果大部分特征重要性都很低,则可能意味着特征与目标关系不大,或者需要更复杂的非线性模型来捕捉关系。

4.2 量化金融建模的“头号公敌”:过拟合

在MachineLearningStocks这类项目中,过拟合是最大挑战。市场数据噪声极大,且存在非平稳性(统计特性随时间变化)。模型很容易在历史数据上找到一些偶然的、无法重复的模式,导致在样本外测试(尤其是真正的未来数据)上表现糟糕。

识别过拟合的迹象:

  1. 训练集性能远优于测试集 :这是最明显的标志。例如训练集R²=0.8,测试集R²=0.05。
  2. 策略在回测中表现“过于完美” :资金曲线平滑上升,几乎没有回撤,夏普比率高得离谱。这通常意味着策略可能过度优化,适应了历史噪声。
  3. 对参数极度敏感 :模型性能或策略收益随着某个参数(如移动平均窗口)的微小变化而剧烈波动。

对抗过拟合的实战技巧:

  1. 简化模型 :从简单的线性模型(如岭回归)开始,作为基准。复杂的模型(如深度神经网络)在数据量不足时更容易过拟合。
  2. 增加正则化 :对于线性模型,使用L1(Lasso)或L2(Ridge)正则化。对于树模型,限制 max_depth min_samples_split min_samples_leaf 等参数。
  3. 使用更稳健的验证方法 :不要用简单的随机划分,要用 时间序列交叉验证(TimeSeriesSplit) scikit-learn 提供了 TimeSeriesSplit ,它能确保在每一折验证中,训练数据都严格在验证数据之前,模拟真实的时间流。
  4. 减少特征数量 :通过特征重要性分析,只保留最重要的特征。也可以使用递归特征消除(RFE)。
  5. 引入更多数据 :使用多只股票、多个市场、更长时期的数据进行训练,让模型学习更通用的模式,而非单只股票的特异性。
  6. 谨慎进行参数优化 :避免在全部数据上使用网格搜索(GridSearchCV)寻找最佳参数,这会导致“前瞻性偏差”。应该在时间序列交叉验证的每一折内独立调参。

4.3 模型调优与进阶策略思路

在基础流程跑通后,可以从以下几个方向进行深化:

1. 模型层面的调优:

  • 尝试不同模型 :除了随机森林,可以尝试梯度提升机(如XGBoost, LightGBM),它们在结构化数据上往往表现更优。也可以试试简单的神经网络(MLP),但要注意数据量和过拟合风险。
  • 集成学习 :将多个不同类型模型的预测结果进行平均或投票(Stacking/Blending),可以降低方差,提高泛化能力。
  • 超参数调优 :使用 TimeSeriesSplit 配合 GridSearchCV RandomizedSearchCV 进行超参数搜索。关键是要确保搜索过程符合时间序列特性。

2. 特征工程进阶:

  • 领域知识注入 :除了技术指标,可以引入基本面数据(市盈率、市净率等,可从 yfinance info 属性获取)、宏观经济指标、市场情绪数据(如新闻情感分析,复杂度高)等。
  • 交互特征与衍生特征 :创建特征之间的乘积或比率,例如 RSI / 布林带宽度 ,可能捕捉到“超买超卖状态下的波动率”这种复合信息。
  • 滞后特征 :不仅使用当天的指标值,还可以加入前1天、前5天、前20天的指标值作为特征,让模型看到短期历史序列。
  • 降维 :如果特征过多,可以使用PCA(主成分分析)进行降维,但会损失可解释性。

3. 预测目标与策略设计优化:

  • 分类而非回归 :将问题从“预测收益率大小”改为“预测涨跌方向”(二分类)或“预测涨幅等级”(多分类)。分类问题有时更稳定。
  • 预测波动率 :另一个有价值的预测目标是未来波动率,可用于风险管理或期权策略。
  • 多因子模型 :不预测绝对价格,而是预测股票的相对排名(横截面预测)。例如,每天预测哪些股票未来一周表现会优于指数,然后做多这些股票,做空表现差的股票。
  • 引入风险管理 :在回测中加入止损、止盈、仓位控制(如凯利公式)、交易成本等,使策略更贴近现实。

5. 常见陷阱、问题排查与经验分享

5.1 数据质量问题与处理

  1. 数据缺失与异常值 :股票数据可能因为节假日、停牌等原因缺失。使用 df.fillna(method='ffill') 向前填充,或直接删除缺失行。异常值(如价格错误)需要检测并处理,可以用滚动均值加减3倍标准差的方法识别。
  2. 幸存者偏差 :如果你只选取当前存在且表现良好的股票(如标普500成分股)回测,结果会过于乐观,因为你忽略了那些已经退市或表现很差的股票。解决方法之一是使用“点-in-time”数据,即回测时只使用当时已知的信息。
  3. 未来函数 :这是最致命的错误。 确保任何特征的计算只用到当前及之前的信息 。例如,计算20日均线,必须用 .rolling(window=20).mean() ,而不能用整个序列的均值。在创建标签时,用 .shift(-N) 来获取未来数据,但要确保在划分训练/测试集前,这些未来标签没有被特征误用。

5.2 回测中的“坑”

  1. 前视偏差 :在策略逻辑中使用了当时不可知的信息。例如,在T日交易时,使用了T日的收盘价(收盘后才知)。实际交易中,你只能基于T日开盘前或盘中已发生的数据做决策。回测应用开盘价或前一日收盘价进行交易计算。
  2. 忽略交易成本 :即使是低佣金的今天,频繁交易的成本也会迅速侵蚀利润。在回测中至少加入一个固定比例(如0.1%)的交易成本。
  3. 忽略流动性 :对于小盘股,你的模拟交易量可能远超实际市场能承受的量,导致无法以假设的价格成交。回测中应对交易量进行限制。
  4. 过度优化 :在历史数据上反复调整参数直到策略表现完美,这几乎必然导致在未来失效。应使用样本外测试和滚动窗口验证来评估策略稳健性。

5.3 实战心得与建议

  • 从模仿开始,以理解告终 :MachineLearningStocks是一个绝佳的起点,但不要停留在运行代码上。要深入理解每一行代码背后的金融和数学逻辑。为什么用这些指标?为什么预测5天收益率?模型评估指标如何选择?
  • 拥抱不完美 :在金融市场上,没有“圣杯”。你的模型大概率无法稳定赚钱。项目的价值在于学习数据处理、特征工程、模型构建和评估的完整流程,并理解量化思维的局限性。
  • 重视基准比较 :任何策略都要与简单的“买入并持有”基准指数(如SPY)进行比较。如果你的复杂策略长期跑不赢指数,那么它的价值就存疑。
  • Paper Trading(模拟交易)是必须的 :在投入真金白银前,一定要进行长时间的模拟交易(至少一个完整的市场周期,包含牛熊市),观察策略在实时市场中的表现和心理感受。
  • 日志和版本控制 :使用Git管理你的代码和实验记录。详细记录每次修改的参数、特征和结果。量化交易是一个迭代的科学实验过程。

这个项目就像给你一套精良的木工工具和一份详细的椅子图纸。你能做出椅子,但椅子牢不牢靠、坐得舒不舒服,取决于你对木材特性(市场)的理解、对工具(算法)的掌握,以及反复打磨(迭代优化)的经验。最终,你可能发现做椅子(预测短期价格)太难,转而尝试做桌子(资产配置)或雕塑(另类数据挖掘)。探索的过程,其价值远大于一个简单的“预测明天涨跌”的答案。

更多推荐