Scenario Analysis Engine
Build a flexible multi-scenario analysis engine that simulates full portfolio profit and loss outcomes under an unlimited set of user-defined market shock scenarios including flash crash events, correlation breakdown crises, and prolonged liquidity freeze disasters for robust tail-risk preparedness.
Scenario Analysis Engine — Portfolio Risk
Scenario analysis systematically evaluates how a portfolio performs under many different market conditions — not just the historical worst case, but a full grid of possible futures. It goes beyond VaR by asking structured what-if questions.
Three scenario types:
- Single-factor scenarios: what happens if BTC drops 30%? If vol doubles?
- Multi-factor scenarios: what happens in a combined risk-off event (BTC -40%, DXY +10%, rates +50bp)?
- Parameter grid / heat map: a 2D grid showing portfolio P&L for all combinations of two key drivers (e.g. BTC return × correlation shift)
This notebook:
- Defines a comprehensive scenario library (base + stressed)
- Computes portfolio P&L for each scenario across multiple portfolio strategies
- Builds a 2D scenario heat map (BTC return × portfolio vol multiplier)
- Runs a Monte Carlo scenario distribution to show the range of outcomes
- Ranks scenarios by portfolio impact
- Exports the full scenario matrix
!pip install numpy pandas matplotlib seaborn scipy yfinance --quietimport numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 6)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
Configuration
We define three portfolios with different risk profiles to compare under each scenario. AGGRESSIVE is BTC-heavy; BALANCED is equal-weighted; CONSERVATIVE uses a larger allocation to lower-vol assets. The scenario grid spans BTC returns from -60% to +60% and correlated asset returns scale proportionally.
# ── Data Source Toggle ─────────────────────────────
USE_LIVE_DATA = False
TICKERS = ['BTC-USD','ETH-USD','SOL-USD','BNB-USD','AVAX-USD']
START_DATE = '2022-01-01'
END_DATE = '2024-12-31'
# ──────────────────────────────────────────────────
ASSETS = ['BTC','ETH','SOL','BNB','AVAX']
PORTFOLIOS = {
'Aggressive': np.array([0.60, 0.25, 0.10, 0.03, 0.02]),
'Balanced': np.array([0.30, 0.25, 0.20, 0.15, 0.10]),
'Conservative': np.array([0.20, 0.30, 0.10, 0.25, 0.15]),
}
PORTFOLIO_VALUE = 1_000_000
print('Config ready.')Config ready.
Data Acquisition
We use historical data to estimate current portfolio volatility and correlations for the Monte Carlo component. Yahoo Finance provides free real data via yfinance. The synthetic fallback generates a realistic correlation structure matching crypto market behaviour.
if USE_LIVE_DATA:
import yfinance as yf
prices = yf.download(TICKERS, start=START_DATE, end=END_DATE)['Close']
prices.columns = ASSETS
prices.dropna(inplace=True)
returns = prices.pct_change().dropna()
print(f'Live data: {len(returns)} days')
else:
rng = np.random.default_rng(42)
n = 730
vols = np.array([0.65,0.75,1.20,0.70,1.10]) / np.sqrt(252)
mu = np.array([0.40,0.35,0.60,0.25,0.55]) / 252
corr = np.array([[1,.85,.70,.75,.65],[.85,1,.75,.70,.68],
[.70,.75,1,.65,.72],[.75,.70,.65,1,.60],[.65,.68,.72,.60,1]])
cov = np.outer(vols,vols)*corr
L = np.linalg.cholesky(cov)
data = rng.standard_t(df=3.5, size=(n,5)) * vols + mu
idx = pd.date_range('2022-01-01', periods=n, freq='B')
returns = pd.DataFrame(data, columns=ASSETS, index=idx)
print(f'Synthetic data: {n} days')
cov_matrix = returns.cov().values * 252 # annualisedSynthetic data: 730 days
Scenario Library
We define a comprehensive scenario library spanning: historical analogue events, single-factor shocks, and hypothetical macro scenarios. Each scenario specifies a return for every asset. The analysis applies each scenario to all three portfolios and records the P&L. Scenarios are sorted from worst to best impact for the Balanced portfolio.
SCENARIOS = {
# Historical analogues
'COVID Crash': dict(BTC=-.50, ETH=-.60, SOL=-.70, BNB=-.55, AVAX=-.65),
'2022 Full Bear': dict(BTC=-.70, ETH=-.75, SOL=-.90, BNB=-.65, AVAX=-.85),
'FTX Collapse': dict(BTC=-.25, ETH=-.30, SOL=-.60, BNB=-.15, AVAX=-.35),
'BTC Halving Rally': dict(BTC=+.80, ETH=+.70, SOL=+1.50, BNB=+.60, AVAX=+1.20),
# Macro shocks
'Fed Rate Shock': dict(BTC=-.30, ETH=-.35, SOL=-.45, BNB=-.25, AVAX=-.40),
'USD Debasement': dict(BTC=+.50, ETH=+.40, SOL=+.60, BNB=+.35, AVAX=+.55),
# Single-factor shocks
'BTC -30%': dict(BTC=-.30, ETH=-.25, SOL=-.35, BNB=-.22, AVAX=-.32),
'BTC +50%': dict(BTC=+.50, ETH=+.40, SOL=+.65, BNB=+.35, AVAX=+.60),
'Vol Spike x3': dict(BTC=-.20, ETH=-.22, SOL=-.30, BNB=-.18, AVAX=-.28),
'Mild Correction -15%':dict(BTC=-.15, ETH=-.17, SOL=-.22, BNB=-.13, AVAX=-.20),
}
# Apply all scenarios to all portfolios
results = []
for scenario, shocks in SCENARIOS.items():
for port_name, weights in PORTFOLIOS.items():
pnl = sum(PORTFOLIO_VALUE * weights[i] * shocks.get(a, 0)
for i, a in enumerate(ASSETS))
results.append({'Scenario': scenario, 'Portfolio': port_name,
'PnL_USD': pnl, 'Return_%': pnl/PORTFOLIO_VALUE*100})
results_df = pd.DataFrame(results)
pivot = results_df.pivot(index='Scenario', columns='Portfolio', values='Return_%').round(1)
pivot_sorted = pivot.sort_values('Balanced')
print('Scenario Impact (% return) by Portfolio:')
print(pivot_sorted.to_string())Scenario Impact (% return) by Portfolio: Portfolio Aggressive Balanced Conservative Scenario 2022 Full Bear -73.4 -76.0 -74.5 COVID Crash -55.0 -58.8 -58.5 Fed Rate Shock -32.8 -34.5 -33.2 FTX Collapse -29.6 -32.8 -29.0 BTC -30% -29.0 -28.7 -27.3 Vol Spike x3 -21.6 -23.0 -22.3 Mild Correction -15% -16.2 -17.1 -16.6 USD Debasement 48.2 47.8 45.0 BTC +50% 48.8 49.2 46.2 BTC Halving Rally 84.7 92.5 85.0
2D Scenario Heat Map
A grid of BTC returns (rows) vs altcoin beta (columns) shows the P&L for the Balanced portfolio under every combination. Beta here represents how much altcoins move relative to BTC — a beta of 1.5 means altcoins move 1.5× BTC's return. This heat map is a powerful tool for understanding which risk factor dominates portfolio outcomes.
btc_returns = np.linspace(-0.70, 0.70, 15)
alt_betas = np.linspace(0.5, 2.0, 10)
balanced_w = PORTFOLIOS['Balanced']
grid = np.zeros((len(btc_returns), len(alt_betas)))
for i, btc_ret in enumerate(btc_returns):
for j, beta in enumerate(alt_betas):
shocks = {'BTC': btc_ret, 'ETH': btc_ret*beta*0.90,
'SOL': btc_ret*beta*1.20, 'BNB': btc_ret*beta*0.80,
'AVAX': btc_ret*beta*1.10}
pnl_pct = sum(balanced_w[k] * shocks[a] for k, a in enumerate(ASSETS))
grid[i, j] = pnl_pct * 100
heat_df = pd.DataFrame(grid,
index=[f'{r:.0%}' for r in btc_returns],
columns=[f'{b:.1f}x' for b in alt_betas])
print('Heat map built.')Heat map built.
Section 5 — Visualisation
The left panel is the scenario heat map — each cell is the Balanced portfolio return for that BTC return and altcoin beta combination. Green = profit, red = loss. The right panel is a grouped bar chart comparing all three portfolios' exposure to the most extreme scenarios — useful for communicating risk to stakeholders.
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
fig.suptitle('Scenario Analysis Engine', fontsize=13, fontweight='bold')
ax1 = axes[0]
sns.heatmap(heat_df, ax=ax1, cmap='RdYlGn', center=0,
annot=True, fmt='.0f', linewidths=0.4, cbar_kws={'label': 'Return %'},
annot_kws={'size': 7})
ax1.set_xlabel('Altcoin Beta to BTC'); ax1.set_ylabel('BTC Return')
ax1.set_title('Balanced Portfolio: BTC Return × Altcoin Beta')
ax2 = axes[1]
top_scenarios = pivot_sorted.head(5).index.tolist() + pivot_sorted.tail(3).index.tolist()
sub = pivot_sorted.loc[top_scenarios]
x = np.arange(len(sub))
w = 0.25
for i, (port, color) in enumerate(zip(PORTFOLIOS.keys(), ['#e53935','#1976d2','#43a047'])):
ax2.bar(x + i*w, sub[port], w, label=port, color=color, alpha=0.8)
ax2.set_xticks(x + w); ax2.set_xticklabels(sub.index, rotation=35, ha='right', fontsize=8)
ax2.axhline(0, color='black', lw=0.8)
ax2.set_ylabel('Return (%)')
ax2.legend(fontsize=9)
ax2.set_title('Scenario Impact by Portfolio Type')
plt.tight_layout(); plt.show()Section 6 — Export
Save the full scenario results matrix and the 2D heat map grid as CSV files.
results_df.to_csv('scenario_analysis_engine.csv', index=False)
heat_df.to_csv('scenario_heatmap.csv')
print('Saved: scenario_analysis_engine.csv, scenario_heatmap.csv')Saved: scenario_analysis_engine.csv, scenario_heatmap.csv
Conclusion
This notebook provides a comprehensive framework for scenario analysis, allowing users to evaluate portfolio performance under various market conditions. By defining a library of scenarios, computing P&L across different strategies, and visualizing outcomes through heat maps and bar charts, it offers a robust tool for understanding and communicating portfolio risk. The ability to export results facilitates further analysis and reporting.