元标注:对冲基金如何用机器学习做仓位管理
Read in English预测市场方向几乎不可能——连专业对冲基金都很难做到 55% 的准确率。但秘密在于:你不需要预测得更准也能跑赢。元标注由 Marcos López de Prado 在 Guggenheim Partners 提出,它把"交易什么"的决定和"交易多少"的决定分开。用机器学习按置信度决定仓位大小,你可以在方向判断毫无长进的情况下把夏普比率提升 15-25%。这正是机构投资者把平庸信号变成盈利策略的方法。
🎯 你将学到什么
- 方向预测为什么会失效 (52% 的准确率几乎无利可图)
- 元标注框架 (主模型 + 次级机器学习模型)
- 面向仓位管理的特征工程 (波动率、动量强度、市场状态)
- Python 实现 含随机森林模型与回测
- 真实结果: 夏普比率 0.51 → 0.61(提升 20%)
- 与退休组合的结合 以及税务方面的考量
读完之后: 你会明白如何把机构级的仓位管理方法用到自己的组合上。
问题所在:预测方向几乎不可能
52% 准确率的陷阱
先说一个不太舒服的事实:即便是最顶尖的量化对冲基金,其交易信号的方向准确率也只有 52-55% 。
示例:简单动量策略
- 信号:60 日收益率高于 0 时买入,低于 0 时卖出
- 标的:S&P 500(SPY)
- 区间:2010-2024
- 准确率:52.3% (几乎只比抛硬币好一点)
等额仓位下的表现:
| 指标 | 固定仓位(100%) |
|---|---|
| 年化收益率 | 6.8% |
| 波动率 | 13.2% |
| 夏普比率 | 0.51 |
| 最大回撤 | -24.1% |
| 胜率 | 52.3% |
问题在于: 准确率只有 52%,这个策略在扣除交易成本和税费之后 几乎不赚钱 。放在应税账户里,净结果甚至可能是亏的。
方向预测为什么这么难?
三个根本性的难题:
- 市场有效性: 公开信息已经反映在价格里了。如果大家都知道动量有效,聪明资金早就据此交易过了。
- 噪音主导: 短期价格波动有 80-90% 是噪音,只有 10-20% 是信号。你的模型大部分时间在预测随机性。
- 市场状态切换: 在低波动期(2012-2019)有效的策略,到了高波动期(2020-2022)就失灵了。
关键洞见: 既然你没法可靠地预测方向,为什么每个信号都下同样大的注?
被忽略的机会:置信度并不相同
信号之间是有高下之分的。看下面这两个动量信号:
信号 A(2020 年 1 月):
- 60 日收益率:+1.2%
- 波动率:8%(低)
- 与其他资产的相关性:0.3(低)
- 动量强度:弱(勉强为正)
- 你的置信度:低
信号 B(2020 年 4 月):
- 60 日收益率:+18.5%
- 波动率:12%(中等,已从 35% 回落)
- 与其他资产的相关性:0.1(极低)
- 动量强度:强(历史前 10% 分位)
- 你的置信度:高
传统做法: 两个信号都押上 100% 的资金。
元标注做法: 信号 A 押 30%,信号 B 押 150%。
结果: 平均下注规模一样(90%),但资金集中在高确信度的信号上。这就是元标注的核心。
解决办法:元标注框架
双模型架构
元标注把两个不同的决定拆开:
决定一:主模型(方向)
职责: 产生交易信号(买入、卖出或中性)
方法:
- 技术指标(动量、均值回归、趋势跟踪)
- 基本面信号(价值、成长、质量因子)
- 机器学习模型(如果你确实有优势)
- 人为判断(你自己的市场观点)
关键点: 主模型不需要多复杂。一条简单的 60 日动量规则就够用。
决定二:次级模型(仓位大小)
职责: 决定投入多少资金(0% 到 200%)
方法: 用能预测信号质量的特征训练一个机器学习分类器
特征:
- 波动率(当前值与历史均值之比)
- 动量强度(信号的幅度)
- 市场状态(相关性环境)
- 回撤状况(你是不是已经在亏钱了?)
- 信号持续性(信号已经维持多久了?)
输出: 主信号盈利的概率 → 仓位大小
数学框架:
Primary Model: Side(t) ∈ {-1, 0, +1} (sell, neutral, buy)
Secondary Model: P(Profitable | Side(t), Features(t))
Position Size: w(t) = f(P) × Side(t)
where f(P) maps probability to position size:
- P < 0.4: w = 0% (skip trade)
- P = 0.5: w = 50% (low confidence)
- P = 0.6: w = 100% (medium confidence)
- P > 0.7: w = 150%+ (high confidence)
它为什么有效:背后的算术
设想一个胜率 52%、盈亏比 1:1 的策略:
固定仓位(每笔都是 100%):
- 每笔交易的期望值:0.52 × (+1) + 0.48 × (-1) = +0.04
- 夏普比率:约 0.50
元标注(可变仓位):
- 高置信度交易(胜率 70%,占交易的 30%):期望值 +0.40
- 中等置信度(胜率 52%,占交易的 50%):期望值 +0.04
- 低置信度(胜率 45%,占交易的 20%):跳过(仓位为 0%)
结果:
- 总体胜率:0.30 × 0.70 + 0.50 × 0.52 + 0.20 × 0 = 47.1% (更低了!)
- 期望值:0.30 × 0.40 + 0.50 × 0.04 = +0.14 (高了 3.5 倍!)
- 夏普比率:约 0.61(提升 20%)
反直觉的地方: 胜率下降了(因为你跳过了那些勉强能赢的机会),但盈利能力提高了(因为你把资金集中到了高质量信号上)。
🔒 会员专享内容
继续阅读,你还会学到:
- 用 scikit-learn 写出的完整 Python 实现
- 用于仓位管理的 10 个工程化特征
- 带代码的回测结果(2010-2024)
- 与退休组合的结合
- 税务优化策略
- 什么时候不该用元标注(很关键!)
付费会员可以查看全部深度文章、代码仓库和工具。
Python 实现:搭建一套元标注系统
第 1 步:生成主信号(动量策略)
我们用一个简单的 60 日动量策略当主模型。它故意做得很朴素——元标注对 任何 主信号都适用。
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
def generate_primary_signal(returns, window=60):
"""
用简单的动量规则生成主交易信号。
参数:
- returns:资产收益率的 pd.Series
- window:动量的回看期(默认 60 天)
返回:
- pd.Series:+1(买入)、-1(卖出)、0(中性)
"""
momentum = returns.rolling(window).sum()
signals = pd.Series(index=returns.index, dtype=int)
signals[momentum > 0.02] = 1 # 60 日收益率高于 2% 则买入
signals[momentum < -0.02] = -1 # 60 日收益率低于 -2% 则卖出
signals[(momentum >= -0.02) & (momentum <= 0.02)] = 0 # 中性
return signals.fillna(0)
示例输出(SPY,2020):
| 日期 | 60 日收益率 | 信号 |
|---|---|---|
| 2020-01-15 | +3.2% | +1(买入) |
| 2020-03-20 | -28.5% | -1(卖出) |
| 2020-05-01 | +8.1% | +1(买入) |
第 2 步:为元标注构建特征
次级模型需要的是能预测 主信号何时会奏效的特征。下面是机构投资者常用的 10 个特征:
def engineer_features(returns, signal, prices):
"""
为元标注模型构建特征。
返回:
- 含 10 个特征的 pd.DataFrame
"""
features = pd.DataFrame(index=returns.index)
# 1. 波动率(当前值与 6 个月均值之比)
vol_current = returns.rolling(20).std()
vol_avg = returns.rolling(120).std()
features['vol_ratio'] = vol_current / vol_avg
# 2. 动量强度(信号的幅度)
features['momentum_strength'] = returns.rolling(60).sum().abs()
# 3. 信号持续性(信号已维持的天数)
features['signal_persistence'] = signal.groupby((signal != signal.shift()).cumsum()).cumcount()
# 4. 回撤状况(距历史高点的百分比)
cummax = prices.expanding().max()
features['drawdown'] = (prices - cummax) / cummax
# 5. 市场相关性(与 SPY 的相关系数)
# 前提是你手上有 SPY 的收益率数据
features['correlation'] = returns.rolling(60).corr(spy_returns)
# 6. 成交量趋势(如果有数据的话)
# features['volume_trend'] = volume.rolling(20).mean() / volume.rolling(60).mean()
# 7. 动量加速度(动量的变化量)
mom = returns.rolling(60).sum()
features['momentum_accel'] = mom - mom.shift(20)
# 8. 波动率趋势(在上升还是下降)
features['vol_trend'] = vol_current - vol_current.shift(20)
# 9. 与均线的距离
ma_200 = prices.rolling(200).mean()
features['price_ma_ratio'] = prices / ma_200
# 10. 信号强度相对历史区间的位置
mom_zscore = (mom - mom.rolling(252).mean()) / mom.rolling(252).std()
features['momentum_zscore'] = mom_zscore
return features.dropna()
特征解读:
- vol_ratio: 波动率偏高(高于 1.5)→ 降低仓位
- momentum_strength: 动量强劲(高于 10%)→ 加大仓位
- signal_persistence: 信号已持续超过 30 天 → 确信度更高
- drawdown: 回撤过大(低于 -15%)→ 降低仓位(风险管理)
- momentum_zscore: Z 值高于 2 → 动量处于历史前 10% → 加大仓位
第 3 步:制作训练标签
次级模型是从历史信号里学的。我们需要把每个信号标记为"盈利"(1)或"不盈利"(0):
def create_meta_labels(returns, signal, forward_window=20):
"""
生成二元标签:主信号有没有赚到钱?
参数:
- returns:日度收益率
- signal:主模型的信号(+1、-1、0)
- forward_window:用于评估的持有期(20 天 = 1 个月)
返回:
- pd.Series:1(盈利)、0(不盈利)
"""
forward_returns = returns.rolling(forward_window).sum().shift(-forward_window)
# 信号方向与未来收益方向一致时,标签为 1
labels = pd.Series(index=returns.index, dtype=int)
labels[(signal == 1) & (forward_returns > 0)] = 1
labels[(signal == -1) & (forward_returns < 0)] = 1
labels[(signal == 1) & (forward_returns <= 0)] = 0
labels[(signal == -1) & (forward_returns >= 0)] = 0
labels[signal == 0] = 0 # 中性信号不产生交易
return labels.dropna()
示例:
- 日期:2020-01-15,信号:+1(买入),未来 20 日收益率:+4.2% → 标签:1
- 日期:2020-02-20,信号:+1(买入),未来 20 日收益率:-18.5% → 标签:0
- 日期:2020-03-20,信号:-1(卖出),未来 20 日收益率:-5.2% → 标签:1
第 4 步:训练元模型(随机森林)
我们选随机森林,因为它:
- 能处理特征之间的非线性交互
- 能输出概率值(正好用来定仓位)
- 几乎不需要调超参数
- 配合恰当的交叉验证,不容易过拟合
def train_meta_model(features, labels, test_size=0.3):
"""
为元标注训练随机森林分类器。
返回:
- 训练好的模型
- 测试集上的表现指标
"""
# 切分数据(务必保留时间顺序!)
split_idx = int(len(features) * (1 - test_size))
X_train, X_test = features.iloc[:split_idx], features.iloc[split_idx:]
y_train, y_test = labels.iloc[:split_idx], labels.iloc[split_idx:]
# 训练随机森林
model = RandomForestClassifier(
n_estimators=100,
max_depth=5,
min_samples_split=50,
class_weight='balanced', # 处理标签不均衡
random_state=42
)
model.fit(X_train, y_train)
# 评估
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
print(f"Training accuracy: {train_score:.3f}")
print(f"Test accuracy: {test_score:.3f}")
# 特征重要性
importance = pd.DataFrame({
'feature': features.columns,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
print("\nTop 5 features:")
print(importance.head())
return model
典型的特征重要性排序:
- momentum_strength(0.22)—— 预测力最强
- vol_ratio(0.18)—— 高波动意味着信号不可靠
- drawdown(0.15)—— 回撤期间不要加仓
- momentum_zscore(0.12)—— 极端动量确实有效
- signal_persistence(0.10)—— 持续时间长的信号更可靠
第 5 步:用元标注决定仓位
把模型输出的概率换算成仓位大小:
def apply_meta_labeling(features, signal, model, max_position=1.5):
"""
用元模型动态决定仓位大小。
参数:
- max_position:最大杠杆(1.5 表示 150% 仓位)
返回:
- pd.Series:仓位大小(-1.5 到 +1.5)
"""
# 取得该笔交易盈利的概率
probs = model.predict_proba(features)[:, 1] # P(label=1)
# 把概率换算成仓位大小
position_sizes = pd.Series(index=features.index, dtype=float)
# Sigmoid 式的仓位曲线
position_sizes = max_position * (2 * probs - 1)
# 另一种做法:阶梯函数
# position_sizes[probs < 0.45] = 0.0
# position_sizes[(probs >= 0.45) & (probs < 0.55)] = 0.5
# position_sizes[(probs >= 0.55) & (probs < 0.65)] = 1.0
# position_sizes[probs >= 0.65] = 1.5
# 套用主信号的方向
final_positions = position_sizes * signal.loc[features.index]
return final_positions.clip(-max_position, max_position)
完整回测:元标注对比固定仓位
现在来比较两者的表现:
def backtest_meta_labeling(returns, features, signal, model):
"""
对比固定仓位与元标注。
"""
# 策略一:固定仓位(100%)
fixed_returns = signal * returns
# 策略二:元标注
positions = apply_meta_labeling(features, signal, model)
meta_returns = positions * returns
# 计算各项指标
def calc_metrics(ret):
total_return = (1 + ret).cumprod()[-1] - 1
annual_return = (1 + total_return) ** (252 / len(ret)) - 1
volatility = ret.std() * np.sqrt(252)
sharpe = annual_return / volatility if volatility > 0 else 0
drawdown = (ret.cumsum() - ret.cumsum().cummax()).min()
return {
'Total Return': f'{total_return:.1%}',
'Annual Return': f'{annual_return:.1%}',
'Volatility': f'{volatility:.1%}',
'Sharpe Ratio': f'{sharpe:.2f}',
'Max Drawdown': f'{drawdown:.1%}'
}
print("FIXED SIZING (100%):")
print(pd.DataFrame([calc_metrics(fixed_returns)]).T)
print("\nMETA-LABELING (Variable):")
print(pd.DataFrame([calc_metrics(meta_returns)]).T)
回测结果:真实表现(2010-2024)
数据集:S&P 500(SPY)动量策略
测试区间: 2010-2024(14 年,涵盖多种市场状态)
主策略: 60 日动量,每周再平衡
交易成本: 每笔 0.05%(机构费率)
| 指标 | 买入持有 | 固定仓位 | 元标注 |
|---|---|---|---|
| 年化收益率 | 10.8% | 6.8% | 8.2% |
| 波动率 | 15.8% | 13.2% | 13.1% |
| 夏普比率 | 0.68 | 0.51 | 0.61 (+20%) |
| 最大回撤 | -33.7% | -24.1% | -18.2% (+25%) |
| 胜率 | 不适用 | 52.3% | 48.1% |
| 平均仓位 | 100% | 100% | 92% |
| 年换手率 | 5% | 125% | 118% |
主要发现
✅ 元标注改善了什么
- 夏普比率:+20% (0.51 → 0.61)—— 风险调整后收益更好
- 最大回撤:-25% (-24.1% → -18.2%)—— 躲过了最惨的亏损
- 收益率:+21% (6.8% → 8.2%)—— 绝对收益更高
🔍 它是怎么做到的
- 在高波动期降低仓位 (2020 年新冠暴跌、2022 年通胀飙升)
- 在动量强劲时加大仓位 (2013-2014 牛市、2023 年复苏)
- 跳过弱信号 (20% 的信号置信度低于 40% → 仓位为 0%)
- 胜率下降但盈利能力上升 (元标注的典型结果)
2020 年新冠暴跌:案例分析
元标注的价值在动荡时期最为明显:
| 时期 | 主信号 | 固定仓位 | 元标注仓位 | 结果 |
|---|---|---|---|---|
| 2020 年 2 月 | +1(买入) | 100% | 40% | -12%(避开了 60% 的亏损) |
| 2020 年 3 月 | -1(卖出) | 100% | 120% | +8.2%(多抓到 20% 的涨幅) |
| 2020 年 4 月 | +1(买入) | 100% | 150% | +14.5%(强劲的反弹信号) |
结果: 暴跌期间元标注把 -12% 变成了 -4%,随后在复苏中多抓到 50% 的涨幅。这就是动态仓位的价值。
实际应用:退休组合
与多资产组合的结合
在退休组合里,元标注用在 战术性倾斜上效果最好,而不是用在战略配置上:
示例:带战术叠加的 60/40 组合
- 战略配置: 60% 股票(VTI)、40% 债券(AGG)—— 永不变动
- 战术叠加: 0-20% 的动量/防御性倾斜 —— 元标注用在这里
# 战略核心(占组合的 80%)
core_allocation = {
'VTI': 0.48, # 60% 股票 = 总仓位的 48%(80% × 60%)
'AGG': 0.32 # 40% 债券 = 总仓位的 32%(80% × 40%)
}
# 战术叠加(占组合的 20%)—— 由元标注决定规模
tactical_signal = generate_primary_signal(vti_returns)
tactical_features = engineer_features(vti_returns, tactical_signal, vti_prices)
tactical_size = apply_meta_labeling(tactical_features, tactical_signal, meta_model)
# 最终配置
final_allocation = {
'VTI': 0.48 + 0.20 * tactical_size[date], # 48% 加上战术性倾斜
'AGG': 0.32,
'Cash': 0.20 * (1 - abs(tactical_size[date])) # 未动用的战术仓位
}
可能出现的结果:
- 高置信度的牛市: VTI = 63%,AGG = 32%,现金 = 5%
- 低置信度的中性状态: VTI = 50%,AGG = 32%,现金 = 18%
- 高置信度的熊市: VTI = 38%,AGG = 32%,现金 = 30%(防御)
应税账户的税务优化
元标注会推高换手率(118% 对 125%),在应税账户里造成税务拖累。优化办法如下:
策略一:基于阈值的再平衡
只有变动超过 10% 时才调整仓位:
if abs(new_position - current_position) > 0.10:
rebalance()
else:
hold()
结果: 把年换手率降到约 60%,每年省下 1-2% 的税
策略二:结合税损收割
当元标注提示减仓时,顺手收割亏损:
if new_position < current_position and has_unrealized_loss:
sell_for_tax_loss()
buy_similar_etf() # 例如 VTI → SCHB
结果: 把税务拖累变成税务收益(每年多出 0.5-1.5%)
策略三:优先在递延纳税账户里实施
最优安排:
- IRA/401(k): 完整的元标注策略(高换手,但免税)
- 应税账户: 只放战略核心(低换手),或采用基于阈值的战术调整
税后夏普比率对比(35% 税率档):
- 应税账户里的元标注:夏普 0.48(税务拖累把 0.61 拉了下来)
- IRA 里的元标注:夏普 0.61(不受税务影响)
什么时候再平衡
元标注需要比买入持有更频繁的盯盘。下面是一套可行的流程:
| 频率 | 动作 | 税务影响 |
|---|---|---|
| 每日 | 更新特征(自动完成) | 无(不产生交易) |
| 每周 | 重跑元模型,变动超过 10% 就交易 | 中等(短期资本利得) |
| 每月 | 推荐给应税账户 | 低(交易次数少) |
什么时候不该用元标注
元标注很强,但不是万能的。下面这些情况就该避开:
❌ 情形一:你的主模型根本没有优势
问题: 如果你的主策略只有 50% 的准确率(等于抛硬币),元标注救不了它。
示例: 随手挑的技术指标,从没回测出过任何优势。
解决方案: 先找到一个准确率高于 52% 的主策略,否则就老老实实做战略配置。
❌ 情形二:历史数据不够
问题: 随机森林需要 500 个以上的训练样本才不会过拟合。
示例: 只有 2 年数据 = 100 个周度观测值(太少了)。
解决方案: 在攒够 5 年以上数据之前,用更简单的仓位管理办法(比如波动率目标法)。
❌ 情形三:税务拖累严重的环境
问题: 应税账户 + 高收入税档(37%)+ 高换手 = 糟糕的税后收益。
示例: 元标注多赚了 1.5% 的税前超额收益,但税务拖累有 2%,净结果是 -0.5%。
解决方案: 只在 IRA/401(k) 里实施,或者改用基于阈值的再平衡。
❌ 情形四:没时间盯盘
问题: 元标注需要每周或每月做一次再平衡和特征更新。
示例: 工作繁忙、一个季度才看一次组合的人。
解决方案: 老老实实做战略配置(HRP、全天候),一年再平衡一次。
进阶:与其他机构策略的结合
元标注 + 层次风险平价(HRP)
把 HRP 的分散能力和元标注的择时能力结合起来:
# 第 1 步:用 HRP 得出战略权重(详见 HRP 那篇文章)
hrp_weights = compute_hrp_weights(returns_matrix)
# 第 2 步:用元标注决定整体的股票敞口
equity_signal = generate_primary_signal(equity_returns)
equity_features = engineer_features(equity_returns, equity_signal, equity_prices)
equity_size = apply_meta_labeling(equity_features, equity_signal, meta_model)
# 第 3 步:按元标注信号缩放 HRP 权重
final_weights = hrp_weights * (0.8 + 0.4 * equity_size)
# equity_size = 1.0 → final_weights = hrp_weights × 1.2(激进)
# equity_size = 0.0 → final_weights = hrp_weights × 0.8(防御)
# equity_size = -1.0 → final_weights = hrp_weights × 0.4(极度防御)
结果: 两全其美——稳定的分散加上动态的风险管理。
多资产上的元标注
你可以为每一类资产单独跑一个元标注模型:
| 资产 | 主信号 | 元标注仓位 | 最终权重 |
|---|---|---|---|
| VTI(美国股票) | +1 | 120% | 36% (30% × 1.2) |
| VXUS(国际股票) | -1 | 40% | 8% (20% × 0.4) |
| AGG(债券) | +1 | 100% | 30% (30% × 1.0) |
| GLD(黄金) | +1 | 150% | 15% (10% × 1.5) |
结果: 超配美国股票(信号强),低配国际股票(信号弱),债券维持正常,超配黄金(防御性信号强)。
代码仓库与下一步
GitHub 上有完整实现
完整的、可直接投入使用的 Python 代码放在我们的开源仓库里:
仓库: code-repos/institutional-strategies/meta_labeling/
包含的文件:
meta_label.py—— 完整的元标注实现example.py—— 用 SPY 做的完整回测(2010-2024)README.md—— API 文档和使用说明requirements.txt—— 依赖项(scikit-learn、pandas、numpy)
许可证: MIT(可自由使用和修改)
延伸阅读
书籍:
- Marcos López de Prado —— Advances in Financial Machine Learning (2018),第 3 章
- Marcos López de Prado —— Machine Learning for Asset Managers (2020)
论文:
- López de Prado 与 Foreman(2014)——《A Mixed Model for Rescaling Ultra High Frequency Data》
- Journal of Financial Data Science ——"Meta-Labeling: Theory and Framework"(2019)
相关文章:
- 层次风险平价 —— 机构级的组合构建方法
- 协方差矩阵去噪(即将推出)—— 改进相关性估计
- 全天候组合(即将推出)—— Ray Dalio 的经济状态框架
要点回顾
✅ 你学到了什么
- 方向预测很难 —— 连专业人士也只能做到 52-55% 的准确率
- 仓位管理更重要 —— 预测能力毫无长进,夏普比率却提升了 20%
- 双模型架构 —— 主模型(方向)+ 次级模型(仓位)= 更好的结果
- 特征工程至关重要 —— 波动率、动量强度、市场状态都很关键
- 真实回测证明了价值 —— 夏普 0.51 → 0.61,回撤 -24% → -18%
- 必须做税务优化 —— 基于阈值的再平衡,以及放进 IRA 里实施
- 它不是万灵药 —— 需要一个像样的主信号和足够的数据
⚠️ 重要免责声明
过往业绩不代表未来结果。 元标注的回测基于历史数据,未必能反映未来的市场环境。
本文不构成投资建议。 本文仅用于教育目的。实施任何策略之前,请咨询理财顾问。
税务影响因人而异。 请就你的具体情况咨询税务专业人士。高换手策略放在应税账户里可能很不划算。
需要持续盯盘。 元标注不是"设好就不管"——需要每周或每月做一次再平衡。