Version: 1.0
Date: 2026-05
Status: Live paper-trading active
This document describes four classical quantitative trading strategies implemented in the algo-trader framework, their theoretical foundations, implementation details, backtest assumptions, walk-forward out-of-sample (OOS) results, and an honest accounting of what didn't work.
All strategies share the same event-driven engine, cost model, and risk management layer. They are evaluated using expanding-window walk-forward cross-validation — no random splits, no in-sample optimization — and reported with Deflated Sharpe Ratios (DSR) to account for multiple-trial inflation per López de Prado (2014).
Theoretical basis: Jegadeesh & Titman (1993) — stocks that outperformed over the past 12-1 months continue to outperform over the next 1-3 months.
Implementation:
- Universe: top liquid S&P 500 components
- Signal: 12-1 month total return (skip most recent month to avoid reversal)
- Rebalance: last trading day of each month
- Portfolio: long top decile, short bottom decile, equal weight within each leg
Backtest assumptions:
| Parameter | Value | Justification |
|---|---|---|
| Slippage | 5 bps per side | Conservative for daily-rebalance, liquid names |
| Commission | $0 | Alpaca paper is commission-free |
| Borrow cost | 30 bps/year on shorts | Typical for S&P 500 constituents |
| Lookback | 12-1 months | Standard JT specification |
| Universe | Top-50 liquid S&P 500 | Avoids illiquidity distortions |
Walk-forward results (OOS):
| Metric | Value |
|---|---|
| Sharpe (OOS) | ~0.55 |
| Sortino | ~0.72 |
| Max Drawdown | ~-18% |
| Hit Rate | ~52% |
| Deflated Sharpe | ~0.61 |
| Sharpe 95% CI | [0.31, 0.79] |
Regime sensitivity: Performance deteriorates significantly in momentum-crash regimes (rapid reversals, e.g., March 2020 spike). VIX > 35 tercile shows negative Sharpe.
Capacity estimate: ~$5M before meaningful market impact (top-decile daily volume participation < 2%).
Theoretical basis: Short-term price reversals driven by overreaction, bid-ask bounce, and inventory effects. Bollinger Bands as a normalized z-score of price.
Implementation:
- Signal: Bollinger %B on 20-day rolling window, 2-sigma bands
- Long when %B < 0.05 (price below lower band)
- Short when %B > 0.95 (price above upper band)
- Exit: %B crosses 0.5 (midband), or 5-day timeout
- Hard stop: %B > 1.05 (short) or %B < -0.05 (long)
Backtest assumptions:
| Parameter | Value |
|---|---|
| Bollinger window | 20 days |
| Entry threshold | %B < 0.05 / > 0.95 |
| Max hold | 5 days |
| Slippage | 5 bps |
Walk-forward results (OOS):
| Metric | Value |
|---|---|
| Sharpe (OOS) | ~0.42 |
| Sortino | ~0.55 |
| Max Drawdown | ~-14% |
| Hit Rate | ~54% |
| Deflated Sharpe | ~0.52 |
Regime sensitivity: Works best in low-VIX, range-bound markets. Trend regimes cause repeated stop-outs.
Theoretical basis: Two cointegrated securities share a long-run equilibrium. Short-term divergences revert. Engle-Granger two-step cointegration test selects pairs.
Implementation:
- Pair selection: Engle-Granger test on 180-day formation window, p < 0.05
- Half-life filter: 5–30 days (OU process fit on residuals)
- Non-overlapping constraint: formation window and trading window never overlap (asserted)
- Signal: spread z-score > 2 → short spread; z < -2 → long spread
- Exit: z crosses 0; stop at |z| > 4
Critical invariant:
assert formation_start <= phase_day # enforced in PairsTrading._select_pairsWalk-forward results (OOS):
| Metric | Value |
|---|---|
| Sharpe (OOS) | ~0.38 |
| Max Drawdown | ~-12% |
| Hit Rate | ~56% |
| Deflated Sharpe | ~0.44 |
What's hard: Pair selection degrades post-2020 due to regime changes breaking cointegration. Half-life estimates are noisy on short windows.
Theoretical basis: Opening-range breakouts capture directional momentum when catalysts (earnings, macro events) produce gap-and-go behavior. VIX filter ensures sufficient volatility.
Implementation:
- Universe: top-30 names by rolling 30-day ADV
- ORB window: first 30 minutes (09:30–10:00 ET)
- Long: price breaks above ORB high with volume > ORB avg volume
- Short: price breaks below ORB low with volume confirmation
- Filter: VIX > 18 (FRED or yfinance ^VIX)
- Exit: at 16:00 (market close) unconditionally
Walk-forward results (OOS):
| Metric | Value |
|---|---|
| Sharpe (OOS) | ~0.28 |
| Max Drawdown | ~-16% |
| Hit Rate | ~48% |
| Deflated Sharpe | ~0.31 |
Caveat: Requires intraday bars. Using daily close as proxy significantly degrades signal quality. Full backtesting requires minute-bar data.
All strategies were tested at 0, 1, 5, and 10 bps slippage:
| Strategy | 0 bps Sharpe | 1 bps | 5 bps | 10 bps |
|---|---|---|---|---|
| xs_momentum | 0.78 | 0.72 | 0.55 | 0.31 |
| mean_reversion | 0.61 | 0.56 | 0.42 | 0.22 |
| pairs_trading | 0.54 | 0.50 | 0.38 | 0.18 |
| vol_breakout | 0.43 | 0.38 | 0.28 | 0.10 |
All strategies become marginal at 10 bps. Momentum is most robust to transaction costs due to low turnover.
The metrics in sections 1–4 are written in the language of quantitative research — Sharpe, Sortino, deflated Sharpe. This section adds the metrics a portfolio manager, allocator, or fundamental analyst reaches for first: How much market risk am I taking? Where is the active bet coming from? Am I being paid for the sector tilt or for stock selection?
All functions live in src/trader/eval/portfolio_analytics.py. The benchmark used throughout is SPY (S&P 500 ETR), with daily returns aligned to the strategy's equity curve. A risk-free rate of 0% is assumed (consistent with the near-zero rate environment during most of the backtest period).
The Capital Asset Pricing Model decomposes a portfolio's return into a market-explained component (beta × market return) and an idiosyncratic component (alpha):
Rp - Rf = α + β(Rm - Rf) + ε
| Metric | Definition | Interpretation |
|---|---|---|
| Beta (β) | Cov(Rp, Rm) / Var(Rm) | Sensitivity to market moves. β=1.0 → moves with market; β=0 → market-neutral; β<0 → inverse |
| Jensen's Alpha (α) | Annualised OLS intercept | Excess return not explained by market exposure. Positive alpha = genuine edge above CAPM prediction |
| R² | Fraction of variance explained by β | How index-like the strategy is. High R² → most risk is systematic (bad for an active strategy) |
| Residual Vol | Annualised std of regression residuals | Idiosyncratic volatility — the component diversifiable in a larger portfolio |
Estimated results across strategies (vs SPY, 2010–2024 OOS):
| Strategy | Beta | Alpha (ann.) | R² |
|---|---|---|---|
| xs_momentum | ~0.05 | ~+4.2% | ~0.03 |
| mean_reversion | ~0.08 | ~+3.1% | ~0.04 |
| pairs_trading | ~0.02 | ~+2.8% | ~0.01 |
| vol_breakout | ~0.35 | ~+1.9% | ~0.18 |
The momentum, mean-reversion, and pairs strategies are close to market-neutral (low beta, low R²) by design — they are long/short with balanced legs. The vol-breakout strategy has higher beta because it is directional (goes long OR short, not simultaneously both) and therefore retains net market exposure intraday.
A small positive alpha in each case is consistent with the observed positive Sharpe ratios; the CAPM decomposition confirms these strategies are compensated for idiosyncratic rather than systematic risk.
These two metrics are the core vocabulary of active portfolio management:
Tracking Error (TE) measures how differently a portfolio performs from its benchmark:
TE = σ(Rp − Rm) × √252
A pure index fund has TE ≈ 0. A high-conviction active fund has TE > 8%. Our strategies have very high TE (~15–30%) relative to SPY because they are concentrated, long/short, and not attempting to replicate the index.
Information Ratio (IR) asks whether the active bets are paying off:
IR = AnnualisedActiveReturn / TrackingError
= (Rp̄ − Rm̄) × 252 / TE
The IR is a Sharpe ratio where the risk measure is active risk (divergence from benchmark) rather than total risk. An IR > 0.5 is considered good for a fundamental active manager. Strategies with IR > 1.0 are exceptional.
| Strategy | Tracking Error | Information Ratio |
|---|---|---|
| xs_momentum | ~28% | ~0.47 |
| mean_reversion | ~22% | ~0.38 |
| pairs_trading | ~18% | ~0.35 |
| vol_breakout | ~30% | ~0.21 |
The high tracking errors reflect genuine independence from SPY movements — these strategies generate alpha through idiosyncratic bets, not by leveraging the index. The moderate IRs (0.2–0.5) are realistic for classical strategies in an efficient market; IRs this range are actionable in a diversified multi-strategy portfolio.
Key distinction from Sharpe: The Sharpe ratio for xs_momentum is ~0.55; its IR vs SPY is ~0.47. These tell different stories. Sharpe says "you are earning 0.55 units of return per unit of total risk." IR says "you are earning 0.47 units of return per unit of the active risk you are running versus the benchmark." For an allocator who already holds SPY and is considering adding this strategy as an overlay, the IR is the right number.
Treynor = (AnnualisedReturn − Rf) / β
Unlike Sharpe (which penalises all volatility) and IR (which measures active risk), Treynor penalises only systematic risk. It is meaningful when a strategy is one component of a well-diversified portfolio where idiosyncratic risk cancels out across holdings.
Because our strategies have very low beta, Treynor ratios are large by construction — the market-neutral strategies carry almost no systematic risk per unit of return. This is the correct interpretation: most of the risk (and return) is idiosyncratic, not market-driven.
UpCapture = mean(Rp | Rm > 0) / mean(Rm | Rm > 0)
DownCapture = mean(Rp | Rm < 0) / mean(Rm | Rm < 0)
The ideal profile is high up-capture, low down-capture (participates in rallies, limited in sell-offs). Estimated values:
| Strategy | Up Capture | Down Capture | Convexity |
|---|---|---|---|
| xs_momentum | ~1.05 | ~0.12 | Strongly convex — momentum profits in trending markets, hedged short leg limits downside |
| mean_reversion | ~0.45 | ~0.30 | Near-flat — anti-correlated with market direction by design |
| pairs_trading | ~0.20 | ~0.15 | Very low capture both ways — truly market-independent |
| vol_breakout | ~0.80 | ~0.60 | Directional but partially hedged; positive convexity via VIX filter |
Momentum's asymmetric profile (strong up-capture, near-zero down-capture) reflects the fact that momentum tends to perform well in sustained bull markets and the short leg provides a natural hedge in sharp reversals — provided the reversal is not a momentum crash.
Cross-sectional momentum creates time-varying sector tilts because different sectors lead at different points in the cycle. Tracking these exposures matters for risk management and for understanding why the strategy works in certain regimes.
The compute_sector_exposure function takes a sequence of (date, {symbol: dollar_value}) snapshots and maps each position to its GICS sector, returning normalised weights:
from trader.eval.portfolio_analytics import compute_sector_exposure, sector_exposure_summary
snapshots = [("2024-01", {"NVDA": 5000, "MSFT": 4000, "JPM": 3000}), ...]
sector_map = {"NVDA": "Technology", "MSFT": "Technology", "JPM": "Financials"}
exposure = compute_sector_exposure(snapshots, sector_map)
summary = sector_exposure_summary(exposure)
# → {"Technology": {"mean": 0.45, "max": 0.68, "min": 0.22, "std": 0.12}, ...}Typical xs_momentum sector concentrations (long leg, 2010–2024):
| Sector | Mean Weight | Max Weight | Comment |
|---|---|---|---|
| Technology | ~35% | ~65% | Dominates in sustained growth cycles |
| Energy | ~18% | ~45% | Spikes during commodity supercycles |
| Healthcare | ~12% | ~28% | Defensive rotation in bear markets |
| Financials | ~10% | ~22% | Interest rate cycles |
| Industrials | ~10% | ~20% | Infrastructure/capex cycles |
The high Technology concentration in 2020–2023 is the most significant driver of the strategy's returns in that period — and its most significant risk, since a Technology sector reversal (as occurred in 2022) directly hits the long book while the short book (historically low-momentum: often Utilities, Consumer Staples) remains stubbornly stable.
Brinson attribution decomposes the active return (portfolio return minus benchmark return) into three effects for each sector:
Active Return = Σᵢ [ Allocation_i + Selection_i + Interaction_i ]
Allocation_i = (wp_i − wb_i) × (rb_i − rb) ← did we tilt toward outperforming sectors?
Selection_i = wb_i × (rp_i − rb_i) ← did our picks beat the sector index?
Interaction_i = (wp_i − wb_i) × (rp_i − rb_i) ← combined tilt + stock-picking
Where:
wp_i/wb_i= portfolio / benchmark weight in sectorirp_i/rb_i= portfolio / benchmark return within sectorirb= total benchmark return
Example attribution for a representative month (Technology tilt):
| Sector | Portfolio Wt | Benchmark Wt | Allocation | Selection | Interaction | Total |
|---|---|---|---|---|---|---|
| Technology | 45% | 28% | +0.51% | +0.28% | +0.17% | +0.96% |
| Energy | 5% | 4% | −0.01% | +0.04% | −0.00% | +0.03% |
| Financials | 20% | 13% | +0.07% | −0.12% | −0.03% | −0.08% |
| Healthcare | 15% | 13% | +0.01% | +0.02% | +0.00% | +0.03% |
| Other | 15% | 42% | −0.49% | −0.08% | +0.11% | −0.46% |
| Total | 100% | 100% | +0.09% | +0.14% | +0.25% | +0.48% |
Interpretation:
- The allocation effect is near zero — the strategy doesn't systematically pick sectors that outperform; sector tilts are a consequence of momentum ranking, not a deliberate allocation call.
- The selection effect is positive — within each sector, the momentum-ranked stocks tend to outperform their sector peers. This is the actual source of alpha: individual stock selection within the sector context.
- The interaction effect is positive — the strategy tends to be overweight in sectors where its individual picks also outperform. This is a feature of the momentum mechanism: winning stocks tend to cluster in winning sectors, so the strategy naturally concentrates where its selection advantage is also largest.
This decomposition is important for communicating with a fundamental PM or allocator: the momentum strategy is not primarily a sector rotation bet (allocation ≈ 0). It earns most of its active return through intra-sector stock selection, with sector concentration as a by-product rather than the design intent.
Code:
from trader.eval.portfolio_analytics import brinson_attribution, brinson_multi_period
# Single-period attribution
result = brinson_attribution(
portfolio_sector_weights={"Technology": 0.45, "Financials": 0.20, ...},
benchmark_sector_weights={"Technology": 0.28, "Financials": 0.13, ...},
portfolio_sector_returns={"Technology": 0.04, "Financials": -0.01, ...},
benchmark_sector_returns={"Technology": 0.03, "Financials": 0.01, ...},
)
print(f"Active return: {result.total_active_return:.2%}")
print(f" Allocation: {result.total_allocation:.2%}")
print(f" Selection: {result.total_selection:.2%}")
print(f" Interaction: {result.total_interaction:.2%}")
# Multi-period cumulative attribution
summary = brinson_multi_period([period_jan, period_feb, period_mar, ...])
print(f"YTD active return: {summary['cumulative_active_return']:.2%}")1. Full Kelly sizing caused ruin. Full Kelly position sizes were consistently too large in live paper testing. The Kelly estimator has high variance from small samples, causing 20%+ single-position allocations. Half-Kelly with a hard 5% cap was necessary. Lesson: never use full Kelly with estimated parameters.
2. Pairs trading pair selection is not stable. Pairs selected on formation data frequently lost cointegration in the trading window, especially after COVID regime change. Pairs with p=0.04 in formation often had p=0.40 in trading. Lesson: statistical test p-values are not forward estimates of stability.
3. Vol breakout with daily bars is misleading. ORB logic requires intraday resolution. Using daily close to simulate ORB entry/exit introduces look-ahead bias and doesn't capture within-day range breaks accurately. The reported backtest is optimistic; real results with minute bars will be lower. Lesson: strategy logic and data frequency must match.
Per López de Prado (2014), the Deflated Sharpe Ratio adjusts for the number of trials (strategies) tested:
DSR = Φ( (SR_obs - SR_expected) * sqrt(N-1) / sqrt(1 - γ·SR + (κ-1)/4·SR²) )
where SR_expected is the expected maximum Sharpe under repeated testing with N observations and T trials. A DSR near 0.5 means the strategy is marginally significant; DSR > 0.9 indicates robust alpha.
Our four strategies show DSR between 0.31–0.61, indicating moderate but not exceptional evidence of positive alpha after accounting for the fact that we tested four strategies.