A comprehensive implementation of the Almgren-Chriss market impact model calibrated to GLD (gold) and SLV (silver) ETFs. This model simulates execution costs at various notional sizes to identify capacity constraints where strategy alpha decays.
This project answers the critical question: "At what AUM does the precious metals strategy stop being viable?"
The model quantifies:
- Implementation shortfall at various order sizes
- Optimal execution horizons
- Strategy capacity constraints
- Cost-risk trade-offs
The Almgren-Chriss framework models execution cost as:
Total Cost = Spread Cost + Temporary Impact + Permanent Impact
Spread Cost:
C_spread = (spread / 2) × X
Temporary Impact:
h(v) = η × σ × (v / V)^β
where:
- v = execution rate (shares per period)
- V = average volume per period
- σ = volatility
- η = temporary impact coefficient (0.142)
- β = temporary impact exponent (0.6)
Permanent Impact:
g(v) = γ × σ × (v / V)^α
where:
- γ = permanent impact coefficient (0.314)
- α = permanent impact exponent (0.6)
Total Implementation Shortfall:
IS = C_spread + Σ[h(v_k) × x_k] + g(v_total) × X
cd execution_cost_model
pip install -r requirements.txtpython main.pyThis will:
- Fetch historical data for GLD and SLV
- Calculate market metrics (ADV, spread, volatility)
- Run execution cost simulations
- Perform capacity analysis
- Generate visualizations
- Output results to
capacity_results.json
from src.data_ingestion import DataIngestion
from src.impact_model import AlmgrenChrissModel, MarketConditions
from src.capacity_analysis import CapacityAnalyzer
# Fetch data
data = DataIngestion(tickers=["GLD", "SLV"], start_date="2015-01-01")
data.fetch_daily_data()
data.calculate_metrics()
# Set up model
model = AlmgrenChrissModel()
market = MarketConditions(
price=180.0,
volatility=0.15,
adv_shares=5_000_000,
adv_dollars=900_000_000,
spread=0.0005,
)
# Calculate execution costs
costs = model.calculate_execution_cost(notional=5_000_000, market=market, execution_periods=60)
print(f"Total cost: {costs.total_cost_bps:.1f} bps")
# Capacity analysis
analyzer = CapacityAnalyzer(gross_sharpe=1.34, annual_turnover=24)
result = analyzer.analyze_capacity("GLD", market)
print(f"Capacity at Sharpe > 1.0: ${result.aum_at_sharpe_1/1e6:.0f}M")execution_cost_model/
├── data/
│ ├── raw/ # Raw price/volume data
│ ├── processed/ # Processed metrics (ADV, spread, vol)
│ └── calibration/ # Calibrated parameters
├── src/
│ ├── __init__.py
│ ├── data_ingestion.py # Data retrieval and processing
│ ├── spread_estimation.py # Bid-ask spread estimators
│ ├── impact_model.py # Almgren-Chriss implementation
│ ├── execution_simulator.py # Execution cost simulation
│ ├── optimal_execution.py # Optimal trajectory calculation
│ ├── capacity_analysis.py # Strategy capacity analysis
│ ├── visualisation.py # Charts and figures
│ └── utils.py # Helper functions
├── notebooks/
│ └── analysis.ipynb # Exploratory analysis
├── tests/
│ └── test_*.py # Unit tests
├── reports/
│ └── figures/ # Generated charts
├── config.yaml # Configuration parameters
├── requirements.txt
├── main.py # Entry point
├── capacity_results.json # Results output
└── README.md
Edit config.yaml to customize:
data:
tickers: ["GLD", "SLV"]
start_date: "2015-01-01"
adv_window: 20
volatility_window: 20
impact_model:
eta: 0.035 # Temporary impact coefficient (calibrated for liquid ETFs)
gamma: 0.08 # Permanent impact coefficient (calibrated for liquid ETFs)
beta: 0.6 # Temporary impact exponent
alpha: 0.6 # Permanent impact exponent
execution_simulation:
notional_sizes: [100000, 500000, 1000000, 5000000, 10000000, 25000000, 50000000]
capacity_analysis:
strategy_gross_sharpe: 1.34
strategy_annual_turnover: 24| Notional | Participation Rate | Spread (bps) | Temp Impact (bps) | Perm Impact (bps) | Total (bps) |
|---|---|---|---|---|---|
| $100K | 0.001% | 1.0 | 1.6 | 3.6 | 6.2 |
| $1M | 0.008% | 1.0 | 6.3 | 14.3 | 21.6 |
| $10M | 0.08% | 1.0 | 25.0 | 57.1 | 83.0 |
| $50M | 0.42% | 1.0 | 65.6 | 149.9 | 216.5 |
| Metric | GLD | SLV |
|---|---|---|
| Average Daily Volume | $1,530M | $466M |
| Bid-Ask Spread (bps) | 2.0 | 4.0 |
| Annualized Volatility | 13.9% | 25.2% |
| Round-trip Cost @ $1M | 43.2 bps | 76.7 bps |
| Round-trip Cost @ $10M | 166.1 bps | 293.5 bps |
| Capacity (Sharpe > 1.0) | $562K | $182K |
| Capacity (Sharpe > 0.5) | $2.81M | $1.02M |
| Capacity (Breakeven) | $6.26M | $2.33M |
For a $5M order:
- Immediate execution: Higher cost, no timing risk
- TWAP (1 hour): ~15% cost reduction
- TWAP (1 day): ~30% cost reduction, increased timing risk
- Immediate Execution - Market order, single period
- TWAP 1 Hour - Time-weighted average over 60 minutes
- TWAP 1 Day - Time-weighted average over trading day
- TWAP 2 Days - Time-weighted average over 2 days
- VWAP 5% - Participate at 5% of volume
- VWAP 10% - Participate at 10% of volume
Four estimators are implemented:
-
Known Spreads (default, most accurate for liquid ETFs): Uses hardcoded bid-ask spreads based on market microstructure data:
- GLD: 2 bps (highly liquid gold ETF)
- SLV: 4 bps (slightly less liquid silver ETF)
Note: High-low range methods dramatically overestimate spreads for liquid ETFs by including intraday volatility.
-
High-Low Range (simple, overestimates):
spread = (high - low) / close⚠️ This captures daily price range (~85 bps for GLD), not actual bid-ask spread. -
Corwin-Schultz Estimator: Separates spread from volatility using two-day high-low ranges.
-
Roll Estimator:
spread = 2 × sqrt(-Cov(r_t, r_{t-1}))
- Implementation shortfall vs order size (log-log plot)
- Cost breakdown stacked bar chart
- Capacity curve: Sharpe ratio vs AUM
- Optimal execution frontier (expected cost vs variance)
- Strategy comparison charts
- Spread and volatility time series
- Parameter sensitivity tornado chart
- Historical cost variation
pytest tests/ -vDefault parameters are conservatively calibrated for precious metals ETFs:
- η (temporary): 0.035
- γ (permanent): 0.08
- β (temporary exponent): 0.6
- α (permanent exponent): 0.6
These are lower than Almgren et al. (2005) literature values (η=0.142, γ=0.314) to reflect the exceptional liquidity of GLD/SLV. The model can be recalibrated using the calibrate_from_data() method which performs regression analysis on historical price-volume relationships.
- Daily Data: Uses daily OHLCV data; intraday data would improve accuracy
- Literature Parameters: Impact parameters from academic literature, not firm-specific calibration
- Linear Temporary Impact: May underestimate costs for very large orders
- No Information Leakage: Does not model alpha decay from information leakage
- Static Assumptions: Assumes constant volatility and liquidity conditions
- ETF Specific: Calibrated for highly liquid ETFs; less accurate for individual stocks
- Almgren, R., & Chriss, N. (2001). Optimal execution of portfolio transactions. Journal of Risk, 3, 5-40.
- Almgren, R., Thum, C., Hauptmann, E., & Li, H. (2005). Direct estimation of equity market impact. Risk, 18(7), 58-62.
- Corwin, S. A., & Schultz, P. (2012). A simple way to estimate bid-ask spreads from daily high and low prices. Journal of Finance, 67(2), 719-760.
- Roll, R. (1984). A simple implicit measure of the effective bid-ask spread in an efficient market. Journal of Finance, 39(4), 1127-1139.
MIT License