361 lines
12 KiB
Plaintext
361 lines
12 KiB
Plaintext
{
|
||
"nbformat": 4,
|
||
"nbformat_minor": 4,
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "Python 3",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"name": "python",
|
||
"version": "3.11.0"
|
||
}
|
||
},
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# cadCAD Full System Monte Carlo\n",
|
||
"\n",
|
||
"Runs the full MycoFi system through cadCAD with Monte Carlo sampling to characterise\n",
|
||
"the distribution of outcomes under normal growth and stress scenarios.\n",
|
||
"\n",
|
||
"**Scenarios:**\n",
|
||
"- `scenario_normal_growth`: Steady deposits, mild ETH volatility, governance ticking — 3 MC runs over 180 days\n",
|
||
"- `scenario_stress_test`: ETH crash at day 30, bank run follows — 5 MC runs over 100 days\n",
|
||
"\n",
|
||
"**Outputs:**\n",
|
||
"- Fan charts (mean ± 1σ) for supply, collateral, and system CR\n",
|
||
"- Tranche CR comparison under stress vs normal\n",
|
||
"- Summary statistics table"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import numpy as np\n",
|
||
"import pandas as pd\n",
|
||
"import matplotlib.pyplot as plt\n",
|
||
"import matplotlib.ticker as mticker\n",
|
||
"import sys\n",
|
||
"import os\n",
|
||
"\n",
|
||
"sys.path.insert(0, os.path.abspath('..'))\n",
|
||
"\n",
|
||
"%matplotlib inline\n",
|
||
"plt.rcParams['figure.figsize'] = (14, 5)\n",
|
||
"plt.rcParams['figure.dpi'] = 100\n",
|
||
"pd.set_option('display.float_format', '{:,.2f}'.format)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 1. Run Normal Growth Scenario\n",
|
||
"\n",
|
||
"3 Monte Carlo runs, 180 timesteps (one per day). Each run uses the same system configuration\n",
|
||
"but different random seeds for deposit arrivals and price movements."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from src.cadcad.config import scenario_normal_growth, scenario_stress_test\n",
|
||
"\n",
|
||
"print(\"Running normal growth scenario (3 runs × 180 days)...\")\n",
|
||
"df_normal = scenario_normal_growth(timesteps=180, runs=3)\n",
|
||
"print(f\"Done. DataFrame shape: {df_normal.shape}\")\n",
|
||
"print(f\"Columns: {list(df_normal.columns)}\")\n",
|
||
"df_normal.head()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 2. Fan Charts — Normal Growth\n",
|
||
"\n",
|
||
"Plot supply, total collateral, and system collateral ratio across all Monte Carlo runs.\n",
|
||
"Shade the 1σ band around the mean to show the spread of outcomes."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def fan_chart(ax, df, col, runs, color, label, formatter=None):\n",
|
||
" \"\"\"Plot a fan chart: individual run traces + mean ± 1σ band.\"\"\"\n",
|
||
" # Get all timesteps\n",
|
||
" timesteps = sorted(df['timestep'].unique())\n",
|
||
"\n",
|
||
" # Collect values per timestep across runs\n",
|
||
" per_ts = []\n",
|
||
" for ts in timesteps:\n",
|
||
" # Take the last substep only (final state per timestep)\n",
|
||
" sub = df[df['timestep'] == ts]\n",
|
||
" if 'substep' in sub.columns:\n",
|
||
" sub = sub[sub['substep'] == sub['substep'].max()]\n",
|
||
" vals = sub[col].values\n",
|
||
" per_ts.append(vals)\n",
|
||
"\n",
|
||
" per_ts = np.array(per_ts, dtype=float) # shape: (timesteps, runs)\n",
|
||
" mean = np.nanmean(per_ts, axis=1)\n",
|
||
" std = np.nanstd(per_ts, axis=1)\n",
|
||
" t = np.array(timesteps, dtype=float)\n",
|
||
"\n",
|
||
" # Individual run traces (thin)\n",
|
||
" for r in range(per_ts.shape[1]):\n",
|
||
" ax.plot(t, per_ts[:, r], color=color, linewidth=0.8, alpha=0.4)\n",
|
||
"\n",
|
||
" # Mean line\n",
|
||
" ax.plot(t, mean, color=color, linewidth=2.5, label=f'{label} (mean)')\n",
|
||
"\n",
|
||
" # ±1σ band\n",
|
||
" ax.fill_between(t, mean - std, mean + std, color=color, alpha=0.15, label=f'{label} ±1σ')\n",
|
||
"\n",
|
||
" if formatter:\n",
|
||
" ax.yaxis.set_major_formatter(formatter)\n",
|
||
"\n",
|
||
" return mean, std\n",
|
||
"\n",
|
||
"\n",
|
||
"fig, axes = plt.subplots(1, 3, figsize=(20, 6))\n",
|
||
"\n",
|
||
"runs = sorted(df_normal['run'].unique()) if 'run' in df_normal.columns else [0]\n",
|
||
"\n",
|
||
"# --- Total supply ---\n",
|
||
"fan_chart(axes[0], df_normal, 'total_supply', runs, '#3498db', 'Total Supply',\n",
|
||
" mticker.FuncFormatter(lambda x, _: f'${x/1e3:.0f}K'))\n",
|
||
"axes[0].set_xlabel('Timestep (day)')\n",
|
||
"axes[0].set_ylabel('Total Supply ($)')\n",
|
||
"axes[0].set_title('Total Token Supply — Normal Growth')\n",
|
||
"axes[0].legend(fontsize=8)\n",
|
||
"\n",
|
||
"# --- Total collateral ---\n",
|
||
"fan_chart(axes[1], df_normal, 'total_collateral_usd', runs, '#2ecc71', 'Total Collateral',\n",
|
||
" mticker.FuncFormatter(lambda x, _: f'${x/1e6:.2f}M'))\n",
|
||
"axes[1].set_xlabel('Timestep (day)')\n",
|
||
"axes[1].set_ylabel('Total Collateral (USD)')\n",
|
||
"axes[1].set_title('Total Collateral — Normal Growth')\n",
|
||
"axes[1].legend(fontsize=8)\n",
|
||
"\n",
|
||
"# --- System CR ---\n",
|
||
"fan_chart(axes[2], df_normal, 'system_cr', runs, '#e74c3c', 'System CR')\n",
|
||
"axes[2].axhline(1.0, color='black', linestyle='--', alpha=0.5, label='CR = 1.0 (par)')\n",
|
||
"axes[2].set_xlabel('Timestep (day)')\n",
|
||
"axes[2].set_ylabel('Collateral Ratio')\n",
|
||
"axes[2].set_title('System Collateral Ratio — Normal Growth')\n",
|
||
"axes[2].legend(fontsize=8)\n",
|
||
"\n",
|
||
"plt.tight_layout()\n",
|
||
"plt.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 3. Run Stress Test Scenario\n",
|
||
"\n",
|
||
"5 Monte Carlo runs, 100 timesteps. ETH crashes at day 30, bank run starts at day 31."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(\"Running stress test scenario (5 runs × 100 days)...\")\n",
|
||
"df_stress = scenario_stress_test(timesteps=100, runs=5)\n",
|
||
"print(f\"Done. DataFrame shape: {df_stress.shape}\")\n",
|
||
"df_stress.head()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 4. Tranche CRs Under Stress vs Normal\n",
|
||
"\n",
|
||
"Compare the collateral ratios for each tranche under both scenarios.\n",
|
||
"Senior should remain most protected; Junior absorbs the most volatility."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"fig, axes = plt.subplots(1, 3, figsize=(20, 6))\n",
|
||
"\n",
|
||
"tranche_cols = [\n",
|
||
" ('senior_cr', '#2ecc71', 'Senior (myUSD-S)'),\n",
|
||
" ('mezzanine_cr', '#f39c12', 'Mezzanine (myUSD-M)'),\n",
|
||
" ('junior_cr', '#e74c3c', 'Junior ($MYCO)'),\n",
|
||
"]\n",
|
||
"\n",
|
||
"# Use only the last 100 timesteps of the normal run for comparison\n",
|
||
"df_normal_sub = df_normal[df_normal['timestep'] <= 100].copy()\n",
|
||
"runs_stress = sorted(df_stress['run'].unique()) if 'run' in df_stress.columns else [0]\n",
|
||
"runs_normal = sorted(df_normal_sub['run'].unique()) if 'run' in df_normal_sub.columns else [0]\n",
|
||
"\n",
|
||
"for ax, (col, color, title) in zip(axes, tranche_cols):\n",
|
||
" # Stress scenario fan\n",
|
||
" fan_chart(ax, df_stress, col, runs_stress, '#e74c3c', 'Stress')\n",
|
||
" # Normal scenario fan (overlaid)\n",
|
||
" fan_chart(ax, df_normal_sub, col, runs_normal, '#2ecc71', 'Normal')\n",
|
||
"\n",
|
||
" ax.axhline(1.0, color='black', linestyle=':', alpha=0.6, label='CR = 1.0')\n",
|
||
" ax.axvline(30, color='gray', linestyle='--', alpha=0.5, label='Shock day 30')\n",
|
||
" ax.set_xlabel('Timestep (day)')\n",
|
||
" ax.set_ylabel('Collateral Ratio')\n",
|
||
" ax.set_title(f'{title} CR')\n",
|
||
" ax.legend(fontsize=7)\n",
|
||
"\n",
|
||
"plt.tight_layout()\n",
|
||
"plt.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 5. Summary Statistics Table\n",
|
||
"\n",
|
||
"Aggregate key metrics at the final timestep for both scenarios, summarised across all MC runs."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"metrics_of_interest = [\n",
|
||
" 'total_supply', 'total_collateral_usd', 'system_cr',\n",
|
||
" 'senior_cr', 'mezzanine_cr', 'junior_cr',\n",
|
||
" 'total_yield', 'ccip_messages', 'proposals_passed',\n",
|
||
"]\n",
|
||
"\n",
|
||
"def final_timestep_stats(df, label):\n",
|
||
" \"\"\"Extract final-timestep rows and compute summary statistics.\"\"\"\n",
|
||
" max_ts = df['timestep'].max()\n",
|
||
" final = df[df['timestep'] == max_ts]\n",
|
||
" if 'substep' in final.columns:\n",
|
||
" final = final[final['substep'] == final['substep'].max()]\n",
|
||
"\n",
|
||
" cols = [c for c in metrics_of_interest if c in final.columns]\n",
|
||
" stats = final[cols].agg(['mean', 'std', 'min', 'max']).T\n",
|
||
" stats.columns = [f'{label}_mean', f'{label}_std', f'{label}_min', f'{label}_max']\n",
|
||
" return stats\n",
|
||
"\n",
|
||
"normal_stats = final_timestep_stats(df_normal, 'normal')\n",
|
||
"stress_stats = final_timestep_stats(df_stress, 'stress')\n",
|
||
"\n",
|
||
"summary = pd.concat([normal_stats, stress_stats], axis=1)\n",
|
||
"print(\"=\" * 80)\n",
|
||
"print(\"Final Timestep Summary Statistics\")\n",
|
||
"print(\"=\" * 80)\n",
|
||
"print(summary.to_string())"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Display as a formatted DataFrame\n",
|
||
"display_cols = [\n",
|
||
" 'normal_mean', 'normal_std',\n",
|
||
" 'stress_mean', 'stress_std',\n",
|
||
"]\n",
|
||
"display_rows = [\n",
|
||
" 'total_supply', 'total_collateral_usd', 'system_cr',\n",
|
||
" 'senior_cr', 'mezzanine_cr', 'junior_cr',\n",
|
||
" 'total_yield',\n",
|
||
"]\n",
|
||
"\n",
|
||
"display_df = summary[[c for c in display_cols if c in summary.columns]]\n",
|
||
"display_df = display_df.loc[[r for r in display_rows if r in display_df.index]]\n",
|
||
"\n",
|
||
"rename_map = {\n",
|
||
" 'total_supply': 'Total Token Supply ($)',\n",
|
||
" 'total_collateral_usd': 'Total Collateral (USD)',\n",
|
||
" 'system_cr': 'System Collateral Ratio',\n",
|
||
" 'senior_cr': 'Senior CR (myUSD-S)',\n",
|
||
" 'mezzanine_cr': 'Mezzanine CR (myUSD-M)',\n",
|
||
" 'junior_cr': 'Junior CR ($MYCO)',\n",
|
||
" 'total_yield': 'Cumulative Yield (USD)',\n",
|
||
"}\n",
|
||
"display_df.index = [rename_map.get(i, i) for i in display_df.index]\n",
|
||
"\n",
|
||
"col_rename = {\n",
|
||
" 'normal_mean': 'Normal (mean)',\n",
|
||
" 'normal_std': 'Normal (±1σ)',\n",
|
||
" 'stress_mean': 'Stress (mean)',\n",
|
||
" 'stress_std': 'Stress (±1σ)',\n",
|
||
"}\n",
|
||
"display_df = display_df.rename(columns=col_rename)\n",
|
||
"\n",
|
||
"display_df"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 6. Normal vs Stress Supply and Collateral Comparison"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"fig, axes = plt.subplots(1, 2, figsize=(16, 6))\n",
|
||
"\n",
|
||
"# --- Total collateral: normal vs stress ---\n",
|
||
"ax = axes[0]\n",
|
||
"fan_chart(ax, df_normal_sub, 'total_collateral_usd', runs_normal, '#2ecc71', 'Normal',\n",
|
||
" mticker.FuncFormatter(lambda x, _: f'${x/1e6:.2f}M'))\n",
|
||
"fan_chart(ax, df_stress, 'total_collateral_usd', runs_stress, '#e74c3c', 'Stress',\n",
|
||
" mticker.FuncFormatter(lambda x, _: f'${x/1e6:.2f}M'))\n",
|
||
"ax.axvline(30, color='gray', linestyle='--', alpha=0.6, label='Shock day 30')\n",
|
||
"ax.set_xlabel('Timestep (day)')\n",
|
||
"ax.set_ylabel('Total Collateral (USD)')\n",
|
||
"ax.set_title('Total Collateral: Normal vs Stress')\n",
|
||
"ax.legend(fontsize=8)\n",
|
||
"\n",
|
||
"# --- System CR: normal vs stress ---\n",
|
||
"ax = axes[1]\n",
|
||
"fan_chart(ax, df_normal_sub, 'system_cr', runs_normal, '#2ecc71', 'Normal')\n",
|
||
"fan_chart(ax, df_stress, 'system_cr', runs_stress, '#e74c3c', 'Stress')\n",
|
||
"ax.axhline(1.0, color='black', linestyle=':', alpha=0.6, label='CR = 1.0 (par)')\n",
|
||
"ax.axvline(30, color='gray', linestyle='--', alpha=0.6, label='Shock day 30')\n",
|
||
"ax.set_xlabel('Timestep (day)')\n",
|
||
"ax.set_ylabel('System Collateral Ratio')\n",
|
||
"ax.set_title('System CR: Normal vs Stress')\n",
|
||
"ax.legend(fontsize=8)\n",
|
||
"\n",
|
||
"plt.tight_layout()\n",
|
||
"plt.show()"
|
||
]
|
||
}
|
||
]
|
||
}
|