Portfolio Beta Calculation
A complete guide to measuring market sensitivity in Sable.
What is Beta?
Beta (β) measures how sensitive an asset or portfolio is to market movements. It's the cornerstone of understanding market risk.
The Fundamental Formula
β = Cov(A, B) / Var(B)
Where A is your asset/portfolio and B is the market benchmark (SPY).
Beta Interpretation
| Beta Value | Meaning | Example |
|---|---|---|
| β = 1.0 | Moves in lockstep with the market | If SPY rises 1%, expect 1% gain |
| β > 1.0 | More volatile than the market | β = 1.5 means 50% more volatile |
| β < 1.0 | Less volatile than the market | β = 0.5 means 50% less volatile |
| β < 0 | Moves inversely to the market | Rare (e.g., inverse ETFs) |
Beta is always calculated relative to a benchmark. In Sable, we use SPY (S&P 500 ETF) as the market proxy.
Four Levels of Beta Calculation
Sable calculates beta at four distinct levels, each serving a different purpose in risk management.
Entity Level
↓
Account Level
↓
Strategy Level
↓
Position Level
Entity Level
- Aggregates: All accounts within an entity
- Use case: Overall organizational market exposure
- Formula:
Return = (Entity NAV_t - Entity NAV_t-1 - CashFlows) / Entity NAV_t-1
Account Level
- Aggregates: All positions within an account
- Use case: Individual account risk profile
- Formula:
Return = (Account NAV_t - Account NAV_t-1 - CashFlows) / Account NAV_t-1
Strategy Level
- Aggregates: All positions within a strategy
- Use case: Strategy-specific market sensitivity
- Formula:
Return = (Strategy NAV_t - Strategy NAV_t-1 - CashFlows) / Strategy NAV_t-1
Position Level
- Individual: Single security beta
- Use case: Stock-specific market sensitivity
- Formula:
β = Cov(R_position, R_SPY) / Var(R_SPY)
Entity, Account, and Strategy levels use realized portfolio returns (actual P&L / NAV). Position level uses individual security returns from price data.
Calculation Methodology
Portfolio Return Calculation (Time-Weighted Return)
For any aggregation level (Entity, Account, Strategy):
Return_t = (NAV_t - NAV_t-1 - NetCashFlows_t) / NAV_t-1
Components:
| Component | Description |
|---|---|
| NAV_t | Net Asset Value at end of day t |
| NAV_t-1 | Net Asset Value at end of previous day |
| NetCashFlows_t | Deposits (+) minus Withdrawals (-) during day t |
TWR removes the impact of external cash flows (deposits/withdrawals) to show pure investment performance. This makes your returns directly comparable to SPY, which has no external cash flows.
Then Calculate Beta
Once you have daily returns for your portfolio and SPY:
β = Cov(Return_Portfolio, Return_SPY) / Var(Return_SPY)
Lookback Period Options:
| Period | Days | Use Case |
|---|---|---|
| Default | 252 trading days | ~1 year |
| Minimum | 60 trading days | ~3 months for statistical significance |
| Options | 63d, 126d, 252d, 504d | Rolling windows |
Why This Approach is Superior
Old Method: Weighted Average (Incorrect)
β_portfolio = Σ(w_i × β_i)
Issues:
- Uses current weights with historical betas
- Doesn't capture actual trading activity
- Ignores positions sold during period
- Assumes constant allocation
- Hypothetical, not realized
New Method: Realized Returns (Correct)
β = Cov(Actual Returns, SPY) / Var(SPY)
Benefits:
- Based on actual realized returns
- Captures all trading activity
- Includes positions held during period
- Handles time-varying weights
- True historical sensitivity
Worked Examples
Example 1: Account-Level Beta
Given Data:
| Date | Account NAV | Cash Flows | SPY Price |
|---|---|---|---|
| Day 0 | $1,000,000 | - | $450.00 |
| Day 1 | $1,012,000 | $0 | $454.50 |
| Day 2 | $1,030,000 | $10,000 | $458.59 |
| Day 3 | $1,025,000 | $0 | $456.13 |
Step 1: Calculate Daily Returns
Day 1 Account Return = (1,012,000 - 1,000,000 - 0) / 1,000,000 = 1.2%
Day 1 SPY Return = (454.50 - 450.00) / 450.00 = 1.0%
Day 2 Account Return = (1,030,000 - 1,012,000 - 10,000) / 1,012,000 = 0.79%
Day 2 SPY Return = (458.59 - 454.50) / 454.50 = 0.9%
Day 3 Account Return = (1,025,000 - 1,030,000 - 0) / 1,030,000 = -0.49%
Day 3 SPY Return = (456.13 - 458.59) / 458.59 = -0.54%
Step 2: Calculate Covariance and Variance
Account Returns: [1.2%, 0.79%, -0.49%]
SPY Returns: [1.0%, 0.9%, -0.54%]
Cov(Account, SPY) = Calculate covariance
Var(SPY) = Calculate variance of SPY returns
Step 3: Calculate Beta
β = Cov(Account, SPY) / Var(SPY)
With more data points (252 days), this gives you the account's actual market sensitivity.
Example 2: Strategy-Level Beta Comparison
Portfolio Composition:
| Strategy | NAV | Calculated Beta | Interpretation |
|---|---|---|---|
| Growth Equity | $5,000,000 | 1.35 | Aggressive - 35% more volatile than market |
| Dividend Value | $3,000,000 | 0.72 | Defensive - 28% less volatile than market |
| Long/Short | $2,000,000 | 0.15 | Market neutral - minimal market exposure |
Entity-Level Beta Calculation:
Instead of weighted average, we calculate entity returns from actual P&L:
Entity Return_t = (Entity NAV_t - Entity NAV_t-1 - CashFlows_t) / Entity NAV_t-1
Then: β_Entity = Cov(Entity Returns, SPY Returns) / Var(SPY Returns)
Result: Entity beta reflects actual blended exposure, accounting for how strategies performed together, rebalancing, and any strategy-level interactions.
Example 3: Impact of Cash Flows
Scenario: Large Deposit Mid-Period
Without TWR Adjustment (WRONG):
Day 1: NAV = $1,000,000 → $1,020,000 (2% gain from trading)
Day 2: Deposit $500,000 → NAV = $1,520,000
Day 3: NAV = $1,520,000 → $1,535,200 (1% gain from trading)
Wrong Return = (1,535,200 - 1,000,000) / 1,000,000 = 53.52% ❌
(This includes the $500K deposit!)
With TWR Adjustment (CORRECT):
Day 1 Return = (1,020,000 - 1,000,000 - 0) / 1,000,000 = 2.0%
Day 2 Return = (1,520,000 - 1,020,000 - 500,000) / 1,020,000 = 0.0%
Day 3 Return = (1,535,200 - 1,520,000 - 0) / 1,520,000 = 1.0%
Correct: Only trading gains/losses, no deposit impact ✅
Without removing cash flows, deposits would artificially inflate returns and withdrawals would deflate them. Beta would be meaningless.
Critical Issues Fixed
Issue #1: Short Beta Formula
Old (Incorrect):
Short β = Σ(|MV_i| × β_i) / Σ|MV_i|
Problem: Treated shorts as adding market exposure instead of reducing it.
Example: Short $1M stock with β=1.5
Old calc: Showed +$1.5M exposure ❌
Should be: -$1.5M exposure
New (Correct):
Short β = Σ(MV_i × β_i) / Σ|MV_i|
Fix: Removed absolute value from numerator. MV_i is negative for shorts.
Example: Short $1M stock with β=1.5
MV = -$1,000,000
Numerator: -$1,000,000 × 1.5 = -$1,500,000
Denominator: $1,000,000
Result: -1.5 ✅ (negative exposure as expected)
Issue #2: GMV Denominator Problem
Using Gross Market Value (GMV) made beta incomparable to traditional metrics.
Example Portfolio:
| Position | Market Value | Beta | Market Exposure |
|---|---|---|---|
| Long positions | $100M | 1.2 | +$120M |
| Short positions | -$80M | 1.0 | -$80M |
| Net | $20M (NMV) | - | $40M |
| Gross | $180M (GMV) | - | - |
Old: Using GMV
β = $40M / $180M = 0.22
Problem: Not comparable to standard beta.
New: Using NMV (Capital)
β = $40M / $20M = 2.0
Interpretation: If market moves 1%, expect 2% return on capital.
- Primary: Beta on Capital (NMV-based) - standard interpretation
- Secondary: Beta on Exposure (GMV-based) - for granular analysis
- Edge case: Market-neutral portfolios (NMV ≈ 0) show warning and rely on exposure metric
Issue #3: Missing SPY Data
Old implementation silently excluded days with missing SPY data. This is a data integrity failure.
SPY is the most liquid ETF in the world. Missing data indicates:
- Broken data feed
- Calendar error
- Pipeline failure
New Behavior:
IF missing_spy_days > 0:
RAISE EXCEPTION:
"BETA CALCULATION BLOCKED: Missing SPY data for X trading days: [dates]
Fix data pipeline before proceeding."
Forces immediate fix rather than working around bad data.
Enhancement #1: Confidence Intervals
Beta is an estimate with uncertainty. Now we can optionally show:
| Metric | Formula |
|---|---|
| Standard Error | SE(β) = √[(1-R²)σ²_asset / (σ²_market(n-2))] |
| 95% CI | β ± 1.96 × SE(β) |
Wide confidence intervals flag positions where beta estimate is unreliable.
Enhancement #2: Rolling Windows
Different time horizons for different use cases:
| Period | Days | Use Case | Characteristics |
|---|---|---|---|
| 1 Year | 252 | Industry standard | Balanced, comparable |
| 6 Months | 126 | Tactical allocation | More responsive |
| 3 Months | 63 | Short-term trends | Quick to adapt, noisier |
| 2 Years | 504 | Long-term stable | Smooth, strategic |
EWMA Alternative: Exponentially Weighted Moving Average (λ = 0.94) weights recent observations more heavily without hard cutoff. Industry standard for risk management.
Implementation Requirements
Data Requirements
Required Tables:
sable.nav_daily- Daily NAV by entity/account/strategysable.cash_flows_daily- Daily deposits/withdrawalssable.instrument_price_daily- SPY prices (must be complete)sable.calendar_date- Trading day calendar
Required Columns for nav_daily:
date,org_identity_id,account_id,strategy_idnav(decimal)
Required Columns for cash_flows_daily:
date,org_identity_id,account_id,strategy_idnet_cash_flow(decimal, positive for deposits)
Database Function Signature
CREATE OR REPLACE FUNCTION sable.f_calculate_portfolio_beta_v2(
p_org_id UUID,
p_entity_id UUID DEFAULT NULL,
p_account_id UUID DEFAULT NULL,
p_strategy_id UUID DEFAULT NULL,
p_lookback_days INTEGER DEFAULT 252,
p_min_trading_days INTEGER DEFAULT 60,
p_benchmark_instrument_id UUID DEFAULT NULL,
p_include_confidence_intervals BOOLEAN DEFAULT FALSE,
p_rolling_window_days INTEGER DEFAULT NULL
)
RETURNS TABLE (
level TEXT, -- 'entity', 'account', 'strategy', 'position'
id UUID,
name TEXT,
beta NUMERIC,
beta_std_error NUMERIC,
beta_ci_lower NUMERIC,
beta_ci_upper NUMERIC,
correlation NUMERIC,
r_squared NUMERIC,
trading_days INTEGER,
data_quality_warning TEXT
)
Implementation Phases
Phase 1: Critical Fixes (BLOCKER)
- Fix short beta formula (remove numerator absolute value)
- Implement dual beta reporting (capital vs exposure)
- Add hard stop on missing SPY data
- Document portfolio beta timing caveat
Timeline: Must complete before next release
Phase 2: Enhancements (NEXT SPRINT)
- Implement NAV-based portfolio beta calculation
- Add rolling window selector (63d, 126d, 252d, 504d)
- Add optional confidence intervals toggle
- Implement EWMA beta overlay
Timeline: 2-3 weeks
Phase 3: Future Improvements
- Multi-factor models (Fama-French 3-factor)
- Options delta integration
- Regime-conditional beta (crisis vs normal)
- Custom benchmarks beyond SPY
Testing Strategy
Unit Tests:
- Short positions reduce market exposure (negative contribution)
- Beta on capital vs exposure differ for leveraged portfolios
- Missing SPY data raises exception
- Market-neutral portfolios handle gracefully
- Confidence intervals widen with fewer observations
- TWR correctly removes cash flow impact
Integration Tests:
- Entity beta aggregates all account betas correctly
- Rolling windows (63d, 126d, 252d) produce different results
- EWMA weights recent data appropriately
- Time-series beta evolves logically
Data Quality Tests:
- Compare to Bloomberg/FactSet published betas (±10% tolerance)
- Out-of-sample prediction accuracy
- Backtest: High beta predicts higher volatility
Existing beta calculations will show different values after this update. This is expected and correct.
Communication plan: Notify users that beta calculations now use realized returns instead of weighted averages, providing more accurate market sensitivity measurements.
Quick Reference
| Concept | Formula/Value |
|---|---|
| Core Formula | β = Cov(A, B) / Var(B) where B = SPY |
| Portfolio Returns | R = (NAV_t - NAV_t-1 - CF) / NAV_t-1 |
| β = 1.0 | Matches market |
| β = 1.5 | 50% more volatile |
| β = 0.5 | 50% less volatile |
| β < 0 | Inverse to market |
Four Calculation Levels: Entity → Account → Strategy → Position
Beta measures market sensitivity. By calculating it from actual realized returns at multiple organizational levels, we get a true picture of risk rather than a hypothetical estimate.
Related
- CVaR (Conditional Value at Risk) - Tail risk measurement
- Returns Overview - Modified Dietz methodology
- Sable CLI Pipeline - Daily data refresh