Portfolio Risk Engine
A Python-based risk analysis engine that computes portfolio risk metrics including volatility, Value at Risk (VaR), Sharpe ratio, Conditional VaR (ES), correlation matrix, beta, and portfolio volatility using LSEG Enterprise Data.
methodology
This engine uses historical VaR rather than parametric VaR. Historical simulation captures the actual skewness and excess kurtosis in return distributions, which a normal distribution assumption (parametric VaR) would fail to represent.
def hist_var(asset_returns, confidence_level=0.95):
assert 0 <= confidence_level <= 1, "confidence level is not between 0 and 1"
var = asset_returns.quantile(1 - confidence_level)
return varHistorical VaR — Value at Risk
Estimates the maximum expected loss at a given confidence level
def cvar(asset_returns, confidence_level=0.95):
assert 0 <= confidence_level <= 1, "confidence level is not between 0 and 1"
var_threshold = hist_var(asset_returns, confidence_level)
bad_returns = asset_returns[asset_returns < var_threshold]
return bad_returns.mean()Estimated Shortfall (ES)
Average of all returns that are worse than your VaR threshold
def sharpe_ratio(asset_returns, risk_free_rate=0.04):
sr = (expected_return(asset_returns) - risk_free_rate) / volatility(asset_returns)
return srSharpe Ratio
Measures risk-adjusted returns
def beta(asset_returns, market_returns):
covariance = asset_returns.cov(market_returns)
m_var = market_returns.var()
return covariance / m_var
Beta
Measures the sensitivity of an asset relative to market movement
def portfolio_volatility(asset_returns, weights):
assert len(weights) == asset_returns.shape[1], "length of weights does not equal # of columns"
assert np.isclose(weights.sum(), 1), " weights don't sum to 1.0."
cov_matrix = asset_returns.cov() * 252
return np.sqrt(weights.T @ cov_matrix @ weights)
Portfolio Volatility (covariance-based)
Calculates how much the overall portfolio fluctuates
def stress_test(asset_returns, start_date, end_date, confidence_level=0.95):
crisis_period = asset_returns.loc[start_date:end_date] # splice data
return hist_var(crisis_period, confidence_level), cvar(crisis_period, confidence_level)
Historical stress testing
Replays a portfolio's actual returns during a historical crisis window to measure how VaR and CVaR behave under real stress conditions.
data pipeline
Python module that pulls daily total returns from LSEG, dividend-adjusts, converts to decimal form, with local caching for reproducibility.
GET_MARKET_DATA
Pulls daily total return data from LSEG, already dividend-adjusted, and converts to decimal form. Supports local caching for reproducible runs.
results
SPY — 95% VaR & CVaR
TSLA — 95% VaR & CVaR
FINDINGS SPY VS. TSLA
Both charts share the same x-axis scale to allow direct comparison of tail width.
At a 95% confidence level, SPY's daily losses are not expected to exceed 1.93%. On days where losses breach that threshold, the average loss (CVaR) is 3.20%.
TSLA's 95% VaR is 6.28%, with an average tail loss (CVaR) of 9.09% — more than 3x wider than SPY's band in absolute terms.
Plotted on the same scale, the difference is visible directly: SPY's distribution clusters tightly near zero with a narrow VaR-CVaR band, while TSLA's is both wider overall and has a substantially fatter left tail.
This illustrates a diversification effect concretely — SPY, as a diversified index, absorbs idiosyncratic shocks that a single volatile equity like TSLA does not.
portfolio-level risk
Expected Return (Annual)
24.7%
Volatility (Annual)
21.3%
VaR (95%)
-2.04%
CVaR (Expected Shortfall)
-2.97%
Correlation Matrix
Blue indicates positive correlation, red indicates negative correlation.
FINDINGS — CORRELATION
The portfolio's diversification benefit is visible in the correlation structure: TLT (bonds) is negatively correlated with SPY (-0.15), providing a partial hedge. TSLA's 0.50 correlation with SPY is notably higher than the other pairs, meaning its risk isn't as diversified away by the rest of the portfolio as its individual volatility might suggest — a large TSLA allocation and a large SPY allocation are more redundant than they appear.
Limitation
This implementation uses static (unconditional) correlation, computed over the full sample period. During market stress, correlations tend to spike toward 1 — assets that normally move independently start moving together. Because static correlation averages across calm and turbulent periods alike, it understates tail risk during crises. A more accurate approach would use Dynamic Conditional Correlation (DCC) or rolling-window correlation to capture how relationships shift under stress.
Tech Stack