summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Makefile17
-rw-r--r--README9
-rw-r--r--doc/ibkr.org294
-rw-r--r--manifest.scm6
-rw-r--r--scripts/compile-docs.el6
-rwxr-xr-xscripts/keepalive.bash7
-rw-r--r--scripts/paper-trade-test.scm95
-rw-r--r--scripts/preview-order.scm17
-rwxr-xr-xscripts/run-gateway.bash8
-rwxr-xr-xscripts/start-gateway.bash (renamed from scripts/deploy.bash)0
-rwxr-xr-xscripts/tickle.py5
-rw-r--r--src/ibkr/api.scm201
-rw-r--r--src/ibkr/types.scm184
-rw-r--r--test/account-response.json33
-rw-r--r--test/api.scm93
-rw-r--r--test/auth-response.json13
-rw-r--r--test/ledger-response.json65
-rw-r--r--test/order-confirmation.json1
-rw-r--r--test/order-example.json32
-rw-r--r--test/order-preview-response.json29
-rw-r--r--test/order-reject-response.json3
-rw-r--r--test/order-reply-response.json7
-rw-r--r--test/order-status-response.json44
-rw-r--r--test/order-warning-response.json12
-rw-r--r--test/position-response.json71
-rw-r--r--test/snapshot-response.json13
-rw-r--r--test/stocks-response.json51
-rw-r--r--test/types.scm146
29 files changed, 1450 insertions, 15 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4d67737
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*~
+*.texi
+*.info
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6e3343a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,17 @@
+clean:
+ rm -rf $$(find . -name "*~") \
+ $$(find . -name "*.texi") \
+ $$(find . -name "*.info") \
+ gw.*
+
+check:
+ guile -L ./src test/types.scm
+ guile -L ./src test/api.scm
+
+doc/ibkr.texi: doc/ibkr.org
+ emacs --script scripts/compile-docs.el
+
+doc/ibkr.info: doc/ibkr.texi
+ texi2any doc/ibkr.texi -o doc/ibkr.info
+
+doc: doc/ibkr.info
diff --git a/README b/README
index 0a0ebd7..3babd0e 100644
--- a/README
+++ b/README
@@ -1,4 +1,7 @@
-Interactive Brokers Client Portal Gateway
-(ibkr-cpgw)
+Interactive Brokers API
+(ibkr)
+
+This module provides a REST-API service for trading with interactive
+brokers. It also provides a tool for configuring and installing the
+IBKR gateway.
-A tool for configuring and installing the IBKR gateway.
diff --git a/doc/ibkr.org b/doc/ibkr.org
new file mode 100644
index 0000000..f879129
--- /dev/null
+++ b/doc/ibkr.org
@@ -0,0 +1,294 @@
+#+TITLE: Interactive Brokers
+#+AUTHOR: Daniel Rostovtsev
+
+#+TEXINFO_DIR_CATEGORY: Trading
+#+TEXINFO_DIR_NAME: Interactive Brokers
+#+TEXINFO_DIR_DESC: Routing requests to the interactive brokers trading API
+
+* History
+
+Interactive Brokers is a publicly-traded broker dealer with an
+investment grade rating by Standard and Poor's. It started as a market
+maker with Thomas Peterffy, who bought a chair on AMEX in 1977 and
+founded "T.P. & Co." in 1978.
+
+In 1979, the company hired its first four employees, and wrote one of
+the first equity options trading algorithms, which they started using
+in 1982. By 1983, they created the first handheld computers to give
+traders recommendations on the floor, and in 1984 they began trading
+with a fully fledged electronic operation.
+
+After a period of rapid expansion in the late 1980s, the company
+rebrands as "Timber Hill" and grows to 150 employees, trading a
+variety of securities internationally as electronic trading is gaining
+wider adoption.
+
+In the late 1990s, the company expands from market making into
+brokerage services. In 1998, they begin to clear S&P futures for
+retail clients, and in 1999 they begin to offer "smart routing"
+services for multiple equity options.
+
+The company has continued to grow and add new retail and institutional
+clients since the early 2000s.
+
+More is available about interactive brokers at their site at:
+https://www.interactivebrokers.com/en/general/about/.
+
+* The Trading API
+
+Interactive Brokers offers a REST API for retail clients to send
+orders, query their accounts, and poll market data.
+
+Here we used the "Web API v1.0", which is documented at:
+https://www.interactivebrokers.com/campus/ibkr-api-page/cpapi-v1/#cpgw
+
+** Connecting to the API
+
+#+CINDEX: Client Gateway
+
+Interactive Brokers expects clients to route orders through a "client
+gateway" which is a service run on the client's machine program which
+manages authenticating and forwarding requests to the actual
+Interactive Brokers API.
+
+#+CINDEX: Procyon
+
+The client gateway is written in Java, and the code can be decompiled
+with tools like "procyon". By default, the gateway is configured to
+forward requests send to port 5000 on the service machine.
+
+#+CINDEX: SSL
+
+By default, the client listens with SSL on port 5000. However, if you
+are comfortable that this connection is secure you can turn off SSL
+and connect to it directly.
+
+We currently provide a tool to start-up a connection and keep it
+alive.
+
+#+BEGIN_SRC bash
+ ./scripts/run-gateway.bash &
+#+END_SRC
+
+After starting up a connection, the user can navigate to
+http://localhost:5000 and login. The connection logs are in ~gw.out~
+and the connection errors are in ~gw.err~. If you want to use a paper
+trading session, be sure to select paper trading and login with your
+paper trading account details.
+
+For more details, read the scripts themselves.
+
+** Idiosyncracies
+
+The IBKR API has a few idiosyncracies and patters specific to their
+implementation. One, their responses are untyped blobs, and the names
+for fixed values tend to change across different endpoints. As such,
+it is the responsibility of the user to normalize their interactions
+with the API and organize the data they use.
+
+In general, the API does not send reponses with return code not equal
+to 200 even when messages trigger errors or warnings that the user has
+to handle. It is up to the user to read the contents of messages to
+understand whether the response the result of normal behaviour or
+needs additional handling.
+
+Lastly, endpoints can return blobs of several "schemas". For instance,
+order messages can have standard reply messages, warning messages, or
+error messages, all of different "types". It is up to the user to
+handle this logic as well.
+
+** Paper Trading
+
+#+CINDEX: Paper Trading
+
+"Paper trading" is the practice of sending mock trades and being
+filled at mock prices to exercise a trading system. Paper trading is
+good for getting a feel for how your system behaves before sending
+real orders.
+
+* Scheme Interface
+
+The IBKR interface really only accept GET and POST requests. GETs
+always have an empty body, and POSTs have a JSON body. The headers
+expected by the API the same across endpoints. To make requests
+easier, we expose the following helpers in scheme.
+
+#+FINDEX: ibkr-get
+~ibkr-get~ (~uri~)
+
+Returns a GET request for an IBKR endpoint located at ~uri~.
+
+#+FINDEX: ibkr-post
+~ibkr-post~ (~uri~) (~body~)
+
+Returns a POST request for an IBKR endpoint located at ~uri~ with body
+~body~.
+
+The scheme interface helps build HTTP requests for common actions. All
+the helper methods take a ~base~ URI for the location of
+gateway. Extra information necessary for specific requests is passed
+in as additional arguments. For more details, see the ~(ibkr api)~
+module.
+
+Useful data structures for interacting with IBKR like orders or
+accounts are defined in ~(ibkr types)~.
+
+Sometimes, it is helpful to make requests manually. For this we offer
+two helper functions in addition to ~ibkr-get~ and ~ibkr-post~.
+
+#+FINDEX: send-ibkr-request
+~(send-ibkr-request request)~: sends a request (as constructed by
+either ~ibkr-get~ or ~ibkr-post~) to IBKR and returns a pair of the
+header and the contents of the response.
+
+#+FINDEX: v1-api
+~(v1-api base)~: returns the "v1" API endpoint relative to the base
+URI.
+
+** Authentication Status
+
+#+FINDEX: auth-status
+~(auth-status base)~: This method simply checks if the current session
+is authenticated.
+
+** Accounts
+
+#+FINDEX: accounts
+~(accounts base)~: Returns as a list the accounts available to the
+session.
+
+** Cash Balance
+
+#+FINDEX: ledger
+~(ledger base account-id currency)~: Returns a "ledger" representing
+the cash balance of a current account available in a given
+session. Currency types include ~"BASE"~ and ~"USD"~.
+
+** Positions
+
+#+FINDEX: positions
+~(positions base account-id)~: Returns the open positions of an
+account as a list.
+
+** Security Definitions
+
+#+FINDEX: stocks-by-symbol
+~(stocks-by-symbol base symbol)~: Returns all contracts matching a
+given symbol.
+
+#+FINDEX: first-contract-on-exchange
+~(first-contract-on-exchange stocks exchange)~: Returns the first
+stock contract on a given exchange.
+
+These two methods are useful in equity markets for determining the
+correct security associated with a given ticker (~symbol~).
+
+** Market Data Snapshots
+
+The following methods are exposed to get snapshots of market data in
+real time.
+
+#+FINDEX: contract-snapshot
+~(contract-snapshot base contract-id stat)~: Returns the latest
+snapshot value of a ~stat~ (as a symbol) for a given ~contract-id~.
+
+An example to get the latest trade in security "HBB:NYSE" is given
+below.
+
+#+BEGIN_SRC lisp
+ (use-modules (ibkr api) (ibkr types))
+ (define base "http://localhost:5000")
+ (define hbb (first-contract-on-exchange
+ (stocks-by-symbol base "HBB") "NYSE"))
+ (contract-snapshot base (contract-id hbb) 'last-trade)
+#+END_SRC
+
+#+FINDEX: snapshot-field
+~(snapshot-field symbol)~: Returns the integer ID that IBKR associates
+with a given market data statistic.
+
+The supported market data snapshot values include:
+
+- Last Trade ~last-trade~ (31)
+
+** Submitting Orders
+
+The "order" data structure helps with sending orders to IBKR.
+It requries the following fields:
+
+- ~account-id~: The ~account-id~ associated with the trade.
+
+- ~contract-id~: The ~contract-id~ of the symbol being traded.
+
+- ~type~: ("MARKET", "LIMIT", etc...)
+
+- ~side~: "BUY" or "SELL"
+
+- ~tif~: "time-in-force". "IOC" for immediate or cancel.
+
+- ~quantity~: The quantity associated with the order.
+
+The order can be constructed with the ~make-order~ method.
+
+#+FINDEX: make-order
+#+BEGIN_SRC lisp
+(define ord (make-order "ACCT1234" "5678" "MARKET" "BUY" "IOC" 20))
+#+END_SRC
+
+This order represents a marketable immediate-or-cancel to purchase 20
+shares of 5678 for account "ACCT1234".
+
+IBKR allows users to preview orders before sending them.
+
+#+FINDEX: order-preview
+~(order-preview base account-id order)~: Previews the placement of
+~order~ for ~account-id~.
+
+The signature to send an order is identical.
+
+#+FINDEX: order-submit
+~(order-submit base account-id order)~: submit ~order~ for
+~account-id~.
+
+Sometimes the server will respond with a warning when sending an
+order. The response message will come with an associated ~reply-id~.
+To acknowledge the warning and proceed, use the ~order-confirm~
+method.
+
+#+FINDEX: order-confirm
+~(order-confirm base reply-id)~: confirm the warnings associated with
+an order and submit.
+
+** Reviewing Trades
+
+We expose the following methods to view the filled and open trades for
+a market session.
+
+#+FINDEX: refresh-live-orders
+~(refresh-live-orders base)~: Tells the IBKR API to refresh its cached
+listing of live orders.
+
+#+FINDEX: list-live-orders
+~(list-live-orders base)~: Lists the live orders (of all status types)
+for the current market session.
+
+#+FINDEX: set-default-account
+~(set-default-account base account-id)~: Sets the account for which
+live orders are queried.
+
+* Indices
+
+** Concepts
+:PROPERTIES:
+:INDEX: cp
+:END:
+
+** Data Types
+:PROPERTIES:
+:INDEX: tp
+:END:
+
+** Functions
+:PROPERTIES:
+:INDEX: fn
+:END:
diff --git a/manifest.scm b/manifest.scm
new file mode 100644
index 0000000..6ff9941
--- /dev/null
+++ b/manifest.scm
@@ -0,0 +1,6 @@
+(let ((build '("make" "guile" "guile-json" "guile-gnutls"))
+ (docs '("emacs" ; for org -> texi
+ "texinfo"))
+ (gateway '("unzip" "openjdk" "curl"))
+ (extras '("coreutils" "which")))
+ (specifications->manifest (append build docs gateway extras)))
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/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/deploy.bash b/scripts/start-gateway.bash
index 0fd8193..0fd8193 100755
--- a/scripts/deploy.bash
+++ b/scripts/start-gateway.bash
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")
diff --git a/src/ibkr/api.scm b/src/ibkr/api.scm
new file mode 100644
index 0000000..cfea408
--- /dev/null
+++ b/src/ibkr/api.scm
@@ -0,0 +1,201 @@
+(define-module (ibkr api)
+ #:use-module (ibkr types)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-43)
+ #:use-module (web uri)
+ #:use-module (web request)
+ #:use-module (web client)
+ #:use-module (ice-9 textual-ports)
+ #:use-module (ice-9 iconv)
+ #:use-module (ice-9 match)
+ #:use-module (json))
+
+(define user-agent-headers '((user-agent . "ibkr-cli")))
+(define json-content-headers '((content-type . "application/json")))
+(define-record-type <ibkr-request>
+ (ibkr-request uri method headers body)
+ ibkr-request?
+ (uri ibkr-request-uri)
+ (method ibkr-request-method)
+ (headers ibkr-request-headers)
+ (body ibkr-request-body))
+(define-public (ibkr-get uri)
+ (ibkr-request uri 'GET user-agent-headers #f))
+(define-public (ibkr-post uri body)
+ (let ((headers (append user-agent-headers
+ ; json-content-headers
+ )))
+ (ibkr-request uri 'POST headers body)))
+(define-public (v1-api base rel) (string-append base "/v1/api" rel))
+
+(define-public (response-header pair) (car pair))
+(define-public (response-body pair) (cdr pair))
+(define-public (send-ibkr-request ibkr-request)
+ (define-values (response-header response-body-bytes)
+ (let ((endpoint (string->uri (ibkr-request-uri ibkr-request)))
+ (headers (ibkr-request-headers ibkr-request))
+ (method (ibkr-request-method ibkr-request))
+ (body (ibkr-request-body ibkr-request)))
+ (match method
+ ('GET
+ (http-request endpoint
+ #:method 'GET
+ #:headers headers))
+ ('POST
+ (http-request endpoint
+ #:method 'POST
+ #:body body
+ #:headers headers)))))
+ (cons response-header (bytevector->string response-body-bytes "UTF-8")))
+(define (typed-response-body request responder json->type)
+ (json->type (response-body (responder request))))
+
+(define-public (auth-status-uri base) (v1-api base "/iserver/auth/status"))
+(define-public (auth-status base)
+ (typed-response-body (ibkr-get (auth-status-uri base))
+ send-ibkr-request
+ json->auth-status))
+
+(define-public (accounts-uri base) (v1-api base "/portfolio/accounts"))
+(define-public (response-body->accounts body)
+ (json->type-list scm->account body))
+(define-public (accounts base)
+ (typed-response-body (ibkr-get (accounts-uri base))
+ send-ibkr-request
+ response-body->accounts))
+
+(define-public (positions-uri base account-id)
+ (v1-api base (string-append "/portfolio2/" account-id "/positions")))
+(define-public (response-body->positions body)
+ (json->type-list scm->position body))
+(define-public (positions base account-id)
+ (typed-response-body (ibkr-get (positions-uri base account-id))
+ send-ibkr-request
+ response-body->positions))
+
+(define-public (ledger-uri base account-id)
+ (v1-api base (string-append "/portfolio/" account-id "/ledger")))
+(define-public (response-body->ledger body currency)
+ (let* ((alist (json-string->scm body))
+ (obj (assoc-ref alist currency)))
+ (if obj (scm->ledger obj) #f)))
+(define-public (ledger base account-id currency)
+ (typed-response-body (ibkr-get (ledger-uri base account-id))
+ send-ibkr-request
+ (lambda (json)
+ (response-body->ledger json currency))))
+
+(define-public (stocks-by-symbol-uri base symbol)
+ (v1-api base (string-append "/trsrv/stocks?symbols=" symbol)))
+(define-public (response-body->stocks-by-symbol body symbol)
+ (let* ((scm (json-string->scm body))
+ (stock-vec (assoc-ref scm symbol)))
+ (map scm->stock (vector->list stock-vec))))
+(define-public (stocks-by-symbol base symbol)
+ (typed-response-body (ibkr-get (stocks-by-symbol-uri base symbol))
+ send-ibkr-request
+ (lambda (json)
+ (response-body->stocks-by-symbol json symbol))))
+(define-public (first-contract-on-exchange stocks exch)
+ (let* ((get-contract-list (lambda (stock) (stock-contracts stock)))
+ (all-contracts (concatenate (map get-contract-list stocks)))
+ (pred (lambda (contract) (equal? (contract-exchange contract) exch)))
+ (res (find pred all-contracts)))
+ res))
+(define-public (contract-for-stock-ticker base ticker exch)
+ (first-contract-on-exchange (stocks-by-symbol base ticker) exch))
+
+(define-public (contract-snapshot-uri base contract-id stat)
+ (v1-api base
+ (string-append "/iserver/marketdata/snapshot"
+ "?conids=" (number->string contract-id)
+ "&fields=" (number->string (snapshot-field stat)))))
+(define-public (response-body->contract-snapshot body stat)
+ (let* ((scm (json-string->scm body))
+ (snap (vector-ref scm 0))
+ (key (number->string (snapshot-field stat))))
+ (string->number (assoc-ref snap key))))
+(define-public (contract-snapshot base contract-id stat)
+ (typed-response-body (ibkr-get (contract-snapshot-uri base contract-id stat))
+ send-ibkr-request
+ (lambda (json)
+ (response-body->contract-snapshot json stat))))
+
+(define-public (order->request-body order)
+ (scm->json-string `(("orders" . #(,(order->scm order))))))
+
+(define-public (order-preview-uri base account-id)
+ (v1-api base
+ (string-append "/iserver/account/" account-id "/orders/whatif")))
+(define-public (order-preview base account-id order)
+ (typed-response-body (ibkr-post (order-preview-uri base account-id)
+ (order->request-body order))
+ send-ibkr-request
+ json->order-preview))
+
+(define-public (order-submit-uri base account-id)
+ (v1-api base (string-append "/iserver/account/" account-id "/orders")))
+(define-public (order-submit base account-id order)
+ (typed-response-body (ibkr-post (order-submit-uri base account-id)
+ (order->request-body order))
+ send-ibkr-request
+ json-string->order-submit-response))
+
+(define-public (order-status-uri base order-id)
+ (v1-api base (string-append "/iserver/account/order/status/" order-id)))
+(define-public (order-status base order-id)
+ (typed-response-body (ibkr-get (order-status-uri base order-id))
+ send-ibkr-request
+ json->order-status))
+
+(define-public (suppress-uri base)
+ (v1-api base "/iserver/questions/suppress"))
+(define-public (suppress-messages base message-ids)
+ (typed-response-body
+ (ibkr-post
+ (suppress-uri base)
+ (scm->json-string `(("messageIds" . ,(list->vector message-ids)))))
+ send-ibkr-request
+ json-string->scm))
+
+(define-public (reset-suppress-uri base)
+ (v1-api base "/iserver/questions/suppress/reset"))
+(define-public (reset-suppress-messages base message-ids)
+ (typed-response-body
+ (ibkr-post (reset-suppress-uri base) "{}")
+ send-ibkr-request
+ json-string->scm))
+
+(define-public (reply-uri base reply-id)
+ (v1-api base (string-append "/iserver/reply/" reply-id)))
+(define-public (order-confirm base reply-id)
+ (typed-response-body (ibkr-post (reply-uri base reply-id)
+ (scm->json-string '(("confirmed" . #t))))
+ send-ibkr-request
+ json-string->order-submit-response))
+
+(define-public (set-default-account-id-uri base)
+ (v1-api base "/iserver/account"))
+(define-public (set-default-account-id base account-id)
+ (typed-response-body
+ (ibkr-post (set-default-account-id-uri base)
+ (scm->json-string `(("acctId" . ,account-id))))
+ send-ibkr-request
+ json-string->scm))
+
+(define-public (refresh-live-orders-uri base)
+ (v1-api base "/iserver/account/orders?force=true"))
+(define-public (refresh-live-orders base)
+ (typed-response-body
+ (ibkr-get (refresh-live-orders-uri base))
+ send-ibkr-request
+ json-string->scm))
+
+(define-public (list-live-orders-uri base)
+ (v1-api base "/iserver/account/orders"))
+(define-public (list-live-orders base)
+ (typed-response-body
+ (ibkr-get (list-live-orders-uri base))
+ send-ibkr-request
+ json-string->scm))
diff --git a/src/ibkr/types.scm b/src/ibkr/types.scm
new file mode 100644
index 0000000..8cc563d
--- /dev/null
+++ b/src/ibkr/types.scm
@@ -0,0 +1,184 @@
+(define-module (ibkr types)
+ #:use-module (json) ; json records
+ #:use-module (srfi srfi-1) ; lists
+ #:use-module (srfi srfi-43) ; vector map (vec in scm <-> array in json)
+ #:export (make-auth-status
+ auth-status?
+ auth-status->json
+ json->auth-status
+ scm->auth-status
+ auth-status-connected
+
+ make-account
+ account?
+ account->json
+ json->account
+ scm->account
+ account-id
+ account-type
+
+ make-position
+ position?
+ position->json
+ json->position
+ scm->position
+ position-contract-id
+ position-quantity
+
+ make-ledger
+ ledger?
+ ledger->json
+ json->ledger
+ scm->ledger
+ ledger-cash-balance
+ ledger-currency
+
+ make-contract
+ contract?
+ contract->json
+ json->contract
+ scm->contract
+ contract-exchange
+ contract-exchange-in-usa
+ contract-id
+
+ make-stock
+ stock?
+ stock->json
+ json->stock
+ scm->stock
+ stock-contracts
+ stock-name
+
+ make-order
+ order?
+ order->json
+ order->scm
+ json->order
+ scm->order
+ order-account-id
+ order-contract-id
+ order-quantity
+ order-side
+ order-time-in-force
+ order-type
+
+ make-order-reply
+ order-reply?
+ order-reply->json
+ json->order-reply
+ scm->order-reply
+ order-reply-order-id
+ order-reply-status
+ order-reply-encrypted
+
+ make-order-warning
+ order-warning?
+ order-warning->json
+ json->order-warning
+ scm->order-warning
+ order-warning-id
+ order-warning-message
+ order-warning-is-suppressed
+ order-warning-message-ids
+
+ make-order-reject
+ order-reject?
+ order-reject->json
+ json->order-reject
+ scm->order-reject
+ order-reject-error
+
+ make-order-confirmation
+ order-confirmation?
+ order-confirmation->json
+ json->order-confirmation
+ scm->order-confirmation
+ order-confirmation-confirmed
+
+ make-order-preview
+ order-preview?
+ order-preview->json
+ json->order-preview
+ scm->order-preview
+ order-preview-amount
+ order-preview-warning
+ order-preview-error
+
+ make-order-preview-amount
+ order-preview-amount?
+ order-preview-amount->json
+ json->order-preview-amount
+ scm->order-preview-amount
+ order-preview-amount-commission
+ order-preview-amount-total
+
+ make-order-status
+ order-status?
+ order-status->json
+ json->order-status
+ scm->order-status
+ order-status-average-price
+ order-status-contract-id
+ order-status-currency
+ order-status-order-id
+ order-status-quantity
+ order-status-side
+ order-status-status
+ order-status-type
+
+ make-suppress-amount
+ suppress-amount?
+ suppress-amount->json
+ json->suppress-amount
+ scm->suppress-amount
+ suppress-message-ids))
+
+(define-json-type <auth-status> (connected))
+(define-json-type <account> (id) (type))
+(define-json-type <position>
+ (contract-id "conid")
+ (market-price "marketPrice")
+ (market-value "marketValue"))
+(define-json-type <ledger> (cash-balance "cashbalance") (currency "currency"))
+(define-json-type <contract> (id "conid") (exchange) (exchange-in-usa "isUS"))
+(define-json-type <stock> (name) (contracts "contracts" #(<contract>)))
+
+(define-json-type <order>
+ (account-id "acctId") (contract-id "conid") (type "orderType")
+ (side) (time-in-force "tif") (quantity))
+(define-json-type <order-reply>
+ (order-id "order_id") (status "order_status")
+ (encrypted "encrypt_message"))
+(define-json-type <order-warning>
+ (id) (message) (is-suppressed "isSuppressed") (message-ids "messageIds"))
+(define-json-type <order-reject> (error))
+(define-json-type <order-confirmation> (confirmed))
+(define-json-type <order-preview>
+ (amount "amount" <order-preview-amount>) (warning "warn") (error))
+(define-json-type <order-preview-amount> (commission) (total))
+(define-json-type <order-status>
+ (order-id "order_id") (contract-id "conid") (currency) (side)
+ (quantity "total_size") (status "order_status")
+ (average-price "average_price"))
+
+(define-json-type <suppress> (message-ids "messageIds"))
+
+(define (position-quantity pos)
+ (/ (position-market-value pos) (position-market-price pos)))
+
+(define-public (json->type-list scm->type json-str)
+ (let ((vec (json-string->scm json-str))
+ (f (lambda (scm) (scm->type scm))))
+ (map f (vector->list vec))))
+
+(define-public (snapshot-field symbol) (assoc-ref '((last-trade . 31)) symbol))
+
+(define-public (json-string->order-submit-response txt)
+ (let ((obj (json-string->scm txt)))
+ (if (assoc-ref obj "error")
+ (scm->order-reject obj)
+ (let ((hd (vector-ref obj 0)))
+ (if (assoc-ref hd "message")
+ (scm->order-warning hd)
+ (scm->order-reply hd))))))
diff --git a/test/account-response.json b/test/account-response.json
new file mode 100644
index 0000000..f0c048c
--- /dev/null
+++ b/test/account-response.json
@@ -0,0 +1,33 @@
+[
+ {
+ "id": "U1234567",
+ "PrepaidCrypto-Z": false,
+ "PrepaidCrypto-P": false,
+ "brokerageAccess": true,
+ "accountId": "U1234567",
+ "accountVan": "U1234567",
+ "accountTitle": "",
+ "displayName": "U1234567",
+ "accountAlias": null,
+ "accountStatus": 1644814800000,
+ "currency": "USD",
+ "type": "DEMO",
+ "tradingType": "PMRGN",
+ "businessType": "IB_PROSERVE",
+ "ibEntity": "IBLLC-US",
+ "faclient": false,
+ "clearingStatus": "O",
+ "covestor": false,
+ "noClientTrading": false,
+ "trackVirtualFXPortfolio": true,
+ "parent": {
+ "mmc": [],
+ "accountId": "",
+ "isMParent": false,
+ "isMChild": false,
+ "isMultiplex": false
+ },
+ "desc": "U1234567"
+ }
+]
+
diff --git a/test/api.scm b/test/api.scm
new file mode 100644
index 0000000..1a85a77
--- /dev/null
+++ b/test/api.scm
@@ -0,0 +1,93 @@
+(use-modules (ibkr api)
+ (ibkr types)
+ (json)
+ (web uri)
+ (web request)
+ (srfi srfi-64)
+ (ice-9 textual-ports)
+ (ice-9 iconv))
+
+;; (test-begin "errors")
+;; (test-error &ibkr-request-error (auth-status-connected "http://localhost:12345"))
+;; (test-end "errors")
+
+(define (read-text path)
+ (call-with-input-file path (lambda (p) (get-string-all p))))
+
+(test-begin "auth-status")
+(test-equal "uri" "http://base/v1/api/iserver/auth/status"
+ (auth-status-uri "http://base"))
+(test-end "auth-status")
+
+(test-begin "accounts")
+(test-equal "uri" "http://base/v1/api/portfolio/accounts"
+ (accounts-uri "http://base"))
+(let ((body (read-text "test/account-response.json")))
+ (test-assert "conversion" (account? (car (response-body->accounts body)))))
+(test-end "accounts")
+
+(test-begin "positions")
+(test-equal "uri" "http://base/v1/api/portfolio2/ACCT/positions"
+ (positions-uri "http://base" "ACCT"))
+(let ((body (read-text "test/position-response.json")))
+ (test-assert "conversion" (position? (car (response-body->positions body)))))
+(test-end "positions")
+
+(test-begin "ledger")
+(test-equal "uri" "http://base/v1/api/portfolio/ACCT/ledger"
+ (ledger-uri "http://base" "ACCT"))
+(let ((body (read-text "test/ledger-response.json")))
+ (test-assert "conversion" (ledger? (response-body->ledger body "USD"))))
+(test-end "ledger")
+
+(test-begin "stocks")
+(test-equal "uri"
+ "http://base/v1/api/trsrv/stocks?symbols=IBKR"
+ (stocks-by-symbol-uri "http://base" "IBKR"))
+(let* ((body (read-text "test/stocks-response.json"))
+ (converted (response-body->stocks-by-symbol body "AAPL")))
+ (test-assert "conversion-list" (list? converted))
+ (test-assert "conversion-type" (stock? (car converted)))
+ (test-assert "contract-list" (list? (stock-contracts (car converted))))
+ (test-equal "exchange-search"
+ 265598 (contract-id (first-contract-on-exchange converted "NASDAQ"))))
+(test-end "stocks")
+
+(test-begin "contract-snapshot")
+(test-equal "uri"
+ "http://base/v1/api/iserver/marketdata/snapshot?conids=265598&fields=31"
+ (contract-snapshot-uri "http://base" 265598 'last-trade))
+(let ((body (read-text "test/snapshot-response.json")))
+ (test-assert "conversion"
+ (number? (response-body->contract-snapshot body 'last-trade))))
+(test-end "contract-snapshot")
+
+(test-begin "order-preview")
+(test-equal "uri" "http://base/v1/api/iserver/account/ACCT/orders/whatif"
+ (order-preview-uri "http://base" "ACCT"))
+(test-end "order-preview")
+
+(test-begin "order-submit")
+(test-equal "uri" "http://base/v1/api/iserver/account/ACCT/orders"
+ (order-submit-uri "http://base" "ACCT"))
+(test-end "order-submit")
+
+(test-begin "order-status")
+(test-equal "uri" "http://base/v1/api/iserver/account/order/status/OID"
+ (order-status-uri "http://base" "OID"))
+(test-end "order-status")
+
+(test-begin "suppress")
+(test-equal "uri" "http://base/v1/api/iserver/questions/suppress"
+ (suppress-uri "http://base"))
+(test-end "suppress")
+
+(test-begin "reset-suppress")
+(test-equal "uri" "http://base/v1/api/iserver/questions/suppress/reset"
+ (reset-suppress-uri "http://base"))
+(test-end "reset-suppress")
+
+(test-begin "reply")
+(test-equal "uri" "http://base/v1/api/iserver/reply/REPID"
+ (reply-uri "http://base" "REPID"))
+(test-end "reply")
diff --git a/test/auth-response.json b/test/auth-response.json
new file mode 100644
index 0000000..cbf663d
--- /dev/null
+++ b/test/auth-response.json
@@ -0,0 +1,13 @@
+{
+ "authenticated": true,
+ "competing": false,
+ "connected": true,
+ "message": "",
+ "MAC": "12:B:B3:23:BF:A0",
+ "serverInfo": {
+ "serverName": "JifN19053",
+ "serverVersion": "Build 10.25.0p, Dec 5, 2023 5:48:12 PM"
+ },
+ "hardware_info": "3b0679ee|98:A2:B3:23:BC:A0",
+ "fail": ""
+}
diff --git a/test/ledger-response.json b/test/ledger-response.json
new file mode 100644
index 0000000..a44b040
--- /dev/null
+++ b/test/ledger-response.json
@@ -0,0 +1,65 @@
+{
+ "USD": {
+ "commoditymarketvalue": 0.0,
+ "futuremarketvalue": -1051.0,
+ "settledcash": 214716688.0,
+ "exchangerate": 1,
+ "sessionid": 1,
+ "cashbalance": 214716688.0,
+ "corporatebondsmarketvalue": 0.0,
+ "warrantsmarketvalue": 0.0,
+ "netliquidationvalue": 215335840.0,
+ "interest": 305569.94,
+ "unrealizedpnl": 39695.82,
+ "stockmarketvalue": 314123.88,
+ "moneyfunds": 0.0,
+ "currency": "USD",
+ "realizedpnl": 0.0,
+ "funds": 0.0,
+ "acctcode": "U1234567",
+ "issueroptionsmarketvalue": 0.0,
+ "key": "LedgerList",
+ "timestamp": 1702582321,
+ "severity": 0,
+ "stockoptionmarketvalue": -2.88,
+ "futuresonlypnl": -1051.0,
+ "tbondsmarketvalue": 0.0,
+ "futureoptionmarketvalue": 0.0,
+ "cashbalancefxsegment": 0.0,
+ "secondkey": "USD",
+ "tbillsmarketvalue": 0.0,
+ "endofbundle": 1,
+ "dividends": 0.0
+ },
+ "BASE": {
+ "commoditymarketvalue": 0.0,
+ "futuremarketvalue": -1051.0,
+ "settledcash": 215100080.0,
+ "exchangerate": 1,
+ "sessionid": 1,
+ "cashbalance": 215100080.0,
+ "corporatebondsmarketvalue": 0.0,
+ "warrantsmarketvalue": 0.0,
+ "netliquidationvalue": 215721776.0,
+ "interest": 305866.88,
+ "unrealizedpnl": 39907.37,
+ "stockmarketvalue": 316365.38,
+ "moneyfunds": 0.0,
+ "currency": "BASE",
+ "realizedpnl": 0.0,
+ "funds": 0.0,
+ "acctcode": "U1234567",
+ "issueroptionsmarketvalue": 0.0,
+ "key": "LedgerList",
+ "timestamp": 1702582321,
+ "severity": 0,
+ "stockoptionmarketvalue": -2.88,
+ "futuresonlypnl": -1051.0,
+ "tbondsmarketvalue": 0.0,
+ "futureoptionmarketvalue": 0.0,
+ "cashbalancefxsegment": 0.0,
+ "secondkey": "BASE",
+ "tbillsmarketvalue": 0.0,
+ "dividends": 0.0
+ }
+}
diff --git a/test/order-confirmation.json b/test/order-confirmation.json
new file mode 100644
index 0000000..fab80e7
--- /dev/null
+++ b/test/order-confirmation.json
@@ -0,0 +1 @@
+{"confirmed":true}
diff --git a/test/order-example.json b/test/order-example.json
new file mode 100644
index 0000000..43f5a02
--- /dev/null
+++ b/test/order-example.json
@@ -0,0 +1,32 @@
+{
+ "acctId": "U1234567",
+ "conid": 265598,
+ "conidex": "265598@SMART",
+ "manualIndicator": true,
+ "extOperator":"person1234",
+ "secType": "265598@STK",
+ "cOID": "AAPL-BUY-100",
+ "parentId": "None",
+ "orderType": "TRAILLMT",
+ "listingExchange": "NASDAQ",
+ "isSingleGroup": false,
+ "outsideRTH": true,
+ "price": 185.50,
+ "auxPrice": 183,
+ "side": "BUY",
+ "ticker": "AAPL",
+ "tif": "GTC",
+ "trailingAmt": 1.00,
+ "trailingType": "amt",
+ "referrer": "QuickTrade",
+ "quantity": 100,
+ "useAdaptive": false,
+ "isCcyConv": false,
+ "strategy": "Vwap",
+ "strategyParameters": {
+ "MaxPctVol":"0.1",
+ "StartTime":"14:00:00 EST",
+ "EndTime":"15:00:00 EST",
+ "AllowPastEndTime":true
+ }
+}
diff --git a/test/order-preview-response.json b/test/order-preview-response.json
new file mode 100644
index 0000000..8306301
--- /dev/null
+++ b/test/order-preview-response.json
@@ -0,0 +1,29 @@
+{
+ "amount": {
+ "amount": "1,977.60 USD (10 Shares)",
+ "commission": "1 USD",
+ "total": "1,978.60 USD"
+ },
+ "equity": {
+ "current": "215,415,594",
+ "change": "-1",
+ "after": "215,415,593"
+ },
+ "initial": {
+ "current": "116,965",
+ "change": "652",
+ "after": "117,617"
+ },
+ "maintenance": {
+ "current": "106,332",
+ "change": "592",
+ "after": "106,924"
+ },
+ "position": {
+ "current": "0",
+ "change": "10",
+ "after": "10"
+ },
+ "warn": "21/You are trying to submit an order without having market data for this instrument. \nIB strongly recommends against this kind of blind trading which may result in \nerroneous or unexpected trades.",
+ "error": null
+}
diff --git a/test/order-reject-response.json b/test/order-reject-response.json
new file mode 100644
index 0000000..a2b5e0c
--- /dev/null
+++ b/test/order-reject-response.json
@@ -0,0 +1,3 @@
+{
+ "error":"We cannot accept an order at the limit price you selected. Please submit your order using a limit price that is closer to the current market price of 197.79. Alternatively, you can convert your order to an Algorithmic Order (IBALGO)."
+}
diff --git a/test/order-reply-response.json b/test/order-reply-response.json
new file mode 100644
index 0000000..a6fe82d
--- /dev/null
+++ b/test/order-reply-response.json
@@ -0,0 +1,7 @@
+[
+ {
+ "order_id": "1234567890",
+ "order_status": "Submitted",
+ "encrypt_message": "1"
+ }
+]
diff --git a/test/order-status-response.json b/test/order-status-response.json
new file mode 100644
index 0000000..bfcc414
--- /dev/null
+++ b/test/order-status-response.json
@@ -0,0 +1,44 @@
+{
+ "sub_type": null,
+ "request_id": "209",
+ "server_id": "0",
+ "order_id": 1799796559,
+ "conidex": "265598",
+ "conid": 265598,
+ "symbol": "AAPL",
+ "side": "S",
+ "contract_description_1": "AAPL",
+ "listing_exchange": "NASDAQ.NMS",
+ "option_acct": "c",
+ "company_name": "APPLE INC",
+ "size": "0.0",
+ "total_size": "5.0",
+ "currency": "USD",
+ "account": "U1234567",
+ "order_type": "MARKET",
+ "cum_fill": "5.0",
+ "order_status": "Filled",
+ "order_ccp_status": "2",
+ "order_status_description": "Order Filled",
+ "tif": "DAY",
+ "fg_color": "#FFFFFF",
+ "bg_color": "#000000",
+ "order_not_editable": true,
+ "editable_fields":"",
+ "cannot_cancel_order": true,
+ "deactivate_order": false,
+ "sec_type": "STK",
+ "available_chart_periods": "#R|1",
+ "order_description": "Sold 5 Market, Day",
+ "order_description_with_contract": "Sold 5 AAPL Market, Day",
+ "alert_active": 1,
+ "child_order_type": "0",
+ "order_clearing_account": "U1234567",
+ "size_and_fills": "5",
+ "exit_strategy_display_price": "193.12",
+ "exit_strategy_chart_description": "Sold 5 @ 192.26",
+ "average_price": "192.26",
+ "exit_strategy_tool_availability": "1",
+ "allowed_duplicate_opposite": true,
+ "order_time": "231211180049"
+}
diff --git a/test/order-warning-response.json b/test/order-warning-response.json
new file mode 100644
index 0000000..6b084a5
--- /dev/null
+++ b/test/order-warning-response.json
@@ -0,0 +1,12 @@
+[
+ {
+ "id": "07a13a5a-4a48-44a5-bb25-5ab37b79186c",
+ "message": [
+ "The following order \"BUY 5 AAPL NASDAQ.NMS @ 150.0\" price exceeds \nthe Percentage constraint of 3%.\nAre you sure you want to submit this order?"
+ ],
+ "isSuppressed": false,
+ "messageIds": [
+ "o163"
+ ]
+ }
+]
diff --git a/test/position-response.json b/test/position-response.json
new file mode 100644
index 0000000..f11e20e
--- /dev/null
+++ b/test/position-response.json
@@ -0,0 +1,71 @@
+[
+ {
+ "acctId": "U1234567",
+ "conid": 756733,
+ "contractDesc": "SPY",
+ "position": 5.0,
+ "marketPrice": 471.16000365,
+ "marketValue": 2355.8,
+ "currency": "USD",
+ "avgCost": 434.93,
+ "avgPrice": 434.93,
+ "realizedPnl": 0.0,
+ "unrealizedPnl": 181.15,
+ "exchs": null,
+ "expiry": null,
+ "putOrCall": null,
+ "multiplier": null,
+ "strike": 0.0,
+ "exerciseStyle": null,
+ "conExchMap": [],
+ "assetClass": "STK",
+ "undConid": 0,
+ "model": ""
+ },
+ {
+ "acctId": "U1234567",
+ "conid": 76792991,
+ "contractDesc": "TSLA",
+ "position": 7.0,
+ "mktPrice": 250.73399355,
+ "mktValue": 1755.14,
+ "currency": "USD",
+ "avgCost": 221.67142855,
+ "avgPrice": 221.67142855,
+ "realizedPnl": 0.0,
+ "unrealizedPnl": 203.44,
+ "exchs": null,
+ "expiry": null,
+ "putOrCall": null,
+ "multiplier": null,
+ "strike": 0.0,
+ "exerciseStyle": null,
+ "conExchMap": [],
+ "assetClass": "STK",
+ "undConid": 0,
+ "model": ""
+ },
+ {
+ "acctId": "U1234567",
+ "conid": 107113386,
+ "contractDesc": "META",
+ "position": 11.0,
+ "mktPrice": 333.1199951,
+ "mktValue": 3664.32,
+ "currency": "USD",
+ "avgCost": 306.6909091,
+ "avgPrice": 306.6909091,
+ "realizedPnl": 0.0,
+ "unrealizedPnl": 290.72,
+ "exchs": null,
+ "expiry": null,
+ "putOrCall": null,
+ "multiplier": null,
+ "strike": 0.0,
+ "exerciseStyle": null,
+ "conExchMap": [],
+ "assetClass": "STK",
+ "undConid": 0,
+ "model": ""
+ }
+]
diff --git a/test/snapshot-response.json b/test/snapshot-response.json
new file mode 100644
index 0000000..4adf0cb
--- /dev/null
+++ b/test/snapshot-response.json
@@ -0,0 +1,13 @@
+[
+ {
+ "_updated": 1702334859712,
+ "conidEx": "265598",
+ "conid": 265598,
+ "server_id": "q1",
+ "6119": "serverId",
+ "31": "193.18",
+ "84": "193.06",
+ "86":"193.14",
+ "6509": "RpB"
+ }
+]
diff --git a/test/stocks-response.json b/test/stocks-response.json
new file mode 100644
index 0000000..f9d6d59
--- /dev/null
+++ b/test/stocks-response.json
@@ -0,0 +1,51 @@
+{
+ "AAPL": [
+ {
+ "name": "APPLE INC",
+ "chineseName": "苹果公司",
+ "assetClass": "STK",
+ "contracts": [
+ {
+ "conid": 265598,
+ "exchange": "NASDAQ",
+ "isUS": true
+ },
+ {
+ "conid": 38708077,
+ "exchange": "MEXI",
+ "isUS": false
+ },
+ {
+ "conid": 273982664,
+ "exchange": "EBS",
+ "isUS": false
+ }
+ ]
+ },
+ {
+ "name": "LS 1X AAPL",
+ "chineseName": null,
+ "assetClass": "STK",
+ "contracts": [
+ {
+ "conid": 493546048,
+ "exchange": "LSEETF",
+ "isUS": false
+ }
+ ]
+ },
+ {
+ "name": "APPLE INC-CDR",
+ "chineseName": "苹果公司",
+ "assetClass": "STK",
+ "contracts": [
+ {
+ "conid": 532640894,
+ "exchange": "AEQLIT",
+ "isUS": false
+ }
+ ]
+ }
+ ]
+}
+
diff --git a/test/types.scm b/test/types.scm
new file mode 100644
index 0000000..d886b2f
--- /dev/null
+++ b/test/types.scm
@@ -0,0 +1,146 @@
+(use-modules (ibkr types)
+ (json)
+ (ice-9 textual-ports)
+ (srfi srfi-1)
+ (srfi srfi-43)
+ (srfi srfi-64))
+
+(define (read-text path)
+ (call-with-input-file path (lambda (p) (get-string-all p))))
+
+(test-begin "json->type-list")
+(let* ((text (read-text "test/account-response.json"))
+ (arr (json-string->scm text)))
+ (test-assert "arr-is-vector" (vector? arr))
+ (let ((res (json->type-list scm->account text)))
+ (test-assert "res-is-list" (list? res))
+ (test-assert "res-is-typed" (account? (car res)))))
+(test-end "json->type-list")
+
+(test-begin "auth-status")
+(let ((auth (json->auth-status (read-text "test/auth-response.json"))))
+ (test-assert "connected" (auth-status-connected auth)))
+(test-end "auth-status")
+
+(test-begin "account")
+(let ((accts (json->type-list
+ scm->account (read-text "test/account-response.json"))))
+ (test-equal "length" 1 (length accts))
+ (let ((acct (car accts)))
+ (test-equal "id" "U1234567" (account-id acct))
+ (test-equal "type" "DEMO" (account-type acct))))
+(test-end "account")
+
+(test-begin "position")
+(let* ((positions
+ (json->type-list
+ scm->position (read-text "test/position-response.json")))
+ (hd (car positions)))
+ (test-equal "contract-id" 756733 (position-contract-id hd))
+ (test-approximate "quantity" 5.0 (position-quantity hd) 1e-6))
+(test-end "position")
+
+(test-begin "ledger")
+(let* ((scm (json-string->scm (read-text "test/ledger-response.json")))
+ (usd (scm->ledger (assoc-ref scm "USD"))))
+ (test-equal "cash-balance" 214716688 (ledger-cash-balance usd))
+ (test-equal "currency" "USD" (ledger-currency usd)))
+(test-end "ledger")
+
+(test-begin "contract")
+(let* ((scm (json-string->scm (read-text "test/stocks-response.json")))
+ (stocks (assoc-ref scm "AAPL"))
+ (stock (vector-ref stocks 0))
+ (contracts (assoc-ref stock "contracts"))
+ (contract (scm->contract (vector-ref contracts 0))))
+ (test-equal "contract-id" 265598 (contract-id contract))
+ (test-equal "exchange" "NASDAQ" (contract-exchange contract))
+ (test-equal "exchange-in-usa" #t (contract-exchange-in-usa contract)))
+(test-end "contract")
+
+(test-begin "stock")
+(let* ((scm (json-string->scm (read-text "test/stocks-response.json")))
+ (stocks (assoc-ref scm "AAPL"))
+ (stock (scm->stock (vector-ref stocks 0)))
+ (contracts (stock-contracts stock))
+ (contract (car contracts)))
+ (test-equal "name" "APPLE INC" (stock-name stock))
+ (test-equal "length" 3 (length contracts))
+ (test-assert "contract-type" (contract? contract))
+ (test-assert "list-type" (list? contracts)))
+(test-end "stock")
+
+(test-begin "snapshot")
+(test-equal "last-trade" 31 (snapshot-field 'last-trade))
+(test-equal "last-trade" #f (snapshot-field 'foobar))
+(test-end "snapshot")
+
+(test-begin "order")
+(let ((ord (json->order (read-text "test/order-example.json"))))
+ (test-equal "account-id" "U1234567" (order-account-id ord))
+ (test-equal "contract-id" 265598 (order-contract-id ord))
+ (test-equal "type" "TRAILLMT" (order-type ord))
+ (test-equal "side" "BUY" (order-side ord)) ; "BUY" or "SELL"
+ (test-equal "time-in-force" "GTC" (order-time-in-force ord))
+ (test-equal "quantity" 100 (order-quantity ord)))
+(test-end "order")
+
+(test-begin "order-reply")
+(let* ((lst (json->type-list json->order-reply
+ (read-text "test/order-reply-response.json")))
+ (reply (car lst)))
+ (test-equal "order-id" "1234567890" (order-reply-order-id reply))
+ (test-equal "status" "Submitted" (order-reply-status reply))
+ (test-equal "encrypted" "1" (order-reply-encrypted reply)))
+(test-end "order-reply")
+
+(test-begin "order-warning")
+(let* ((lst (json->type-list json->order-warning
+ (read-text "test/order-warning-response.json")))
+ (warning (car lst)))
+ (test-equal "id" "07a13a5a-4a48-44a5-bb25-5ab37b79186c"
+ (order-warning-id warning))
+ (test-assert "message" (vector? (order-warning-message warning))) ; of strings
+ (test-equal "suppressed" #f (order-warning-is-suppressed warning))
+ (test-equal "encrypted" #("o163") (order-warning-message-ids warning)))
+(test-end "order-warning")
+
+(test-begin "order-reject")
+(let* ((reject (json->order-reject
+ (read-text "test/order-reject-response.json"))))
+ (test-assert "error" (string? (order-reject-error reject))))
+(test-end "order-reject")
+
+(test-begin "order-confirmation")
+(let* ((conf (json->order-confirmation
+ (read-text "test/order-confirmation.json"))))
+ (test-assert "confirmed" (order-confirmation-confirmed conf)))
+(test-end "order-confirmation")
+
+(test-begin "order-preview")
+(let* ((preview (json->order-preview (read-text "test/order-preview-response.json")))
+ (amt (order-preview-amount preview)))
+ (test-equal "total" "1,978.60 USD" (order-preview-amount-total amt))
+ (test-equal "commission" "1 USD" (order-preview-amount-commission amt))
+ (test-assert "warning" (string? (order-preview-warning preview))))
+(test-end "order-preview")
+
+(test-begin "order-status")
+(let ((status (json->order-status (read-text "test/order-status-response.json"))))
+ (test-equal "order-id" 1799796559 (order-status-order-id status))
+ (test-equal "average-price" "192.26" (order-status-average-price status))
+ (test-equal "currency" "USD" (order-status-currency status))
+ (test-equal "contract-id" 265598 (order-status-contract-id status))
+ (test-equal "side" "S" (order-status-side status)) ; "B" or "S"
+ (test-equal "status" "Filled" (order-status-status status))
+ (test-equal "quantity" "5.0" (order-status-quantity status)))
+(test-end "order-status")
+
+(test-begin "json-string->order-submit-response")
+(let ((reply (read-text "test/order-reply-response.json"))
+ (warning (read-text "test/order-warning-response.json"))
+ (reject (read-text "test/order-reject-response.json")))
+ (test-assert "reply" (order-reply? (json-string->order-submit-response reply)))
+ (test-assert "warning" (order-warning? (json-string->order-submit-response warning)))
+ (test-assert "reject" (order-reject? (json-string->order-submit-response reject))))
+(test-begin "json-string->order-submit-response")