下面是一个使用 LightGBM 库内置的 LTR 功能(以 rank:ndcg 为例)来实现量化投资中截面策略排序模型的 Python 教程。

这个教程将模拟一个典型的截面因子策略场景:在每个调仓日,我们有多个资产,每个资产有一组因子(特征),我们希望模型能学习如何根据这些因子来预测未来一段时间的相对表现(排序),以便我们能买入预期表现最好的资产,卖出预期表现最差的资产。

核心概念:

  1. Query/Group: 在 LTR 中,“Query” 指的是一次独立的排序任务。在截面策略中,一个调仓日 (rebalancing date) 就是一个 Query。属于同一个调仓日的所有资产构成一个 “Group”。LightGBM 需要明确知道哪些样本(资产)属于同一个 Group。
  2. Features (X): 每个资产在调仓日的因子值。例如,动量因子、估值因子、质量因子等。
  3. Labels (y): 表示每个资产在 未来一个持有期 的 “好坏程度” 或 “相关性得分”。这通常是未来收益率,或者基于未来收益率计算的等级。对于 rank:ndcg,这个标签通常是 整数,表示相关性等级(例如,未来收益最高的 10% 资产标记为 4,次高的 10% 标记为 3,…,最低的 10% 标记为 0)。标签的数值越大,表示越相关/越好
  4. Group Information: 一个数组,记录每个 Group(每个调仓日)包含多少个样本(资产)。

教程步骤:

import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import QuantileTransformer # 用于将收益率转换为等级标签

# --- 1. 数据模拟 ---
# 模拟一个截面策略的数据集
# 假设我们有 n_dates 个调仓日,每个调仓日有 n_assets_per_date 个资产
# 每个资产有 n_features 个因子值

np.random.seed(42)
n_dates = 100  # 模拟100个调仓日 (Queries)
n_assets_per_date = 50 # 每个调仓日50个资产 (Items per Query)
n_features = 10 # 10个因子 (Features)
n_samples = n_dates * n_assets_per_date

# 创建日期和资产标识
dates = np.repeat(pd.to_datetime(pd.date_range(start='2020-01-01', periods=n_dates, freq='M')), n_assets_per_date)
asset_ids = np.tile(np.arange(n_assets_per_date), n_dates)

# 创建特征数据 (X)
# 随机生成因子数据
X = np.random.rand(n_samples, n_features)

# 创建目标标签 (y) - 这是 LTR 的关键
# 模拟未来一个月的收益率,并假设部分因子与未来收益有一定(带噪声的)线性关系
true_factor_weights = np.random.randn(n_features) * 0.5
future_returns = X @ true_factor_weights + np.random.normal(0, 0.5, n_samples) # 加入噪声

# *** 将连续的未来收益率转换为离散的相关性等级 (LGBM的NDCG需要整数等级) ***
# 方法1:按分位数划分等级 (例如,划分为5个等级: 0, 1, 2, 3, 4)
n_bins = 5 # 分为5个等级
y = np.zeros(n_samples, dtype=int)
for i in range(n_dates):
    start_idx = i * n_assets_per_date
    end_idx = (i + 1) * n_assets_per_date
    date_returns = future_returns[start_idx:end_idx]
    # 使用 qcut 将当前日期内的资产按收益率分为 n_bins 个等级
    # labels=False 返回整数索引 0 到 n_bins-1
    ranks = pd.qcut(date_returns, q=n_bins, labels=False, duplicates='drop')
    y[start_idx:end_idx] = ranks.astype(int)

# 方法2(备选):直接使用未来收益率作为标签 (如果使用 rank:pairwise 或其他支持连续标签的目标)
# y = future_returns # 如果用 pairwise,可以直接用收益率,但ndcg通常用等级

# 创建 Group 信息
# LightGBM 需要知道每个 group (调仓日) 有多少个样本 (资产)
# 在这个模拟中,每个调仓日资产数相同
group = np.full(n_dates, n_assets_per_date, dtype=int)
print(f"Total samples: {n_samples}")
print(f"Feature shape: {X.shape}")
print(f"Label shape: {y.shape}")
print(f"Group array shape: {group.shape}")
print(f"Group array sample: {group[:5]}... (Indicates first 5 groups have {n_assets_per_date} items each)")
print(f"Example labels (first date): {y[:n_assets_per_date]}") # 查看第一个日期的标签分布

# --- 2. 数据划分 (训练集 / 测试集) ---
# 按照 LTR 的要求,划分时必须保持 Group 的完整性
# 不能简单地随机打乱所有样本,需要按日期(Query)划分

# 这里简单地按日期前后划分
train_dates_count = int(n_dates * 0.8)
test_dates_count = n_dates - train_dates_count

train_samples_count = train_dates_count * n_assets_per_date
test_samples_count = test_dates_count * n_assets_per_date

X_train, X_test = X[:train_samples_count], X[train_samples_count:]
y_train, y_test = y[:train_samples_count], y[train_samples_count:]
group_train, group_test = group[:train_dates_count], group[train_dates_count:]

print(f"\nTrain shapes: X={X_train.shape}, y={y_train.shape}, group={group_train.shape}")
print(f"Test shapes: X={X_test.shape}, y={y_test.shape}, group={group_test.shape}")

# --- 3. 初始化并训练 LightGBM Ranker 模型 ---

# 使用 LGBMRanker
# 重要参数:
# objective='lambdarank' (等价于 rank:pairwise,基于 LambdaRank/MART)
# objective='rank_xendcg' (等价于 rank:ndcg,直接优化 NDCG)
# metric='ndcg' (评估指标,可以设置 ndcg@k,如 'ndcg@10')
# label_gain: list of integers. NDCG 计算需要,定义每个标签等级的增益值。
#             例如,如果标签是 0, 1, 2, 3, 4,可以设置 label_gain=[0, 1, 2, 3, 4] 或 [0, 3, 7, 15, 31] (2^label - 1)
# boosting_type='gbdt'
# n_estimators=100 (树的数量)
# learning_rate=0.1
# ... 其他LGBM参数 (num_leaves, max_depth, reg_alpha, reg_lambda 等)

lgbm_ranker = lgb.LGBMRanker(
    objective='rank_xendcg', # 使用 NDCG 优化目标
    metric='ndcg',           # 评估指标也用 NDCG
    label_gain=list(range(n_bins)), # 标签增益,与y的等级对应 [0, 1, 2, 3, 4]
    boosting_type='gbdt',
    n_estimators=100,
    learning_rate=0.1,
    random_state=42,
    n_jobs=-1,
    # 可以加入其他参数进行调优
    # num_leaves=31,
    # max_depth=-1,
    # reg_alpha=0.0,
    # reg_lambda=0.0,
)

print("\nTraining LightGBM Ranker...")
# 训练时需要传入 group_train
# 可以加入 eval_set 来进行早停和性能监控
lgbm_ranker.fit(
    X_train,
    y_train,
    group=group_train,
    eval_set=[(X_test, y_test)],
    eval_group=[group_test],
    eval_at=[1, 3, 5, 10], # 计算 NDCG@1, @3, @5, @10
    callbacks=[lgb.early_stopping(stopping_rounds=10, verbose=True)] # 早停
)
print("Training completed.")

# --- 4. 预测和评估 ---

print("\nPredicting on test set...")
# predict() 返回的是排序分数,不是类别或等级
# 分数越高,表示模型预测的排名越靠前
predicted_scores = lgbm_ranker.predict(X_test)

print(f"Predicted scores shape: {predicted_scores.shape}")
print(f"Example predicted scores (first test date): {predicted_scores[:n_assets_per_date]}")

# 评估(可以在 fit 的 eval_set 中看到,也可以手动计算)
# LightGBM 在 fit 过程中已经打印了验证集上的 NDCG 分数
# 如果需要手动计算,可以使用 sklearn.metrics.ndcg_score,但需要注意处理 group
# 这里我们主要依赖 fit 过程中的评估输出

# 查看特征重要性
print("\nFeature Importances:")
feature_importances = pd.DataFrame({
    'feature': [f'Factor_{i}' for i in range(n_features)],
    'importance': lgbm_ranker.feature_importances_
}).sort_values('importance', ascending=False)
print(feature_importances)

# --- 5. 如何在截面策略中使用预测分数 ---

# 假设现在是新的一个调仓日 T
# 1. 获取该日所有资产的因子数据 X_today (形状:[n_assets_today, n_features])
# 2. 使用训练好的模型预测分数:scores_today = lgbm_ranker.predict(X_today)
# 3. 根据分数排序资产:
#    df_today = pd.DataFrame({'asset_id': asset_ids_today, 'score': scores_today})
#    df_today_ranked = df_today.sort_values('score', ascending=False) # 分数越高越好
# 4. 构建投资组合:
#    top_n_percent = 0.1 # 买入排名前 10%
#    bottom_n_percent = 0.1 # 卖出排名后 10%
#    num_assets_today = len(df_today_ranked)
#    long_assets = df_today_ranked.head(int(num_assets_today * top_n_percent))['asset_id']
#    short_assets = df_today_ranked.tail(int(num_assets_today * bottom_n_percent))['asset_id']
# 5. 执行交易,进入下一个调仓日

print("\nConceptual usage in strategy:")
print("1. Get features for assets on a new rebalancing date.")
print("2. Use lgbm_ranker.predict(X_new_date) to get scores.")
print("3. Rank assets based on predicted scores (higher score is better).")
print("4. Form long/short portfolios based on ranks (e.g., long top 10%, short bottom 10%).")

代码解释和注意事项:

  1. 数据模拟: 关键在于模拟出 X (特征), y (相关性等级标签), 和 group (分组信息)。y 的生成尤为重要,它应该反映你希望模型学习的排序依据(这里是未来收益的分位数等级)。
  2. 标签转换: rank:ndcg 需要整数形式的相关性等级。我们使用 pd.qcut 将模拟的未来收益率在每个调仓日内部分成了 n_bins 个等级。label_gain 参数需要与这些等级对应,告诉 LightGBM 不同等级的重要性。
  3. 数据划分: 必须按 Group(日期)划分,以模拟真实的回测过程,避免未来数据泄露。
  4. LGBMRanker: 这是 LightGBM 中专门用于排序任务的类。
  5. objective='rank_xendcg': 选择优化 NDCG。如果你的目标是优化配对准确率,可以使用 'lambdarank'
  6. metric='ndcg': 训练过程中监控 NDCG 指标。eval_at 参数可以指定计算 NDCG@k 中的 k 值。
  7. fit() 方法: 注意传入 group=group_traineval_seteval_group 用于在验证集上评估,callbacks=[lgb.early_stopping(...)] 用于防止过拟合。
  8. predict() 方法: 返回的是 排序分数 (scores),而不是排名或类别。分数越高,表示模型认为该资产的排名应该越靠前。
  9. 策略应用: 得到分数后,你需要根据这些分数对当天的资产进行排序,然后根据排名来构建你的多空组合。
  10. 真实数据: 在实际应用中,你需要用真实的因子数据替换 X,用真实的未来收益(或其衍生等级)替换 y,并根据你的数据结构正确生成 group 数组。资产数量在不同日期可能会变化,group 数组需要准确反映每个日期的实际资产数量。
  11. 调优: 和其他机器学习模型一样,LGBMRanker 的性能依赖于超参数调优。你需要使用验证集和合适的调优方法(如 Grid Search, Random Search, Bayesian Optimization)来找到最优参数。

这个教程提供了一个基础框架,你可以根据自己的具体策略需求、因子数据和目标来修改和扩展。

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐