A personal Android trading client for Interactive Brokers, with UI inspired by Longbridge / ้ฟๆกฅ่ฏๅธ.
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.
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 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.
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 8000Then in another shell:
cd ibkr-mobile/android
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apkOpen 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.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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.
Two flows:
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.
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-Timestampis 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.
| 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.
- 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)
- Native Canvas K-line chart (
- Fullscreen chart mode (landscape lock + larger viewport)
- Option chain browser with strike grid and side/expiry selector
- 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
- 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
- Single WebSocket multiplex with ref-counted subscriptions
- Auto-reconnect with exponential backoff
- LongPort L1 stream (free, real-time) + IBKR fallback
- ็บขๆถจ็ปฟ่ท (Chinese convention: red = up, green = down)
- Material 3 dark theme tuned to Longbridge palette
- Adaptive icon (animated
Cpulse) - Bottom-bar navigation, 4 tabs (่ช้ / ่กๆ / ๆไป / ่ฎพ็ฝฎ)
- 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
Backend (backend/)
- FastAPI, Uvicorn, Pydantic v2
ib_async2.1.0 โ modern async IBKR API clientlongportPython SDK 3.0.23 โ free L1 quotes + K-lines- SQLite for execution history persistence
uvfor 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
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 --reloadVerify:
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 | jqSee backend/README.md for the full smoke-test set and troubleshooting.
cd android
./gradlew assembleDebug
# APK at: app/build/outputs/apk/debug/app-debug.apkInstall on device:
adb install -r app/build/outputs/apk/debug/app-debug.apkOpen the app โ Settings tab โ fill in:
- Backend URL:
http://<host LAN IP>:8000(orhttps://your.domainif cloud-hosted) - API Token: same value as
API_TOKENin backend.env - Tap ๆต่ฏ่ฟๆฅ โ should turn green
- Tap ไฟๅญ
Scroll down to ่ฎพๅค้ ๅฏน and tap ้ ๅฏนๆญค่ฎพๅค:
- The app calls
POST /devices/paironce 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_TOKENinbackend/.envand restart the backend. Already-paired devices are unaffected by token rotation.
Now the Positions / Market / Watchlist tabs will populate from your paper account.
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 itFull 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.
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
| 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. |
ROADMAP.mdโ v2 plan: multi-user, Longbridge SDK in-app, dynamic Gateway lifecycleBUILD.mdโ architecture, topology, decisionsDESIGN_NOTES.mdโ Longbridge UI spec, screen-by-screenONBOARDING.mdโ IBKR auth gotchas + Android/Gradle troubleshootingbackend/README.mdโ backend setup + smoke testsopenspec/changes/multi-user-v2/โ v2 detailed spec (proposal, design, tasks)
- 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.
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.
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.







