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

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 or https://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

start screen

and after successfull login list of our positions will be drawn 💪