Skip to content

Commit 627737c

Browse files
authored
Merge pull request #15 from second-state/feat/svb-backtest-example
feat: add SVB bank run backtest example
2 parents d9dc428 + 1eae354 commit 627737c

2 files changed

Lines changed: 234 additions & 0 deletions

File tree

examples/backtest/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Historical trading simulations with forward PnL analysis. Each script simulates
1010
| `ftx_crypto_contagion.py` | 2022-11-08 | Crypto contagion hedge | Short BTC + Short ETH + Long GOLD | BTC -15%, ETH -18%; crypto shorts drove profit |
1111
| `nvda_earnings_alpha.py` | 2023-05-25 | Sector alpha pair | Long NVDA + Short SP500 | NVDA +5% while SP500 flat; pure alpha |
1212
| `ukraine_oil_shock.py` | 2022-02-24 | Commodity supply shock | Long OIL + Long GOLD + Short SP500 | Oil surged 16% in 7 days on sanctions |
13+
| `svb_crypto_haven.py` | 2023-03-13 | Crypto as digital gold | Long BTC + Long ETH + Long GOLD + Short SP500 | BTC +15%, portfolio +4.8% in 7 days on banking crisis |
1314

1415
## Setup
1516

@@ -27,6 +28,7 @@ python3 examples/backtest/covid_crash_hedge.py
2728
python3 examples/backtest/ftx_crypto_contagion.py
2829
python3 examples/backtest/nvda_earnings_alpha.py
2930
python3 examples/backtest/ukraine_oil_shock.py
31+
python3 examples/backtest/svb_crypto_haven.py
3032

3133
# Override binary path
3234
python3 examples/backtest/covid_crash_hedge.py --backtest /path/to/backtest
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#!/usr/bin/env python3
2+
"""
3+
SVB Bank Run — Crypto as Digital Gold
4+
=======================================
5+
6+
Date: March 13, 2023 (Monday after Silicon Valley Bank collapsed)
7+
8+
Thesis: On March 10, 2023, Silicon Valley Bank was seized by the FDIC —
9+
the second-largest bank failure in US history. Signature Bank followed
10+
on March 12. Traditional finance was in crisis. The contrarian bet:
11+
crypto isn't just speculative — it's a decentralized alternative to
12+
a fractured banking system. Meanwhile, gold catches safe-haven flows
13+
and equities face contagion risk.
14+
15+
Starting capital: $1,000 split evenly across 4 legs ($250 each):
16+
1. Long BTC — digital gold, decentralized store of value ($250)
17+
2. Long ETH — DeFi as banking alternative ($250)
18+
3. Long GOLD — traditional safe haven ($250)
19+
4. Short SP500 — banking contagion risk ($250)
20+
21+
The thesis is that all four legs win in a banking crisis: crypto
22+
rallies on "be your own bank" narrative, gold on fear, equities
23+
on contagion. If crypto fails to rally, gold and the short hedge
24+
limit the damage.
25+
26+
Usage: python3 examples/backtest/svb_crypto_haven.py
27+
"""
28+
29+
import argparse
30+
import json
31+
import os
32+
import subprocess
33+
import sys
34+
from pathlib import Path
35+
36+
SCRIPT_DIR = Path(__file__).resolve().parent
37+
REPO_DIR = SCRIPT_DIR.parent.parent
38+
39+
DEFAULTS = {
40+
"backtest": os.environ.get("BACKTEST", str(REPO_DIR / "target" / "release" / "backtest")),
41+
}
42+
43+
DATE = "2023-03-13"
44+
CAPITAL = 1000.0
45+
NUM_LEGS = 4
46+
PER_LEG = CAPITAL / NUM_LEGS
47+
48+
49+
def cli(cmd: dict, binary: str, date: str) -> dict:
50+
"""Call the backtest CLI in JSON mode. Returns parsed JSON output."""
51+
try:
52+
result = subprocess.run(
53+
[binary, "--at", date, "--json", json.dumps(cmd)],
54+
capture_output=True, text=True, timeout=30,
55+
)
56+
if result.returncode != 0:
57+
return {"error": result.stderr.strip() or f"exit code {result.returncode}"}
58+
return json.loads(result.stdout)
59+
except (json.JSONDecodeError, subprocess.TimeoutExpired) as e:
60+
return {"error": str(e)}
61+
62+
63+
def print_trade(result: dict):
64+
if "error" in result:
65+
print(f" ERROR: {result['error']}")
66+
return
67+
trade = result.get("trade", {})
68+
pnl = result.get("pnl", [])
69+
symbol = trade.get("symbol", "?")
70+
side = trade.get("side", "?")
71+
amount = trade.get("amount", 0)
72+
price = trade.get("price", 0)
73+
total = amount * price
74+
print(f" {side.upper()} {amount} {symbol} @ ${price:,.2f} (${total:,.2f} notional)")
75+
if pnl:
76+
print()
77+
print(f" {'':>10} {' +1 day':>14} {' +2 days':>14} {' +4 days':>14} {' +7 days':>14}")
78+
print(f" {'':>10} {'':->14} {'':->14} {'':->14} {'':->14}")
79+
prices = "".join(f" ${float(p.get('price', 0)):>10,.2f}" for p in pnl)
80+
pnl_dollars = "".join(
81+
f" {'+' if float(p.get('pnl', 0)) >= 0 else ''}{float(p.get('pnl', 0)):>10,.2f}"
82+
for p in pnl
83+
)
84+
pnl_pcts = "".join(
85+
f" {'+' if float(p.get('pnlPct', 0)) >= 0 else ''}{float(p.get('pnlPct', 0)):>9,.2f}%"
86+
for p in pnl
87+
)
88+
print(f" {'Price':>10}{prices}")
89+
print(f" {'PnL $':>10}{pnl_dollars}")
90+
print(f" {'PnL %':>10}{pnl_pcts}")
91+
print()
92+
93+
portfolio = result.get("portfolio", {})
94+
cash = float(portfolio.get("cashBalance", 0))
95+
print(f" [PORTFOLIO] Cash balance: ${cash:,.2f}")
96+
for pos in portfolio.get("positions", []):
97+
print(
98+
f" [PORTFOLIO] {pos['type']} {pos['side']} {pos['symbol']}: "
99+
f"{abs(pos['quantity']):.4f} @ avg ${pos['avgEntryPrice']}"
100+
)
101+
print()
102+
return pnl
103+
104+
105+
def print_portfolio(balance: dict, positions: dict):
106+
cash = float(balance.get("cashBalance", 0))
107+
total_trades = balance.get("totalTrades", 0)
108+
pos_list = positions if isinstance(positions, list) else positions.get("positions", [])
109+
print(f" Cash balance: ${cash:,.2f}")
110+
print(f" Total trades: {total_trades}")
111+
print(f" Open positions: {len(pos_list)}")
112+
if pos_list:
113+
print()
114+
print(f" {'Symbol':<10} {'Type':<6} {'Side':<8} {'Quantity':>12} {'Avg Entry':>14}")
115+
print(f" {'-' * 54}")
116+
for p in pos_list:
117+
print(
118+
f" {p['symbol']:<10} {p['type']:<6} {p['side']:<8} "
119+
f"{abs(p['quantity']):>12.4f} {float(p['avgEntryPrice']):>14.2f}"
120+
)
121+
print()
122+
123+
124+
def run(cfg: dict):
125+
bt = cfg["backtest"]
126+
127+
print()
128+
print("=" * 62)
129+
print(" SVB Bank Run — Crypto as Digital Gold")
130+
print(f" March 13, 2023 | Starting capital: ${CAPITAL:,.0f}")
131+
print(f" ${PER_LEG:,.0f} per leg x {NUM_LEGS} legs")
132+
print("=" * 62)
133+
print()
134+
135+
# Reset portfolio
136+
cli({"command": "reset"}, bt, DATE)
137+
138+
# Scout prices
139+
print("-- Scouting prices on", DATE, "--")
140+
print()
141+
142+
btc = cli({"command": "quote", "symbol": "BTC"}, bt, DATE)
143+
eth = cli({"command": "quote", "symbol": "ETH"}, bt, DATE)
144+
gold = cli({"command": "quote", "symbol": "GOLD"}, bt, DATE)
145+
sp = cli({"command": "quote", "symbol": "SP500"}, bt, DATE)
146+
btc_price = float(btc.get("price", 0))
147+
eth_price = float(eth.get("price", 0))
148+
gold_price = float(gold.get("price", 0))
149+
sp_price = float(sp.get("price", 0))
150+
151+
print(f" BTC: ${btc_price:,.2f}")
152+
print(f" ETH: ${eth_price:,.2f}")
153+
print(f" GOLD: ${gold_price:,.2f}")
154+
print(f" SP500: ${sp_price:,.2f}")
155+
print()
156+
157+
# Calculate position sizes for $250 per leg
158+
btc_amount = round(PER_LEG / btc_price, 6)
159+
eth_amount = round(PER_LEG / eth_price, 4)
160+
gold_amount = round(PER_LEG / gold_price, 4)
161+
sp_amount = round(PER_LEG / sp_price, 4)
162+
163+
all_pnl = []
164+
165+
# Leg 1: Long BTC
166+
print(f"-- Leg 1: Long BTC (${PER_LEG:.0f} — digital gold, decentralized store of value) --")
167+
pnl = print_trade(cli({"command": "buy", "symbol": "BTC", "amount": btc_amount, "price": btc_price}, bt, DATE))
168+
all_pnl.append(("BTC long", pnl))
169+
170+
# Leg 2: Long ETH
171+
print(f"-- Leg 2: Long ETH (${PER_LEG:.0f} — DeFi as banking alternative) --")
172+
pnl = print_trade(cli({"command": "buy", "symbol": "ETH", "amount": eth_amount, "price": eth_price}, bt, DATE))
173+
all_pnl.append(("ETH long", pnl))
174+
175+
# Leg 3: Long GOLD
176+
print(f"-- Leg 3: Long GOLD (${PER_LEG:.0f} — traditional safe haven) --")
177+
pnl = print_trade(cli({"command": "buy", "symbol": "GOLD", "amount": gold_amount, "price": gold_price}, bt, DATE))
178+
all_pnl.append(("GOLD long", pnl))
179+
180+
# Leg 4: Short SP500
181+
print(f"-- Leg 4: Short SP500 (${PER_LEG:.0f} — banking contagion risk) --")
182+
pnl = print_trade(cli({"command": "sell", "symbol": "SP500", "amount": sp_amount, "price": sp_price}, bt, DATE))
183+
all_pnl.append(("SP500 short", pnl))
184+
185+
# Combined PnL summary
186+
print("=" * 62)
187+
print(" Combined Portfolio PnL (starting $1,000)")
188+
print("=" * 62)
189+
offsets = ["+1 day", "+2 days", "+4 days", "+7 days"]
190+
print()
191+
print(f" {'Leg':<14} {' +1 day':>10} {' +2 days':>10} {' +4 days':>10} {' +7 days':>10}")
192+
print(f" {'-' * 54}")
193+
totals = [0.0, 0.0, 0.0, 0.0]
194+
for name, pnl in all_pnl:
195+
if pnl:
196+
vals = [float(p.get("pnl", 0)) for p in pnl]
197+
for i, v in enumerate(vals):
198+
totals[i] += v
199+
row = "".join(f" {'+' if v >= 0 else ''}{v:>8.2f}" for v in vals)
200+
print(f" {name:<14}{row}")
201+
print(f" {'-' * 54}")
202+
total_row = "".join(f" {'+' if t >= 0 else ''}{t:>8.2f}" for t in totals)
203+
print(f" {'TOTAL':<14}{total_row}")
204+
print()
205+
206+
balance_row = "".join(f" ${CAPITAL + t:>8.2f}" for t in totals)
207+
print(f" {'Balance':<14}{balance_row}")
208+
pct_row = "".join(f" {'+' if t >= 0 else ''}{t / CAPITAL * 100:>7.2f}%" for t in totals)
209+
print(f" {'Return':<14}{pct_row}")
210+
print()
211+
212+
# Portfolio state
213+
print("=" * 62)
214+
print(" Final Portfolio State")
215+
print("=" * 62)
216+
balance = cli({"command": "balance"}, bt, DATE)
217+
positions = cli({"command": "positions"}, bt, DATE)
218+
print_portfolio(balance, positions)
219+
220+
# Cleanup
221+
cli({"command": "reset"}, bt, DATE)
222+
223+
224+
def main():
225+
parser = argparse.ArgumentParser(description="SVB bank run — crypto as digital gold backtest")
226+
parser.add_argument("--backtest", default=DEFAULTS["backtest"], help="Path to backtest binary")
227+
args = parser.parse_args()
228+
run({"backtest": args.backtest})
229+
230+
231+
if __name__ == "__main__":
232+
main()

0 commit comments

Comments
 (0)