When to Use
Use this skill when deciding how much capital a quantitative strategy can absorb before its own trading destroys its edge. As AUM scales, traded notional scales with it, and execution cost per dollar traded grows with the square root of participation. That drag compounds against a fixed gross alpha, so net return per dollar falls and the realized Sharpe ratio decays. The engine walks an AUM grid, prices half-spread and market-impact drag at each level, and reports the largest AUM that still clears both a minimum net Sharpe gate and a maximum ADV participation cap — along with which of the two bound first.
Reach for it before an allocation increase, when sizing a new strategy's target book, or when a research backtest run at $1M is about to be promoted to institutional size.
When NOT to Use
- As an alpha-decay model.
annual_gross_return_pctis assumed invariant to AUM. Real gross alpha decays with size independently of execution cost — signal capacity exhausts, the book dilutes into worse names, and crowding erodes the edge. Every capacity number this engine produces is therefore an upper bound. Treat it as a ceiling to stay under, never a target to reach. - As a per-name liquidity check. The engine compares aggregate portfolio turnover against a single aggregate
avg_daily_volume_usd. A real book spreads turnover across names whose ADVs differ by orders of magnitude, and capacity binds on the least liquid names long before it binds on the aggregate. Pair withliquidity-adjusted-position-sizingandconcentration-risk-single-name-limits. - With an uncalibrated
impact_gamma. The square-root prefactor $Y$ is the single largest lever on the result and the default sits at the optimistic end of the empirical range. An unfitted value produces a confident but meaningless capacity figure. - As an execution scheduler or a risk control. Nothing here slices orders, sets a participation schedule, or halts anything. For scheduling see
execution-algo-twap-vwap-slicingandmulti-day-execution-schedules-for-very-large-orders; for enforcement seekill-switch-and-drawdown-circuit-breakers. - For a strategy that never clears its Sharpe gate at any size. That result comes back as
BELOW_MIN_SHARPE_AT_ALL_SIZESand is a strategy-quality verdict, not a capacity limit — more liquidity would not relieve it.
Prerequisites
- Strategy performance parameters (
StrategyParameters:strategy_id,annual_gross_return_pct,annual_volatility_pct,daily_turnover_pct,avg_daily_volume_usd,avg_daily_volatility_pct,half_spread_bps,max_participation_rate_pct,min_acceptable_sharpe,risk_free_rate_pct). - Unit contract: returns, volatilities, and turnover are fractions (
0.25= 25%);half_spread_bpsis in basis points;max_participation_rate_pctis a percentage (5.0= 5% of ADV).avg_daily_volume_usdmust be in the same currency as AUM. daily_turnover_pctis one-way notional traded per day as a fraction of AUM, paired with a half-spread charged once on that notional. If your turnover figure is two-way, halve it before passing it in or you double every cost in the model.- An
impact_gammafitted to your own realized slippage. Empirical values for stocks and futures fall roughly in $0.5 \dots 1.0$; the0.5default is the optimistic end.
Workflow
- Fix the unit and horizon contract before anything else:
- Non-finite inputs are rejected, not priced. A NaN return propagates to a NaN Sharpe, and because every comparison against NaN is
Falsethe gate silently passes — the engine would otherwise report a confident capacity for an unpriced strategy. - Zero volatility and zero ADV are rejected rather than divided by. Zero volatility is a division by zero, not an infinitely good strategy; zero ADV is an untradeable instrument, not one of unbounded capacity.
- Decide whether
annual_gross_return_pctis a total or an excess return. If total, setrisk_free_rate_pct; leaving it at0.0credits the risk-free rate to the strategy and overstates every Sharpe by $r_f/\sigma$.
- Non-finite inputs are rejected, not priced. A NaN return propagates to a NaN Sharpe, and because every comparison against NaN is
- Daily friction modelling at each AUM level:
- Daily one-way notional $Q = \text{AUM} \times \text{turnover}$; participation $= Q / \text{ADV}$.
- Square-root impact law: $I(Q) = Y \cdot \sigma_{\text{daily}} \cdot \sqrt{Q / V}$ — not Almgren-Chriss, which is a linear-impact optimal-execution model (see
references/standards.md). - Half-spread cost $= Q \times \text{half_spread_bps} / 10^4$, charged once on one-way notional.
- Both are annualised over 252 trading days. Crypto and FX venues do not follow that convention.
- AUM grid construction:
- The grid is $\text{step}, 2 \times \text{step}, \dots \le \text{max_search}$, index-derived rather than accumulated — repeated
+=on a non-representable step drifts and can drop the final point. - A non-positive step and a step wider than the search range are rejected: the first never terminates, the second yields an empty curve whose zero capacity is indistinguishable from a real answer.
- The grid is $\text{step}, 2 \times \text{step}, \dots \le \text{max_search}$, index-derived rather than accumulated — repeated
- Net Sharpe decay: $\text{Sharpe}{\text{net}} = (R{\text{net}} - r_f) / \sigma_{\text{strategy}}$, using gross strategy volatility. Costs are modelled as a deterministic drag, so realized impact variance is ignored and net Sharpe is biased upward.
- Capacity limit and limiting factor:
- Capacity is the largest AUM with an unbroken feasible run beneath it — the search stops at the first breach. Taking the last feasible point anywhere on the grid would jump across a breached region.
- Classify the binding constraint:
ADV_PARTICIPATION_LIMIT,MIN_SHARPE_BREACH,BELOW_MIN_SHARPE_AT_ALL_SIZES, orSEARCH_RANGE_EXHAUSTED. The last means no gate broke inside the searched range — the answer is censored by the loop bound and is not evidence of unlimited capacity. Widenmax_search_aum_usdand re-run before treating it as anything.
- Read the optimum correctly:
optimal_sharpe_capacity_aum_usdmaximises net dollar PnL among feasible points only. Net dollar PnL keeps climbing well past the point where the strategy breaches its own gates, so the unconstrained peak is reported separately asunconstrained_max_pnl_aum_usd— a diagnostic, never an allocation target. - Execution output: structured
StrategyCapacityReport, carryingcapacity_resolution_usd,search_range_exhausted, and theimpact_gammaandrisk_free_rate_pctactually used, so the number is auditable rather than merely reported.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Calling the square-root law "Almgren-Chriss". Almgren and Chriss (2000) solve optimal liquidation under linear temporary and permanent impact; they do not propose a square-root law. The $\sigma\sqrt{Q/V}$ form is a separate empirical regularity from Torre/BARRA (1997) and Grinold and Kahn (1999). Earlier versions of this skill made exactly this mis-attribution. Citing the wrong paper hides that the exponent is an empirical fit — and one that Almgren et al. (2005) and Kyle and Obizhaeva (2016) put nearer $0.6$ than $0.5$.
- Shipping the default
impact_gamma. Impact drag is linear in $Y$, but capacity is not: where the Sharpe gate binds it scales as $Y^{-2}$, so moving from the $0.5$ default to the top of the measured $0.5\dots1.0$ range cuts estimated capacity roughly fourfold — on the reference parameters, from $133M to $33M. Running uncalibrated does not produce a rough estimate; it produces a systematically optimistic one, and the direction of the error is always toward over-allocation. (Where the participation cap binds instead, capacity is independent of $Y$ — check thelimiting_factorbefore deciding how much the calibration matters.) - Reading
SEARCH_RANGE_EXHAUSTEDas unlimited capacity. It means the loop ended, not that the strategy scales. Scaling tomax_capacity_aum_usdin that state is scaling to a function argument you chose arbitrarily. - Allocating to the unconstrained PnL peak. With the reference parameters, net dollar PnL is still rising at $100M against a true capacity of $25M. Bigger is more profitable right up until the participation cap makes the fills unachievable, and the dollar-PnL curve gives no warning at that boundary.
- Treating
max_capacity_aum_usdas exact. It is a grid point. True capacity lies withincapacity_resolution_usdabove it, and0.0means "below one grid step", not "exactly zero". - Dividing total return by volatility and calling it Sharpe. A Sharpe ratio is an excess return per unit of risk (Sharpe 1994). At a 4% risk-free rate against 15% volatility, omitting $r_f$ adds $+0.27$ — enough on its own to carry a strategy over a 1.0 gate it does not clear.
- Believing the 5% ADV cap is a rule. It is a practitioner risk convention with no general regulatory backing. The US ADV-anchored limit that does exist — SEC Rule 10b-18's 25% ADTV volume condition — is a non-exclusive safe harbour for issuer repurchases and does not apply here. See
references/standards.md. - Applying an aggregate ADV to a diversified book. Comparing total portfolio turnover against a single blended ADV hides the illiquid tail, which is where capacity actually binds first.
- Projecting a frictionless backtest Sharpe onto institutional AUM. A strategy that shows 1.67 at $1M is not a 1.67 strategy at $100M, and the gap is not a rounding error.
Verification
- Instantiate
StrategyCapacityEstimatorEngine(impact_gamma=0.5)with a 25% gross / 15% vol strategy, 10% one-way daily turnover, $50M ADV, 1.5% daily volatility, 1 bp half-spread, 5% participation cap. Verify frictionless Sharpe $1.67$ at $r_f = 0$. - At AUM $$25\text{M}$, verify against the hand derivation: participation exactly $5.00%$, spread cost $$63{,}000$, impact cost $$1{,}056{,}542.12$, net PnL $$5{,}130{,}457.88$, net Sharpe $1.3681221015$.
- Verify the participation cap is inclusive: $$25\text{M}$ is feasible and $$26\text{M}$ is not, giving
max_capacity_aum_usd$= $25\text{M}$ andlimiting_factor$=$ADV_PARTICIPATION_LIMIT. - Verify
optimal_sharpe_capacity_aum_usd$\le$max_capacity_aum_usd, whileunconstrained_max_pnl_aum_usdreaches the $$100\text{M}$ search ceiling — the feasible optimum must not follow the dollar-PnL curve past the cap. - Set $r_f = 0.04$ and verify frictionless Sharpe drops to exactly $1.40$, and that every curve point drops by $0.04/0.15$.
- Verify impact obeys the square-root law: quadrupling traded notional exactly doubles impact, and doubling
impact_gammaexactly doubles it. - Verify a search that breaches nothing returns
SEARCH_RANGE_EXHAUSTEDwithsearch_range_exhausted=True, neverUNLIMITED. - Verify NaN/Inf inputs, zero volatility, zero or negative ADV, negative turnover, a non-positive
aum_step_usd, and a step wider than the search range all raise rather than returning a report. - Run
python -m unittest discover -s skills/strategy-capacity-estimation-before-scaling-capital/scripts.