Portfolio & Risk·Portfolio Risk Analysis·Intermediate

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.

portfolio-risk-analysisrisk-management

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:

  1. Defines a comprehensive scenario library (base + stressed)
  2. Computes portfolio P&L for each scenario across multiple portfolio strategies
  3. Builds a 2D scenario heat map (BTC return × portfolio vol multiplier)
  4. Runs a Monte Carlo scenario distribution to show the range of outcomes
  5. Ranks scenarios by portfolio impact
  6. Exports the full scenario matrix
[ ]
!pip install numpy pandas matplotlib seaborn scipy yfinance --quiet
[ ]
import 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  # annualised
Synthetic 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()
cell output

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.

Scenario Analysis Engine · BitPredict