diff --git a/scripts/surfer/ibkr_paper_book.py b/scripts/surfer/ibkr_paper_book.py new file mode 100644 index 000000000..ed1102c12 --- /dev/null +++ b/scripts/surfer/ibkr_paper_book.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Run the adaptive multi-strat book on IBKR PAPER trading (realistic fills, no capital). + +Gets target weights from the harness (Yahoo data), connects to your IB Gateway/TWS paper account, +and rebalances the ETF book on it. Crypto sleeve -> IBIT (Bitcoin ETF: in-brokerage, no crypto- +exchange counterparty tail). Default DRY-RUN (shows intended orders); pass `rebalance` to place them. + +SETUP (you do this — your credentials, never me): + 1. Create a free IBKR paper-trading account (or use your live account's paper login). + 2. Run IB Gateway (or TWS) and log in to the PAPER account. + 3. In Gateway/TWS: API settings -> enable "ActiveX and Socket Clients", note the port + (IB Gateway paper = 4002, TWS paper = 7497). Add 127.0.0.1 to trusted IPs. + 4. Then: python3 scripts/surfer/ibkr_paper_book.py status (test the connection) + python3 scripts/surfer/ibkr_paper_book.py dry (show intended orders) + python3 scripts/surfer/ibkr_paper_book.py rebalance (place orders on PAPER) + Override port: ... --port 7497 + +Sizing uses Yahoo last-close (consistent with the book); IBKR fills at market. Hysteresis: only +trade a name if its target $ drifts > 3% of NLV. Run weekly/monthly via cron once validated. +""" +import datetime +import json +import math +import os +import sys +import urllib.request + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from multistrat_paper import build, book_series, INSTR # noqa: E402 + +TICKER = {"equity": "SPY", "bond": "IEF", "gold": "GLD", "commod": "PDBC", "trend": "DBMF", "crypto": "IBIT"} +HYST = 0.03 # only trade if target weight drifts > 3% of NLV + + +def yhist_last(sym): + res = json.loads(urllib.request.urlopen(urllib.request.Request( + f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=5d", + headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0] + c = [x for x in res["indicators"]["quote"][0]["close"] if x is not None] + return float(c[-1]) + + +def target_book(): + """{ticker: weight} from the adaptive harness (Yahoo data).""" + dates, R = build() + book, w, L = book_series(R) + return {TICKER[nm]: float(w[j] * L) for j, (_, nm) in enumerate(INSTR)} + + +def main(): + mode = sys.argv[1] if len(sys.argv) > 1 else "dry" + port = int(sys.argv[sys.argv.index("--port") + 1]) if "--port" in sys.argv else 4002 + + tw = target_book() + px = {t: yhist_last(t if t != "IBIT" else "IBIT") for t in tw} # IBIT trades on Yahoo too + print(f"target weights (adaptive book, {datetime.date.today()}):") + for t, w in tw.items(): + print(f" {t:>6}: {100*w:>5.1f}% (px ~${px[t]:.2f})") + + if mode == "target": + return + + from ib_async import IB, Stock, MarketOrder + ib = IB() + try: + ib.connect("127.0.0.1", port, clientId=7, timeout=15) + except Exception as e: + print(f"\nCONNECT FAILED on 127.0.0.1:{port} — is IB Gateway/TWS running + logged in to PAPER, API enabled?") + print(f" ({type(e).__name__}: {str(e)[:80]}) | try --port 7497 for TWS paper") + return + nlv = next((float(v.value) for v in ib.accountSummary() if v.tag == "NetLiquidation"), 0.0) + cash = next((float(v.value) for v in ib.accountSummary() if v.tag == "TotalCashValue"), 0.0) + pos = {p.contract.symbol: p.position for p in ib.positions()} + print(f"\nIBKR PAPER account: NLV ${nlv:,.0f} cash ${cash:,.0f} | positions: {pos or 'none'}") + if nlv <= 0: + print(" (no NLV — check the paper account is funded with paper cash)"); ib.disconnect(); return + + orders = [] + for t, w in tw.items(): + tgt_sh = nlv * w / px[t] + cur = pos.get(t, 0.0) + delta = tgt_sh - cur + if abs(delta * px[t]) > HYST * nlv: + orders.append((t, "BUY" if delta > 0 else "SELL", round(abs(delta), 4))) + print(f"\nintended orders (hysteresis {int(HYST*100)}% of NLV):") + for t, side, qty in orders: + print(f" {side:>4} {qty:>9.4f} {t} (~${qty*px[t]:,.0f})") + if not orders: + print(" none — book already within hysteresis band.") + + if mode == "rebalance" and orders: + print("\nPLACING on PAPER...") + for t, side, qty in orders: + c = Stock(t, "SMART", "USD"); ib.qualifyContracts(c) + o = MarketOrder(side, qty); o.account = ib.managedAccounts()[0] + tr = ib.placeOrder(c, o); ib.sleep(1) + print(f" {side} {qty} {t}: {tr.orderStatus.status}") + ib.sleep(3) + print("done — check fills in Gateway/TWS.") + elif orders: + print("\n(dry-run — pass 'rebalance' to actually place these on the paper account.)") + ib.disconnect() + + +if __name__ == "__main__": + main()