Skip to content

Turnover Analysis: The Transaction Cost Reality Check

A factor that looks great on paper may not survive transaction costs. Turnover analysis quantifies how much your portfolio changes over time and whether the net edge is positive.

10 of 10 ~3 min

You have a factor with IC = 0.04 and a long-short spread of 0.15% per day. Sounds great. But if your portfolio turns over 50% per day, you’re paying 0.15% × 50 = 7.5% per day in transaction costs. Your net edge is gone.

Turnover analysis is how you avoid this trap.

What Is Portfolio Turnover?

Portfolio turnover measures how much your portfolio weights change over time:

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

Where w_i are the portfolio weights for each asset. Turnover of 50% means half your portfolio changed in that period.

In a long-short equity strategy:

  • Daily rebalancing → 50-100% annual turnover (depends on factor IC decay)
  • Weekly rebalancing → 15-30% annual turnover
  • Monthly rebalancing → 5-15% annual turnover

The Net Edge Calculation

For a long-short strategy, the net edge after transaction costs is:

NetEdge=GrossSpread−Turnover×TransactionCostRateNetEdge = GrossSpread - Turnover \times TransactionCostRate

Where TransactionCostRate is the cost of trading 1% of portfolio (typically 1-5 basis points for liquid equities, higher for illiquid stocks).

import numpy as np
import pandas as pd

def compute_portfolio_turnover(weights_df):
    """
    Compute turnover for each rebalancing period.

    Parameters
    ----------
    weights_df : DataFrame (date × asset)
        Portfolio weights over time

    Returns
    -------
    turnover_series : Series
        Turnover at each rebalancing date
    """
    turnover = []
    dates = weights_df.index

    for i in range(1, len(dates)):
        w_prev = weights_df.iloc[i-1].fillna(0)
        w_curr = weights_df.iloc[i].fillna(0)

        t = 0.5 * np.sum(np.abs(w_curr - w_prev))
        turnover.append({'date': dates[i], 'turnover': t})

    return pd.DataFrame(turnover).set_index('date')['turnover']


def compute_net_edge(weights_df, returns_df, spread_bps=10, cost_per_pct_bps=5):
    """
    Compute net edge after transaction costs.

    Parameters
    ----------
    weights_df : DataFrame (date × asset)
        Portfolio weights
    returns_df : DataFrame (date × asset)
        Asset returns
    spread_bps : float
        Gross long-short spread in basis points per day
    cost_per_pct_bps : float
        Transaction cost per 1% turnover in basis points
    """
    turnover = compute_portfolio_turnover(weights_df)

    # Daily gross spread earned
    daily_spread = spread_bps / 10000

    # Daily turnover rate
    daily_turnover_rate = turnover / 100  # Convert % to fraction

    # Transaction cost per day
    daily_cost = daily_turnover_rate * (cost_per_pct_bps / 10000)

    # Net edge
    net_edge = daily_spread - daily_cost

    return {
        'mean_turnover_pct': turnover.mean(),
        'mean_daily_cost_bps': daily_cost.mean() * 10000,
        'net_edge_daily_bps': net_edge.mean() * 10000,
        'net_edge_annual_bps': net_edge.mean() * 252 * 10000
    }

Why Turnover Matters More Than You Think

Compounding effect. A 40% annual turnover strategy pays transaction costs on 40% of the portfolio every year. Over 10 years, you’ve paid 400% of the portfolio in transaction costs. A 10% turnover strategy pays 100% over 10 years.

Bid-ask spread. For illiquid stocks, the bid-ask spread can be 10-50 bps. Trading 20% of the portfolio in illiquid stocks means paying 2-10 bps per rebalance. This can easily exceed the gross edge.

Market impact. Large orders move the market against you. If you’re trading 5% of a stock’s daily volume, your market impact can be 10-30 bps. This is especially problematic for small-cap factors that trade illiquid names.

Turnover and Factor Decay Are Linked

The faster your factor decays (short half-life), the faster your portfolio ranking changes, and the higher your turnover. This is why factor half-life is a critical determinant of viability.

A factor with IC = 0.05 but a 2-day half-life requires daily rebalancing to capture the signal. If your transaction costs exceed the edge, the factor is not tradeable.

A factor with IC = 0.03 but a 30-day half-life only requires monthly rebalancing. The lower transaction costs may make it more viable.

Practical Thresholds

Annual TurnoverAssessment
< 20%Low — most factors can sustain this
20-50%Moderate — requires low transaction costs
50-100%High — requires very liquid markets or large edge
> 100%Very high — usually not viable after costs

For a long-short equity strategy targeting 10-20% annual return, you want annual turnover < 50%. For a market-neutral strategy, you want < 30%.

Common Pitfalls

Only reporting gross spread. A common mistake: reporting the backtest return without accounting for transaction costs. Always report net return after estimated transaction costs.

Underestimating market impact. Transaction cost models that only include broker commissions and exchange fees miss market impact. For factors trading small-cap stocks, market impact can be 5-10x the spread.

Ignoring rebalancing frequency. If your backtest rebalances daily but your strategy rebalances monthly, your turnover estimate is wrong by a factor of 20.

Where It Fits

Turnover analysis is a critical component of backtesting that is often neglected. Factor decay studies determine the optimal rebalancing frequency. Factor combination must account for the turnover of each factor when building multi-factor portfolios.

References

  • López de Prado, Advances in Financial Machine Learning, Ch. 10. Wiley, 2018. ISBN: 978-1-119-48208-6.
  • Ang, Asset Management, Ch. 12. Oxford University Press, 2014. ISBN: 978-0-19-995932-3.
  • Garleanu, Pedersen, “Dynamic Portfolio Choice with Friction,” Journal of Financial Economics, 2016.