From 0716a22d1fab76a18606ec031d33914ccbc56633 Mon Sep 17 00:00:00 2001 From: Dan Rostovtsev Date: Mon, 6 Apr 2026 15:48:14 -0400 Subject: First working IBKR API implementation. * doc/ibkr.org: Docs for using the IBKR API. * manifest.scm: Guix Manifest of all project dependencies. * scripts/run-gateway.bash: A script for building and deploying the IBKR Client Gateway. * src/ibkr/api.scm: Support for specific endpoints, and generic tools for using the IBKR API. * src/ibkr/types.scm: Basic types for the IBKR endpoints. Orders, positions, securities, etc. * test/*.json: IBKR response and request examples for testing. * test/api.scm: Response handling and endpoint construction. * test/types.scm: Tests JSON parsing of IBKR requests and responses. --- scripts/compile-docs.el | 6 +++ scripts/deploy.bash | 19 --------- scripts/keepalive.bash | 7 ---- scripts/paper-trade-test.scm | 95 ++++++++++++++++++++++++++++++++++++++++++++ scripts/preview-order.scm | 17 ++++++++ scripts/run-gateway.bash | 8 ++++ scripts/start-gateway.bash | 19 +++++++++ scripts/tickle.py | 5 --- 8 files changed, 145 insertions(+), 31 deletions(-) create mode 100644 scripts/compile-docs.el delete mode 100755 scripts/deploy.bash delete mode 100755 scripts/keepalive.bash create mode 100644 scripts/paper-trade-test.scm create mode 100644 scripts/preview-order.scm create mode 100755 scripts/run-gateway.bash create mode 100755 scripts/start-gateway.bash delete mode 100755 scripts/tickle.py (limited to 'scripts') diff --git a/scripts/compile-docs.el b/scripts/compile-docs.el new file mode 100644 index 0000000..e13f7a8 --- /dev/null +++ b/scripts/compile-docs.el @@ -0,0 +1,6 @@ +(require 'ox-texinfo) +(let ((infile "./doc/ibkr.org") + (outfile "./doc/ibkr.texi")) + (with-temp-buffer + (insert-file-contents infile) + (org-export-to-file 'texinfo outfile))) diff --git a/scripts/deploy.bash b/scripts/deploy.bash deleted file mode 100755 index 0fd8193..0000000 --- a/scripts/deploy.bash +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/bash - -set -e - -# create env -TMP=`mktemp -t -d ibkr-cpgw.XXXXXX` -mkdir -p $TMP -cp resources/clientportal.gw.zip $TMP -cp resources/custom.yaml $TMP - -# build env -cd $TMP -unzip clientportal.gw.zip -cp custom.yaml ./root/custom.yaml - -# run gateway -./bin/run.sh ./root/custom.yaml - -rm -rf $TMP diff --git a/scripts/keepalive.bash b/scripts/keepalive.bash deleted file mode 100755 index 3b2a1ae..0000000 --- a/scripts/keepalive.bash +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/bash - -while [ 0 ]; do - ./scripts/tickle.py - echo teehee - sleep 60 -done diff --git a/scripts/paper-trade-test.scm b/scripts/paper-trade-test.scm new file mode 100644 index 0000000..29d1218 --- /dev/null +++ b/scripts/paper-trade-test.scm @@ -0,0 +1,95 @@ +(use-modules (ibkr api) (ibkr types) + (json) + (srfi srfi-1) (srfi srfi-64) + (ice-9 format)) + +;; connect +(display "connecting to ibkr api\n") +(define base "http://localhost:5000") +(format #t "connected: ~a\n" (auth-status-connected (auth-status base))) +(sleep 1) + +;; check mkt +(display "querying market data\n") +(define ibkr (contract-for-stock-ticker base "IBKR" "NASDAQ")) +(define ibkr-id (contract-id ibkr)) +(define ibkr-ref-px (contract-snapshot base ibkr-id 'last-trade)) +(format #t "contract-id(IBKR)=~a\n" ibkr-id) +(format #t "last-trade(IBKR)=$~a\n" ibkr-ref-px) +(sleep 1) + +;; acct info +(display "querying account info\n") +(define accts (accounts base)) +(define acct (car accts)) +(define acct-id (account-id acct)) +(define pos-t0 (positions base acct-id)) +(define bal-t0 (ledger-cash-balance (ledger base acct-id "USD"))) +(format #t "balance(t0)=$~a\n" bal-t0) +(format #t "account-type=~a\n" (account-type acct)) +(if (not (equal? (account-type acct) "DEMO")) + (exit 1)) +(sleep 1) + +;; buy +(define buy (make-order acct-id ibkr-id "MARKET" "BUY" "IOC" 10.0)) +(format #t "buy-order=~a\n" buy) +(define buy-preview (order-preview base acct-id buy)) +(format #t "buy-preview=~a\n" buy-preview) +(define buy-warning (order-submit base acct-id buy)) ;; no market data? +(format #t "buy-warning=~a\n" buy-warning) +(define buy-reply (order-confirm base (order-warning-id buy-warning))) +(format #t "buy-reply=~a\n" buy-reply) +(define buy-oid (order-reply-order-id buy-reply)) +(format #t "buy-oid=~a\n" buy-oid) +(format #t "sleeping to wait for fill\n") (sleep 5) ;; wait for fill +(define buy-status (order-status base buy-oid)) +(format #t "buy-status=~a\n" buy-status) +(define buy-px (string->number (order-status-average-price buy-status))) +(define buy-qty (string->number (order-status-quantity buy-status))) +(define pos-t1 (positions base acct-id)) +(define bal-t1 (ledger-cash-balance (ledger base acct-id "USD"))) + +(display "sleeping to avoid rate limit\n") (sleep 5) + +;; sell +(define sell (make-order acct-id ibkr-id "MARKET" "SELL" "IOC" 10.0)) +(format #t "sell-order=~a\n" sell) +(define sell-preview (order-preview base acct-id sell)) +(format #t "sell-preview=~a\n" sell-preview) +(define sell-warning (order-submit base acct-id sell)) ;; no market data? +(format #t "sell-warning=~a\n" sell-warning) +(define sell-reply (order-confirm base (order-warning-id sell-warning))) +(format #t "sell-reply=~a\n" sell-reply) +(define sell-oid (order-reply-order-id sell-reply)) +(format #t "sell-oid=~a\n" sell-oid) +(format #t "sleeping to wait for fill\n") (sleep 5) ;; wait for fill +(define sell-status (order-status base sell-oid)) +(format #t "sell-status=~a\n" buy-status) +(define sell-px (string->number (order-status-average-price sell-status))) +(define sell-qty (string->number (order-status-quantity sell-status))) +(define pos-t2 (positions base acct-id)) +(define bal-t2 (ledger-cash-balance (ledger base acct-id "USD"))) + +;; checks +(define (position poslst contract-id) + (find + (lambda (p) + (equal? (string->number (position-contract-id p)) contract-id)) + poslst)) +(define (abs-rel-diff a b) (/ (* 2 (abs (- a b))) (+ (abs a) (abs b)))) +(define (within-n-percent n a b) (< (* 100 (abs-rel-diff a b)) n)) +(test-begin "paper-trade") +(test-approximate "ibkr-qty-t0" 0 (position-quantity (position pos-t0 ibkr-id)) 1e-3) +(test-approximate "ibkr-qty-t1" 10 (position-quantity (position pos-t1 ibkr-id)) 1e-3) +(test-approximate "ibkr-qty-t2" 0 (position-quantity (position pos-t2 ibkr-id)) 1e-3) +(test-approximate "ibkr-buy-qty" 10 buy-qty 1e-3) +(test-approximate "ibkr-sell-qty" 10 sell-qty 1e-3) +(test-assert "buy-px-close-to-ref-px" (within-n-percent 5 buy-px ibkr-ref-px)) +(test-assert "sell-px-close-to-ref-px" (within-n-percent 5 sell-px ibkr-ref-px)) + +;; don't forget commission! +(test-approximate "bal-t1-expected" (- bal-t0 (* 10.0 buy-px)) bal-t1 1.05) +(test-approximate "bal-t2-expected" (+ bal-t1 (* 10.0 sell-px)) bal-t2 1.05) + +(test-end "paper-trade") diff --git a/scripts/preview-order.scm b/scripts/preview-order.scm new file mode 100644 index 0000000..5550a88 --- /dev/null +++ b/scripts/preview-order.scm @@ -0,0 +1,17 @@ +(use-modules (ibkr api) (ibkr types) (ice-9 format)) +(define base "http://localhost:5000") + +(format #t "auth-status=~a\n" (auth-status base)) +(define accts (accounts base)) +(format #t "accounts=~a\n" accts) +(define acct-id (account-id (car accts))) +(format #t "positions=~a\n" (positions base acct-id)) +(format #t "ledger=~a\n" (ledger base acct-id "USD")) +(format #t "stocks-by-symbol(IBKR)=~a\n" (stocks-by-symbol base "IBKR")) +(define ibkr-id (contract-id (contract-for-stock-ticker base "IBKR" "NASDAQ"))) +(format #t "contract-id(IBKR)=~a\n" ibkr-id) +(format #t "last-trade=~a\n" (contract-snapshot base ibkr-id 'last-trade)) +(define order (make-order acct-id ibkr-id "MARKET" "BUY" "IOC" 100.0)) +(format #t "order=~a\n" order) +(format #t "order-json=\"[~a]\"\n" (order->json order)) +(format #t "order-preview=~a\n" (order-preview base acct-id order)) diff --git a/scripts/run-gateway.bash b/scripts/run-gateway.bash new file mode 100755 index 0000000..fdd07c3 --- /dev/null +++ b/scripts/run-gateway.bash @@ -0,0 +1,8 @@ +#!/usr/bin/bash + +./scripts/start-gateway.bash > gw.out 2> gw.err & + +while [ 0 ]; do + sleep 60 + curl -G http://localhost:5000/v1/api/tickle > /dev/null 2> /dev/null +done diff --git a/scripts/start-gateway.bash b/scripts/start-gateway.bash new file mode 100755 index 0000000..0fd8193 --- /dev/null +++ b/scripts/start-gateway.bash @@ -0,0 +1,19 @@ +#!/usr/bin/bash + +set -e + +# create env +TMP=`mktemp -t -d ibkr-cpgw.XXXXXX` +mkdir -p $TMP +cp resources/clientportal.gw.zip $TMP +cp resources/custom.yaml $TMP + +# build env +cd $TMP +unzip clientportal.gw.zip +cp custom.yaml ./root/custom.yaml + +# run gateway +./bin/run.sh ./root/custom.yaml + +rm -rf $TMP diff --git a/scripts/tickle.py b/scripts/tickle.py deleted file mode 100755 index 59902d0..0000000 --- a/scripts/tickle.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 - -import requests - -requests.get("http://localhost:5000/v1/api/tickle") -- cgit v1.3