Interactive Brokers API
Short note of how to get up and running IBRK API
Prerequisites
#!/usr/bin/env bash
# Preparations:
# - java
# - client
# - certs
# Java (TODO: cross platform)
if ! [ -d java ]
then
case "$(uname -s)" in
Linux*) JRE_OS=linux;;
Darwin*) JRE_OS=mac;;
CYGWIN*) JRE_OS=windows;;
MINGW*) JRE_OS=windows;;
*) echo "Unsupported platform"; exit 1;;
esac
# echo "JRE_OS: $JRE_OS"
case "$(uname -m)" in
x86_64) JRE_ARCH=x64;;
arm64) JRE_ARCH=aarch64;;
*) echo "Unsupported architecture"; exit 1;;
esac
# echo "JRE_ARCH: $JRE_ARCH"
# curl -s https://api.adoptopenjdk.net/v3/info/available_releases | jq -r ".most_recent_lts"
JRE_LTS=$(curl -s https://api.adoptopenjdk.net/v3/info/available_releases | jq -r ".most_recent_lts")
# echo "JRE_LTS: $JRE_LTS"
# curl -s "https://api.adoptopenjdk.net/v3/assets/latest/$(curl -s https://api.adoptopenjdk.net/v3/info/available_releases | jq -r ".most_recent_lts")/hotspot"
JRE_LINK=$(curl -s "https://api.adoptopenjdk.net/v3/assets/latest/$(curl -s https://api.adoptopenjdk.net/v3/info/available_releases | jq -r ".most_recent_lts")/hotspot" | jq -r ".[] | .binary | select(.os == \"$JRE_OS\" and .architecture == \"$JRE_ARCH\" and .image_type == \"jre\") | .package.link")
# echo "JRE_LINK: $JRE_LINK"
# if JRE_OS == "windows"
# if [ "$JRE_OS" = "windows" ]
# then
# JRE_LINK=$(echo $JRE_LINK | sed 's/https/http/')
# else
# wget $JRE_LINK
# tar -xzf OpenJDK*.tar.gz
# mv jdk-*-jre java
# rm OpenJDK*.tar.gz
# fi
wget $JRE_LINK
tar -xzf OpenJDK*.tar.gz
mv jdk-*-jre java
rm OpenJDK*.tar.gz
fi
# Client
if ! [ -d jars ]
then
wget https://download2.interactivebrokers.com/portal/clientportal.gw.zip
unzip clientportal.gw.zip
rm clientportal.gw.zip
# mkdir jars
# cp build/lib/runtime/*.jar jars/
# cp dist/*.jar jars/
# rm -rf bin build dist doc root || true
fi
# Certs
# https://get.localhost.direct
# https://github.com/Upinel/localhost.direct
# wget -O certs.zip https://aka.re/localhost
# unzip -P localhost certs.zip
# rm certs.zip
# JKS (TODO: what is openssl and especially keytool are not installed?)
# openssl pkcs12 -export -in localhost.direct.crt -inkey localhost.direct.key -out keystore.p12 -name ib -passout pass:HelloWorldFromInteractiveBrokers2023
# keytool -importkeystore -srckeystore keystore.p12 -srcstoretype PKCS12 -destkeystore keystore.jks -deststoretype JKS -storepass HelloWorldFromInteractiveBrokers2023 -srcstorepass HelloWorldFromInteractiveBrokers2023
# rm localhost.direct.crt localhost.direct.key keystore.p12 || true
# Start
# TODO: JAVA_HOME depends on platform
# export JAVA_HOME=./java/Contents/Home
# java -server -cp "./:jars/*" ibgroup.web.core.clientportal.gw.GatewayStart --conf ./conf.yaml
rm root/webapps/demo/index.html
ln -s "$PWD/index.html" "$PWD/root/webapps/demo/index.html"
Notes:
- technically all we need from gateway - jar files
- also we may want to swap self generated certificates with something trusted, but some how it did not worked out
- idea behind the script was to have cleaned up version, leaving it here as is for future
- html file is replaced with customized demo
Starting gateway
#!/usr/bin/env bash
export JAVA_HOME=./java/Contents/Home
./bin/run.sh ./root/conf.yaml
Notes:
- under the hood we are starting
java -jar gateway.jar
- it will start an server on port 5000 with self signed cert
- any request sent to server will be proxied to ibkr under the hood preserving cookies
- request to home page will serve
https://api.ibkr.com/sso/Login?forwardTo=22&RL=1&ip2loc=US
- safari does not recognize login response and downloads it, but after login
https://localhost:5000/demo/
should work
Sample web page
Attaching an sample web page with examples of how to:
- get current positions
- subscribe to "realtime" updates
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link rel="icon" type="image/ico" href="https://www.interactivebrokers.com/favicon.ico" />
<title>ib</title>
<style>
html, body {
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
main {
text-align: center;
}
main table {
margin: auto;
}
</style>
</head>
<body>
<main>
<div id="stats"></div>
<div id="positions"></div>
<img src="https://api.ibkr.com/images/web/logos/ib-logo-text-black.svg" width="200" />
</main>
<script>
/*
Swagger: https://www.interactivebrokers.com/api/doc.html
*/
// Sample: retrieve account and its positions
fetch('https://localhost:5000/v1/api/portfolio/accounts')
.then(r => r.json())
.then(a => fetch(`https://localhost:5000/v1/api/portfolio/${a[0].accountId}/positions/0`))
.then(r => r.json())
.then(p => {
console.log(p)
p = p.filter(p => ['GOOGL', 'AAPL', 'MSFT', 'AMZN'].includes(p.ticker))
positions.innerHTML = '<table cellpadding="5" cellspacing="0" border="0">' + p.map(({conid, ticker, mktPrice, unrealizedPnl}) => `<tr data-conid="${conid}">
<td>${ticker}</td>
<td class="price">${mktPrice.toFixed(2)}</td>
<td class="change"></td>
<!--
<td><font color="${unrealizedPnl > 0 ? 'green' : 'red'}">${unrealizedPnl.toFixed(2)}</font></td>
-->
</tr>`).join('') + '</table>'
for(var x of p) {
ws.send("smd+"+x.conid+"+{\"fields\":[\"31\",\"83\"],\"tempo\":2000,\"snapshot\":true}")
}
})
// Sample: web socket connection
// https://interactivebrokers.github.io/cpwebapi/websockets
var ws = new WebSocket("wss://localhost:5000/v1/api/ws")
ws.onopen = function() {
console.log("open")
}
ws.onmessage = async function(e) {
var txt = await e.data.text()
try {
var msg = JSON.parse(txt)
if (msg.topic === 'system' && 'hb' in msg) {
return
} else if (msg.topic === 'act') {
// activated
ws.send('spl{}') // example: subscribe to overall stats for account, to unsubscribe send `upl{}`
// ws.send("smd+208813719+{\"fields\":[\"31\",\"83\"],\"tempo\":2000,\"snapshot\":true}") // example: subscribe to concrete ticker updates, to unsubscribe send `umd+498854138+{}`
} else if (msg.topic.startsWith('smd+')) { // subscription to concrete ticker updates
var tr = document.querySelector(`tr[data-conid="${msg.conid}"]`)
console.log('smd', msg)
if (!msg.conid || (!msg['31'] && !msg['83'])) {
}
if (tr) {
if (msg['31']) {
tr.querySelector('td.price').innerHTML = `<font>${msg['31']}</font>`
}
if (msg['83']) {
tr.querySelector('td.change').innerHTML = `<font color="${parseInt(msg['83']) > 0 ? 'green' : 'red'}">${msg['83']}</font>`
}
}
} else if (msg.topic === 'spl') {
// subscribe: ws.send('spl{}')
// unsubscribe: ws.send('upl{}')
var dpl = msg.args[Object.keys(msg.args).shift()].dpl
var upl = msg.args[Object.keys(msg.args).shift()].upl
// document.getElementById('stats').innerHTML = `dpl: <font color="${dpl > 0 ? 'green' : 'red'}">${dpl}</font>, upl: <font color="${upl > 0 ? 'green' : 'red'}">${upl}</font>`
document.getElementById('stats').innerHTML = `<font color="${dpl > 0 ? 'green' : 'red'}">${dpl}</font>`
} else {
console.log('message', msg)
}
} catch(e) {
console.log('onmessage',txt, e.message)
}
}
ws.onclose = function() {
console.log("closed")
}
ws.onerror = function(e) {
console.log(e)
}
/*
ws.send('ech+hb') // "ech+hb"
ws.send('spl{}') // "{\"topic\":\"spl\",\"args\":{\"U3413059.Core\":{\"rowType\":1,\"dpl\":23.61,\"nl\":14570.0,\"upl\":2258.0,\"el\":296.7,\"mv\":14270.0}}}"
ws.send('upl{}')
ws.send('uor+{}') // orders?
// curl -k 'https://localhost:5000/v1/api/md/snapshot?conids=498854138&fields=31&fields=84&fields=85&fields=86&fields=88'
// https://www.interactivebrokers.com/api/doc.html#tag/Market-Data/paths/~1md~1snapshot/get
ws.send("smd+498854138+{\"fields\":[\"31\",\"84\",\"85\",\"86\",\"87\",\"88\",\"7059\",\"7634\"],\"tempo\":2000,\"snapshot\":true}")
// "{\"server_id\":\"q118\",\"conidEx\":\"498854138\",\"conid\":498854138,\"_updated\":1696011002361,\"6119\":\"q118\",\"86\":\"29.54\",\"topic\":\"smd+498854138\"}"
ws.send('umd+498854138+{}')
ws.send("smd+498854138+{\"fields\":[\"31\",\"7676\"],\"tempo\":2000,\"snapshot\":true}")
31 Last Price - The last price at which the contract traded. May contain one of the following prefixes: C - Previous day's closing price. H - Trading has halted.
70 High - Current day high price
71 Low - Current day low price
82 Change - The difference between the last price and the close on the previous trading day
83 Change % - The difference between the last price and the close on the previous trading day in percentage.
7295 Open - Today's opening price.
7296 Close - Today's closing price.
7674 EMA(200) - Exponential moving average (N=200).
7676 EMA(50) - Exponential moving average (N=50).
6509 Market Data Availability. The field may contain three chars. First char defines: R = RealTime, D = Delayed, Z = Frozen, Y = Frozen Delayed, N = Not Subscribed. Second char defines: P = Snapshot, p = Consolidated. Third char defines: B = Book
RealTime - Data is relayed back in real time without delay, market data subscription(s) are required.
Delayed - Data is relayed back 15-20 min delayed.
Frozen - Last recorded data at market close, relayed back in real time.
Frozen Delayed - Last recorded data at market close, relayed back delayed.
Not Subscribed - User does not have the required market data subscription(s) to relay back either real time or delayed data.
Snapshot - Snapshot request is available for contract.
Consolidated - Market data is aggregated across multiple exchanges or venues.
Book - Top of the book data is available for contract.
curl -k -X POST 'https://localhost:5000/v1/api/tickle' -d ''
{"session":"bd842d05d887a85a8362ac6b4620b999","ssoExpires":434805,"collission":false,"userId":45049960,"hmds":{"error":"no bridge"},"iserver":{"authStatus":{"authenticated":true,"competing":false,"connected":true,"message":"","MAC":"F4:03:43:E6:57:70","serverInfo":{"serverName":"JifZ23102","serverVersion":"Build 10.25.0d, Sep 21, 2023 6:05:00 PM"}}}}
curl -k -X POST 'https://localhost:5000/v1/api/iserver/auth/status' -d ''
{"authenticated":true,"competing":false,"connected":true,"message":"","MAC":"F4:03:43:E6:57:70","serverInfo":{"serverName":"JifZ23102","serverVersion":"Build 10.25.0d, Sep 21, 2023 6:05:00 PM"},"fail":""}
curl -k -X POST 'https://localhost:5000/v1/api/sso/validate' -d ''
{"USER_ID":45049960,"USER_NAME":"marche985","RESULT":true,"AUTH_TIME":1696010968984,"SF_ENABLED":true,"IS_FREE_TRIAL":false,"CREDENTIAL":"marche985","IP":"178.150.44.191","EXPIRES":300102,"QUALIFIED_FOR_MOBILE_AUTH":null,"LANDING_APP":"PORTAL","IS_MASTER":false,"lastAccessed":1696011245028,"features":{"env":"PROD","wlms":true,"realtime":true,"bond":true,"optionChains":true,"calendar":true,"newMf":true}}
curl -k -X POST 'https://localhost:5000/v1/api/iserver/reauthenticate' -d ''
{"message":"triggered"}
https://www.interactivebrokers.com/api/doc.html#tag/Market-Data/paths/~1md~1snapshot/get
curl -k 'https://localhost:5000/v1/api/iserver/account/orders'
{"orders":[],"snapshot":false}
*/
</script>
</body>
</html>
Notes:
- even so changes are streamed they are approx 15min delayed
Cleanup
#!/usr/bin/env bash
rm -rf .vertx jars java bin build dist doc logs root || true
How it works
- We are starting proxy that will serve any request from api.ibkr.com preserving cookies
- Homepage is served from
https://api.ibkr.com/sso/Login?forwardTo=22&RL=1&ip2loc=US
- After login cookies are saved
- Subsequent requests like
https://localhost:5000/v1/api/iserver/auth/status
orhttps://localhost:5000/v1/api/iserver/accounts
will be proxied to api.ibkr.com with saved cookies
Really good alternative approach to understand is to create your own app with electron
Creating custom Interactive Brokers Client
Following quick start
mkdir ib
cd ib
npm init -y -f
npm install -D electron
Here is the main.js
:
// main.js
// Modules to control application life and create native browser window
const { app, BrowserWindow } = require('electron')
const path = require('node:path')
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1248,
height: 768,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// and load the index.html of the app.
// mainWindow.loadFile('index.html') // instead of loading local file we are going to serve api.ibrk.com directly
// mainWindow.loadURL('https://api.ibkr.com/sso/Login') // note that we need following query string parameters taken from gateway
mainWindow.loadURL('https://api.ibkr.com/sso/Login?forwardTo=22&RL=1&ip2loc=US')
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
it is almost the same as in quick start guide except we are serving api.ibkr.com instead of local file
Our preload.js
looks like this:
// preload.js
// All the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
// const replaceText = (selector, text) => {
// const element = document.getElementById(selector)
// if (element) element.innerText = text
// }
// for (const dependency of ['chrome', 'node', 'electron']) {
// replaceText(`${dependency}-version`, process.versions[dependency])
// }
if (window.location.toString().startsWith('https://api.ibkr.com/sso/Login')) {
document.querySelectorAll('.download-text, footer, .xyz-sso-login-header, p.text-end.link-password, .xyzblock-username-submit > .text-center').forEach(el => el.parentNode.removeChild(el))
}
else if(window.location.toString().startsWith('https://ndcdyn.interactivebrokers.com/Universal/servlet/AccountAccess.AuthenticateSSO')) {
console.log('REDIRECT')
window.location = 'https://api.ibkr.com/'
} else if (window.location.toString() === 'https://api.ibkr.com/') {
window.title = 'IBKR'
fetch('https://api.ibkr.com/v1/api/portfolio/accounts').then(r => r.json()).then(data => {
document.body.innerHTML += '<h1>' + data?.shift()?.accountId + '</h1>'
})
fetch('https://api.ibkr.com/v1/api/portfolio/accounts')
.then(r => r.json())
.then(a => fetch(`https://api.ibkr.com/v1/api/portfolio/${a[0].accountId}/positions/0`))
.then(r => r.json())
.then(positions => {
document.body.innerHTML += '<table cellpadding="5" cellspacing="0" border="1"><tr><td>ticker</td><td>avgprice</td><td>mrkprice</td><td>pnl</td></tr>' + positions.map(({ticker, avgPrice, mktPrice, unrealizedPnl}) => `<tr><td>${ticker}</td><td>${avgPrice.toFixed(2)}</td><td>${mktPrice.toFixed(2)}</td><td style="color:${unrealizedPnl > 0 ? 'green' : 'red'}">${unrealizedPnl.toFixed(2)}</td></tr>`).join('') + '</table>'
})
} else {
console.log('OTHER', window.location.toString())
}
})
Think of this - this script will be injected and run on each page load, aka the same way if it was an browser extension
And because we are serving api.ibkr.com we will have all wanted cookies already
So after login we are opening home page which is empty by default and drawing everything we want there
Our package.json
so far looks like this:
{
"name": "ibkr-api",
"version": "1.0.0",
"private": true,
"main": "main.js",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make"
},
"devDependencies": {
"@electron-forge/cli": "^6.4.2",
"@electron-forge/maker-deb": "^6.4.2",
"@electron-forge/maker-rpm": "^6.4.2",
"@electron-forge/maker-squirrel": "^6.4.2",
"@electron-forge/maker-zip": "^6.4.2",
"@electron-forge/plugin-auto-unpack-natives": "^6.4.2",
"electron": "^27.0.0"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0"
}
}
So we can run it like so:
npm start
and after successfull login list of our positions will be drawn 💪
To publish it
npm install -D @electron-forge/cli
npx electron-forge import
npm run make
And your binary will be here out/ibkr-api-darwin-arm64/ibkr-api
For custom icon I have added following:
module.exports = {
packagerConfig: {
asar: true,
icon: 'images/icon' // no file extension required
},
// ...
}
Here is how it looks like:
Techinically similar approach may be used in SwiftUI which will be probably an next milestone