diff --git a/examples/backtest/README.md b/examples/backtest/README.md new file mode 100644 index 0000000..8b2f823 --- /dev/null +++ b/examples/backtest/README.md @@ -0,0 +1,91 @@ +# Backtest Strategy Examples + +Historical trading simulations with forward PnL analysis. Each script simulates a multi-leg trade at a specific historical date and shows what the PnL would have been at +1, +2, +4, and +7 days. + +## Available Scenarios + +| Script | Date | Strategy | Assets | Outcome | +|--------|------|----------|--------|---------| +| `covid_crash_hedge.py` | 2020-02-21 | Flight-to-safety | Long GOLD + Short SP500 | SP500 fell 12% in 7 days; short leg dominated | +| `ftx_crypto_contagion.py` | 2022-11-08 | Crypto contagion hedge | Short BTC + Short ETH + Long GOLD | BTC -15%, ETH -18%; crypto shorts drove profit | +| `nvda_earnings_alpha.py` | 2023-05-25 | Sector alpha pair | Long NVDA + Short SP500 | NVDA +5% while SP500 flat; pure alpha | +| `ukraine_oil_shock.py` | 2022-02-24 | Commodity supply shock | Long OIL + Long GOLD + Short SP500 | Oil surged 16% in 7 days on sanctions | + +## Setup + +No API keys or wallet configuration needed. Just build the backtest binary: + +```bash +cargo build --release +``` + +## Usage + +```bash +# Run any scenario +python3 examples/backtest/covid_crash_hedge.py +python3 examples/backtest/ftx_crypto_contagion.py +python3 examples/backtest/nvda_earnings_alpha.py +python3 examples/backtest/ukraine_oil_shock.py + +# Override binary path +python3 examples/backtest/covid_crash_hedge.py --backtest /path/to/backtest +``` + +Each script: +1. Resets the portfolio to a clean state +2. Fetches historical prices at the scenario date +3. Executes simulated trades (spot buy/sell) +4. Displays a PnL table at +1, +2, +4, +7 days for each leg +5. Shows the final portfolio summary (cash balance, positions) +6. Cleans up (resets portfolio) + +## How It Works + +The `backtest` CLI fetches historical OHLCV data from Yahoo Finance (with CoinGecko fallback for crypto) and computes forward PnL by looking up actual prices at future dates. + +- **Auto-pricing**: If `--price` is omitted, the historical close price is used +- **Portfolio state**: Trades are tracked in `~/.fintool/backtest_portfolio.json` +- **Cash balance**: Buying subtracts cost, selling adds proceeds (spot trades only) +- **Perp trades**: Use `perp_buy`/`perp_sell` with leverage for leveraged PnL + +## Dependencies + +- **Python 3.10+** (no third-party packages — uses only stdlib) +- **backtest** CLI binary (compiled from this repo) + +## Writing Your Own + +Create a new Python script following this pattern: + +```python +#!/usr/bin/env python3 +import json, os, subprocess +from pathlib import Path + +REPO_DIR = Path(__file__).resolve().parent.parent.parent +BT = os.environ.get("BACKTEST", str(REPO_DIR / "target" / "release" / "backtest")) +DATE = "2024-01-15" + +def cli(cmd, date=DATE): + r = subprocess.run([BT, "--at", date, "--json", json.dumps(cmd)], + capture_output=True, text=True, timeout=30) + return json.loads(r.stdout) + +# Reset +cli({"command": "reset"}) + +# Quote +btc = cli({"command": "quote", "symbol": "BTC"}) +print(f"BTC on {DATE}: ${btc['price']}") + +# Trade +result = cli({"command": "buy", "symbol": "BTC", "amount": 0.01}) +for p in result["pnl"]: + print(f" {p['offset']}: ${p['price']} (PnL: {p['pnl']}, {p['pnlPct']}%)") + +# Cleanup +cli({"command": "reset"}) +``` + +Supported symbols include all major crypto (BTC, ETH, SOL, ...), stocks (AAPL, NVDA, TSLA, ...), commodities (GOLD, SILVER, OIL), and indices (SP500, NASDAQ, DOW). diff --git a/examples/backtest/covid_crash_hedge.py b/examples/backtest/covid_crash_hedge.py new file mode 100755 index 0000000..6c0cba4 --- /dev/null +++ b/examples/backtest/covid_crash_hedge.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +COVID-19 Crash — Flight-to-Safety Hedge +======================================== + +Date: February 21, 2020 (Friday before the crash accelerated) + +Thesis: By late February 2020, COVID-19 had spread to Italy and South +Korea. The S&P 500 hit its all-time high on Feb 19. A pandemic-driven +selloff was imminent. The classic hedge: go long gold (safe haven) and +short equities. This is a dollar-neutral pair: ~$5,000 each side. + +Legs: + 1. Long GOLD — 3 oz at ~$1,645/oz ($4,934 notional) + 2. Short SP500 — 1.5 units at ~$3,337 ($5,006 notional) + +What happened: The S&P 500 fell ~12% over the next 7 trading days +(the fastest correction from ATH in history). Gold initially held +steady, then pulled back as margin calls hit. + +Result: The short equity leg dominates — this hedge captured the +crash while the gold leg acts as a stabilizer. + +Usage: python3 examples/backtest/covid_crash_hedge.py +""" + +import argparse +import json +import os +import subprocess +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +REPO_DIR = SCRIPT_DIR.parent.parent + +DEFAULTS = { + "backtest": os.environ.get("BACKTEST", str(REPO_DIR / "target" / "release" / "backtest")), +} + +DATE = "2020-02-21" + + +def cli(cmd: dict, binary: str, date: str) -> dict: + """Call the backtest CLI in JSON mode. Returns parsed JSON output.""" + try: + result = subprocess.run( + [binary, "--at", date, "--json", json.dumps(cmd)], + capture_output=True, text=True, timeout=30, + ) + if result.returncode != 0: + return {"error": result.stderr.strip() or f"exit code {result.returncode}"} + return json.loads(result.stdout) + except (json.JSONDecodeError, subprocess.TimeoutExpired) as e: + return {"error": str(e)} + + +def run(cfg: dict): + bt = cfg["backtest"] + + print() + print("=" * 62) + print(" COVID-19 Crash Hedge — February 21, 2020") + print(" Long GOLD + Short S&P 500 (dollar-neutral pair)") + print("=" * 62) + print() + + # Reset portfolio + cli({"command": "reset"}, bt, DATE) + + # Scout prices + print("-- Scouting prices on", DATE, "--") + print() + + gold = cli({"command": "quote", "symbol": "GOLD"}, bt, DATE) + sp = cli({"command": "quote", "symbol": "SP500"}, bt, DATE) + gold_price = float(gold.get("price", 0)) + sp_price = float(sp.get("price", 0)) + + print(f" GOLD: ${gold_price:.2f} / oz") + print(f" SP500: ${sp_price:.2f}") + print() + + # Leg 1: Long GOLD + print("-- Leg 1: Long GOLD (flight to safety) --") + result = cli({"command": "buy", "symbol": "GOLD", "amount": 3, "price": gold_price}, bt, DATE) + print_trade(result) + + # Leg 2: Short SP500 + print("-- Leg 2: Short S&P 500 (equity crash) --") + result = cli({"command": "sell", "symbol": "SP500", "amount": 1.5, "price": sp_price}, bt, DATE) + print_trade(result) + + # Portfolio summary + print("=" * 62) + print(" Portfolio Summary") + print("=" * 62) + balance = cli({"command": "balance"}, bt, DATE) + positions = cli({"command": "positions"}, bt, DATE) + print_portfolio(balance, positions) + + # Cleanup + cli({"command": "reset"}, bt, DATE) + + +def print_trade(result: dict): + if "error" in result: + print(f" ERROR: {result['error']}") + return + trade = result.get("trade", {}) + pnl = result.get("pnl", []) + symbol = trade.get("symbol", "?") + side = trade.get("side", "?") + amount = trade.get("amount", 0) + price = trade.get("price", 0) + total = amount * price + print(f" {side.upper()} {amount} {symbol} @ ${price:,.2f} (${total:,.2f} notional)") + if pnl: + print() + print(f" {'':>10} {' +1 day':>14} {' +2 days':>14} {' +4 days':>14} {' +7 days':>14}") + print(f" {'':>10} {'':->14} {'':->14} {'':->14} {'':->14}") + prices = "".join(f" ${float(p.get('price', 0)):>10,.2f}" for p in pnl) + pnl_dollars = "".join( + f" {'+' if float(p.get('pnl', 0)) >= 0 else ''}{float(p.get('pnl', 0)):>10,.2f}" + for p in pnl + ) + pnl_pcts = "".join( + f" {'+' if float(p.get('pnlPct', 0)) >= 0 else ''}{float(p.get('pnlPct', 0)):>9,.2f}%" + for p in pnl + ) + print(f" {'Price':>10}{prices}") + print(f" {'PnL $':>10}{pnl_dollars}") + print(f" {'PnL %':>10}{pnl_pcts}") + print() + + portfolio = result.get("portfolio", {}) + cash = float(portfolio.get("cashBalance", 0)) + print(f" [PORTFOLIO] Cash balance: ${cash:,.2f}") + for pos in portfolio.get("positions", []): + print( + f" [PORTFOLIO] {pos['type']} {pos['side']} {pos['symbol']}: " + f"{abs(pos['quantity']):.4f} @ avg ${pos['avgEntryPrice']}" + ) + print() + + +def print_portfolio(balance: dict, positions: dict): + cash = float(balance.get("cashBalance", 0)) + total_trades = balance.get("totalTrades", 0) + pos_list = positions if isinstance(positions, list) else positions.get("positions", []) + print(f" Cash balance: ${cash:,.2f}") + print(f" Total trades: {total_trades}") + print(f" Open positions: {len(pos_list)}") + if pos_list: + print() + print(f" {'Symbol':<10} {'Type':<6} {'Side':<8} {'Quantity':>12} {'Avg Entry':>14}") + print(f" {'-' * 54}") + for p in pos_list: + print( + f" {p['symbol']:<10} {p['type']:<6} {p['side']:<8} " + f"{abs(p['quantity']):>12.4f} {float(p['avgEntryPrice']):>14.2f}" + ) + print() + + +def main(): + parser = argparse.ArgumentParser(description="COVID-19 crash hedge backtest") + parser.add_argument("--backtest", default=DEFAULTS["backtest"], help="Path to backtest binary") + args = parser.parse_args() + run({"backtest": args.backtest}) + + +if __name__ == "__main__": + main() diff --git a/examples/backtest/covid_crash_hedge.sh b/examples/backtest/covid_crash_hedge.sh deleted file mode 100755 index 3414e2e..0000000 --- a/examples/backtest/covid_crash_hedge.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -# -# COVID-19 Crash — Flight-to-Safety Hedge -# ======================================== -# -# Date: February 21, 2020 (Friday before the crash accelerated) -# -# Thesis: By late February 2020, COVID-19 had spread to Italy and South -# Korea. The S&P 500 hit its all-time high on Feb 19. A pandemic-driven -# selloff was imminent. The classic hedge: go long gold (safe haven) and -# short equities. This is a dollar-neutral pair: ~$5,000 each side. -# -# Legs: -# 1. Long GOLD — 3 oz at ~$1,645/oz ($4,934 notional) -# 2. Short SP500 — 1.5 units at ~$3,337 ($5,006 notional) -# -# What happened: The S&P 500 fell ~12% over the next 7 trading days -# (the fastest correction from ATH in history). Gold initially held -# steady, then pulled back as margin calls hit. -# -# Result: The short equity leg dominates — this hedge captured the -# crash while the gold leg acts as a stabilizer. -# -# Usage: ./examples/backtest/covid_crash_hedge.sh -# -set -uo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -BT="${BACKTEST:-$SCRIPT_DIR/../../target/release/backtest}" - -echo "" -echo "══════════════════════════════════════════════════════════════" -echo " COVID-19 Crash Hedge — February 21, 2020" -echo " Long GOLD + Short S&P 500 (dollar-neutral pair)" -echo "══════════════════════════════════════════════════════════════" -echo "" - -# ── Reset portfolio ──────────────────────────────────────────────── -$BT --at 2020-02-21 reset 2>/dev/null - -# ── Scout: get prices on Feb 21, 2020 ───────────────────────────── -echo "── Scouting prices on 2020-02-21 ──" -echo "" - -GOLD_PRICE=$($BT --at 2020-02-21 --json '{"command":"quote","symbol":"GOLD"}' 2>/dev/null | jq -r '.price') -SP_PRICE=$($BT --at 2020-02-21 --json '{"command":"quote","symbol":"SP500"}' 2>/dev/null | jq -r '.price') - -echo " GOLD: \$$GOLD_PRICE / oz" -echo " SP500: \$$SP_PRICE" -echo "" - -# ── Leg 1: Long gold — safe-haven bid ───────────────────────────── -echo "── Leg 1: Long GOLD (flight to safety) ──" -$BT --at 2020-02-21 buy GOLD --amount 3 --price "$GOLD_PRICE" - -# ── Leg 2: Short S&P 500 — pandemic selloff ─────────────────────── -echo "── Leg 2: Short S&P 500 (equity crash) ──" -$BT --at 2020-02-21 sell SP500 --amount 1.5 --price "$SP_PRICE" - -# ── Portfolio snapshot ───────────────────────────────────────────── -echo "══════════════════════════════════════════════════════════════" -echo " Portfolio Summary" -echo "══════════════════════════════════════════════════════════════" -$BT --at 2020-02-21 balance -$BT --at 2020-02-21 positions - -# ── Cleanup ──────────────────────────────────────────────────────── -$BT --at 2020-02-21 reset > /dev/null 2>&1 diff --git a/examples/backtest/ftx_crypto_contagion.py b/examples/backtest/ftx_crypto_contagion.py new file mode 100755 index 0000000..9d3f251 --- /dev/null +++ b/examples/backtest/ftx_crypto_contagion.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +FTX Collapse — Crypto Contagion Hedge +======================================== + +Date: November 8, 2022 (Binance announced it would sell its FTT holdings) + +Thesis: On Nov 6, CoinDesk reported Alameda's balance sheet was full +of FTT tokens. On Nov 8, Binance announced it would liquidate its +~$530M in FTT, triggering a bank run on FTX. Crypto was about to +enter a contagion spiral — but it wasn't clear how far traditional +markets would follow. The trade: short crypto (BTC + ETH) and go +long gold as a safe-haven hedge in case the contagion spread. + +Legs: + 1. Short BTC — 0.15 BTC at ~$18,500 ($2,775 notional) + 2. Short ETH — 2.0 ETH at ~$1,340 ($2,680 notional) + 3. Long GOLD — 3 oz at ~$1,712/oz ($5,136 notional) + +Gold is the anchor — roughly sized to match the combined crypto short. +If crypto crashes and gold holds or rises, both sides win. If crypto +recovers, the gold leg limits the damage. + +What happened: BTC dropped from $18.5k to $15.7k (-15%) in 7 days. +ETH fell from $1,340 to $1,100 (-18%). Gold was roughly flat, +providing stability. The crypto short was the main profit driver. + +Usage: python3 examples/backtest/ftx_crypto_contagion.py +""" + +import argparse +import json +import os +import subprocess +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +REPO_DIR = SCRIPT_DIR.parent.parent + +DEFAULTS = { + "backtest": os.environ.get("BACKTEST", str(REPO_DIR / "target" / "release" / "backtest")), +} + +DATE = "2022-11-08" + + +def cli(cmd: dict, binary: str, date: str) -> dict: + """Call the backtest CLI in JSON mode. Returns parsed JSON output.""" + try: + result = subprocess.run( + [binary, "--at", date, "--json", json.dumps(cmd)], + capture_output=True, text=True, timeout=30, + ) + if result.returncode != 0: + return {"error": result.stderr.strip() or f"exit code {result.returncode}"} + return json.loads(result.stdout) + except (json.JSONDecodeError, subprocess.TimeoutExpired) as e: + return {"error": str(e)} + + +def print_trade(result: dict): + if "error" in result: + print(f" ERROR: {result['error']}") + return + trade = result.get("trade", {}) + pnl = result.get("pnl", []) + symbol = trade.get("symbol", "?") + side = trade.get("side", "?") + amount = trade.get("amount", 0) + price = trade.get("price", 0) + total = amount * price + print(f" {side.upper()} {amount} {symbol} @ ${price:,.2f} (${total:,.2f} notional)") + if pnl: + print() + print(f" {'':>10} {' +1 day':>14} {' +2 days':>14} {' +4 days':>14} {' +7 days':>14}") + print(f" {'':>10} {'':->14} {'':->14} {'':->14} {'':->14}") + prices = "".join(f" ${float(p.get('price', 0)):>10,.2f}" for p in pnl) + pnl_dollars = "".join( + f" {'+' if float(p.get('pnl', 0)) >= 0 else ''}{float(p.get('pnl', 0)):>10,.2f}" + for p in pnl + ) + pnl_pcts = "".join( + f" {'+' if float(p.get('pnlPct', 0)) >= 0 else ''}{float(p.get('pnlPct', 0)):>9,.2f}%" + for p in pnl + ) + print(f" {'Price':>10}{prices}") + print(f" {'PnL $':>10}{pnl_dollars}") + print(f" {'PnL %':>10}{pnl_pcts}") + print() + + portfolio = result.get("portfolio", {}) + cash = float(portfolio.get("cashBalance", 0)) + print(f" [PORTFOLIO] Cash balance: ${cash:,.2f}") + for pos in portfolio.get("positions", []): + print( + f" [PORTFOLIO] {pos['type']} {pos['side']} {pos['symbol']}: " + f"{abs(pos['quantity']):.4f} @ avg ${pos['avgEntryPrice']}" + ) + print() + + +def print_portfolio(balance: dict, positions: dict): + cash = float(balance.get("cashBalance", 0)) + total_trades = balance.get("totalTrades", 0) + pos_list = positions if isinstance(positions, list) else positions.get("positions", []) + print(f" Cash balance: ${cash:,.2f}") + print(f" Total trades: {total_trades}") + print(f" Open positions: {len(pos_list)}") + if pos_list: + print() + print(f" {'Symbol':<10} {'Type':<6} {'Side':<8} {'Quantity':>12} {'Avg Entry':>14}") + print(f" {'-' * 54}") + for p in pos_list: + print( + f" {p['symbol']:<10} {p['type']:<6} {p['side']:<8} " + f"{abs(p['quantity']):>12.4f} {float(p['avgEntryPrice']):>14.2f}" + ) + print() + + +def run(cfg: dict): + bt = cfg["backtest"] + + print() + print("=" * 62) + print(" FTX Collapse — November 8, 2022") + print(" Short BTC + Short ETH + Long GOLD (contagion hedge)") + print("=" * 62) + print() + + # Reset portfolio + cli({"command": "reset"}, bt, DATE) + + # Scout prices + print("-- Scouting prices on", DATE, "--") + print() + + btc = cli({"command": "quote", "symbol": "BTC"}, bt, DATE) + eth = cli({"command": "quote", "symbol": "ETH"}, bt, DATE) + gold = cli({"command": "quote", "symbol": "GOLD"}, bt, DATE) + btc_price = float(btc.get("price", 0)) + eth_price = float(eth.get("price", 0)) + gold_price = float(gold.get("price", 0)) + + print(f" BTC: ${btc_price:,.2f}") + print(f" ETH: ${eth_price:,.2f}") + print(f" GOLD: ${gold_price:,.2f}") + print() + + # Leg 1: Short BTC + print("-- Leg 1: Short BTC (FTX contagion, forced selling) --") + result = cli({"command": "sell", "symbol": "BTC", "amount": 0.15, "price": btc_price}, bt, DATE) + print_trade(result) + + # Leg 2: Short ETH + print("-- Leg 2: Short ETH (correlated crypto drawdown) --") + result = cli({"command": "sell", "symbol": "ETH", "amount": 2.0, "price": eth_price}, bt, DATE) + print_trade(result) + + # Leg 3: Long GOLD + print("-- Leg 3: Long GOLD (safe haven, hedge against reversal) --") + result = cli({"command": "buy", "symbol": "GOLD", "amount": 3, "price": gold_price}, bt, DATE) + print_trade(result) + + # Portfolio summary + print("=" * 62) + print(" Portfolio Summary") + print("=" * 62) + balance = cli({"command": "balance"}, bt, DATE) + positions = cli({"command": "positions"}, bt, DATE) + print_portfolio(balance, positions) + + # Cleanup + cli({"command": "reset"}, bt, DATE) + + +def main(): + parser = argparse.ArgumentParser(description="FTX crypto contagion hedge backtest") + parser.add_argument("--backtest", default=DEFAULTS["backtest"], help="Path to backtest binary") + args = parser.parse_args() + run({"backtest": args.backtest}) + + +if __name__ == "__main__": + main() diff --git a/examples/backtest/ftx_crypto_contagion.sh b/examples/backtest/ftx_crypto_contagion.sh deleted file mode 100755 index 97762cb..0000000 --- a/examples/backtest/ftx_crypto_contagion.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# -# FTX Collapse — Crypto Contagion Hedge -# ======================================== -# -# Date: November 8, 2022 (Binance announced it would sell its FTT holdings) -# -# Thesis: On Nov 6, CoinDesk reported Alameda's balance sheet was full -# of FTT tokens. On Nov 8, Binance announced it would liquidate its -# ~$530M in FTT, triggering a bank run on FTX. Crypto was about to -# enter a contagion spiral — but it wasn't clear how far traditional -# markets would follow. The trade: short crypto (BTC + ETH) and go -# long gold as a safe-haven hedge in case the contagion spread. -# -# Legs: -# 1. Short BTC — 0.15 BTC at ~$18,500 ($2,775 notional) -# 2. Short ETH — 2.0 ETH at ~$1,340 ($2,680 notional) -# 3. Long GOLD — 3 oz at ~$1,712/oz ($5,136 notional) -# -# Gold is the anchor — roughly sized to match the combined crypto short. -# If crypto crashes and gold holds or rises, both sides win. If crypto -# recovers, the gold leg limits the damage. -# -# What happened: BTC dropped from $18.5k to $15.7k (-15%) in 7 days. -# ETH fell from $1,340 to $1,100 (-18%). Gold was roughly flat, -# providing stability. The crypto short was the main profit driver. -# -# Usage: ./examples/backtest/ftx_crypto_contagion.sh -# -set -uo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -BT="${BACKTEST:-$SCRIPT_DIR/../../target/release/backtest}" - -echo "" -echo "══════════════════════════════════════════════════════════════" -echo " FTX Collapse — November 8, 2022" -echo " Short BTC + Short ETH + Long GOLD (contagion hedge)" -echo "══════════════════════════════════════════════════════════════" -echo "" - -# ── Reset portfolio ──────────────────────────────────────────────── -$BT --at 2022-11-08 reset 2>/dev/null - -# ── Scout: get prices on Nov 8, 2022 ────────────────────────────── -echo "── Scouting prices on 2022-11-08 ──" -echo "" - -BTC_PRICE=$($BT --at 2022-11-08 --json '{"command":"quote","symbol":"BTC"}' 2>/dev/null | jq -r '.price') -ETH_PRICE=$($BT --at 2022-11-08 --json '{"command":"quote","symbol":"ETH"}' 2>/dev/null | jq -r '.price') -GOLD_PRICE=$($BT --at 2022-11-08 --json '{"command":"quote","symbol":"GOLD"}' 2>/dev/null | jq -r '.price') - -echo " BTC: \$$BTC_PRICE" -echo " ETH: \$$ETH_PRICE" -echo " GOLD: \$$GOLD_PRICE" -echo "" - -# ── Leg 1: Short BTC — exchange contagion ───────────────────────── -echo "── Leg 1: Short BTC (FTX contagion, forced selling) ──" -$BT --at 2022-11-08 sell BTC --amount 0.15 --price "$BTC_PRICE" - -# ── Leg 2: Short ETH — crypto-wide selloff ──────────────────────── -echo "── Leg 2: Short ETH (correlated crypto drawdown) ──" -$BT --at 2022-11-08 sell ETH --amount 2.0 --price "$ETH_PRICE" - -# ── Leg 3: Long gold — safe-haven anchor ────────────────────────── -echo "── Leg 3: Long GOLD (safe haven, hedge against reversal) ──" -$BT --at 2022-11-08 buy GOLD --amount 3 --price "$GOLD_PRICE" - -# ── Portfolio snapshot ───────────────────────────────────────────── -echo "══════════════════════════════════════════════════════════════" -echo " Portfolio Summary" -echo "══════════════════════════════════════════════════════════════" -$BT --at 2022-11-08 balance -$BT --at 2022-11-08 positions - -# ── Cleanup ──────────────────────────────────────────────────────── -$BT --at 2022-11-08 reset > /dev/null 2>&1 diff --git a/examples/backtest/nvda_earnings_alpha.py b/examples/backtest/nvda_earnings_alpha.py new file mode 100755 index 0000000..2498d06 --- /dev/null +++ b/examples/backtest/nvda_earnings_alpha.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +NVIDIA Earnings Blowout — AI Sector Alpha +============================================ + +Date: May 25, 2023 (day after NVDA's historic Q1 FY24 earnings) + +Thesis: On May 24, 2023, NVIDIA reported revenue of $7.19B (vs +$6.52B expected) and guided Q2 to $11B — 50% above consensus. +The stock surged 25% after hours. This was the moment the market +realized AI infrastructure demand was real. The play: capture +NVDA's alpha while hedging broad market risk. + +Legs: + 1. Long NVDA — 13 shares at ~$379/share ($4,930 notional) + 2. Short SP500 — 1.2 units at ~$4,151 ($4,981 notional) + +This is a classic long/short equity pair: long the winner, short +the index. If the market rallies, NVDA should rally more. If the +market drops, NVDA's AI tailwind should cushion the loss. + +What happened: NVDA continued climbing from $379 to $400+ over +the next week as analysts rushed to upgrade. The S&P 500 was +roughly flat. Pure alpha. + +Usage: python3 examples/backtest/nvda_earnings_alpha.py +""" + +import argparse +import json +import os +import subprocess +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +REPO_DIR = SCRIPT_DIR.parent.parent + +DEFAULTS = { + "backtest": os.environ.get("BACKTEST", str(REPO_DIR / "target" / "release" / "backtest")), +} + +DATE = "2023-05-25" + + +def cli(cmd: dict, binary: str, date: str) -> dict: + """Call the backtest CLI in JSON mode. Returns parsed JSON output.""" + try: + result = subprocess.run( + [binary, "--at", date, "--json", json.dumps(cmd)], + capture_output=True, text=True, timeout=30, + ) + if result.returncode != 0: + return {"error": result.stderr.strip() or f"exit code {result.returncode}"} + return json.loads(result.stdout) + except (json.JSONDecodeError, subprocess.TimeoutExpired) as e: + return {"error": str(e)} + + +def print_trade(result: dict): + if "error" in result: + print(f" ERROR: {result['error']}") + return + trade = result.get("trade", {}) + pnl = result.get("pnl", []) + symbol = trade.get("symbol", "?") + side = trade.get("side", "?") + amount = trade.get("amount", 0) + price = trade.get("price", 0) + total = amount * price + print(f" {side.upper()} {amount} {symbol} @ ${price:,.2f} (${total:,.2f} notional)") + if pnl: + print() + print(f" {'':>10} {' +1 day':>14} {' +2 days':>14} {' +4 days':>14} {' +7 days':>14}") + print(f" {'':>10} {'':->14} {'':->14} {'':->14} {'':->14}") + prices = "".join(f" ${float(p.get('price', 0)):>10,.2f}" for p in pnl) + pnl_dollars = "".join( + f" {'+' if float(p.get('pnl', 0)) >= 0 else ''}{float(p.get('pnl', 0)):>10,.2f}" + for p in pnl + ) + pnl_pcts = "".join( + f" {'+' if float(p.get('pnlPct', 0)) >= 0 else ''}{float(p.get('pnlPct', 0)):>9,.2f}%" + for p in pnl + ) + print(f" {'Price':>10}{prices}") + print(f" {'PnL $':>10}{pnl_dollars}") + print(f" {'PnL %':>10}{pnl_pcts}") + print() + + portfolio = result.get("portfolio", {}) + cash = float(portfolio.get("cashBalance", 0)) + print(f" [PORTFOLIO] Cash balance: ${cash:,.2f}") + for pos in portfolio.get("positions", []): + print( + f" [PORTFOLIO] {pos['type']} {pos['side']} {pos['symbol']}: " + f"{abs(pos['quantity']):.4f} @ avg ${pos['avgEntryPrice']}" + ) + print() + + +def print_portfolio(balance: dict, positions: dict): + cash = float(balance.get("cashBalance", 0)) + total_trades = balance.get("totalTrades", 0) + pos_list = positions if isinstance(positions, list) else positions.get("positions", []) + print(f" Cash balance: ${cash:,.2f}") + print(f" Total trades: {total_trades}") + print(f" Open positions: {len(pos_list)}") + if pos_list: + print() + print(f" {'Symbol':<10} {'Type':<6} {'Side':<8} {'Quantity':>12} {'Avg Entry':>14}") + print(f" {'-' * 54}") + for p in pos_list: + print( + f" {p['symbol']:<10} {p['type']:<6} {p['side']:<8} " + f"{abs(p['quantity']):>12.4f} {float(p['avgEntryPrice']):>14.2f}" + ) + print() + + +def run(cfg: dict): + bt = cfg["backtest"] + + print() + print("=" * 62) + print(" NVIDIA AI Earnings — May 25, 2023") + print(" Long NVDA + Short S&P 500 (sector alpha pair)") + print("=" * 62) + print() + + # Reset portfolio + cli({"command": "reset"}, bt, DATE) + + # Scout prices + print("-- Scouting prices on", DATE, "--") + print() + + nvda = cli({"command": "quote", "symbol": "NVDA"}, bt, DATE) + sp = cli({"command": "quote", "symbol": "SP500"}, bt, DATE) + nvda_price = float(nvda.get("price", 0)) + sp_price = float(sp.get("price", 0)) + + print(f" NVDA: ${nvda_price:.2f}") + print(f" SP500: ${sp_price:.2f}") + print() + + # Leg 1: Long NVDA + print("-- Leg 1: Long NVDA (AI infrastructure demand) --") + result = cli({"command": "buy", "symbol": "NVDA", "amount": 13, "price": nvda_price}, bt, DATE) + print_trade(result) + + # Leg 2: Short SP500 + print("-- Leg 2: Short S&P 500 (hedge broad market risk) --") + result = cli({"command": "sell", "symbol": "SP500", "amount": 1.2, "price": sp_price}, bt, DATE) + print_trade(result) + + # Portfolio summary + print("=" * 62) + print(" Portfolio Summary") + print("=" * 62) + balance = cli({"command": "balance"}, bt, DATE) + positions = cli({"command": "positions"}, bt, DATE) + print_portfolio(balance, positions) + + # Cleanup + cli({"command": "reset"}, bt, DATE) + + +def main(): + parser = argparse.ArgumentParser(description="NVDA earnings alpha backtest") + parser.add_argument("--backtest", default=DEFAULTS["backtest"], help="Path to backtest binary") + args = parser.parse_args() + run({"backtest": args.backtest}) + + +if __name__ == "__main__": + main() diff --git a/examples/backtest/nvda_earnings_alpha.sh b/examples/backtest/nvda_earnings_alpha.sh deleted file mode 100755 index 5f09291..0000000 --- a/examples/backtest/nvda_earnings_alpha.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -# -# NVIDIA Earnings Blowout — AI Sector Alpha -# ============================================ -# -# Date: May 25, 2023 (day after NVDA's historic Q1 FY24 earnings) -# -# Thesis: On May 24, 2023, NVIDIA reported revenue of $7.19B (vs -# $6.52B expected) and guided Q2 to $11B — 50% above consensus. -# The stock surged 25% after hours. This was the moment the market -# realized AI infrastructure demand was real. The play: capture -# NVDA's alpha while hedging broad market risk. -# -# Legs: -# 1. Long NVDA — 13 shares at ~$379/share ($4,930 notional) -# 2. Short SP500 — 1.2 units at ~$4,151 ($4,981 notional) -# -# This is a classic long/short equity pair: long the winner, short -# the index. If the market rallies, NVDA should rally more. If the -# market drops, NVDA's AI tailwind should cushion the loss. -# -# What happened: NVDA continued climbing from $379 to $400+ over -# the next week as analysts rushed to upgrade. The S&P 500 was -# roughly flat. Pure alpha. -# -# Usage: ./examples/backtest/nvda_earnings_alpha.sh -# -set -uo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -BT="${BACKTEST:-$SCRIPT_DIR/../../target/release/backtest}" - -echo "" -echo "══════════════════════════════════════════════════════════════" -echo " NVIDIA AI Earnings — May 25, 2023" -echo " Long NVDA + Short S&P 500 (sector alpha pair)" -echo "══════════════════════════════════════════════════════════════" -echo "" - -# ── Reset portfolio ──────────────────────────────────────────────── -$BT --at 2023-05-25 reset 2>/dev/null - -# ── Scout: get prices on May 25, 2023 ───────────────────────────── -echo "── Scouting prices on 2023-05-25 ──" -echo "" - -NVDA_PRICE=$($BT --at 2023-05-25 --json '{"command":"quote","symbol":"NVDA"}' 2>/dev/null | jq -r '.price') -SP_PRICE=$($BT --at 2023-05-25 --json '{"command":"quote","symbol":"SP500"}' 2>/dev/null | jq -r '.price') - -echo " NVDA: \$$NVDA_PRICE" -echo " SP500: \$$SP_PRICE" -echo "" - -# ── Leg 1: Long NVDA — AI momentum ─────────────────────────────── -echo "── Leg 1: Long NVDA (AI infrastructure demand) ──" -$BT --at 2023-05-25 buy NVDA --amount 13 --price "$NVDA_PRICE" - -# ── Leg 2: Short S&P 500 — market-neutral hedge ────────────────── -echo "── Leg 2: Short S&P 500 (hedge broad market risk) ──" -$BT --at 2023-05-25 sell SP500 --amount 1.2 --price "$SP_PRICE" - -# ── Portfolio snapshot ───────────────────────────────────────────── -echo "══════════════════════════════════════════════════════════════" -echo " Portfolio Summary" -echo "══════════════════════════════════════════════════════════════" -$BT --at 2023-05-25 balance -$BT --at 2023-05-25 positions - -# ── Cleanup ──────────────────────────────────────────────────────── -$BT --at 2023-05-25 reset > /dev/null 2>&1 diff --git a/examples/backtest/ukraine_oil_shock.py b/examples/backtest/ukraine_oil_shock.py new file mode 100755 index 0000000..dac026a --- /dev/null +++ b/examples/backtest/ukraine_oil_shock.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +Russia-Ukraine Invasion — Commodity Supply Shock +================================================== + +Date: February 24, 2022 (the day Russia invaded Ukraine) + +Thesis: Russia is a top-3 global oil producer. A full-scale invasion +means sanctions, supply disruption, and an energy price spike. At the +same time, risk assets sell off on geopolitical uncertainty. This is +a classic macro shock trade: long commodities, short equities. + +Legs: + 1. Long OIL — 35 bbl at ~$92/bbl ($3,220 notional) + 2. Long GOLD — 2 oz at ~$1,909/oz ($3,818 notional) — war safe haven + 3. Short SP500 — 1.5 units at ~$4,225 ($6,338 notional) + +The short leg is sized larger to roughly balance the two long legs. +Oil is the directional bet; gold is the defensive anchor. + +What happened: Oil surged to $115+ within 2 weeks (eventually $130 +in March). Gold rallied to $2,050. The S&P 500 dropped ~3% in the +first week before stabilizing. + +Result: Both commodity legs profit on the supply shock, while the +equity short captures the initial risk-off move. + +Usage: python3 examples/backtest/ukraine_oil_shock.py +""" + +import argparse +import json +import os +import subprocess +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +REPO_DIR = SCRIPT_DIR.parent.parent + +DEFAULTS = { + "backtest": os.environ.get("BACKTEST", str(REPO_DIR / "target" / "release" / "backtest")), +} + +DATE = "2022-02-24" + + +def cli(cmd: dict, binary: str, date: str) -> dict: + """Call the backtest CLI in JSON mode. Returns parsed JSON output.""" + try: + result = subprocess.run( + [binary, "--at", date, "--json", json.dumps(cmd)], + capture_output=True, text=True, timeout=30, + ) + if result.returncode != 0: + return {"error": result.stderr.strip() or f"exit code {result.returncode}"} + return json.loads(result.stdout) + except (json.JSONDecodeError, subprocess.TimeoutExpired) as e: + return {"error": str(e)} + + +def print_trade(result: dict): + if "error" in result: + print(f" ERROR: {result['error']}") + return + trade = result.get("trade", {}) + pnl = result.get("pnl", []) + symbol = trade.get("symbol", "?") + side = trade.get("side", "?") + amount = trade.get("amount", 0) + price = trade.get("price", 0) + total = amount * price + print(f" {side.upper()} {amount} {symbol} @ ${price:,.2f} (${total:,.2f} notional)") + if pnl: + print() + print(f" {'':>10} {' +1 day':>14} {' +2 days':>14} {' +4 days':>14} {' +7 days':>14}") + print(f" {'':>10} {'':->14} {'':->14} {'':->14} {'':->14}") + prices = "".join(f" ${float(p.get('price', 0)):>10,.2f}" for p in pnl) + pnl_dollars = "".join( + f" {'+' if float(p.get('pnl', 0)) >= 0 else ''}{float(p.get('pnl', 0)):>10,.2f}" + for p in pnl + ) + pnl_pcts = "".join( + f" {'+' if float(p.get('pnlPct', 0)) >= 0 else ''}{float(p.get('pnlPct', 0)):>9,.2f}%" + for p in pnl + ) + print(f" {'Price':>10}{prices}") + print(f" {'PnL $':>10}{pnl_dollars}") + print(f" {'PnL %':>10}{pnl_pcts}") + print() + + portfolio = result.get("portfolio", {}) + cash = float(portfolio.get("cashBalance", 0)) + print(f" [PORTFOLIO] Cash balance: ${cash:,.2f}") + for pos in portfolio.get("positions", []): + print( + f" [PORTFOLIO] {pos['type']} {pos['side']} {pos['symbol']}: " + f"{abs(pos['quantity']):.4f} @ avg ${pos['avgEntryPrice']}" + ) + print() + + +def print_portfolio(balance: dict, positions: dict): + cash = float(balance.get("cashBalance", 0)) + total_trades = balance.get("totalTrades", 0) + pos_list = positions if isinstance(positions, list) else positions.get("positions", []) + print(f" Cash balance: ${cash:,.2f}") + print(f" Total trades: {total_trades}") + print(f" Open positions: {len(pos_list)}") + if pos_list: + print() + print(f" {'Symbol':<10} {'Type':<6} {'Side':<8} {'Quantity':>12} {'Avg Entry':>14}") + print(f" {'-' * 54}") + for p in pos_list: + print( + f" {p['symbol']:<10} {p['type']:<6} {p['side']:<8} " + f"{abs(p['quantity']):>12.4f} {float(p['avgEntryPrice']):>14.2f}" + ) + print() + + +def run(cfg: dict): + bt = cfg["backtest"] + + print() + print("=" * 62) + print(" Russia-Ukraine Invasion — February 24, 2022") + print(" Long OIL + Long GOLD + Short S&P 500") + print("=" * 62) + print() + + # Reset portfolio + cli({"command": "reset"}, bt, DATE) + + # Scout prices + print("-- Scouting prices on", DATE, "--") + print() + + oil = cli({"command": "quote", "symbol": "OIL"}, bt, DATE) + gold = cli({"command": "quote", "symbol": "GOLD"}, bt, DATE) + sp = cli({"command": "quote", "symbol": "SP500"}, bt, DATE) + oil_price = float(oil.get("price", 0)) + gold_price = float(gold.get("price", 0)) + sp_price = float(sp.get("price", 0)) + + print(f" OIL: ${oil_price:.2f} / bbl") + print(f" GOLD: ${gold_price:.2f} / oz") + print(f" SP500: ${sp_price:.2f}") + print() + + # Leg 1: Long OIL + print("-- Leg 1: Long OIL (supply shock from sanctions) --") + result = cli({"command": "buy", "symbol": "OIL", "amount": 35, "price": oil_price}, bt, DATE) + print_trade(result) + + # Leg 2: Long GOLD + print("-- Leg 2: Long GOLD (war premium + safe haven) --") + result = cli({"command": "buy", "symbol": "GOLD", "amount": 2, "price": gold_price}, bt, DATE) + print_trade(result) + + # Leg 3: Short SP500 + print("-- Leg 3: Short S&P 500 (risk-off) --") + result = cli({"command": "sell", "symbol": "SP500", "amount": 1.5, "price": sp_price}, bt, DATE) + print_trade(result) + + # Portfolio summary + print("=" * 62) + print(" Portfolio Summary") + print("=" * 62) + balance = cli({"command": "balance"}, bt, DATE) + positions = cli({"command": "positions"}, bt, DATE) + print_portfolio(balance, positions) + + # Cleanup + cli({"command": "reset"}, bt, DATE) + + +def main(): + parser = argparse.ArgumentParser(description="Ukraine oil shock backtest") + parser.add_argument("--backtest", default=DEFAULTS["backtest"], help="Path to backtest binary") + args = parser.parse_args() + run({"backtest": args.backtest}) + + +if __name__ == "__main__": + main() diff --git a/examples/backtest/ukraine_oil_shock.sh b/examples/backtest/ukraine_oil_shock.sh deleted file mode 100755 index e0f3770..0000000 --- a/examples/backtest/ukraine_oil_shock.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# -# Russia-Ukraine Invasion — Commodity Supply Shock -# ================================================== -# -# Date: February 24, 2022 (the day Russia invaded Ukraine) -# -# Thesis: Russia is a top-3 global oil producer. A full-scale invasion -# means sanctions, supply disruption, and an energy price spike. At the -# same time, risk assets sell off on geopolitical uncertainty. This is -# a classic macro shock trade: long commodities, short equities. -# -# Legs: -# 1. Long OIL — 35 bbl at ~$92/bbl ($3,220 notional) -# 2. Long GOLD — 2 oz at ~$1,909/oz ($3,818 notional) — war safe haven -# 3. Short SP500 — 1.5 units at ~$4,225 ($6,338 notional) -# -# The short leg is sized larger to roughly balance the two long legs. -# Oil is the directional bet; gold is the defensive anchor. -# -# What happened: Oil surged to $115+ within 2 weeks (eventually $130 -# in March). Gold rallied to $2,050. The S&P 500 dropped ~3% in the -# first week before stabilizing. -# -# Result: Both commodity legs profit on the supply shock, while the -# equity short captures the initial risk-off move. -# -# Usage: ./examples/backtest/ukraine_oil_shock.sh -# -set -uo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -BT="${BACKTEST:-$SCRIPT_DIR/../../target/release/backtest}" - -echo "" -echo "══════════════════════════════════════════════════════════════" -echo " Russia-Ukraine Invasion — February 24, 2022" -echo " Long OIL + Long GOLD + Short S&P 500" -echo "══════════════════════════════════════════════════════════════" -echo "" - -# ── Reset portfolio ──────────────────────────────────────────────── -$BT --at 2022-02-24 reset 2>/dev/null - -# ── Scout: get prices on Feb 24, 2022 ───────────────────────────── -echo "── Scouting prices on 2022-02-24 ──" -echo "" - -OIL_PRICE=$($BT --at 2022-02-24 --json '{"command":"quote","symbol":"OIL"}' 2>/dev/null | jq -r '.price') -GOLD_PRICE=$($BT --at 2022-02-24 --json '{"command":"quote","symbol":"GOLD"}' 2>/dev/null | jq -r '.price') -SP_PRICE=$($BT --at 2022-02-24 --json '{"command":"quote","symbol":"SP500"}' 2>/dev/null | jq -r '.price') - -echo " OIL: \$$OIL_PRICE / bbl" -echo " GOLD: \$$GOLD_PRICE / oz" -echo " SP500: \$$SP_PRICE" -echo "" - -# ── Leg 1: Long crude oil — supply disruption ───────────────────── -echo "── Leg 1: Long OIL (supply shock from sanctions) ──" -$BT --at 2022-02-24 buy OIL --amount 35 --price "$OIL_PRICE" - -# ── Leg 2: Long gold — geopolitical safe haven ──────────────────── -echo "── Leg 2: Long GOLD (war premium + safe haven) ──" -$BT --at 2022-02-24 buy GOLD --amount 2 --price "$GOLD_PRICE" - -# ── Leg 3: Short S&P 500 — risk-off selloff ────────────────────── -echo "── Leg 3: Short S&P 500 (risk-off) ──" -$BT --at 2022-02-24 sell SP500 --amount 1.5 --price "$SP_PRICE" - -# ── Portfolio snapshot ───────────────────────────────────────────── -echo "══════════════════════════════════════════════════════════════" -echo " Portfolio Summary" -echo "══════════════════════════════════════════════════════════════" -$BT --at 2022-02-24 balance -$BT --at 2022-02-24 positions - -# ── Cleanup ──────────────────────────────────────────────────────── -$BT --at 2022-02-24 reset > /dev/null 2>&1 diff --git a/skills/SKILL.md b/skills/SKILL.md index 9095c5a..8ffe4bc 100644 --- a/skills/SKILL.md +++ b/skills/SKILL.md @@ -1,6 +1,6 @@ --- name: fintool -description: "Financial trading CLIs — spot and perp trading on Hyperliquid, Binance, Coinbase, OKX. Prediction markets on Polymarket. Deposit and withdraw across chains. LLM-enriched price quotes with trend analysis. News and SEC filings. Use when: user asks about stock/crypto prices, wants to trade, deposit, withdraw, or check portfolio." +description: "Financial trading CLIs — spot and perp trading on Hyperliquid, Binance, Coinbase, OKX. Prediction markets on Polymarket. Deposit and withdraw across chains. LLM-enriched price quotes with trend analysis. News and SEC filings. Historical backtesting with forward PnL analysis. Use when: user asks about stock/crypto prices, wants to trade, deposit, withdraw, check portfolio, or backtest a strategy." homepage: https://github.com/second-state/fintool metadata: { "openclaw": { "emoji": "📈", "requires": { "bins": ["curl"] } } } --- @@ -19,6 +19,7 @@ A suite of CLI tools for market intelligence and trading across multiple exchang | `coinbase` | Coinbase trading (spot, deposits) | `{baseDir}/scripts/coinbase` | | `okx` | OKX trading (spot, perp, deposits, withdrawals) | `{baseDir}/scripts/okx` | | `polymarket` | Polymarket prediction markets | `{baseDir}/scripts/polymarket` | +| `backtest` | Historical simulation + forward PnL (no keys needed) | `{baseDir}/scripts/backtest` | - **Config**: `~/.fintool/config.toml` - **Mode**: Always use JSON mode — ` --json ''`. All input and output is structured JSON. @@ -51,24 +52,29 @@ cat ~/.fintool/config.toml 2>/dev/null - **Polymarket**: Uses `wallet.private_key` (same as Hyperliquid) for prediction market trading - If none configured: Ask the user which exchange they want to use and request the credentials. +**Exception — `backtest`**: The backtest binary requires **no configuration** — no API keys, no wallet. It uses public Yahoo Finance and CoinGecko data. You can use it immediately. + **If the user provides credentials**, edit `~/.fintool/config.toml` directly to add them. ## Exchange Capabilities -| Feature | `hyperliquid` | `binance` | `coinbase` | `okx` | `polymarket` | -|---------|---------------|-----------|------------|-------|--------------| -| Spot orders | buy, sell | buy, sell | buy, sell | buy, sell | — | -| Perp orders | perp buy/sell | perp buy/sell | — | perp buy/sell | — | -| Prediction markets | — | — | — | — | buy, sell, list, quote | -| Orderbook | spot + perp | spot + perp | spot | spot + perp | — | -| Deposit | Unit + Across | API | API | API | bridge | -| Withdraw | Bridge2 + Unit + Across | API | API | API | bridge | -| Transfer | spot ↔ perp ↔ dex | spot ↔ futures | — | funding ↔ trading | — | -| Balance | balance | balance | balance | balance | balance | -| Open orders | orders | orders | orders | orders | — | -| Cancel | cancel | cancel | cancel | cancel | — | -| Positions | positions | positions | — | positions | positions | -| Funding rate | — | — | — | perp funding_rate | — | +| Feature | `hyperliquid` | `binance` | `coinbase` | `okx` | `polymarket` | `backtest` | +|---------|---------------|-----------|------------|-------|--------------|------------| +| Spot orders | buy, sell | buy, sell | buy, sell | buy, sell | — | simulated buy/sell | +| Perp orders | perp buy/sell | perp buy/sell | — | perp buy/sell | — | simulated perp buy/sell | +| Prediction markets | — | — | — | — | buy, sell, list, quote | — | +| Orderbook | spot + perp | spot + perp | spot | spot + perp | — | — | +| Deposit | Unit + Across | API | API | API | bridge | — | +| Withdraw | Bridge2 + Unit + Across | API | API | API | bridge | — | +| Transfer | spot ↔ perp ↔ dex | spot ↔ futures | — | funding ↔ trading | — | — | +| Balance | balance | balance | balance | balance | balance | simulated | +| Open orders | orders | orders | orders | orders | — | — | +| Cancel | cancel | cancel | cancel | cancel | — | — | +| Positions | positions | positions | — | positions | positions | simulated | +| Funding rate | — | — | — | perp funding_rate | — | — | +| Historical quote | — | — | — | — | — | quote | +| Forward PnL | — | — | — | — | — | +1d/+2d/+4d/+7d | +| SEC filings (dated) | — | — | — | — | — | report list/annual/quarterly | ## Error Handling @@ -212,6 +218,51 @@ cat ~/.fintool/config.toml 2>/dev/null - Trading commands (`buy`, `sell`, `deposit`, `withdraw`) require `wallet.private_key` in config. - Use the market slug (from `list`) or condition ID as the `market` value. +### Backtesting (`backtest`) + +**Important**: The `backtest` binary requires `--at YYYY-MM-DD` as a CLI flag (not in the JSON body). No API keys or wallet needed. + +```bash +# backtest --at --json '' +``` + +```json +// Historical price +{"command": "quote", "symbol": "BTC"} +{"command": "quote", "symbol": "AAPL"} +{"command": "quote", "symbol": "GOLD"} + +// Simulated spot trades — returns forward PnL at +1/+2/+4/+7 days +{"command": "buy", "symbol": "ETH", "amount": 0.5} +{"command": "buy", "symbol": "AAPL", "amount": 10, "price": 237} +{"command": "sell", "symbol": "BTC", "amount": 0.01, "price": 105000} + +// Simulated perp trades — returns leveraged forward PnL +{"command": "perp_leverage", "symbol": "ETH", "leverage": 5} +{"command": "perp_buy", "symbol": "ETH", "amount": 0.5, "price": 3300} +{"command": "perp_sell", "symbol": "BTC", "amount": 0.01, "price": 100000} + +// SEC filings filtered by date +{"command": "report_list", "symbol": "AAPL", "limit": 5} +{"command": "report_annual", "symbol": "TSLA"} +{"command": "report_quarterly", "symbol": "AAPL"} + +// Portfolio management +{"command": "balance"} +{"command": "positions"} +{"command": "reset"} + +// News stub (historical news not available) +{"command": "news", "symbol": "BTC"} +``` + +**Notes:** +- If `price` is omitted on buy/sell, the historical close price at the `--at` date is used automatically. +- Portfolio state persists to `~/.fintool/backtest_portfolio.json`. Use `reset` to clear. +- `balance` returns `cashBalance` (spot trades only), `positions`, `totalTrades`, `leverageSettings`. +- Trade output includes a `pnl` array with forward price, dollar PnL, and percentage PnL at each offset. +- Data sources: Yahoo Finance (stocks, crypto, commodities, indices) with CoinGecko fallback for crypto. + ## Workflows ### Workflow 1: Spot Trading @@ -422,9 +473,64 @@ Returns: matching markets with question, outcomes, prices, volume, liquidity. By {baseDir}/scripts/polymarket --json '{"command":"balance"}' ``` +### Workflow 7: Backtesting a Trading Strategy + +**Goal**: Develop a thesis, simulate trades at historical dates, and evaluate forward PnL before live trading. + +**Use backtest when** the agent is developing a strategy, validating a thesis with historical data, or the user asks "what if I had bought X on date Y?" + +**Step 1 — Develop a thesis using current data:** +```bash +{baseDir}/scripts/fintool --json '{"command":"news","symbol":"BTC"}' +{baseDir}/scripts/fintool --json '{"command":"report_list","symbol":"AAPL","limit":5}' +``` +Use news and SEC filings to identify a catalyst or thesis (e.g., "NVDA earnings blowout", "oil supply shock"). + +**Step 2 — Reset the backtest portfolio:** +```bash +{baseDir}/scripts/backtest --at 2025-01-15 --json '{"command":"reset"}' +``` + +**Step 3 — Scout historical prices:** +```bash +{baseDir}/scripts/backtest --at 2025-01-15 --json '{"command":"quote","symbol":"BTC"}' +{baseDir}/scripts/backtest --at 2025-01-15 --json '{"command":"quote","symbol":"GOLD"}' +``` + +**Step 4 — Execute simulated trades:** +```bash +# Spot buy (auto-price from historical close) +{baseDir}/scripts/backtest --at 2025-01-15 --json '{"command":"buy","symbol":"BTC","amount":0.01}' + +# Spot short +{baseDir}/scripts/backtest --at 2025-01-15 --json '{"command":"sell","symbol":"SP500","amount":1.5,"price":5900}' + +# Leveraged perp +{baseDir}/scripts/backtest --at 2025-01-15 --json '{"command":"perp_leverage","symbol":"ETH","leverage":5}' +{baseDir}/scripts/backtest --at 2025-01-15 --json '{"command":"perp_buy","symbol":"ETH","amount":0.5,"price":3300}' +``` +Each trade returns forward PnL at +1, +2, +4, +7 days — review these to evaluate the thesis. + +**Step 5 — Review portfolio state:** +```bash +{baseDir}/scripts/backtest --at 2025-01-15 --json '{"command":"balance"}' +{baseDir}/scripts/backtest --at 2025-01-15 --json '{"command":"positions"}' +``` + +**Step 6 — Iterate:** +- Try different entry dates to test timing sensitivity +- Adjust position sizes for risk management +- Test multi-leg strategies (e.g., long one asset + short another) +- Check SEC filings before the trade date for fundamental context: + ```bash + {baseDir}/scripts/backtest --at 2024-06-01 --json '{"command":"report_list","symbol":"AAPL","limit":3}' + ``` + +**Step 7 — If the backtest validates the thesis, proceed to live trading** using the appropriate exchange binary (hyperliquid, binance, etc.). + ## Symbol Aliases -Common indices and commodities have convenient aliases (used with `fintool quote`): +Common indices and commodities have convenient aliases (used with `fintool quote` and `backtest quote`): | Alias | Description | |-------|-------------| @@ -446,3 +552,5 @@ Common indices and commodities have convenient aliases (used with `fintool quote - **All output is JSON** — Parse the response and present relevant fields to the user in a readable format. - **Amount is in symbol units** — `"amount": 0.1` on ETH means 0.1 ETH, not $0.10. Calculate the size from the price quote. - **Check for errors** — Every response may contain `{"error": "..."}`. Always check before presenting results. +- **Backtest before trading** — When developing a thesis or strategy, use `backtest` to simulate at historical dates and validate with forward PnL before committing real capital. +- **No config for backtest** — The `backtest` binary needs no API keys or wallet. Use it freely for research and strategy development. diff --git a/skills/bootstrap.sh b/skills/bootstrap.sh index ec1845b..edc1bf6 100755 --- a/skills/bootstrap.sh +++ b/skills/bootstrap.sh @@ -42,7 +42,7 @@ esac echo "Downloading ${ARTIFACT}..." curl -L -o /tmp/fintool.zip "${RELEASE_BASE}/${ARTIFACT}.zip" unzip -o /tmp/fintool.zip -d /tmp/fintool-extract -BINARIES="fintool hyperliquid binance coinbase polymarket okx" +BINARIES="fintool hyperliquid binance coinbase polymarket okx backtest" for bin in $BINARIES; do src="/tmp/fintool-extract/${ARTIFACT}/${bin}" if [ -f "$src" ]; then diff --git a/skills/install.md b/skills/install.md index f29bf3f..b7e0620 100644 --- a/skills/install.md +++ b/skills/install.md @@ -23,7 +23,7 @@ rm -rf /tmp/fintool-repo The bootstrap script will: 1. Clone skill files to `~/.openclaw/skills/fintool/` 2. Detect your platform (Linux x86_64/aarch64, macOS Apple Silicon) -3. Download all binaries (fintool, hyperliquid, binance, coinbase, polymarket, okx) from the latest GitHub release +3. Download all binaries (fintool, hyperliquid, binance, coinbase, polymarket, okx, backtest) from the latest GitHub release 4. Run `fintool init` to create `~/.fintool/config.toml` (never overwrites existing) 5. Check config for required keys and tell you what's missing @@ -58,7 +58,7 @@ If the bootstrap script fails: ```bash mkdir -p ~/.openclaw/skills/fintool/scripts unzip fintool-.zip - cp fintool-/{fintool,hyperliquid,binance,coinbase,polymarket,okx} ~/.openclaw/skills/fintool/scripts/ + cp fintool-/{fintool,hyperliquid,binance,coinbase,polymarket,okx,backtest} ~/.openclaw/skills/fintool/scripts/ chmod +x ~/.openclaw/skills/fintool/scripts/* ``` 4. Copy the skill definition: