410 lines
16 KiB
Plaintext
410 lines
16 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": [
|
|
"# Risk Tranche System\n",
|
|
"\n",
|
|
"Explores the three-tranche risk layering system in MycoFi:\n",
|
|
"\n",
|
|
"- **myUSD-S (Senior)**: Lowest risk, first collateral claim, 3% APY target\n",
|
|
"- **myUSD-M (Mezzanine)**: Medium risk, second claim, 8% APY target\n",
|
|
"- **$MYCO (Junior/Equity)**: Highest risk, residual claim, absorbs volatility\n",
|
|
"\n",
|
|
"Yield waterfall: staking rewards flow Senior → Mezzanine → Junior.\n",
|
|
"Loss waterfall: losses absorbed Junior → Mezzanine → Senior."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import numpy as np\n",
|
|
"import matplotlib.pyplot as plt\n",
|
|
"import matplotlib.patches as mpatches\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"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 1. System Setup\n",
|
|
"\n",
|
|
"Create a `TrancheParams` and `RiskTrancheSystem`, deposit \\$1M collateral, then mint all three tranches."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from src.primitives.risk_tranching import (\n",
|
|
" TrancheParams,\n",
|
|
" RiskTrancheSystem,\n",
|
|
" TrancheState,\n",
|
|
" deposit_collateral,\n",
|
|
" mint_tranche,\n",
|
|
" mint_capacity,\n",
|
|
" distribute_yield,\n",
|
|
" apply_loss,\n",
|
|
" check_liquidation,\n",
|
|
" get_tranche_metrics,\n",
|
|
")\n",
|
|
"\n",
|
|
"# Instantiate system\n",
|
|
"params = TrancheParams(\n",
|
|
" senior_collateral_ratio=1.5,\n",
|
|
" mezzanine_collateral_ratio=1.2,\n",
|
|
" senior_yield_target=0.03,\n",
|
|
" mezzanine_yield_target=0.08,\n",
|
|
" max_senior_fraction=0.50,\n",
|
|
" max_mezzanine_fraction=0.30,\n",
|
|
")\n",
|
|
"\n",
|
|
"from dataclasses import field\n",
|
|
"\n",
|
|
"system = RiskTrancheSystem(\n",
|
|
" params=params,\n",
|
|
" senior=TrancheState(name=\"myUSD-S\"),\n",
|
|
" mezzanine=TrancheState(name=\"myUSD-M\"),\n",
|
|
" junior=TrancheState(name=\"$MYCO\"),\n",
|
|
")\n",
|
|
"\n",
|
|
"# Deposit $1M collateral\n",
|
|
"system = deposit_collateral(system, 1_000_000.0)\n",
|
|
"print(f\"Total collateral after deposit: ${system.total_collateral:,.0f}\")\n",
|
|
"\n",
|
|
"# Check mint capacity\n",
|
|
"caps = mint_capacity(system)\n",
|
|
"print(f\"\\nMint capacity:\")\n",
|
|
"for tranche, cap in caps.items():\n",
|
|
" print(f\" {tranche:12s}: ${cap:,.0f}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Mint all three tranches\n",
|
|
"system, senior_minted = mint_tranche(system, \"senior\", 300_000.0)\n",
|
|
"system, mez_minted = mint_tranche(system, \"mezzanine\", 150_000.0)\n",
|
|
"system, junior_minted = mint_tranche(system, \"junior\", 100_000.0)\n",
|
|
"\n",
|
|
"print(f\"Minted:\")\n",
|
|
"print(f\" myUSD-S (Senior): ${senior_minted:>12,.0f}\")\n",
|
|
"print(f\" myUSD-M (Mezzanine): ${mez_minted:>12,.0f}\")\n",
|
|
"print(f\" $MYCO (Junior): ${junior_minted:>12,.0f}\")\n",
|
|
"\n",
|
|
"metrics = get_tranche_metrics(system)\n",
|
|
"print(f\"\\nSystem collateral ratio: {metrics['system_cr']:.3f}\")\n",
|
|
"print(f\"Senior CR: {metrics['senior']['cr']:.3f}\")\n",
|
|
"print(f\"Mezzanine CR: {metrics['mezzanine']['cr']:.3f}\")\n",
|
|
"print(f\"Junior CR: {metrics['junior']['cr']:.3f}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 2. 365-Day Yield Simulation\n",
|
|
"\n",
|
|
"Simulate 365 daily steps with a 4% staking APY applied to total collateral.\n",
|
|
"Record tranche supplies, collateral ratios, and cumulative yield at each step."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import copy\n",
|
|
"\n",
|
|
"DAYS = 365\n",
|
|
"DT = 1.0 / 365 # One day in years\n",
|
|
"STAKING_APY = 0.04 # 4% annual staking yield\n",
|
|
"\n",
|
|
"# Snapshot function\n",
|
|
"def snapshot(sys, day):\n",
|
|
" m = get_tranche_metrics(sys)\n",
|
|
" return {\n",
|
|
" \"day\": day,\n",
|
|
" \"total_collateral\": sys.total_collateral,\n",
|
|
" \"system_cr\": m[\"system_cr\"],\n",
|
|
" \"senior_supply\": m[\"senior\"][\"supply\"],\n",
|
|
" \"senior_cr\": m[\"senior\"][\"cr\"],\n",
|
|
" \"senior_yield\": m[\"senior\"][\"yield\"],\n",
|
|
" \"mez_supply\": m[\"mezzanine\"][\"supply\"],\n",
|
|
" \"mez_cr\": m[\"mezzanine\"][\"cr\"],\n",
|
|
" \"mez_yield\": m[\"mezzanine\"][\"yield\"],\n",
|
|
" \"junior_supply\": m[\"junior\"][\"supply\"],\n",
|
|
" \"junior_cr\": m[\"junior\"][\"cr\"],\n",
|
|
" \"junior_yield\": m[\"junior\"][\"yield\"],\n",
|
|
" }\n",
|
|
"\n",
|
|
"# Deep-copy system so we can reuse the initial state for the stress test\n",
|
|
"sim_system = copy.deepcopy(system)\n",
|
|
"history = [snapshot(sim_system, 0)]\n",
|
|
"\n",
|
|
"for day in range(1, DAYS + 1):\n",
|
|
" daily_yield = sim_system.total_collateral * STAKING_APY * DT\n",
|
|
" sim_system = distribute_yield(sim_system, daily_yield, DT)\n",
|
|
" history.append(snapshot(sim_system, day))\n",
|
|
"\n",
|
|
"days = [h[\"day\"] for h in history]\n",
|
|
"senior_sup = [h[\"senior_supply\"] for h in history]\n",
|
|
"mez_sup = [h[\"mez_supply\"] for h in history]\n",
|
|
"junior_sup = [h[\"junior_supply\"] for h in history]\n",
|
|
"senior_cr = [h[\"senior_cr\"] for h in history]\n",
|
|
"mez_cr = [h[\"mez_cr\"] for h in history]\n",
|
|
"junior_cr = [h[\"junior_cr\"] for h in history]\n",
|
|
"system_cr = [h[\"system_cr\"] for h in history]\n",
|
|
"senior_yield = [h[\"senior_yield\"] for h in history]\n",
|
|
"mez_yield = [h[\"mez_yield\"] for h in history]\n",
|
|
"junior_yield = [h[\"junior_yield\"] for h in history]\n",
|
|
"\n",
|
|
"print(f\"Final day 365 cumulative yield:\")\n",
|
|
"print(f\" Senior: ${senior_yield[-1]:>12,.0f}\")\n",
|
|
"print(f\" Mezzanine: ${mez_yield[-1]:>12,.0f}\")\n",
|
|
"print(f\" Junior: ${junior_yield[-1]:>12,.0f}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 3. Visualizations — Normal Operation"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n",
|
|
"\n",
|
|
"# --- Plot 1: Tranche supplies over time ---\n",
|
|
"ax = axes[0]\n",
|
|
"ax.stackplot(days, senior_sup, mez_sup, junior_sup,\n",
|
|
" labels=['myUSD-S (Senior)', 'myUSD-M (Mezzanine)', '$MYCO (Junior)'],\n",
|
|
" colors=['#2ecc71', '#f39c12', '#e74c3c'], alpha=0.8)\n",
|
|
"ax.set_xlabel('Day')\n",
|
|
"ax.set_ylabel('Token Supply ($)')\n",
|
|
"ax.set_title('Tranche Supplies Over Time')\n",
|
|
"ax.legend(loc='upper left', fontsize=8)\n",
|
|
"ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'${x/1e6:.2f}M'))\n",
|
|
"\n",
|
|
"# --- Plot 2: Collateral ratios ---\n",
|
|
"ax = axes[1]\n",
|
|
"ax.plot(days, senior_cr, color='#2ecc71', label='Senior CR')\n",
|
|
"ax.plot(days, mez_cr, color='#f39c12', label='Mezzanine CR')\n",
|
|
"ax.plot(days, junior_cr, color='#e74c3c', label='Junior CR')\n",
|
|
"ax.plot(days, system_cr, color='#3498db', linestyle='--', label='System CR')\n",
|
|
"ax.axhline(1.5, color='#2ecc71', linestyle=':', alpha=0.5, label='Senior min CR')\n",
|
|
"ax.axhline(1.2, color='#f39c12', linestyle=':', alpha=0.5, label='Mezzanine min CR')\n",
|
|
"ax.set_xlabel('Day')\n",
|
|
"ax.set_ylabel('Collateral Ratio')\n",
|
|
"ax.set_title('Collateral Ratios by Tranche')\n",
|
|
"ax.legend(fontsize=7)\n",
|
|
"\n",
|
|
"# --- Plot 3: Cumulative yield waterfall ---\n",
|
|
"ax = axes[2]\n",
|
|
"ax.stackplot(days, senior_yield, mez_yield, junior_yield,\n",
|
|
" labels=['Senior yield', 'Mezzanine yield', 'Junior yield'],\n",
|
|
" colors=['#2ecc71', '#f39c12', '#e74c3c'], alpha=0.8)\n",
|
|
"ax.set_xlabel('Day')\n",
|
|
"ax.set_ylabel('Cumulative Yield ($)')\n",
|
|
"ax.set_title('Cumulative Yield Waterfall')\n",
|
|
"ax.legend(loc='upper left', fontsize=8)\n",
|
|
"ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'${x/1e3:.1f}K'))\n",
|
|
"\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 4. Stress Test — 40% Collateral Loss at Day 180\n",
|
|
"\n",
|
|
"At day 180 we simulate a sudden 40% drop in collateral value (e.g. correlated LST slashing or depeg).\n",
|
|
"The loss waterfall means Junior absorbs first, then Mezzanine, then Senior as a last resort."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"STRESS_DAY = 180\n",
|
|
"LOSS_FRACTION = 0.40\n",
|
|
"\n",
|
|
"stress_system = copy.deepcopy(system) # Start fresh from initial mint\n",
|
|
"stress_history = [snapshot(stress_system, 0)]\n",
|
|
"\n",
|
|
"for day in range(1, DAYS + 1):\n",
|
|
" # Apply daily staking yield\n",
|
|
" daily_yield = stress_system.total_collateral * STAKING_APY * DT\n",
|
|
" stress_system = distribute_yield(stress_system, daily_yield, DT)\n",
|
|
"\n",
|
|
" # Apply 40% loss at day 180\n",
|
|
" if day == STRESS_DAY:\n",
|
|
" loss = stress_system.total_collateral * LOSS_FRACTION\n",
|
|
" stress_system = apply_loss(stress_system, loss)\n",
|
|
" print(f\"Day {day}: Applied ${loss:,.0f} loss ({LOSS_FRACTION*100:.0f}% of collateral)\")\n",
|
|
" liq = check_liquidation(stress_system)\n",
|
|
" print(f\" Liquidation flags: {liq}\")\n",
|
|
" m = get_tranche_metrics(stress_system)\n",
|
|
" print(f\" Junior cumulative losses: ${m['junior']['losses']:,.0f}\")\n",
|
|
" print(f\" Mezzanine cumulative losses: ${m['mezzanine']['losses']:,.0f}\")\n",
|
|
" print(f\" Senior cumulative losses: ${m['senior']['losses']:,.0f}\")\n",
|
|
"\n",
|
|
" stress_history.append(snapshot(stress_system, day))\n",
|
|
"\n",
|
|
"s_days = [h[\"day\"] for h in stress_history]\n",
|
|
"s_senior_cr = [h[\"senior_cr\"] for h in stress_history]\n",
|
|
"s_mez_cr = [h[\"mez_cr\"] for h in stress_history]\n",
|
|
"s_junior_cr = [h[\"junior_cr\"] for h in stress_history]\n",
|
|
"s_system_cr = [h[\"system_cr\"] for h in stress_history]\n",
|
|
"s_total_col = [h[\"total_collateral\"] for h in stress_history]"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"fig, axes = plt.subplots(1, 2, figsize=(16, 6))\n",
|
|
"\n",
|
|
"# --- Plot 1: Collateral ratios under stress ---\n",
|
|
"ax = axes[0]\n",
|
|
"ax.plot(s_days, s_senior_cr, color='#2ecc71', linewidth=2, label='Senior CR')\n",
|
|
"ax.plot(s_days, s_mez_cr, color='#f39c12', linewidth=2, label='Mezzanine CR')\n",
|
|
"ax.plot(s_days, s_junior_cr, color='#e74c3c', linewidth=2, label='Junior CR')\n",
|
|
"ax.plot(s_days, s_system_cr, color='#3498db', linestyle='--', linewidth=1.5, label='System CR')\n",
|
|
"ax.axvline(STRESS_DAY, color='black', linestyle='--', alpha=0.7, label=f'Loss event (day {STRESS_DAY})')\n",
|
|
"ax.axhline(1.0, color='gray', linestyle=':', alpha=0.5, label='CR = 1.0 (par)')\n",
|
|
"ax.set_xlabel('Day')\n",
|
|
"ax.set_ylabel('Collateral Ratio')\n",
|
|
"ax.set_title(f'Collateral Ratios — 40% Loss at Day {STRESS_DAY}')\n",
|
|
"ax.legend(fontsize=8)\n",
|
|
"ax.set_xlim(0, DAYS)\n",
|
|
"\n",
|
|
"# --- Plot 2: Total collateral and loss absorption breakdown ---\n",
|
|
"ax = axes[1]\n",
|
|
"ax.fill_between(s_days, 0, s_total_col, color='#3498db', alpha=0.4, label='Total Collateral')\n",
|
|
"ax.plot(s_days, s_total_col, color='#3498db', linewidth=2)\n",
|
|
"\n",
|
|
"# Overlay the loss waterfall at the shock event\n",
|
|
"shock_day_idx = STRESS_DAY\n",
|
|
"m_before = get_tranche_metrics(copy.deepcopy(stress_system))\n",
|
|
"stress_snap = stress_history[STRESS_DAY]\n",
|
|
"\n",
|
|
"ax.axvline(STRESS_DAY, color='black', linestyle='--', alpha=0.7, label=f'Shock (day {STRESS_DAY})')\n",
|
|
"ax.set_xlabel('Day')\n",
|
|
"ax.set_ylabel('Total Collateral ($)')\n",
|
|
"ax.set_title('Total Collateral Value Under Stress')\n",
|
|
"ax.legend(fontsize=8)\n",
|
|
"ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'${x/1e6:.2f}M'))\n",
|
|
"ax.set_xlim(0, DAYS)\n",
|
|
"\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 5. Loss Absorption Bar Chart\n",
|
|
"\n",
|
|
"Compare the cumulative losses absorbed by each tranche after the stress event."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"final_metrics = get_tranche_metrics(stress_system)\n",
|
|
"\n",
|
|
"tranches = ['Junior ($MYCO)', 'Mezzanine (myUSD-M)', 'Senior (myUSD-S)']\n",
|
|
"losses_abs = [\n",
|
|
" final_metrics['junior']['losses'],\n",
|
|
" final_metrics['mezzanine']['losses'],\n",
|
|
" final_metrics['senior']['losses'],\n",
|
|
"]\n",
|
|
"yields_acc = [\n",
|
|
" final_metrics['junior']['yield'],\n",
|
|
" final_metrics['mezzanine']['yield'],\n",
|
|
" final_metrics['senior']['yield'],\n",
|
|
"]\n",
|
|
"\n",
|
|
"fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
|
|
"\n",
|
|
"colors = ['#e74c3c', '#f39c12', '#2ecc71']\n",
|
|
"\n",
|
|
"ax = axes[0]\n",
|
|
"bars = ax.bar(tranches, losses_abs, color=colors, alpha=0.85, edgecolor='black', linewidth=0.7)\n",
|
|
"for bar, val in zip(bars, losses_abs):\n",
|
|
" ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 500,\n",
|
|
" f'${val:,.0f}', ha='center', va='bottom', fontsize=9)\n",
|
|
"ax.set_ylabel('Cumulative Losses Absorbed ($)')\n",
|
|
"ax.set_title('Loss Absorption by Tranche (End of Simulation)')\n",
|
|
"ax.tick_params(axis='x', labelsize=9)\n",
|
|
"\n",
|
|
"ax = axes[1]\n",
|
|
"bars = ax.bar(tranches, yields_acc, color=colors, alpha=0.85, edgecolor='black', linewidth=0.7)\n",
|
|
"for bar, val in zip(bars, yields_acc):\n",
|
|
" ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 50,\n",
|
|
" f'${val:,.0f}', ha='center', va='bottom', fontsize=9)\n",
|
|
"ax.set_ylabel('Cumulative Yield Received ($)')\n",
|
|
"ax.set_title('Yield Distribution by Tranche (End of Simulation)')\n",
|
|
"ax.tick_params(axis='x', labelsize=9)\n",
|
|
"\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()\n",
|
|
"\n",
|
|
"print(\"\\nFinal system state:\")\n",
|
|
"print(f\" Total collateral: ${final_metrics['total_collateral']:>12,.0f}\")\n",
|
|
"print(f\" System CR: {final_metrics['system_cr']:.4f}\")\n",
|
|
"print(f\" Senior CR: {final_metrics['senior']['cr']:.4f} (healthy: {final_metrics['senior']['healthy']})\")\n",
|
|
"print(f\" Mezzanine CR: {final_metrics['mezzanine']['cr']:.4f} (healthy: {final_metrics['mezzanine']['healthy']})\")\n",
|
|
"print(f\" Junior CR: {final_metrics['junior']['cr']:.4f} (healthy: {final_metrics['junior']['healthy']})\")"
|
|
]
|
|
}
|
|
]
|
|
}
|