Skip to content

Version 2.0.1.1 Released ​

Date: 17th May 2026

Major Feature Release: WhatsApp Bot Integration — Event-Driven Alerts + Slash-Command Queries, Plus a WebSocket Reliability Sweep Across 14 Brokers, New Broker Integrations (IIFLCapital Streaming, Groww Option Chain + WS Depth, Upstox GLOBAL_INDEX), and Per-Install Fernet Salt Rotation with Crash-Safe Auto-Migration

This release spans 70+ commits since v2.0.1.0. The headline change is WhatsApp — a self-hosted, event-driven WhatsApp bot that fires order alerts on the same event bus that drives Telegram, accepts slash-command queries (/orderbook, /positions, /quote, …) from the operator's own phone, and exposes a single send endpoint over REST. Pairing happens once from the Tradeboard admin UI with a QR scan; the encrypted session blob is then stored in tradeboard.db and the bot auto-reconnects on every server boot. Alongside WhatsApp, this release lands a WebSocket reliability sweep — subscribe batching across 6+ brokers, reconnect hardening across 4+ brokers, file-descriptor leak fixes in two streaming layers, and proxy-level fixes (ZMQ bind, mode normalization, request_id correlation). Three new broker integrations land: IIFLCapital streaming, Groww option chain + WS depth, and Upstox GLOBAL_INDEX world feeds. Security: a per-install random FERNET_SALT now feeds the Fernet KDF, with a crash-safe online migration of existing ciphertext.


Highlights

  • WhatsApp Bot — event-driven alerts + slash-command queries — Brand-new /whatsapp admin page with auto-rotating QR pair flow, a single trader-facing POST /api/v1/whatsapp/notify endpoint that accepts text + image + document attachments, a dedicated worker thread that satisfies wars's PyO3 unsendable contract, and a subscribers/whatsapp_subscriber that wires every order topic on the existing event bus so order/position/batch events fire WhatsApp messages in parallel with Telegram. Single-user gate via WhatsApp's own is_from_me=True mark — random contacts who message the operator's number cannot drive the bot.
  • client.whatsapp() in the tradeboard Python SDK (1.0.50) — One unified call for every common case: send to self, to a single E.164 number, to up to 5 numbers (ToS-safety cap), with text, image, or document payloads. wait_for_delivery=True by default so the response carries a real per-recipient delivery report.
  • WebSocket reliability sweep across 14 brokers — Subscribe batching for Kotak (HSI multi-scrip frames + 50ms debounce), AliceBlue, Nubra, Shoonya, Angel, Flattrade. Reconnect hardening for Dhan (auto-resubscribe + data-stall watchdog), Dhan-Sandbox (eventlet-safe asyncio + single-loop reconnect), Upstox (stall-vs-network reconnect logging), Fyers TBT (batch queue + pong validation + exponential backoff + health check). Cold-subscribe latency cut ~5× on Shoonya; Zerodha lost a ~4s sleep floor.
  • WebSocket-proxy + client fixes — Bind ZMQ publisher to ZMQ_HOST instead of all interfaces (#1378), normalize subscribe/unsubscribe mode case-insensitively (#1375), correlate ack via request_id (#1376), route cache invalidation through SharedZmqPublisher to eliminate the PUB→PUB topology (#1374).
  • Three new broker integrations — IIFLCapital streaming (full WS adapter; file-descriptor leaks closed across reconnect cycles, #1416 + #1430), Groww (full option chain + WS depth, expiry filter for expired contracts, broker-symbol mangling fix, #1392), Upstox GLOBAL_INDEX (US30 / JAPAN225 / HANGSENG world feeds via the existing Upstox WS adapter).
  • Per-install random Fernet salt — FERNET_SALT env var is now provisioned per install (32-byte hex, generated by utils/env_check.py) and feeds the Fernet KDF in database/auth_db.py. Crash-safe online migration moves existing ciphertext from the legacy static salt to the new one; if the process dies mid-migration the next boot resumes from the persisted state. Tightens broker-auth-token confidentiality and is the same domain-separated salt the new WhatsApp session blob uses.
  • Platform version bump — 2.0.1.0 → 2.0.1.1. SDK pin (tradeboard) 1.0.49 → 1.0.50.

WhatsApp Bot — feature deep dive

WhatsApp brings parity with Telegram for both outbound alerts and interactive command queries, with security choices tuned for the single-user-per-deployment model Tradeboard runs under.

Architecture in one sentence: the wars (PyO3 over whatsapp-rust) library hosts a fully linked WhatsApp Web device inside the Flask process; outgoing alerts flow event-bus → whatsapp_subscriber → WhatsAppBotThread → wars.send(); incoming slash-commands flow wars.on_message → is_from_me=True gate → command dispatcher → Tradeboard SDK call → reply via the same bot thread.

Pairing (admin only) — c44a420b:

  • POST /whatsapp/pair (session-cookie auth) spawns a temp-DB wars instance, registers an on_qr callback that streams whatsapp_qr SocketIO events with a data:image/png URL, and waits on wait_until_ready(timeout=300) for the phone-side scan
  • QR refreshes ~every 30 seconds; React /whatsapp page swaps the <img> source on each whatsapp_qr event without polling
  • On pair success: export_session() → Fernet-encrypted → persisted to whatsapp_config.session_blob in tradeboard.db → temp file unlinked
  • Pair-code path (POST /whatsapp/pair with phone parameter) for users who prefer not to scan a QR

Encryption at rest — database/whatsapp_db.py:

  • Fernet key derived via PBKDF2-SHA256(API_KEY_PEPPER, FERNET_SALT + b":whatsapp-session", 100k iters), 32-byte output, base64-urlsafe encoded
  • Domain separator (:whatsapp-session) means the same (PEPPER, FERNET_SALT) pair derives different Fernet keys for broker auth tokens, Telegram bot tokens, and WhatsApp session blob — compromising one channel's ciphertext gives no leverage against the others
  • Idempotent SQLite ALTER TABLE ADD COLUMN migration runs at every init so existing installs pick up the owner_user_id / owner_username columns without manual schema work

The unsendable PyO3 trap — c44a420b:

  • wars.WhatsApp is #[pyclass(unsendable)] — every method call panics if invoked from a thread other than its creator
  • Solution: a dedicated WhatsAppBotThread owns the wars instance for its lifetime; request threads enqueue (op, args, result_holder, event) on a queue.Queue and wait on a threading.Event for the worker to dispatch wars.send()
  • Re-entrant: command handlers (which wars dispatches on the bot thread itself) bypass the queue via a threading.get_ident() == self._bot_thread_id check so they don't deadlock on themselves
  • Same shape Telegram uses for python-telegram-bot, for the same reason

REST surface — intentionally narrow — restx_api/whatsapp_bot.py:

  • POST /api/v1/whatsapp/notify is the only public endpoint
  • Pairing, start/stop, config, users, broadcast, stats, preferences live behind the session-authed /whatsapp/* blueprint — admin only
  • A leaked API key cannot re-pair the device, enumerate linked recipients, change rate limits, or fan out to the operator's contact list
  • Hard precheck — every send path refuses with 409 "WhatsApp is not paired or not connected. Pair the device first from the /whatsapp page in Tradeboard before sending." if is_ready() is false; we explicitly do not queue when unpaired

Send paths — one unified call:

  • client.whatsapp("Build #482 deployed") → wars's single-arg send("text") form, routes to the paired device's own number (no need to know own JID)
  • client.whatsapp("hi", to="919876543210") → single recipient
  • client.whatsapp("alert", to=[...]) → broadcast (capped at 5 server-side — anything beyond is dropped; ToS-safety guardrail)
  • client.whatsapp("EOD chart", to="919...", image="/srv/charts/nifty.png", caption="...") → image with caption
  • client.whatsapp("report", username="alice", document="/srv/reports/eod.pdf", filename="EOD.pdf") → document
  • Attachment paths validated against WHATSAPP_ATTACHMENT_ROOTS allowlist (default: <tradeboard>/db/attachments/); paths with .., paths under /etc /proc /sys /root /var/log /C:\Windows, or paths that resolve outside the allowlist are rejected with 400 image_path is not allowed

Inbound commands — services/whatsapp_bot_service.py:

CommandMaps to
/help, /menu, /startCommand list
/statusBot connection + paired status
/orderbookclient.orderbook()
/tradebookclient.tradebook()
/positionsclient.positionbook()
/holdingsclient.holdings()
/fundsclient.funds()
/pnlclient.pnl() or client.positionbook()
/quote SYM [EXCH]client.quotes()
/closeallclient.closeposition()
/modelive or analyze

Auth: the bot only responds when is_from_me=True (WhatsApp's multi-device protocol marks messages mirrored from the operator's primary phone with this flag). Random contacts who message the operator's WhatsApp number arrive with is_from_me=False and are silently ignored. The Tradeboard SDK calls run with the operator's API key, looked up from auth_db by the owner_username captured at pair time — there is no /link flow because there is no second user to authorize.

Auto-reconnect on app boot — app.py:_autostart_whatsapp_bot:

  • Background thread spawned during _init_databases_and_schedulers (after db_ready.set())
  • Loads the encrypted session blob, calls wars.WhatsApp.from_bytes(blob), starts the worker thread, registers handlers
  • No QR scan on restart — is_ready() flips to true within ~1s of boot
  • If wars isn't installed (fresh checkout that hasn't uv sync'd), the autostart logs a warning and degrades gracefully — the rest of the Flask app boots normally

Frontend — frontend/src/pages/whatsapp/WhatsAppIndex.tsx:

  • Single-page React UI: Status card with Pair QR + Disconnect button, "Send a one-off message" card, no Linked-Users table (single-user app)
  • SocketIO subscriptions for whatsapp_qr (auto-rotates the QR image), whatsapp_pair_code, whatsapp_paired, whatsapp_pair_status, whatsapp_status
  • New MessageCircle icon in the profile dropdown so WhatsApp visually differs from Telegram's MessageSquare

RUST_LOG quieting — services/whatsapp_bot_service.py:

  • Three known-noisy targets silenced at module import (before any import wars): wacore::send (stale-device warnings), whatsapp_rust::message (PN→LID migration chatter), wacore_libsignal::protocol::session_cipher (no-current-session errors that the upper layer already handles)
  • os.environ.setdefault("RUST_LOG", ...) — operator can still override via shell or .env for diagnostics

tradeboard Python SDK 1.0.50 — released to PyPI alongside this version:

  • New WhatsAppAPI mix-in adds client.whatsapp(...) with the four recipient forms and image/document payloads
  • Mirrors the client.telegram() ergonomics for traders already familiar with the SDK
  • Available at https://pypi.org/project/tradeboard/1.0.50/

WebSocket reliability sweep — 14 brokers touched

BrokerChangeCommit
IIFLCapitalFull websocket adapter integrated + file-descriptor leaks closed across reconnect cycles0ad69cf3 (#1416), 15179371 (#1430)
PaytmNSE_INDEX/BSE_INDEX symbol normalization + fd-leak fixes in streaming layer + batch ws subscribe + graceful empty for option chain / OI tracker / historical8dba1ea3 (#1413)
KotakBatch subscribe via HSI multi-scrip frames + cut batch debounce 500ms → 50ms + log emitted scrips8fef2a57 (#1399)
GrowwFull option chain + websocket depth integration + drop expired expiries + stop mangling broker symbols440b78ef (#1392)
AliceBlueBatch ws subscriptions like Zerodha + event-driven connect + leading-edge subscribe debounce09667a6e (#1389)
Dhan-SandboxEventlet-safe asyncio + heartbeat align + single-loop reconnect + batch queueb5675653 (#1344)
NubraCoalesce per-symbol subscribes into batched SDK calls6473aa2e (#1366)
ShoonyaBatch queue + auth-fail short-circuit + env-var tuning + interruptible sleeps; cold-subscribe latency cut ~5× via leading-edge debounce4b8578d6 (#1381)
DhanAuto-resubscribe on reconnect + data-stall watchdogd214a598 (#1372)
UpstoxDistinguish stall-triggered reconnects from network-induced ones in logs42927546 (#1357)
AngelSubscribe batch-queue to coalesce per-symbol bursts + defensive .get() in place_order response handling790cc64d (#1352), a4bdac18 (#846)
ZerodhaRemove ~4s sleep floor from subscribe path + add auth-fail short-circuit + interruptible sleeps + wire MCX_INDEX through quote/history/depth/ws659d53ab, f21f40cc (#1371), cd4095eb (#1385)
FyersHarden TBT client with batch queue, pong validation, exponential backoff, and health check463a3004 (#1361)
FlattradeBatch-queue subscriptions like Zerodha adaptered37dbc2 (#1341)
KotakAlign place/modify order payload with official Neo specb06ef4a8 (#1398)

WebSocket-proxy + client layer:

  • dd9cb64c (#1378) — fix(websocket_proxy): bind ZMQ publisher to ZMQ_HOST instead of all interfaces
  • 6ddff2bb (#1375) — fix(websocket_proxy): normalize subscribe/unsubscribe mode case-insensitively
  • 9a0f5e42 (#1376) — fix(websocket_client): correlate subscribe/unsubscribe acks via request_id
  • 521ea129 (#1374) — fix(cache_invalidation): route through SharedZmqPublisher to eliminate PUB->PUB topology
  • 25eed728 — fix(ui/websocket-test): keep LTP card live when Depth is also subscribed
  • f8a1ff4f (#1386) — chore(websocket): remove dead get_supported_brokers_list()
  • 06d4cb34 — docs(audit): add WebSocket broker priority audit

New broker integrations + exchange expansion

  • 0ad69cf3 / 15179371 — IIFLCapital websockets integrated + broker hardening + option-chain perf
  • 440b78ef — Groww full option chain + websocket depth integration
  • c0335582 — Upstox GLOBAL_INDEX world feeds (US30 / JAPAN225 / HANGSENG, plus IFSC-routed GIFTNIFTY) on indices and indicators
  • cd4095eb — Zerodha MCX_INDEX wired through quote/history/depth/ws
  • ca15b333 — Groww NSE_INDEX/BSE_INDEX in historical (#1338/#1342)
  • 11778af5 — Symbol search surfaces FUT-only MCX underlyings (#1385)

Security hardening — Fernet per-install salt

The Fernet KDF in database/auth_db.py (which encrypts broker auth tokens) previously used a hardcoded static salt. This release lands a per-install random salt with a crash-safe online migration.

  • 2f2e9bee — fix(security): rotate Fernet to per-install salt with crash-safe auto-migration
  • e3c5285d — place FERNET_SALT adjacent to API_KEY_PEPPER in .env, handle the 4 file-state cases cleanly (placeholder / valid / missing / mid-migration)
  • a1455ff1 — atomic .env rewrite falls back to in-place on Docker bind mounts
  • 84922078 — degrade gracefully when .env is unwritable + pin Docker UID 1000 (#1394)
  • 2981ff52 — post-FERNET_SALT cleanup — security perm-check, dedupe, pool invalidate (#1394)
  • b9301b78 — never recommend rotating API_KEY_PEPPER on a populated DB (carried in from late v2.0.0.9 work, re-asserted here)

What this means operationally:

  • New installs: utils/env_check.py generates a 32-byte hex FERNET_SALT, writes it adjacent to API_KEY_PEPPER in .env, and uses it directly. No migration runs.
  • Existing installs upgrading to 2.0.1.1: on first boot, env_check detects the placeholder/missing state, generates the new salt, re-encrypts every broker-auth-token ciphertext on the fly with the new key, and atomically swaps. If the process dies mid-migration, the persisted FERNET_SALT lets the next boot resume cleanly.
  • Same salt entropy feeds the new WhatsApp session blob via the :whatsapp-session domain separator.

UI + frontend

  • cd692653 — feat(orderbook): make Quantity editable in Modify Order dialog
  • f968b403 — fix(ui): align table content with stats cards on Positions/Orderbook/Holdings/Tradebook
  • d3aa8b6b — fix(frontend): auto-reload on stale-chunk import failure (#1393) — fixes the "ChunkLoadError" trader sees after a deploy when the browser is still holding the old index-*.js reference
  • d63ec927 — drop hero gradient on the home page, use solid text-primary
  • 3f4e2b2b — home: "New in V2 — 12-Tool Options Analytics Suite" pill
  • 624f8726 — home: Integrates With + Made for AI sections
  • 61370758 / 54d83c07 — restyle the MCP OAuth consent page to match Tradeboard dashboard + fix alignment

Configuration changes

.sample.env:

  • FERNET_SALT — new placeholder line auto-rotated on first boot, placed adjacent to API_KEY_PEPPER
  • No new keys for WhatsApp — WHATSAPP_KEY_SALT reuses the existing FERNET_SALT with a :whatsapp-session domain suffix (zero new env vars to manage)
  • Optional: WHATSAPP_ATTACHMENT_ROOTS (comma-separated absolute dirs; defaults to <tradeboard>/db/attachments/)
  • Optional: RUST_LOG — defaults to a filter that silences three known-noisy wars/whatsapp-rust modules

pyproject.toml:

  • version = "2.0.1.1"
  • New: wars==0.1.3 dependency (also added to requirements.txt + requirements-nginx.txt)
  • SDK pin (tradeboard) 1.0.49 → 1.0.50

utils/version.py:

  • VERSION = "2.0.1.1"

requirements.txt + requirements-nginx.txt:

  • wars==0.1.3 added after python-telegram-bot==22.6
  • tradeboard==1.0.49 → tradeboard==1.0.50

frontend/src/stores/alertStore.ts:

  • New whatsapp toast category alongside telegram

Database schema

New tables created by database/whatsapp_db.py on first boot:

TablePurpose
whatsapp_configSingleton row holding the Fernet-encrypted session blob + bot config + owner identity
whatsapp_usersOptional linked recipients (unused in single-user mode, retained for future multi-recipient deployments)
whatsapp_command_logsSlash-command audit log (sensitive args like /link's api_key are scrubbed before write)
whatsapp_notification_queueRetry queue for failed alerts (currently unused — single-user model drops on not-paired instead of queueing)
whatsapp_user_preferencesPer-user notification toggles + summary time + language + timezone

Idempotent PRAGMA table_info migration adds owner_user_id + owner_username columns to whatsapp_config on existing installs.


Dependencies

  • wars==0.1.3 added — PyO3 binding over the whatsapp-rust crate; provides the WhatsApp Web client. Wheels available for Python 3.12+ via abi3.
  • tradeboard SDK pin: 1.0.49 → 1.0.50 (PyPI: https://pypi.org/project/tradeboard/1.0.50/)
  • urllib3 2.6.3 → 2.7.0 (4519f55f) — clears 8 Dependabot high-severity alerts
  • axios 1.15 → 1.16, python-multipart 0.0.26 → 0.0.27, pin pip>=26.1 (6d61e5c6)

Documentation

  • New: docs/api/whatsapp-services/README.md — architecture + security model + slash-command reference
  • New: docs/api/whatsapp-services/notify.md — full endpoint reference for POST /api/v1/whatsapp/notify
  • New: collections/tradeboard/IN_stock/whatsapp_notify.bru — Bruno collection entry (auto-discovered by /playground)
  • Updated: docs/api/README.md — adds the WhatsApp Services section under the existing service taxonomy
  • Updated: docs/prompt/tradeboard python sdk.md — full client.whatsapp(...) reference with all four recipient forms, image/document attachments, fire-and-forget vs synchronous delivery, and inbound slash-command reference
  • 52eb8650 — docs(mcp): rewrite Remote MCP userguide for traders, drop stale install paths
  • 3b3054be — docs(claude): update CLAUDE.md with new product surfaces, Ruff tooling, and architecture details (#1412)
  • 1a7d3a0a — docs(claude): clarify sandbox terminology and split /sandbox vs /analyzer surfaces
  • c7e5f4a9 — docs(services): align order field names with canonical code (pricetype, product)
  • 83499518 / e2de15ec — Ubuntu Server Installation guide refresh

Install + infrastructure

  • 79557be5 — feat(install): inline Remote MCP prompt in install-docker.sh and install-multi.sh
  • 5c8b64b9 — feat(install): prompt to enable Remote MCP during install.sh
  • 04eac147 — feat(install): simplify single-deploy paths + drop enable-remote-mcp.sh
  • 647183bd — feat(remote-mcp): UI controls for master switch + posture toggles
  • 9ec851ab — fix(mcp): use HOST_SERVER for SDK loopback so install.sh deploys work
  • 5fa17bd3 — fix(diagnostics): correct dead secret keys + git info inside Docker (#1388)
  • f786e21a — chore: add Caddyfile for local https://tradeboard.local dev
  • 4e09da8b — fix(python-strategy): Stop button works under gunicorn-eventlet (#1404)

Bug fixes (non-WebSocket)

  • a4bdac18 — fix(angel/api): defensive .get() in place_order response handling (#846)
  • b06ef4a8 — fix(kotak): align place/modify order payload with official Neo spec (#1398)
  • ca15b333 — fix(groww/api): support NSE_INDEX/BSE_INDEX in historical (#1338) (#1342)
  • 11778af5 — fix(search): surface FUT-only MCX underlyings in symbol search (#1385)

Upgrade procedure

For existing installs (Native Ubuntu):

bash
cd /var/python/tradeboard-flask/<deploy-name>/tradeboard
sudo ./install/update.sh
# update.sh runs migrate_all.py — the new whatsapp_config / whatsapp_users
# tables and the owner_user_id column are created automatically. The
# FERNET_SALT migration runs on first boot inside env_check; existing
# broker-auth ciphertext is re-encrypted on the fly.

For existing installs (Docker):

bash
cd /opt/tradeboard/<domain>
sudo docker compose pull
sudo docker compose up -d
# The container's start.sh runs migrate_all.py before gunicorn boots.
# FERNET_SALT migration runs on first start.

For local developers (uv):

bash
git pull origin main
uv sync
cd frontend && npm install && npm run build
uv run app.py

Enabling WhatsApp (post-upgrade, optional):

  1. Log in to Tradeboard, open /whatsapp.
  2. Click Start pairing. A QR code appears.
  3. On your phone: WhatsApp → Settings → Linked devices → Link a device → scan.
  4. Done. The bot auto-starts and reconnects on every server boot from the encrypted session in tradeboard.db.

The session blob never leaves your server. There is no second-party service to register with — it's a direct Signal-Protocol connection to WhatsApp.


Contributors

  • @marketcalls (Rajandran) — release management; WhatsApp architecture and full implementation (database schema with Fernet-encrypted session blob + domain-separated salt, dedicated WhatsAppBotThread to satisfy PyO3's unsendable contract, event-bus subscriber wired into all 13 order topics, send-only REST API + session-authed admin blueprint, React /whatsapp page with auto-rotating QR, RUST_LOG suppression for the three known-noisy wars modules, attachment-path allowlist with traversal-token rejection, lazy own-JID capture from is_from_me=True messages, slash-command dispatcher with is_from_me gate, auto-reconnect on app boot); tradeboard Python SDK 1.0.50 release with new client.whatsapp(...) API; WebSocket reliability sweep across 14 brokers (subscribe batching, reconnect hardening, fd-leak fixes); new IIFLCapital streaming adapter (#1416, #1430); Groww option chain + WS depth (#1392); Upstox GLOBAL_INDEX world feeds; Zerodha MCX_INDEX wiring (#1385); per-install Fernet salt rotation with crash-safe migration; websocket-proxy fixes (ZMQ bind #1378, mode normalization #1375, request_id correlation #1376, SharedZmqPublisher topology #1374); UI alignment fixes and stale-chunk auto-reload; comprehensive WhatsApp documentation (endpoint reference, SDK prompt-doc, Bruno collection entry).

Links