Skip to content

whtis/ibkr-mobile

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

51 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

ibkr-mobile

A personal Android trading client for Interactive Brokers, with UI inspired by Longbridge / ้•ฟๆกฅ่ฏๅˆธ.

License: MIT Kotlin Compose FastAPI ib_async Contributors Last commit

English ยท ไธญๆ–‡

Built because the official IBKR mobile app is slow, ugly, and missing the things a Longbridge user takes for granted: snappy K-lines with crosshair, intraday session classification (็›˜ๅ‰ / ็›˜ไธญ / ็›˜ๅŽ / ๅคœ็›˜), red-up / green-down (็บขๆถจ็ปฟ่ทŒ), aggregated positions PnL, one-tap quick-actions on long-press, and a UI that doesn't look like it was designed in 2008.

โš ๏ธ Disclaimer This is a personal project for educational purposes. It is not financial advice, not affiliated with Interactive Brokers or Longbridge, and is provided as-is with no warranty. Trading involves substantial risk of loss. Use a paper account first. You are solely responsible for any orders submitted through this software.


Screenshots

Positions K-line chart with MA + MACD + cost basis Intraday with VWAP and 4-channel session colors Market overview

From left: Positions with aggregated PnL ยท K-line with MA/MACD/cost basis ยท Intraday with VWAP and pre/regular/post/overnight session coloring ยท Market overview with movers

More screenshots

Long-press quick actions Company info Financials Watchlist empty state

Long-press quick actions ยท Company panorama ยท Financials ยท Watchlist empty state

All screenshots are from the bundled mock mode running against a freshly-built emulator โ€” no real account is needed to see the app like this. See Quickstart below.


Try it in 60 seconds (mock mode)

Don't have an IBKR or LongPort account and just want to look around?

git clone https://github.com/whtis/ibkr-mobile.git
cd ibkr-mobile/backend
cp .env.example .env
echo "MOCK_MODE=yes" >> .env          # one line is the only required change
echo "API_TOKEN=anything-you-want" >> .env
uv sync
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000

Then in another shell:

cd ibkr-mobile/android
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Open the app โ†’ Settings โ†’ backend URL http://10.0.2.2:8000 (emulator) or http://<your-LAN-ip>:8000 (real device), token = whatever you put in .env โ†’ save. Done. Mock mode serves a synthesized portfolio of AAPL, TSLA, NVDA, MSFT, GOOGL, BABA, SPY with realistic K-lines, intraday charts, and option chains.


Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Android App (Kotlin + Jetpack Compose) โ”‚
โ”‚  - Material 3, Canvas charts            โ”‚
โ”‚  - WebSocket realtime quotes            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚  HTTPS
                   โ”‚  X-Timestamp + X-Signature + X-Device-ID
                   โ”‚  (HMAC-SHA256, key in AndroidKeyStore TEE/StrongBox)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  FastAPI + ib_async                     โ”‚
โ”‚  - REST + WebSocket                     โ”‚
โ”‚  - SQLite (execution history)           โ”‚
โ”‚  - LongPort SDK (free L1 quotes)        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚  TWS binary socket :4002 (paper) / :4001 (live)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  IB Gateway in Docker (gnzsnz image)    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
              IBKR servers

The backend is the only place that holds IBKR credentials. The Android app authenticates with HMAC-SHA256 request signatures. The signing key never leaves the phone's AndroidKeyStore (TEE / StrongBox); only the signing operation is exposed. A static bearer token is used only for the one-time pairing endpoint โ€” after that, every request carries X-Timestamp + X-Signature (+ optional X-Device-ID), and the token can be rotated without disturbing paired devices. See Authentication below.


Authentication

Two flows:

1. Pairing โ€” one-time, Bearer token

phone                                                backend
  โ”‚                                                    โ”‚
  โ”‚โ”€โ”€ POST /devices/pair                               โ”‚
  โ”‚    Authorization: Bearer <api_token>               โ”‚
  โ”‚                                                    โ”‚โ”€โ”€ generate 32-byte HMAC key K
  โ”‚                                                    โ”‚โ”€โ”€ INSERT into devices (id, K, label)
  โ”‚                                                    โ”‚
  โ”‚<โ”€โ”€ { device_id, hmac_key_hex }                     โ”‚
  โ”‚                                                    โ”‚
  โ”‚โ”€โ”€ KeystoreHmac.importKey(K)                        โ”‚
  โ”‚    K is now sealed inside TEE / StrongBox;         โ”‚
  โ”‚    the app cannot read K back, only invoke         โ”‚
  โ”‚    Mac.sign() through a system call                โ”‚

After pairing, rotate the bearer token on the backend (backend/.env โ†’ API_TOKEN). Existing paired devices keep working โ€” they sign with K, not the token.

2. Per-request signing โ€” every call after pairing

For every authenticated endpoint:

canonical = METHOD + "\n" + PATH_AND_QUERY + "\n" + TIMESTAMP_MS + "\n" + sha256_hex(body)
signature = HMAC_SHA256(K, canonical)

X-Timestamp: 1717933200000
X-Signature: ab12cd34...
X-Device-ID: a54e703c4d3b479b   (optional; lets backend skip the device lookup)

The backend (backend/app/auth.py::require_signature) accepts the request only when:

  • X-Timestamp is within ยฑ60 seconds of server time
  • the signature recomputes for some paired device's stored K
  • the signature has not been seen in the last 60 seconds (in-process nonce cache)

All failure modes collapse to 401 auth failed over the wire; logs carry the real reason.

Threat model

Attack Defense Result
Token leaked via screenshot / repo / chat Token alone cannot read data, only pair Attacker can pair a rogue device โ€” visible to you in /devices, revocable
APK reverse-engineered K lives in Keystore, not in code No key extracted; pair on attacker's device yields a different K we never accept
Single HTTPS request captured (MITM + cert) Timestamp window + nonce cache Replay rejected within 60s by nonce, after 60s by timestamp
Long-term MITM, attacker tries to forge new request HMAC key never leaves the phone Attacker cannot compute HMAC(K, new_canonical) without K
Phone stolen, screen unlocked Out of scope โ€” software cannot help Use device PIN / biometric / Find My Device remote wipe

This is the same pattern used by AWS SigV4, Google Cloud Storage authenticated requests, and Stripe webhook signatures โ€” HMAC + canonical request + timestamp + nonce. The unique part here is keeping K inside Android Keystore so a stolen APK or DataStore dump doesn't reveal it.


Features

๐Ÿ“ˆ Market

  • Watchlist with auto-refresh and pull-to-update
  • Search by symbol with debounced lookup
  • Stock detail page with three sub-tabs (่กŒๆƒ… / ๅ…จๆ™ฏ / ่ดขๅŠก):
    • Native Canvas K-line chart (1m / 5m / 15m / 30m / 60m / 1d / 1w / 1mo) with crosshair, zoom, pan
    • Intraday chart with 4-channel session classification (pre-market / regular / after-hours / overnight)
    • MACD sub-chart with linked crosshair
    • Company info + key metrics (P/E TTM, P/B, EPS, BPS, dividend yield)
  • Fullscreen chart mode (landscape lock + larger viewport)
  • Option chain browser with strike grid and side/expiry selector

๐Ÿ’ผ Positions

  • Multi-account switching โ€” pick between linked accounts on the holdings page
  • Aggregated PnL across positions (more accurate than IBKR per-account summary)
  • Four sort modes: market value / unrealized PnL / daily PnL / symbol
  • Long-press quick actions: ๅŠ ไป“ / ๅ‡ไป“ / ๆŸฅ็œ‹่ฏฆๆƒ… / ๅคๅˆถไปฃ็ 
  • Active orders panel with one-tap cancel

๐Ÿ›’ Orders

  • Market / Limit / Stop / Stop-Limit
  • TIF: DAY / GTC / IOC / FOK
  • RTH-only toggle
  • Stocks + Options (with expiry / strike / right pre-filled from option chain)
  • Live preview of margin impact

โšก Realtime

  • Single WebSocket multiplex with ref-counted subscriptions
  • Auto-reconnect with exponential backoff
  • LongPort L1 stream (free, real-time) + IBKR fallback

๐ŸŽจ Polish

  • ็บขๆถจ็ปฟ่ทŒ (Chinese convention: red = up, green = down)
  • Material 3 dark theme tuned to Longbridge palette
  • Adaptive icon (animated C pulse)
  • Bottom-bar navigation, 4 tabs (่‡ช้€‰ / ่กŒๆƒ… / ๆŒไป“ / ่ฎพ็ฝฎ)

๐Ÿ”” Updates & alerts

  • In-app updates โ€” checks GitHub releases on launch and from Settings, then downloads + installs the APK in place
  • Gateway 2FA push โ€” a phone notification (FCM) when IB Gateway needs second-factor approval on its weekly cold login. The backend tails the gateway logs and pushes via a Cloudflare Worker relay, so it works even when the backend is behind the GFW

Tech stack

Backend (backend/)

  • FastAPI, Uvicorn, Pydantic v2
  • ib_async 2.1.0 โ€” modern async IBKR API client
  • longport Python SDK 3.0.23 โ€” free L1 quotes + K-lines
  • SQLite for execution history persistence
  • uv for dependency management
  • Docker Compose + gnzsnz/ib-gateway

Android (android/)

  • Kotlin 2.1.20, Jetpack Compose BOM 2026.05.01
  • Material 3
  • Navigation Compose 2.9.8
  • Ktor + OkHttp engine (HTTP + WebSocket)
  • DataStore (settings persistence)
  • kotlinx.serialization (JSON)
  • Compose Canvas (native charts โ€” no WebView)
  • AGP 8.10.1 / Gradle 8.13 / minSdk 26 / compileSdk 36

Quickstart

1. Backend

cd backend
cp .env.example .env

# edit .env โ€” at minimum:
#   TWS_USERID=<your IBKR paper-trading username>
#   TWS_PASSWORD=<your IBKR paper-trading password>
#   API_TOKEN=$(openssl rand -hex 32)
#   LONGPORT_APP_KEY / SECRET / ACCESS_TOKEN โ€” optional, but recommended for free quotes
#     (get at https://open.longportapp.com โ†’ Developer Center โ†’ Create App)
#
# OR: skip the IBKR + LongPort fields entirely and set MOCK_MODE=yes for synthetic data.
# See "Try it in 60 seconds" above.

# pull and start IB Gateway (Docker)
docker compose pull
docker compose up -d

# wait ~30s, watch Gateway login
docker compose logs -f ibgateway     # look for "API server listening on port 4002"

# install Python deps (first time only)
uv sync

# run FastAPI
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

Verify:

TOKEN=$(grep ^API_TOKEN .env | cut -d= -f2)
curl -s http://localhost:8000/health | jq                  # ib_connected: true
curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:8000/account/positions | jq

See backend/README.md for the full smoke-test set and troubleshooting.

2. Android

cd android
./gradlew assembleDebug
# APK at: app/build/outputs/apk/debug/app-debug.apk

Install on device:

adb install -r app/build/outputs/apk/debug/app-debug.apk

Open the app โ†’ Settings tab โ†’ fill in:

  • Backend URL: http://<host LAN IP>:8000 (or https://your.domain if cloud-hosted)
  • API Token: same value as API_TOKEN in backend .env
  • Tap ๆต‹่ฏ•่ฟžๆŽฅ โ†’ should turn green
  • Tap ไฟๅญ˜

Scroll down to ่ฎพๅค‡้…ๅฏน and tap ้…ๅฏนๆญค่ฎพๅค‡:

  • The app calls POST /devices/pair once with the bearer token
  • The returned HMAC key is imported into AndroidKeyStore (hardware-backed)
  • All subsequent requests use that key for signatures; the token is no longer needed
  • Recommended: immediately rotate API_TOKEN in backend/.env and restart the backend. Already-paired devices are unaffected by token rotation.

Now the Positions / Market / Watchlist tabs will populate from your paper account.

3. (Optional) Cloud deployment

Quote-only deployments (LongPort quotes without IBKR trading) can run on any small VPS โ€” no Mac required for the Gateway:

# on your server
git clone <this repo>
cd ibkr-mobile/backend
cp .env.example .env
# fill LONGPORT_* keys, leave TWS_* blank or set READ_ONLY_API=yes
uv sync
# put behind nginx + Let's Encrypt + systemd, point your domain at it

Full IBKR trading needs IB Gateway, which has stricter networking + 2FA requirements. The Mac-as-gateway model is recommended for the trading path; the VPS can serve as a public quote endpoint.


Project layout

ibkr-mobile/
โ”œโ”€โ”€ android/                   Kotlin + Compose app
โ”‚   โ”œโ”€โ”€ app/src/main/
โ”‚   โ”‚   โ”œโ”€โ”€ java/com/tis/ibkr/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ data/          API client, DataStore, models,
โ”‚   โ”‚   โ”‚   โ”‚                  KeystoreHmac (TEE-backed HMAC),
โ”‚   โ”‚   โ”‚   โ”‚                  RequestSigning (Ktor signing plugin)
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ ui/screens/    Watchlist, Market, Positions, StockDetail, ...
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ ui/components/ Charts, sub-bars, quick-action sheet
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ ui/theme/      Longbridge-inspired Material 3 theme
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ viewmodel/     One ViewModel per screen
โ”‚   โ”‚   โ””โ”€โ”€ res/               icons (adaptive), strings
โ”‚   โ””โ”€โ”€ gradle/libs.versions.toml
โ”œโ”€โ”€ backend/                   FastAPI + ib_async
โ”‚   โ”œโ”€โ”€ app/
โ”‚   โ”‚   โ”œโ”€โ”€ main.py            FastAPI app, lifespan, CORS
โ”‚   โ”‚   โ”œโ”€โ”€ ibkr.py            IB Gateway connector
โ”‚   โ”‚   โ”œโ”€โ”€ longbridge.py      LongPort SDK wrapper
โ”‚   โ”‚   โ”œโ”€โ”€ db.py              SQLite executions store
โ”‚   โ”‚   โ”œโ”€โ”€ auth.py            Bearer (pair-only) + HMAC signature deps
โ”‚   โ”‚   โ””โ”€โ”€ routes/
โ”‚   โ”‚       โ”œโ”€โ”€ account.py     summary, positions
โ”‚   โ”‚       โ”œโ”€โ”€ devices.py     pair, list, revoke (signature auth)
โ”‚   โ”‚       โ”œโ”€โ”€ orders.py      place, cancel, list active
โ”‚   โ”‚       โ”œโ”€โ”€ executions.py  history (SQLite + ib_async)
โ”‚   โ”‚       โ”œโ”€โ”€ options.py     option chain, contract lookup
โ”‚   โ”‚       โ”œโ”€โ”€ quote.py       quote, intraday, bars (K-line)
โ”‚   โ”‚       โ””โ”€โ”€ ws_quotes.py   WebSocket realtime stream
โ”‚   โ”œโ”€โ”€ docker-compose.yml     IB Gateway container
โ”‚   โ””โ”€โ”€ pyproject.toml
โ”œโ”€โ”€ BUILD.md                   Architecture decisions
โ”œโ”€โ”€ DESIGN_NOTES.md            UI/UX spec referencing Longbridge
โ”œโ”€โ”€ ONBOARDING.md              Setup gotchas (IBKR auth quirks, etc.)
โ””โ”€โ”€ README.md                  โ† you are here

Why these choices

Decision Reason
Compose Canvas charts (not WebView) First paint ~50 ms vs ~800 ms for a WebView+JS chart lib. Memory ~5 MB vs ~50 MB.
LongPort SDK + IBKR fallback LongPort gives free real-time L1 + K-lines for US + HK + CN; IBKR market data is paid. Trading still goes through IBKR.
ib_async over ibapi/ib-insync ib-insync is no longer maintained; ib_async is its modern fork, native async, type-hinted.
gnzsnz/ib-gateway in Docker Headless IB Gateway with IBC auto-login, VNC for debugging, daily restart. Avoids manual Gateway baby-sitting.
One Mac as the gateway host Gateway needs persistent network identity + 2FA approval. A always-on Mac is simpler than fighting Docker NAT + cloud IP rotation.
WebSocket subscription multiplex with refcounts Multiple Compose screens can subscribe to the same symbol cheaply; cleanup is automatic when the last subscriber leaves.
SQLite execution history IBKR reqExecutions only returns 7 days. Local persistence preserves full history.
uv over pip ~10ร— faster install, lockfile, pyproject-native.
HMAC + AndroidKeyStore for app auth A stolen token alone cannot read data โ€” it only pairs. A stolen APK cannot extract the signing key (TEE / StrongBox holds it). Replay attacks are bounded to a 60s window by timestamp + nonce cache. Same pattern as AWS SigV4.

Documentation


Status

  • v1: shipped, in personal daily use. Single-user, paper account.
  • v2: design complete, implementation pending โ€” see ROADMAP.md. Adds per-device multi-user, moves Longbridge SDK in-app, makes the backend credential-free.

Active development. APIs and screens may change without notice.

Tested with: Pixel-class Android devices and emulators, IBKR paper account, LongPort developer account.


Contributing

PRs are welcome, especially ones aligned with ROADMAP.md. For anything non-trivial, please open an issue first so we can discuss direction before you write the code.

See CONTRIBUTING.md for dev setup, code style, what kinds of PRs are most likely to land, and what to avoid. Note that mock mode (described in Quickstart above) lets you contribute without any brokerage credentials โ€” start there.

Looking for somewhere to start? Check the good first issue and help wanted labels.

This project follows a Code of Conduct.


License

MIT. See LICENSE.

The MIT license grants you broad rights, but it does not grant you a right to safety. Read the disclaimer above. Trade with money you can afford to lose.


Built by @whtis. Contributions from anyone who finds it useful.

About

Personal Android trading client for Interactive Brokers, UI inspired by Longbridge. FastAPI + ib_async backend, Kotlin + Compose app with native Canvas charts.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors