Caloogy Article
中文

Transaction Cost Modeling: Commissions, Slippage, and Market Impact

Date
Jun 8, 2025
Series
Quant
Reading
5 min
Topics
QuantQuant devTransaction costSlippage

The one-sentence version

A strategy with a 70% annualized return in backtesting might not make a single cent in live trading. The gap often comes from transaction costs: commissions, bid-ask spreads, and market impact, where your own orders push the price in an unfavorable direction. The first two are roughly a fixed percentage, while the last one increases as your trading volume grows: empirically, impact costs are proportional to the square root of the "trade size relative to market volume." This implies a capacity limit for any strategy: the more capital you deploy, the higher the cost per unit of capital, until at a certain scale, the returns are entirely eaten away.

Disclaimer

This article is for technical educational purposes only and does not constitute investment advice. The code uses simulated data, and parameters are for illustrative purposes only.


Layer 1: The Intuition

1.1 Costs in a Wet Market

Imagine you are buying vegetables at a wet market:

Cost Wet Market Equivalent Trading Equivalent
Commission Parking fee Brokerage and exchange fees
Spread The difference between the vendor's buy and sell price The difference between the Bid and Ask; you pay half of it each trade
Market Impact You try to buy half the cabbage stock at once, and the vendor immediately raises the price Large orders push the price up (buy) or down (sell)

The first two remain roughly constant as a proportion regardless of how much you buy; the third one becomes more expensive the more you buy.

1.2 Why Backtests Often Underestimate Costs


Layer 2: Cost Model

2.1 Components of One-Sided Cost

cost=fee⏟transaction fee+12 spread⏟half-spread+Y⋅σ⋅QV⏟market impact\text{cost} = \underbrace{\text{fee}}_{\text{transaction fee}} + \underbrace{\tfrac{1}{2}\,\text{spread}}_{\text{half-spread}} + \underbrace{Y \cdot \sigma \cdot \sqrt{\frac{Q}{V}}}_{\text{market impact}}

Symbol Meaning
σ\sigma Daily volatility of the asset
QQ Your trading volume (amount or number of shares)
VV Average Daily Volume (ADV)
YY Empirical coefficient, usually between 0.5 and 1, to be calibrated with your own execution data

2.2 The Square Root Law

"Impact ∝ Q/V\sqrt{Q/V}" is one of the most robust empirical laws in market microstructure research, observed repeatedly across different markets such as stocks, futures, options, and cryptocurrencies.

Intuitively: If trading volume quadruples, the unit impact only doubles, but the total impact cost (unit impact × volume) increases eightfold. Therefore:

2.3 Methods to Reduce Costs

Method Principle
Reduce turnover Signal smoothing, setting rebalancing thresholds (don't rebalance if the change is insignificant)
Order slicing Algorithms like TWAP or VWAP split large orders into small ones, spreading them over time
Limit orders Placing orders to wait for execution, earning the spread rather than paying it (though there is execution risk)
Select liquid assets Small spreads and high ADV
Cost-aware portfolio optimization Subtracting expected costs directly from the optimization objective

Layer 3: Code: How Costs Eat a Strategy

3.1 A High-Turnover Strategy

Simulating a daily-frequency strategy that changes direction almost every day. It has some real predictive power (Gross Sharpe ≈ 3.5, which is extremely rare in reality; this is used here to clearly demonstrate the effect of costs):

import numpy as np, pandas as pd
rng = np.random.default_rng(5)
n = 252 * 4
ret = pd.Series(rng.normal(0, 0.015, n))
# A daily signal with true predictive power
signal = pd.Series(0.25 * ret.shift(-1).fillna(0) / 0.015 + rng.normal(0, 1, n))
pos = np.sign(signal).shift(1).fillna(0)          # Yesterday's signal, today's position (+1 / -1)
gross = pos * ret

def cost_per_trade(trade_value, adv, sigma, fee_bps=1, half_spread_bps=2, Y=1.0):
    """One-sided cost (as a % of trade value) = commission + half-spread + square root impact"""
    impact = Y * sigma * np.sqrt(trade_value / adv)
    return fee_bps / 1e4 + half_spread_bps / 1e4 + impact

turnover = pos.diff().abs().fillna(0)               # Daily turnover (change in position)
def net_returns(capital, adv=5e8, sigma=0.015):
    c = cost_per_trade(capital * turnover.clip(lower=1e-12), adv, sigma)
    return gross - turnover * c

def sharpe(x): return x.mean() / x.std() * np.sqrt(252)
print("Average daily turnover:", round(turnover.mean(), 2))
print("Gross Sharpe:", round(sharpe(gross), 2))
for cap in [0, 1e5, 1e6, 1e7, 1e8]:
    print(f"Capital {cap:>12,.0f}: Net Sharpe {sharpe(net_returns(cap)):.2f}  Annualized Net Return {net_returns(cap).mean()*252*100:.1f}%")

About Simulated Signals

The signal in the code uses ret.shift(-1) (tomorrow's return). This is to artificially create a known strength of predictive power to make the impact of costs easier to observe. In a real strategy, you must never do this, as that is look-ahead bias (see "The Seven Sins of Backtesting").

Output:

Avg Daily Turnover: 0.99
Gross Sharpe: 3.49
Capital            0: Net Sharpe 3.17  Annualized Net Return 73.3%
Capital      100,000: Net Sharpe 2.84  Annualized Net Return 65.8%
Capital    1,000,000: Net Sharpe 2.14  Annualized Net Return 49.7%
Capital   10,000,000: Net Sharpe -0.06  Annualized Net Return -1.3%
Capital  100,000,000: Net Sharpe -5.75  Annualized Net Return -162.6%

3.2 Interpretation

Capital What happened
0 (Only commissions and spread) Gross Sharpe 3.49 → 3.17; fixed costs have already eaten a portion
100k Impact begins to appear, but the effect is minor
1M Net Sharpe drops to 2.14
10M Net returns hit zero: This is roughly the capacity of this strategy on this asset
100M Significant losses: Every day, the strategy is pushing the price in an unfavorable direction with its own orders

(The annualized return of −162%-162\% is because simple returns are summed for the annualization here, and it assumes a full position reversal every day; in reality, trading would have stopped long before this.)

Key conclusion: Strategy returns are not a single number, but a function of capital scale. Discussing returns without considering capital scale is meaningless.

3.3 A Simple Way to Estimate Capacity

Set the expected return of a single trade equal to the cost:

Expected alpha=fee+12spread+YσQ/V⇒Qmax⁡=V(α−fee−12spreadYσ)2.\text{Expected alpha} = \text{fee} + \tfrac{1}{2}\text{spread} + Y\sigma\sqrt{Q/V} \quad\Rightarrow\quad Q_{\max} = V\left(\frac{\alpha - \text{fee} - \tfrac{1}{2}\text{spread}}{Y\sigma}\right)^2.

This gives the upper limit for a single trade size; multiplying this by the number of assets and the turnover frequency provides a rough estimate of the total strategy capacity.

3.4 Practical Key points

Key points Description
Calibrate with your own data Record the "decision price" and "actual average execution price" for every live trade; the difference is the real cost (implementation shortfall)
Differentiate by asset The cost differences between large-cap stocks, small-cap stocks, major coins, and altcoins are massive
Consider time periods Spreads and impact increase during market open, market close, and around major news events
Special costs in crypto Funding rates, withdrawal fees, on-chain gas, and losses from being sandwiched on decentralized exchanges (see "MEV and Sandwich Attacks")
Be conservative with cost models It is better to overestimate costs than to underestimate them

Summary

Layer Key points
Components Commission + Half-spread + Market Impact
Square Root Law Impact ∝σQ/V\propto \sigma\sqrt{Q/V}; Total impact grows super-linearly with scale
Capacity Returns are a function of capital scale; the example strategy hits zero returns at ≈ 10M
Practicality Calibrate with live execution data; reduce turnover; use order slicing; be conservative with cost models