跳到正文

换手率分析:你的因子策略的隐藏成本

高换手率可能摧毁一个高 IC 因子。换手率分析测量你的组合在再平衡期间变化多少——以及扣除成本后你的净 Sharpe 是否仍然为正。

第 4 篇 · 共 10 篇 约 2 分钟

你有一个 IC = 0.05 的因子。很优秀。但你的组合日换手率 100%——你实际上每天都在替换整个组合。扣除每笔交易 10 bps 的交易成本后,你的总 Sharpe 1.2 变成净 Sharpe 0.3。突然这个因子就没那么好了。

换手率分析是测量你的组合在再平衡期间变化多少,以及你的因子在扣除成本后是否仍有收益的做法。

定义换手率

组合换手率是两个再平衡日期之间变化的组合权重的分数:

τ=12∑i∣wi(t)−wi(t−1)∣\tau = \frac{1}{2} \sum_i |w_i^{(t)} - w_i^{(t-1)}|

½ 因子确保换手率在 0(无变化)和 1(完全替换)之间。

对于多空组合:

τls=12∑i∣wilong−wishort∣\tau_{ls} = \frac{1}{2} \sum_i |w_i^{long} - w_i^{short}|

在 Python 中计算换手率

import pandas as pd
import numpy as np

def compute_turnover(weights_df):
    """
    计算连续再平衡期间之间的组合换手率。

    Parameters
    ----------
    weights_df : DataFrame
        组合权重 (date x asset)

    Returns
    -------
    turnover : Series
        每个再平衡日期的换手率
    """
    turnover = (weights_df.diff().abs().sum(axis=1) / 2)
    return turnover


def compute_factor_turnover(factor_values, n_quantiles=5):
    """
    计算基于因子排名的换手率。
    """
    # 每日因子排名
    ranks = factor_values.rank(axis=1, pct=True)

    # 五分位边界
    quantile_bounds = np.linspace(0, 1, n_quantiles + 1)

    # 转换为五分位权重(等权在每个五分位内)
    def to_quantile_weights(ranks):
        weights = pd.DataFrame(0, index=ranks.index, columns=ranks.columns)
        for i in range(n_quantiles):
            low = quantile_bounds[i]
            high = quantile_bounds[i + 1]
            mask = (ranks >= low) & (ranks < high)
            weights[mask] = 1.0 / mask.sum(axis=1)
        # 最后一期包含 high=1
        mask = ranks == 1.0
        weights[mask] = 1.0 / mask.sum(axis=1)
        return weights

    quantile_weights = to_quantile_weights(ranks)
    turnover = compute_turnover(quantile_weights)

    return turnover


def compute_turnover_decomposition(weights_df, factor_values):
    """
    将换手率分解为:
    - 信号换手率:由于因子信号变化
    - 噪声换手率:由于随机权重波动
    """
    signal_turnover = compute_turnover(weights_df)

    # 纯信号:从纯因子排名合成的权重(无优化噪声)
    rank_weights = factor_values.apply(lambda x: x / x.abs().sum(), axis=1)
    pure_signal_turnover = compute_turnover(rank_weights)

    noise_turnover = signal_turnover - pure_signal_turnover

    return {
        'total_turnover': signal_turnover,
        'signal_turnover': pure_signal_turnover,
        'noise_turnover': noise_turnover
    }

换手率和净 Sharpe

关键关系:

Net Sharpe≈2⋅Gross Sharpe⋅TC⋅IC−TC\text{Net Sharpe} \approx \sqrt{2 \cdot \text{Gross Sharpe} \cdot TC \cdot \text{IC}} - TC

其中 TC 是交易成本(作为分数)。这个近似表明 交易成本对高换手率策略有不成比例的影响。

对于 IC = 0.05 且日再平衡的因子:

  • 总 Sharpe ≈ 0.8
  • 日换手率 ≈ 80% (0.8)
  • TC = 0.001(往返 10 bps)
  • 净 Sharpe ≈ √(2 × 0.8 × 0.001 × 0.05) − 0.001 ≈ 0.0003

该因子在日再平衡 + 10 bps 成本下基本上无法交易。

净 Sharpe 计算器

def estimate_net_sharpe(gross_sharpe, daily_turnover, tc_bps, n_days=252):
    """
    估计扣除交易成本后的净 Sharpe。

    Parameters
    ----------
    gross_sharpe : float
        再平衡前的总 Sharpe
    daily_turnover : float
        日换手率(0 到 1 之间)
    tc_bps : float
        每次交易的交易成本(以 bps 为单位)
    n_days : int
        年化天数
    """
    tc = tc_bps / 10000  # 转换为分数

    # 年换手率
    annual_turnover = daily_turnover * n_days

    # 交易成本对收益的年化影响
    # 每笔交易成本 * 换手次数
    tc_cost = annual_turnover * tc

    # 假设收益与换手率成比例减少
    gross_return = gross_sharpe * np.sqrt(n_days)  # 粗略近似
    net_return = gross_return - tc_cost

    # 假设波动率不变
    volatility = 1.0  # 归一化
    net_sharpe = net_return / volatility

    return {
        'gross_sharpe': gross_sharpe,
        'net_sharpe': net_sharpe,
        'annual_turnover': annual_turnover,
        'annual_tc_cost': tc_cost,
        'tc_erosion_pct': tc_cost / gross_return * 100
    }

降低换手率而不破坏 alpha 的方法

信号平滑。 在排名之前对因子值应用指数移动平均。减少换手率代价是信号延迟。

def smooth_factor(factor_values, halflife=5):
    """
    应用指数平滑减少换手率。
    """
    return factor_values.ewm(halflife=halflife).mean()


def decay_adjusted_ranks(factor_values, decay_half_life=10):
    """
    根据衰减调整因子排名。

    衰减慢的因子给予更高权重,
    这样可以以较低频率再平衡。
    """
    smoothed = smooth_factor(factor_values, halflife=decay_half_life)
    return smoothed.rank(axis=1, pct=True)

阈值再平衡。 只有当前权重与目标权重的偏差超过阈值时才再平衡。

def threshold_rebalance(current_weights, target_weights, threshold=0.02):
    """
    只有当权重差异超过阈值时才再平衡。
    """
    diff = (target_weights - current_weights).abs()
    trades = pd.Series(0, index=current_weights.index)
    trades[diff > threshold] = target_weights[diff > threshold] - current_weights[diff > threshold]
    return trades


def compute_threshold_turnover(target_weights, threshold=0.02):
    """
    计算阈值再平衡策略的换手率。
    """
    n_periods = len(target_weights)
    turnover_list = []

    current = target_weights.iloc[0].fillna(0)

    for t in range(1, n_periods):
        target = target_weights.iloc[t].fillna(0)
        diff = (target - current).abs()
        rebalance_mask = diff > threshold

        turnover_period = diff[rebalance_mask].sum() / 2
        turnover_list.append(turnover_period)

        # 更新当前权重(仅对超过阈值的头寸)
        current = current.copy()
        current[rebalance_mask] = target[rebalance_mask]

    return pd.Series(turnover_list, index=target_weights.index[1:])

关键阈值

日换手率分类可行性
< 5%低高度可行
5-20%中等合理成本下可行
20-50%高需要低 costs 或强 alpha
> 50%极高几乎不可能盈利交易

常见错误

报告总 Sharpe 而不提换手率。 显示 Sharpe 1.5 + 日再平衡 + 100% 换手率的回测,在扣除交易成本后可能是 Sharpe 0.4。始终报告净 Sharpe。

优化权重放大换手率。 均值-方差优化可能产生极端权重,在期间之间翻转符号。使用权重约束限制换手率。

忽略市场冲击。 如果你运行 1 亿美元组合,日再平衡需要交易平均日成交量的 2%,市场冲击将超过你的 alpha。

假设固定交易成本。 实际交易成本随市场条件变化。在高波动日,买卖价差扩大,实际交易成本可能比你假设的高 2-3 倍。

参考文献

  • López de Prado, Advances in Financial Machine Learning, 第 4 章. Wiley, 2018. ISBN: 978-1-119-48208-6.
  • Ang, Asset Management, 第 11 章. Oxford University Press, 2014. ISBN: 978-0-19-995932-3.