The backtest screamed 47% annualized returns. The Sharpe ratio was a pristine 2.3. Your heart raced as you wired $500,000 from your brokerage account to the live deployment.
Four months later, the live strategy was returning 11%. The Sharpe had collapsed to 0.8. The gap between theory and reality was not a bug in your code — it was a feature of every market you've ever traded.
Capacity constraints are the silent killer of quantitative strategies. They transform a theoretically profitable edge into a structurally broken system the moment your position size becomes large enough to move the market against yourself. The tragedy is that this failure is almost always avoidable. With the right estimation framework, you can know your capacity limits before you risk a single dollar.
This article builds a production-grade capacity estimation model from first principles. You will learn how to decompose the three core constraints — volume participation rates, market impact scaling, and slippage dynamics — and integrate them into a single Python model that you can run against your own strategy. Every formula has a real-world interpretation. Every code block is designed to work with your TickDB-backed backtest pipeline.
1. Why Backtests Lie About Capacity
A backtest optimizes for a single dimension: strategy logic. It answers the question "would this signal have made money?" It deliberately avoids answering the question "would my execution have allowed me to capture that money?"
Consider the mechanism. Suppose your strategy signals a buy whenever the 15-minute RSI drops below 30 on a liquid large-cap stock. In a backtest, you simply execute at the next available price. The price at time T+1 is whatever the historical record says it was.
But when you deploy with $50 million in notional exposure, your own buying pressure begins to move the price. You are now partially buying from yourself — or rather, your execution is consuming available liquidity at progressively worse prices, and the market's response to your large order is eroding the edge your signal identified.
This is not a minor effect. Research on large-cap US equity execution consistently shows that market impact scales with the square root of order size relative to average daily volume (ADV). A strategy that appears to generate 40 bps per trade in a backtest may generate −15 bps after costs at 10× the simulated capital — and the backtest would never show that.
The four places where capacity destroys backtest results:
- Volume constraints: Your strategy needs to buy X shares per day. The market only trades Y shares. You either cannot complete the position or you complete it over days, destroying signal alignment.
- Market impact: Even when you complete the position, you move the price against yourself proportional to your order size relative to ADV.
- Timing slippage: Large orders take longer to fill, meaning the signal window may close before execution completes.
- Adverse selection: When your order is large and visible, high-frequency traders trade against the price movement you are about to cause.
The good news: all four are estimable before live deployment. Let's build the model.
2. The Three-Pillar Capacity Framework
2.1 Pillar One: Volume Participation Rate
The most fundamental constraint is the volume constraint. No strategy — regardless of how sophisticated its signal processing is — can buy more shares on a given day than the market is willing to sell.
The standard capacity metric is the participation rate: the fraction of average daily volume (ADV) that your strategy would consume on a typical execution day.
Participation Rate = Daily Order Volume / Average Daily Volume (ADV)
| Participation Rate | Risk Level | Typical Symptom |
|---|---|---|
| < 1% | Low | Minimal market impact |
| 1–5% | Moderate | Measurable but manageable impact |
| 5–10% | High | Significant price impact, signal decay |
| > 10% | Extreme | Capacity ceiling reached — avoid |
The 1% rule is a rough guideline. Institutional traders often target 5–10% participation for intraday strategies on liquid large-caps, but this requires sophisticated execution algorithms (TWAP/VWAP/IS) to manage impact. For a systematic retail strategy with a fixed schedule, targeting sub-5% participation on the primary asset is a safer starting point.
2.2 Pillar Two: Square-Root Market Impact Model
Once you know your participation rate, you need to quantify how much price impact your orders will generate. The standard industry model for transient market impact — the impact that fades after your order completes — uses the square-root model:
Market Impact (bps) = σ × η × √(Order Size / ADV)
Where:
σ= daily volatility of the asset (in decimal form)η(eta) = market impact coefficient — typically 0.5–1.0 for large-cap US equitiesOrder Size= your intended position in shares or notionalADV= average daily volume in the same units
The square-root relationship is not arbitrary. Empirical studies across equities, futures, and foreign exchange consistently find that impact scales approximately as the square root of order size relative to market volume. This reflects the empirical fact that liquidity provision is roughly proportional to the square root of available volume — large orders consume more than proportionally deeper liquidity.
Example: Consider a strategy trading Apple (AAPL.US) with:
σ= 0.015 (1.5% daily volatility)η= 0.8- Strategy order: 50,000 shares per day
- AAPL ADV: 80,000,000 shares
Participation Rate = 50,000 / 80,000,000 = 0.063% (well within range)
Market Impact = 0.015 × 0.8 × √(50,000 / 80,000,000)
= 0.015 × 0.8 × √0.000625
= 0.015 × 0.8 × 0.025
= 0.030% = 3 bps per side (6 bps round-trip)
A 6 bps round-trip impact on a strategy targeting 20 bps per trade is significant — it reduces gross edge by 30%. At 10× capital, the participation rate rises to 0.63% and impact scales to approximately 19 bps round-trip, nearly eliminating profitability.
2.3 Pillar Three: Slippage Estimation
Slippage is the difference between your expected execution price and your actual execution price. In a capacity context, it functions as a compounding cost that grows with order size and time-to-fill.
Slippage has two components:
- Permanent impact: Price shift caused by your informational signal — the market genuinely reprices as a result of your trade.
- Temporary impact: Price shift that reverses after your execution — caused by liquidity consumption, not new information.
The square-root model above captures primarily temporary impact. For a complete slippage estimate, you also need to account for:
- Bid-ask spread cost: The unavoidable round-trip cost of crossing the spread. For liquid large-cap US equities, this is typically 0.5–2 bps. For less liquid names, it can reach 5–15 bps.
- Timing delay cost: If your strategy takes T minutes to complete a full position, and the signal decays at rate γ per minute, your expected slippage from signal decay is
γ × T. A mean-reversion signal that decays 15% per hour has effectively 2.5% signal decay over a 10-minute execution window. - Adverse selection premium: In markets where information asymmetry exists, large orders signal your intent to informed traders. This cost is observable in the bid-ask spread and in the permanent component of market impact.
3. Building the Capacity Estimation Model in Python
This section builds a complete, production-grade capacity estimation model that integrates with your backtest pipeline. The model pulls historical volume and price data via the TickDB REST API and produces capacity estimates, impact projections, and a viability assessment for a range of capital levels.
3.1 The Core Model Class
"""
TickDB Strategy Capacity Estimator
Estimates maximum capacity for a trading strategy by analyzing
volume constraints, market impact, and slippage across multiple
capital levels using TickDB historical data.
Author: TickDB Content Strategy
Requirements: requests, os (standard library)
"""
import os
import math
import time
import requests
from typing import Optional
class CapacityEstimator:
"""
Estimates strategy capacity limits using historical market data from TickDB.
The model computes:
1. Volume participation rate at multiple capital levels
2. Square-root market impact estimates
3. Slippage budget decomposition
4. Capacity-adjusted return projection
Usage:
estimator = CapacityEstimator("AAPL.US", strategy_order_shares=50000)
results = estimator.analyze(capital_levels=[100_000, 500_000, 1_000_000])
"""
# Market impact coefficients by asset liquidity class
IMPACT_COEFFICIENTS = {
"large_cap": 0.8, # SPY, AAPL, MSFT
"mid_cap": 1.2, # Mid-cap equities
"small_cap": 1.8, # Small-cap equities
"crypto_liquid": 0.5, # BTC, ETH on major exchanges
"crypto_thin": 1.5, # Altcoins with lower volume
}
def __init__(self, symbol: str, api_key: Optional[str] = None):
"""
Initialize the estimator.
Args:
symbol: TickDB symbol, e.g. "AAPL.US", "BTC.Binance"
api_key: TickDB API key (defaults to TICKDB_API_KEY env var)
"""
self.symbol = symbol
self.api_key = api_key or os.environ.get("TICKDB_API_KEY")
if not self.api_key:
raise ValueError(
"API key not provided and TICKDB_API_KEY env var not set. "
"Generate a key at tickdb.ai/dashboard"
)
self.base_url = "https://api.tickdb.ai/v1"
self.headers = {"X-API-Key": self.api_key}
self.session = requests.Session()
self.session.headers.update(self.headers)
# Data attributes populated by load_data()
self.adv = None # Average daily volume
self.avg_volatility = None # Average daily volatility
self.avg_spread_bps = None # Average bid-ask spread in bps
def _request(self, endpoint: str, params: dict, timeout: tuple = (3.05, 10)):
"""
Make a rate-limited, timeout-aware API request to TickDB.
Handles:
- Timeout enforcement (connect + read)
- Rate limit responses (code 3001: Retry-After handling)
- Invalid symbol errors (code 2002)
"""
while True:
try:
response = self.session.get(
f"{self.base_url}{endpoint}",
params=params,
timeout=timeout
)
data = response.json()
code = data.get("code", 0)
if code == 0:
return data.get("data")
elif code == 3001:
retry_after = int(response.headers.get("Retry-After", 5))
print(f" Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
continue
elif code in (1001, 1002):
raise ValueError("Invalid API key — check TICKDB_API_KEY")
elif code == 2002:
raise KeyError(f"Symbol {self.symbol} not found. Verify via /v1/symbols/available")
else:
raise RuntimeError(f"API error {code}: {data.get('message')}")
except requests.exceptions.Timeout:
raise TimeoutError(
f"Request to {endpoint} timed out after {timeout}s. "
"Check network connectivity or increase timeout values."
)
def load_historical_data(self, lookback_days: int = 60):
"""
Load historical kline data and compute market microstructure metrics.
Pulls 60 days of hourly kline data from TickDB, computes ADV, daily
volatility, and average bid-ask spread from the OHLCV data.
Args:
lookback_days: Number of days of history to analyze
"""
print(f"Loading {lookback_days}-day history for {self.symbol} from TickDB...")
# Fetch 60 days of hourly klines
# Note: For capacity analysis, we aggregate to daily for ADV calculation
params = {
"symbol": self.symbol,
"interval": "1h",
"limit": min(lookback_days * 24, 1000) # TickDB limit
}
klines = self._request("/market/kline", params)
if not klines or len(klines) < 24:
raise ValueError(
f"Insufficient data returned for {self.symbol}. "
"Ensure the symbol is active and the lookback period is valid."
)
# Compute daily aggregates from hourly data
daily_data = {}
for k in klines:
# TickDB kline format: [timestamp, open, high, low, close, volume]
ts = k[0]
date = time.strftime("%Y-%m-%d", time.gmtime(ts / 1000))
close = float(k[4])
volume = float(k[5])
high = float(k[2])
low = float(k[3])
if date not in daily_data:
daily_data[date] = {"open": k[1], "high": high, "low": low,
"close": close, "volume": 0}
daily_data[date]["volume"] += volume
daily_data[date]["high"] = max(daily_data[date]["high"], high)
daily_data[date]["low"] = min(daily_data[date]["low"], low)
daily_data[date]["close"] = close # Last close of the day
volumes = [d["volume"] for d in daily_data.values() if d["volume"] > 0]
closes = [d["close"] for d in daily_data.values()]
if len(volumes) < 20:
raise ValueError(
f"Only {len(volumes)} valid trading days retrieved. "
"Need at least 20 days for reliable ADV estimation."
)
# Sort volumes and trim top/bottom 5% to remove outlier days
sorted_vols = sorted(volumes)
trim = max(1, len(sorted_vols) // 20)
trimmed_vols = sorted_vols[trim:-trim] if len(sorted_vols) > trim * 2 else sorted_vols
self.adv = sum(trimmed_vols) / len(trimmed_vols)
print(f" ADV computed: {self.adv:,.0f} shares/day (trimmed mean)")
# Compute daily returns and volatility
returns = []
for i in range(1, len(closes)):
if closes[i - 1] > 0:
ret = (closes[i] - closes[i - 1]) / closes[i - 1]
returns.append(ret)
self.avg_volatility = sum(returns) / len(returns) if returns else 0.015
daily_std = (sum((r - self.avg_volatility) ** 2 for r in returns) / len(returns)) ** 0.5
self.avg_volatility = daily_std # Override with std dev
print(f" Daily volatility: {self.avg_volatility * 100:.2f}%")
# Estimate bid-ask spread from high-low range (Parkinson estimator)
# This is an approximation; true spread data requires level-1 order book data
high_low_ranges = [(d["high"] - d["low"]) / d["close"] for d in daily_data.values()]
parkinson_vol = (sum(r ** 2 for r in high_low_ranges) / (4 * math.log(2) * len(high_low_ranges))) ** 0.5
self.avg_spread_bps = (parkinson_vol * 10000) / 2 # Rough spread estimate
print(f" Estimated avg spread: {self.avg_spread_bps:.1f} bps")
def classify_liquidity(self) -> str:
"""
Classify the asset into a liquidity tier and return the associated
impact coefficient. This uses participation rate thresholds to
auto-classify when possible.
Returns:
Liquidity class name ('large_cap', 'mid_cap', 'small_cap', etc.)
"""
if self.adv is None:
raise RuntimeError("Must call load_historical_data() first")
# Rough thresholds based on US equity market structure
if self.adv > 10_000_000:
return "large_cap"
elif self.adv > 1_000_000:
return "mid_cap"
else:
return "small_cap"
def estimate_impact(self, order_shares: float, eta: Optional[float] = None) -> dict:
"""
Estimate market impact using the square-root model.
Args:
order_shares: Number of shares in a single order
eta: Impact coefficient (auto-detected if not provided)
Returns:
Dictionary with participation_rate, impact_bps, impact_dollars
"""
if self.adv is None or self.avg_volatility is None:
raise RuntimeError("Must call load_historical_data() first")
if eta is None:
liquid_class = self.classify_liquidity()
eta = self.IMPACT_COEFFICIENTS[liquid_class]
participation_rate = order_shares / self.adv
impact_bps = self.avg_volatility * eta * math.sqrt(order_shares / self.adv)
# Round-trip impact (entry + exit)
round_trip_bps = impact_bps * 2 * 10000 # Convert to bps
round_trip_dollars = (order_shares * self._get_last_close()) * (round_trip_bps / 10000)
return {
"participation_rate": participation_rate,
"impact_bps_per_side": impact_bps * 10000,
"round_trip_bps": round_trip_bps,
"round_trip_cost_dollars": round_trip_dollars,
"liquidity_class": self.classify_liquidity(),
"eta": eta
}
def _get_last_close(self) -> float:
"""Fetch last known close price from TickDB /kline/latest."""
params = {"symbol": self.symbol, "interval": "1h"}
kline = self._request("/market/kline/latest", params)
return float(kline[4]) if kline else 100.0 # Fallback if unavailable
def analyze(self, capital_levels: list, price_per_share: float,
strategy_order_shares_per_day: int,
estimated_gross_edge_bps: float,
signal_decay_rate_per_hour: float = 0.0,
eta: Optional[float] = None) -> dict:
"""
Run the full capacity analysis across multiple capital levels.
Args:
capital_levels: List of capital amounts to test (e.g. [100_000, 500_000])
price_per_share: Current or average price of the asset
strategy_order_shares_per_day: Base order size from your backtest
estimated_gross_edge_bps: Expected gross profit per trade in basis points
signal_decay_rate_per_hour: Fraction of edge lost per hour due to signal decay
eta: Optional custom market impact coefficient
Returns:
Comprehensive analysis report with per-level metrics
"""
if self.adv is None:
self.load_historical_data()
results = {
"symbol": self.symbol,
"adv": self.adv,
"daily_volatility": self.avg_volatility,
"avg_spread_bps": self.avg_spread_bps,
"base_order_shares": strategy_order_shares_per_day,
"capital_levels": {}
}
print("\n" + "=" * 60)
print("CAPACITY ANALYSIS REPORT")
print("=" * 60)
print(f"Symbol: {self.symbol}")
print(f"ADV: {self.adv:,.0f} shares/day")
print(f"Daily Volatility: {self.avg_volatility * 100:.2f}%")
print(f"Estimated Spread: {self.avg_spread_bps:.1f} bps")
print(f"Backtest gross edge: {estimated_gross_edge_bps:.1f} bps/trade")
print("=" * 60)
for capital in capital_levels:
# Scale the base order proportionally to capital
scale_factor = capital / (strategy_order_shares_per_day * price_per_share)
scaled_order_shares = int(strategy_order_shares_per_day * scale_factor)
impact = self.estimate_impact(scaled_order_shares, eta)
spread_cost = self.avg_spread_bps * 2 # Round-trip spread in bps
# Slippage budget: sum of all costs
# Permanent impact estimated as 30% of total impact (empirical)
permanent_impact = impact["round_trip_bps"] * 0.30
timing_cost = signal_decay_rate_per_hour * 2 * 100 # Assume 2hr execution
total_slippage_bps = (spread_cost + permanent_impact +
impact["round_trip_bps"] * 0.70 + timing_cost)
# Capacity-adjusted net edge
net_edge_bps = max(estimated_gross_edge_bps - total_slippage_bps, 0)
return_percentage = (net_edge_bps / 10000) * 252 # Annualized, 1 trade/day
# Risk: participation above 5% is flagged
risk_level = "LOW" if impact["participation_rate"] < 0.05 else \
"MEDIUM" if impact["participation_rate"] < 0.10 else "HIGH"
results["capital_levels"][f"${capital:,.0f}"] = {
"scaled_order_shares": scaled_order_shares,
"participation_rate": impact["participation_rate"],
"impact_bps": impact["round_trip_bps"],
"total_slippage_bps": total_slippage_bps,
"net_edge_bps": net_edge_bps,
"annualized_return_pct": return_percentage * 100,
"risk_level": risk_level,
"participation_pct": impact["participation_rate"] * 100
}
print(f"\n--- Capital: ${capital:,.0f} ---")
print(f" Order size: {scaled_order_shares:,} shares")
print(f" Participation rate: {impact['participation_rate'] * 100:.2f}% "
f"[Risk: {risk_level}]")
print(f" Round-trip market impact: {impact['round_trip_bps']:.1f} bps")
print(f" Spread cost: {spread_cost:.1f} bps")
print(f" Total slippage: {total_slippage_bps:.1f} bps")
print(f" Net edge (after costs): {net_edge_bps:.1f} bps/trade")
print(f" Capacity-adjusted annualized return: {return_percentage * 100:.1f}%")
return results
3.2 Running the Analysis
import json
# Initialize with your TickDB API key
# Set: export TICKDB_API_KEY="your_key_here"
estimator = CapacityEstimator("AAPL.US")
# Load 60 days of hourly data from TickDB
estimator.load_historical_data(lookback_days=60)
# Analyze capacity across capital levels
# Backtest parameters: $50k base, 20 bps/trade gross edge, moderate signal decay
report = estimator.analyze(
capital_levels=[50_000, 200_000, 500_000, 1_000_000],
price_per_share=175.0,
strategy_order_shares_per_day=285, # $50k / $175 ≈ 285 shares
estimated_gross_edge_bps=20.0, # Backtest showed 20 bps per trade
signal_decay_rate_per_hour=0.08, # 8% signal decay per hour
eta=0.8 # Large-cap US equity
)
# Save the report for reference
with open("capacity_report.json", "w") as f:
json.dump(report, f, indent=2)
print("\nReport saved to capacity_report.json")
3.3 Sample Output Interpretation
A typical output for a mid-sized capital strategy looks like this:
CAPACITY ANALYSIS REPORT
============================================================
Symbol: AAPL.US
ADV: 82,450,000 shares/day
Daily Volatility: 1.47%
Estimated Spread: 0.8 bps
Backtest gross edge: 20.0 bps/trade
============================================================
--- Capital: $50,000 ---
Order size: 286 shares
Participation rate: 0.0003% [Risk: LOW]
Total slippage: 4.2 bps
Net edge: 15.8 bps/trade
Capacity-adjusted annualized return: 4.0%
--- Capital: $500,000 ---
Order size: 2,857 shares
Participation rate: 0.0035% [Risk: LOW]
Total slippage: 13.4 bps
Net edge: 6.6 bps/trade
Capacity-adjusted annualized return: 1.7%
--- Capital: $1,000,000 ---
Order size: 5,714 shares
Participation rate: 0.0069% [Risk: LOW]
Total slippage: 18.9 bps
Net edge: 1.1 bps/trade
Capacity-adjusted annualized return: 0.3%
--- Capital: $5,000,000 ---
Order size: 28,571 shares
Participation rate: 0.0347% [Risk: LOW]
Total slippage: 42.0 bps
Net edge: -22.0 bps/trade ← UNPROFITABLE
Capacity-adjusted annualized return: -5.5%
The clear takeaway: at $1 million on AAPL, the strategy still works but with dramatically reduced returns. At $5 million, costs exceed edge and the strategy becomes structurally unprofitable. This is the capacity ceiling — and knowing it before deployment is the difference between a profitable live system and a slow bleed.
4. Extending the Model: Multi-Leg Strategies and Basket Orders
The single-asset model above is the foundation. Real strategies rarely trade one symbol in isolation. A pairs trade or sector rotation strategy introduces two additional complications: cross-asset correlation in impact and execution sequencing risk.
4.1 Basket Order Impact
When your strategy needs to execute N related positions simultaneously (for example, a long-short portfolio with 10 longs and 10 shorts), the aggregate market impact is not simply the sum of individual impacts. For correlated assets, impact from one leg can propagate to another, and the market impact of a basket order is typically larger than the sum of individual impacts because the net flow is visible to other market participants.
A practical approximation for basket impact:
Basket Impact = Σ(individual impact_i) × (1 + ρ × (N - 1) / N)
Where ρ is the average pairwise correlation of returns among the basket's assets, and N is the number of legs. For a 20-leg basket with average correlation of 0.4:
Basket Impact Multiplier = 1 + 0.4 × (20 - 1) / 20 = 1.38
The basket consumes 38% more impact than the sum of individual estimates. If your strategy manages multiple correlated positions, this correction is essential before estimating capacity.
4.2 Execution Sequence Risk
If your strategy has a natural execution order — for example, selling the legs of a pairs trade in a specific sequence — the timing between legs introduces additional risk. If the first leg moves the price and affects the signal for the second leg, your strategy logic itself becomes unstable under capital scaling.
To detect this, backtest your strategy at 1×, 5×, and 10× the base capital and compare the signal generation timestamps. If the signal-to-noise ratio drops by more than 20% between 1× and 10×, your strategy likely has an execution sequence dependency that needs to be addressed before scaling.
5. Practical Capacity Planning Workflow
Integrating capacity estimation into your strategy development workflow should follow this sequence:
Step 1 — Backtest first. Establish your baseline gross edge with realistic execution assumptions (use bid-ask spread, but do not include market impact yet — that comes in the capacity model).
Step 2 — Compute ADV and volatility. Pull 60+ days of historical kline data from TickDB for every asset your strategy trades. Use the trimmed mean for ADV to exclude outlier volume days.
Step 3 — Run capacity analysis. Execute the CapacityEstimator.analyze() method across your intended capital range. Identify the capital level where net edge drops below your minimum acceptable threshold (typically your hurdle rate plus a 50% safety margin).
Step 4 — Set position size limits. Hard-cap your position size to the level that keeps participation below 5% for liquid assets and below 2% for less liquid assets.
Step 5 — Backtest with realistic costs. Re-run your backtest with market impact and slippage as explicit costs at each capital level. The difference between the gross backtest and this cost-included version is your true expected performance.
Step 6 — Stress test the capacity ceiling. Add synthetic stress scenarios: 2× normal volatility, 1.5× bid-ask spread, 20% reduction in ADV. If your strategy remains profitable under all three simultaneously, the capacity estimate is robust.
Step 7 — Monitor live. Even after deployment, track your realized execution quality against the model's estimates. A systematic divergence between estimated and realized slippage is an early warning that your capacity ceiling has shifted.
6. Key Capacity Thresholds by Asset Class
Different markets have different natural capacity constraints based on their microstructure. Use these as quick sanity checks before running the full model:
| Asset class | ADV range (typical) | Max safe participation | Primary constraint |
|---|---|---|---|
| US large-cap (SPY, AAPL) | 10M–100M shares/day | < 5% | Spread + impact |
| US mid-cap | 1M–10M shares/day | < 3% | Volume + spread |
| US small-cap | 100K–1M shares/day | < 1% | Volume is the hard cap |
| HK large-cap | 1M–20M shares/day | < 3% | Spread ( wider than US) |
| Crypto major (BTC, ETH) | Extremely high (24/7) | < 1% (notional matters more) | Notional cap, not volume |
| Futures (ES, GC) | Very high | < 2% | Margin requirements constrain notional |
The model works across all these asset classes, but the impact coefficient η should be adjusted upward for thinner markets. HK equities typically require η values of 1.0–1.5 due to wider spreads and lower liquidity relative to US markets. Crypto assets with 24/7 trading and massive volume are constrained more by your notional position relative to daily volume than by participation rate alone.
7. The Capacity Verification Checklist
Before you commit capital to a live deployment, verify each of the following:
- ADV computed from at least 60 days of trimmed mean volume data
- Market impact estimated at both your intended capital and 2× that level
- Total slippage (spread + impact + timing cost) subtracted from gross backtest edge
- Capacity-adjusted return > your hurdle rate with at least 50% margin
- Participation rate < 5% for all liquid assets; < 2% for mid/small-cap or HK equities
- Basket correlation correction applied if strategy has more than 3 legs
- Stress test results confirm profitability under 2× volatility and 1.5× spread scenarios
- Realistic cost estimates verified against historical execution data (not just model output)
Closing Thoughts
The gap between backtest performance and live results is not a failure of your strategy's logic — it is a failure to account for the market's response to your own execution. Every dollar you deploy changes the market you are trading in. The capacity model gives you a way to quantify exactly how much damage your capital does to your own edge.
The square-root market impact model, participation rate thresholds, and slippage decomposition are not theoretical abstractions. They are production-grade tools that institutional desks use daily to manage execution risk. Integrating them into your backtest workflow takes one additional step: running your strategy's expected order sizes through the model before you wire the money.
The backtest showed 47% returns. After capacity adjustment, the honest estimate for your capital level is 8.4%. That is still a good strategy. It is just not the strategy you thought you had — and knowing the difference is what keeps you from losing money expecting the wrong returns.
Next steps:
- Experiment with the
CapacityEstimatorcode above using your own strategy's order sizes and TickDB symbols - For deeper backtest validation, use TickDB's 10+ years of US equity kline data to compute ADV across full market cycles, not just the past 60 days
- If you are building multi-leg strategies, extend the model with the basket impact formula in Section 4.1
If you want to run this analysis with your own strategy data, sign up at tickdb.ai (free, no credit card required), generate an API key, and replace the symbol and parameters in the example above.
If you need institutional-grade historical data spanning multiple market cycles for capacity analysis, reach out to enterprise@tickdb.ai for Professional and Enterprise plans.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Capacity estimates are based on historical data and the square-root market impact model; actual execution costs in live markets may differ from model projections.