diff options
| author | Dan Rostovtsev <dan@rostovtsev.org> | 2026-04-06 15:48:14 -0400 |
|---|---|---|
| committer | Dan Rostovtsev <dan@rostovtsev.org> | 2026-04-06 15:48:14 -0400 |
| commit | 0716a22d1fab76a18606ec031d33914ccbc56633 (patch) | |
| tree | d63cc7ee4fd73bac935a776d09dc0ec78c8335e2 /test | |
| parent | 2c6cf786c151118232533a6cfbc769ce1514aa9e (diff) | |
* 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.
Diffstat (limited to 'test')
| -rw-r--r-- | test/account-response.json | 33 | ||||
| -rw-r--r-- | test/api.scm | 93 | ||||
| -rw-r--r-- | test/auth-response.json | 13 | ||||
| -rw-r--r-- | test/ledger-response.json | 65 | ||||
| -rw-r--r-- | test/order-confirmation.json | 1 | ||||
| -rw-r--r-- | test/order-example.json | 32 | ||||
| -rw-r--r-- | test/order-preview-response.json | 29 | ||||
| -rw-r--r-- | test/order-reject-response.json | 3 | ||||
| -rw-r--r-- | test/order-reply-response.json | 7 | ||||
| -rw-r--r-- | test/order-status-response.json | 44 | ||||
| -rw-r--r-- | test/order-warning-response.json | 12 | ||||
| -rw-r--r-- | test/position-response.json | 71 | ||||
| -rw-r--r-- | test/snapshot-response.json | 13 | ||||
| -rw-r--r-- | test/stocks-response.json | 51 | ||||
| -rw-r--r-- | test/types.scm | 146 |
15 files changed, 613 insertions, 0 deletions
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") |
