
Electrobun
- 3 installs
- 2 repo stars
- Updated February 20, 2026
- marketcalls/electrobun-skill
Builds production algo-trading desktop apps with Electrobun and Bun, covering broker integration, real-time market data, type-safe RPC, security, and storage.
About
Encodes best practices for building algo-trading desktop applications with Electrobun on the Bun runtime, covering architecture, broker integration, WebSocket market data, security, and the full Electrobun API. A developer uses it when building a trading platform or broker-integrated desktop app.
- Bun-backend architecture with type-safe RPC and no secrets in the webview
- Broker integration, real-time WebSocket data, and options analytics references
Electrobun by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #847 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/marketcalls/electrobun-skill --skill electrobunAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 2 |
| Last updated | February 20, 2026 |
| Repository | marketcalls/electrobun-skill ↗ |
What it does
Builds production algo-trading desktop apps with Electrobun and Bun, covering broker integration, real-time market data, type-safe RPC, security, and storage.
Files
Electrobun Algo Trading Application Skill
Build production-grade algo trading desktop applications with Electrobun + Bun runtime. This skill encodes best practices for security, performance, broker integration, real-time data, and the complete Electrobun API surface.
Quick Reference
For detailed guides on specific topics, see:
- architecture.md - Application architecture and project structure
- api-reference.md - Complete Electrobun API reference (all classes, methods, events)
- security.md - Authentication, encryption, API key management, sandboxing
- broker-integration.md - Broker APIs, order management, position tracking
- websockets-realtime.md - WebSocket connections, market data streaming, reconnection
- storage.md - SQLite, time-series data, trade logging, caching
- performance.md - Memory efficiency, GC tuning, data streaming, profiling
OpenAlgo Migration & Advanced Features
- openalgo-migration.md - Full OpenAlgo → Electrobun migration guide, RPC schema, phased plan
- broker-plugin-system.md - 29-broker plugin architecture, registry, symbol mapping
- options-analytics.md - Greeks (Black-Scholes), IV, max pain, PCR, GEX, straddle pricing
- strategy-execution.md - Python strategy subprocess, flow engine, webhooks, action center, Telegram, market calendar
- sandbox-paper-trading.md - Paper trading engine, virtual capital, margin simulation, order routing
- monitoring-logging.md - Structured logging, traffic tracking, latency monitoring, health checks, system metrics
Core Principles
1. Bun is the backend - All sensitive logic (broker keys, order execution, DB access) runs in the Bun main process. The webview is ONLY for UI rendering. 2. Type-safe RPC - All communication between Bun and webview uses defineRPC with strict TypeScript schemas. Never pass raw strings or untyped data. 3. Sandbox untrusted content - Any external content (broker login pages, charts from third-party) must use sandbox: true or separate <electrobun-webview> with navigation rules. 4. No secrets in the webview - API keys, tokens, database connections - all stay in src/bun/. The webview calls Bun RPC handlers which proxy all sensitive operations. 5. Fail-safe order management - Every order operation must have timeout handling, duplicate prevention, and state reconciliation.
Project Structure for Trading Apps
trading-app/
electrobun.config.ts
package.json
src/
bun/
index.ts # App entry, window creation, menu setup
broker/
interface.ts # Abstract broker interface
zerodha.ts # Zerodha/Kite implementation
types.ts # Order, Position, Holding types
api/
server.ts # Local HTTP/WebSocket API server
auth.ts # Authentication middleware
routes.ts # API route handlers
db/
index.ts # Database initialization
migrations.ts # Schema migrations
queries.ts # Prepared statements
services/
order-manager.ts # Order lifecycle management
position-tracker.ts # Real-time position tracking
market-data.ts # WebSocket market data handler
risk-manager.ts # Pre-trade risk checks
strategy-engine.ts # Strategy execution
utils/
logger.ts # Structured logging
crypto.ts # Encryption utilities
config.ts # App configuration
mainview/
index.html # Main UI shell
index.css # Global styles
index.ts # Electroview + RPC setup
dashboard/
index.html # Dashboard view
index.ts # Dashboard logic
index.css
orderbook/
index.html # Order book view
index.ts
index.csselectrobun.config.ts Template
import type { ElectrobunConfig } from "electrobun";
import pkg from "./package.json";
export default {
app: {
name: "Trading Platform",
identifier: "com.yourcompany.trading",
version: pkg.version,
},
build: {
bun: {
entrypoint: "src/bun/index.ts",
},
views: {
mainview: { entrypoint: "src/mainview/index.ts" },
dashboard: { entrypoint: "src/dashboard/index.ts" },
orderbook: { entrypoint: "src/orderbook/index.ts" },
},
copy: {
"src/mainview/index.html": "views/mainview/index.html",
"src/mainview/index.css": "views/mainview/index.css",
"src/dashboard/index.html": "views/dashboard/index.html",
"src/dashboard/index.css": "views/dashboard/index.css",
"src/orderbook/index.html": "views/orderbook/index.html",
"src/orderbook/index.css": "views/orderbook/index.css",
},
mac: { bundleCEF: false, codesign: true, notarize: true },
linux: { bundleCEF: false },
win: { bundleCEF: false },
},
runtime: {
exitOnLastWindowClosed: true,
},
release: {
baseUrl: "https://your-update-server.com/releases",
generatePatch: true,
},
} satisfies ElectrobunConfig;RPC Schema Pattern for Trading
import type { RPCSchema } from "electrobun/bun";
// Define in a shared types file, import on both sides
type TradingRPC = {
bun: RPCSchema<{
requests: {
// Authentication
login: { params: { apiKey: string; apiSecret: string; totp?: string }; response: { success: boolean; error?: string } };
logout: { params: {}; response: { success: boolean } };
getAuthState: { params: {}; response: { authenticated: boolean; broker: string } };
// Market Data
getLTP: { params: { symbols: string[] }; response: Record<string, number> };
getQuote: { params: { symbol: string }; response: QuoteData };
getHistorical: { params: { symbol: string; from: string; to: string; interval: string }; response: OHLC[] };
// Orders
placeOrder: { params: OrderParams; response: { orderId: string; status: string } };
modifyOrder: { params: ModifyOrderParams; response: { success: boolean } };
cancelOrder: { params: { orderId: string }; response: { success: boolean } };
getOrders: { params: {}; response: Order[] };
getOrderHistory: { params: { orderId: string }; response: OrderUpdate[] };
// Portfolio
getPositions: { params: {}; response: Position[] };
getHoldings: { params: {}; response: Holding[] };
getMargins: { params: {}; response: MarginData };
// Strategy
startStrategy: { params: { strategyId: string; config: StrategyConfig }; response: { success: boolean } };
stopStrategy: { params: { strategyId: string }; response: { success: boolean } };
getStrategyState: { params: { strategyId: string }; response: StrategyState };
};
messages: {
logMessage: { level: string; message: string; context?: string };
};
}>;
webview: RPCSchema<{
requests: {};
messages: {
// Real-time updates pushed from Bun to UI
tickUpdate: { symbol: string; ltp: number; change: number; volume: number };
orderUpdate: { orderId: string; status: string; message: string };
positionUpdate: { positions: Position[] };
strategyLog: { strategyId: string; message: string; level: string };
connectionStatus: { broker: string; status: "connected" | "disconnected" | "reconnecting" };
};
}>;
};Main Process Entry Pattern
// src/bun/index.ts
import {
ApplicationMenu, BrowserView, BrowserWindow, Tray, Updater, Utils
} from "electrobun/bun";
import type { TradingRPC } from "./types";
import { initDatabase } from "./db";
import { createBrokerConnection } from "./broker/interface";
import { startMarketDataStream } from "./services/market-data";
import { OrderManager } from "./services/order-manager";
// Initialize database
const db = initDatabase();
const broker = createBrokerConnection(db);
const orderManager = new OrderManager(broker, db);
// Define RPC
const rpc = BrowserView.defineRPC<TradingRPC>({
maxRequestTime: 30000,
handlers: {
requests: {
login: async (params) => broker.authenticate(params),
placeOrder: async (params) => orderManager.place(params),
cancelOrder: async (params) => orderManager.cancel(params.orderId),
getPositions: async () => broker.getPositions(),
getOrders: async () => broker.getOrders(),
getMargins: async () => broker.getMargins(),
getLTP: async ({ symbols }) => broker.getLTP(symbols),
// ... all handlers
},
messages: {
logMessage: ({ level, message }) => console.log(`[${level}] ${message}`),
},
},
});
// Create main window
const mainWindow = new BrowserWindow({
title: "Trading Platform",
url: "views://mainview/index.html",
frame: { x: 100, y: 100, width: 1400, height: 900 },
rpc,
});
// Push real-time data to UI
broker.on("tick", (data) => {
mainWindow.webview.rpc?.send.tickUpdate(data);
});
broker.on("order-update", (data) => {
mainWindow.webview.rpc?.send.orderUpdate(data);
});
// System tray for background operation
const tray = new Tray({ title: "Trading", template: true });
tray.setMenu([
{ label: "Show Window", action: "show" },
{ label: "Connection Status", action: "status" },
{ type: "separator" },
{ label: "Emergency Stop All", action: "emergency-stop" },
{ type: "separator" },
{ label: "Quit", action: "quit" },
]);
tray.on("tray-clicked", async (e) => {
if (e.data.action === "show") mainWindow.show();
if (e.data.action === "emergency-stop") await orderManager.cancelAll();
if (e.data.action === "quit") Utils.quit();
});
// Graceful shutdown
mainWindow.on("close", async () => {
await broker.disconnect();
db.close();
Utils.quit();
});Webview Entry Pattern
// src/mainview/index.ts
import { Electroview, type RPCSchema } from "electrobun/view";
import type { TradingRPC } from "../shared/types";
const rpc = Electroview.defineRPC<TradingRPC>({
maxRequestTime: 30000,
handlers: {
requests: {},
messages: {
tickUpdate: (data) => updateTickerUI(data),
orderUpdate: (data) => updateOrderPanel(data),
positionUpdate: (data) => updatePositionTable(data),
connectionStatus: (data) => updateStatusBar(data),
strategyLog: (data) => appendStrategyLog(data),
},
},
});
const _ev = new Electroview({ rpc });
// All data fetching goes through RPC - never direct API calls from webview
async function init() {
const auth = await rpc.request.getAuthState({});
if (!auth.authenticated) showLoginScreen();
else loadDashboard();
}
async function loadDashboard() {
const [positions, orders, margins] = await Promise.all([
rpc.request.getPositions({}),
rpc.request.getOrders({}),
rpc.request.getMargins({}),
]);
renderDashboard({ positions, orders, margins });
}
init();Critical Rules for $ARGUMENTS
When building features, ALWAYS:
1. Proxy all broker API calls through Bun RPC - never call broker APIs from webview 2. Validate order parameters in Bun before sending to broker (quantity > 0, valid price, valid symbol) 3. Use prepared SQLite statements - never string-concatenate SQL 4. Implement circuit breakers - max orders/minute, max loss limits, position size limits 5. Log every order action to SQLite with timestamps for audit trail 6. Handle WebSocket reconnection with exponential backoff and state reconciliation 7. Encrypt sensitive config (API keys) at rest using bun:crypto 8. Use navigation rules to restrict webview to only views://* URLs 9. Use `sandbox: true` for any window loading external broker OAuth pages 10. Run market data processing in Bun - send only UI-ready data to webview via RPC messages
Electrobun Complete API Reference
Imports
// Bun-side (main process)
import {
BrowserWindow, BrowserView, ApplicationMenu, ContextMenu,
Tray, Updater, Utils, BuildConfig, Screen, Session,
GlobalShortcut, Socket, PATHS,
} from "electrobun/bun";
import type { RPCSchema } from "electrobun/bun";
// Webview-side (browser)
import { Electroview } from "electrobun/view";
import type { RPCSchema } from "electrobun/view";
// Global events
import Electrobun from "electrobun/bun";
Electrobun.events.on("event-name", handler);BrowserWindow
Constructor
const win = new BrowserWindow({
title: "Window Title", // Default: "Electrobun"
url: "views://mainview/index.html", // views:// or https://
html: "<html>...</html>", // Alternative: raw HTML
preload: "views://mypreload.js", // Script before page loads
renderer: "native", // "native" | "cef"
frame: { x: 100, y: 100, width: 800, height: 600 },
titleBarStyle: "default", // "default" | "hidden" | "hiddenInset"
transparent: false,
sandbox: false, // true disables RPC
navigationRules: "views://*", // Glob-based URL rules
rpc: rpcObject, // From BrowserView.defineRPC()
styleMask: { // macOS window style
Borderless: false, Titled: true, Closable: true,
Miniaturizable: true, Resizable: true,
FullSizeContentView: false, UtilityWindow: false,
},
});Properties
win.id— Unique window ID (number)win.title— Window title (string)win.frame— {x, y, width, height}win.url— Initial URLwin.webview— Default BrowserView instancewin.webviewId— Default webview ID
Methods
win.setTitle(title: string): void
win.close(): void
win.focus(): void
win.show(): void
win.minimize(): void
win.unminimize(): void
win.isMinimized(): boolean
win.maximize(): void
win.unmaximize(): void
win.isMaximized(): boolean
win.setFullScreen(fs: boolean): void
win.isFullScreen(): boolean
win.setAlwaysOnTop(top: boolean): void
win.isAlwaysOnTop(): boolean
win.setPosition(x: number, y: number): void
win.setSize(width: number, height: number): void
win.setFrame(x: number, y: number, w: number, h: number): void
win.getFrame(): { x, y, width, height }
win.getPosition(): { x, y }
win.getSize(): { width, height }
win.on(event: string, handler: Function): void
// Static
BrowserWindow.getById(id: number): BrowserWindowEvents
| Event | Data |
|---|---|
close | { id } |
resize | { id, x, y, width, height } |
move | { id, x, y } |
focus | { id } |
---
BrowserView
Constructor (usually auto-created by BrowserWindow)
const view = new BrowserView({
url: "views://page/index.html",
html: null,
preload: null,
renderer: "native",
partition: "persist:trading",
frame: { x: 0, y: 0, width: 800, height: 600 },
rpc: rpcObject,
sandbox: false,
autoResize: true,
navigationRules: "views://*,https://api.broker.com/*",
});Methods
view.loadURL(url: string): void
view.loadHTML(html: string): void
view.executeJavascript(js: string): void // Fire-and-forget
view.findInPage(text: string, opts?: { forward?: boolean; matchCase?: boolean }): void
view.stopFindInPage(): void
view.openDevTools(): void
view.closeDevTools(): void
view.toggleDevTools(): void
view.setNavigationRules(rules: string): void
view.on(event: string, handler: Function): void
// Built-in RPC method (available on all views with RPC)
const result = await view.rpc?.request.evaluateJavascriptWithResponse({
script: "document.title"
});
// Static
BrowserView.getAll(): BrowserView[]
BrowserView.getById(id: number): BrowserView | undefined
BrowserView.defineRPC<T>(config): TEvents
will-navigate, did-navigate, did-navigate-in-page, did-commit-navigation, dom-ready, new-window-open, host-message, download-started, download-progress, download-completed, download-failed
---
RPC System
Defining Schema
type MyRPC = {
bun: RPCSchema<{
requests: {
methodName: { params: ParamType; response: ResponseType };
};
messages: {
messageName: PayloadType;
};
}>;
webview: RPCSchema<{
requests: {
methodName: { params: ParamType; response: ResponseType };
};
messages: {
messageName: PayloadType;
};
}>;
};Bun-Side
const rpc = BrowserView.defineRPC<MyRPC>({
maxRequestTime: 30000, // Timeout in ms
handlers: {
requests: {
methodName: async (params) => { return response; },
},
messages: {
messageName: (payload) => { /* handle */ },
"*": (name, payload) => { /* wildcard handler */ },
},
},
});
// Pass to window
const win = new BrowserWindow({ url: "...", rpc });
// Call webview methods
const result = await win.webview.rpc?.request.webviewMethod(params);
// Send messages to webview
win.webview.rpc?.send.messageName(payload);Webview-Side
const rpc = Electroview.defineRPC<MyRPC>({
maxRequestTime: 30000,
handlers: {
requests: { /* webview-side request handlers */ },
messages: { /* handle messages from bun */ },
},
});
const _ev = new Electroview({ rpc });
// Call bun methods
const result = await rpc.request.bunMethod(params);
// Send messages to bun
rpc.send.messageName(payload);---
ApplicationMenu
ApplicationMenu.setApplicationMenu([
{
label: "App Name",
submenu: [
{ role: "about" },
{ type: "separator" },
{ label: "Custom Item", action: "my-action", accelerator: "k", data: { key: "val" } },
{ label: "Disabled", action: "x", enabled: false },
{ label: "Checked", action: "y", checked: true },
{ label: "Hidden", action: "z", hidden: true },
{ role: "quit" },
],
},
]);
// Listen for clicks
Electrobun.events.on("application-menu-clicked", (e) => {
console.log(e.data.action, e.data.data);
});Menu Item Types
{ type: "normal", label, action, accelerator, data, enabled, checked, hidden, tooltip, submenu }
{ type: "separator" } // or "divider"
{ role: "about" | "quit" | "hide" | "copy" | "paste" | ... }---
ContextMenu
ContextMenu.showContextMenu([
{ label: "Cut", action: "cut", accelerator: "x" },
{ label: "Copy", action: "copy", accelerator: "c" },
{ type: "separator" },
{ label: "Submenu", submenu: [
{ label: "Option A", action: "opt-a", data: { id: 1 } },
]},
]);
Electrobun.events.on("context-menu-clicked", (e) => {
console.log(e.data.action, e.data.data);
});---
Tray
const tray = new Tray({
title: "My App",
image: "views://icon.png",
template: true, // macOS template image
width: 18,
height: 18,
});
tray.setMenu([ /* same format as context menu */ ]);
tray.setTitle("New Title");
tray.setImage("views://new-icon.png");
tray.remove();
tray.on("tray-clicked", (e) => {
console.log(e.data.action, e.data.data);
});
// Static
Tray.getById(id): Tray
Tray.getAll(): Tray[]
Tray.removeById(id): void---
Utils
// File operations
Utils.moveToTrash(path: string): void
Utils.showItemInFolder(path: string): void
Utils.openPath(path: string): void
Utils.openExternal(url: string): void
// Notifications
Utils.showNotification({ title, body?, subtitle?, silent? }): void
// Message box
const result = await Utils.showMessageBox({
type: "info" | "warning" | "error" | "question",
title: string,
message: string,
detail?: string,
buttons: string[],
defaultId?: number,
cancelId?: number,
});
// result.response = button index clicked
// File dialog
const paths = await Utils.openFileDialog({
startingFolder?: string,
allowedFileTypes?: string, // "ts,js,json"
canChooseFiles?: boolean,
canChooseDirectory?: boolean,
allowsMultipleSelection?: boolean,
});
// Clipboard
Utils.clipboardWriteText(text: string): void
Utils.clipboardReadText(): string | null
Utils.clipboardReadImage(): Uint8Array | null
Utils.clipboardWriteImage(png: Uint8Array): void
Utils.clipboardAvailableFormats(): string[]
Utils.clipboardClear(): void
// Lifecycle
Utils.quit(): voidUtils.paths (System Directories)
Utils.paths.home // Home directory
Utils.paths.appData // ~/Library/Application Support (mac)
Utils.paths.config // ~/Library/Preferences (mac)
Utils.paths.cache // ~/Library/Caches (mac)
Utils.paths.temp // System temp
Utils.paths.logs // ~/Library/Logs (mac)
Utils.paths.documents // ~/Documents
Utils.paths.downloads // ~/Downloads
Utils.paths.desktop // ~/Desktop
Utils.paths.pictures // ~/Pictures
Utils.paths.music // ~/Music
Utils.paths.videos // ~/Movies (mac)
Utils.paths.userData // {appData}/{identifier}/{channel}
Utils.paths.userCache // {cache}/{identifier}/{channel}
Utils.paths.userLogs // {logs}/{identifier}/{channel}---
PATHS (Resource Paths)
import { PATHS } from "electrobun/bun";
PATHS.RESOURCES_FOLDER // app Resources directory (read-only)
PATHS.VIEWS_FOLDER // app views directory---
GlobalShortcut
GlobalShortcut.register("CommandOrControl+Shift+I", () => { /* handler */ }): boolean
GlobalShortcut.isRegistered("CommandOrControl+Shift+I"): boolean
GlobalShortcut.unregister("CommandOrControl+Shift+I"): void
GlobalShortcut.unregisterAll(): voidAccelerator format: Modifier+Key where modifiers are Command, Control, CommandOrControl, Alt, Shift, Super. Keys: A-Z, 0-9, F1-F12, Space, Enter, Tab, Escape, Backspace, Delete, arrows, etc.
---
Screen
Screen.getPrimaryDisplay(): Display
// { id, bounds: {x,y,width,height}, workArea: {x,y,width,height}, scaleFactor, isPrimary }
Screen.getAllDisplays(): Display[]
Screen.getCursorScreenPoint(): { x: number; y: number }---
Session
const session = Session.fromPartition("persist:trading");
const defaultSession = Session.defaultSession;
// Cookies
await session.cookies.set({ name, value, domain, path, secure, httpOnly, sameSite, expirationDate });
const cookies = await session.cookies.get({ url?, name?, domain?, path?, secure?, session? });
await session.cookies.remove(url: string, name: string);
await session.cookies.clear();
// Storage
await session.clearStorageData(["localStorage", "cache", "cookies", "indexedDB", "all"]);---
Updater
// Local info
await Updater.localInfo.version(): string
await Updater.localInfo.channel(): string // "dev" | "canary" | "stable"
await Updater.localInfo.hash(): string
// Paths
Updater.appDataFolder(): string
Updater.channelBucketUrl(): string
// Update lifecycle
const info = await Updater.checkForUpdate();
// { version, hash, updateAvailable, updateReady, error }
await Updater.downloadUpdate();
await Updater.applyUpdate(); // Quits, replaces binary, relaunches
// Status tracking
Updater.onStatusChange((entry: UpdateStatusEntry) => { });
Updater.getStatusHistory(): UpdateStatusEntry[]
Updater.clearStatusHistory(): void---
BuildConfig
const config = await BuildConfig.get();
// { defaultRenderer, availableRenderers, cefVersion?, bunVersion?, runtime? }
const cached = BuildConfig.getCached(); // Sync, may be null---
Global Events
import Electrobun from "electrobun/bun";
// Application events
Electrobun.events.on("application-menu-clicked", (e) => { });
Electrobun.events.on("context-menu-clicked", (e) => { });
Electrobun.events.on("open-url", (e) => { }); // URL scheme (macOS)
Electrobun.events.on("before-quit", async (e) => { }); // Cancellable
// Global window events (all windows)
Electrobun.events.on("close", (e) => { });
Electrobun.events.on("resize", (e) => { });
Electrobun.events.on("move", (e) => { });
Electrobun.events.on("focus", (e) => { });
// Global webview events (all webviews)
Electrobun.events.on("will-navigate", (e) => { });
Electrobun.events.on("did-navigate", (e) => { });
Electrobun.events.on("dom-ready", (e) => { });
Electrobun.events.on("new-window-open", (e) => { });
// Cancel navigation
Electrobun.events.on("will-navigate", (e) => {
if (e.data.detail.includes("blocked.com")) {
e.response = { allow: false };
}
});
// Remove listener
Electrobun.events.off("event-name", handler);---
Navigation Rules
// Comma-separated glob patterns. ^ prefix = block. Last match wins.
const rules = "views://*,https://api.broker.com/*,^https://evil.com/*,^*";
// On window creation
new BrowserWindow({ navigationRules: rules });
// Or dynamically
webview.setNavigationRules(rules);---
Webview Tag (Browser-Side)
<electrobun-webview
src="https://example.com"
preload="views://preload.js"
renderer="native"
partition="persist:browsing"
sandbox
style="width: 100%; height: 400px;">
</electrobun-webview>const wv = document.querySelector("electrobun-webview");
wv.loadURL(url);
wv.loadHTML(html);
wv.goBack();
wv.goForward();
wv.reload();
await wv.canGoBack();
await wv.canGoForward();
wv.findInPage(text, opts);
wv.stopFindInPage();
wv.openDevTools();
wv.toggleDevTools();
wv.setNavigationRules(rules);
wv.addMaskSelector(".toolbar");
wv.removeMaskSelector(".toolbar");
wv.on("dom-ready", handler);
wv.off("dom-ready", handler);---
Draggable Regions
<!-- CSS class approach -->
<div class="electrobun-webkit-app-region-drag">Custom Titlebar</div>
<!-- Inline style approach -->
<div style="app-region: drag;">Custom Titlebar</div>---
CLI Commands
electrobun init [name] [--template=hello-world|react-tailwind-vite|photo-booth|multitab-browser|svelte]
electrobun build [--env=dev|canary|stable]
electrobun dev---
Platform Support
| Feature | macOS | Windows | Linux |
|---|---|---|---|
| Native Renderer | WKWebView | WebView2 | WebKitGTK |
| CEF Renderer | Optional | Optional | Recommended |
| App Menu | Full | Full | Limited |
| Context Menu | Full | Simple | Not supported |
| Tray | Full | Full | Full |
| Global Shortcuts | Full | Full | Full |
| Code Signing | Full + Notarize | N/A | N/A |
| URL Schemes | Full | N/A | N/A |
Electrobun Algo Trading Architecture
Process Model
Electrobun uses a multi-process architecture:
┌─────────────────────────────────────────────────┐
│ Launcher (Zig) │
│ Tiny binary → spawns Bun → inits native GUI │
└──────────────────────┬──────────────────────────┘
│
┌──────────────────────▼──────────────────────────┐
│ Bun Main Process │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Broker APIs │ │ SQLite DB │ │
│ │ WebSockets │ │ Order Mgmt │ │
│ │ Risk Engine │ │ Strategy │ │
│ └──────┬──────┘ └──────┬───────┘ │
│ │ Encrypted RPC │ │
│ │ (AES-256-GCM) │ │
│ ┌──────▼────────────────▼───────┐ │
│ │ RPC Transport Layer │ │
│ │ WebSocket localhost + FFI │ │
│ └──────────────┬────────────────┘ │
└─────────────────┼───────────────────────────────┘
│
┌─────────────────▼───────────────────────────────┐
│ Native Webview (per-window) │
│ WKWebView (mac) / WebView2 (win) / GTK (linux) │
│ ┌─────────────────────────────────────┐ │
│ │ Electroview + UI (HTML/CSS/TS) │ │
│ │ Dashboard, Charts, Order Entry │ │
│ │ NO direct broker/DB access │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘Layer Responsibilities
Bun Layer (src/bun/) — The Trading Engine
This is where ALL business logic lives:
- Broker API authentication and session management
- WebSocket connections to market data feeds
- Order placement, modification, cancellation
- Position tracking and P&L calculation
- Risk management (pre-trade checks, circuit breakers)
- Strategy execution engine
- SQLite database operations (trades, logs, config)
- Encryption and key management
- Local HTTP/WebSocket server for external tool integration
Webview Layer (src/mainview/) — The Display
Pure presentation:
- Renders data received via RPC messages
- Sends user actions (place order, start strategy) via RPC requests
- Chart rendering, table displays, forms
- NEVER holds API keys, tokens, or database handles
- NEVER makes direct HTTP calls to brokers
Shared Types (src/shared/) — The Contract
- RPC schema type definitions
- Data model interfaces (Order, Position, OHLC, etc.)
- Enum definitions (OrderType, OrderStatus, Exchange)
Multi-Window Architecture
Trading apps often need multiple windows:
import { BrowserWindow, BrowserView } from "electrobun/bun";
// Main trading window
const mainWindow = new BrowserWindow({
title: "Trading Dashboard",
url: "views://mainview/index.html",
frame: { x: 0, y: 0, width: 1400, height: 900 },
rpc: mainRpc,
});
// Separate order book window
const orderbookWindow = new BrowserWindow({
title: "Order Book",
url: "views://orderbook/index.html",
frame: { x: 1400, y: 0, width: 500, height: 900 },
rpc: orderbookRpc,
});
// Chart window (can load external charting lib in sandboxed webview)
const chartWindow = new BrowserWindow({
title: "Charts",
url: "views://charts/index.html",
frame: { x: 0, y: 900, width: 1900, height: 600 },
rpc: chartRpc,
});
// Broadcast tick data to all windows
function broadcastTick(data: TickData) {
mainWindow.webview.rpc?.send.tickUpdate(data);
orderbookWindow.webview.rpc?.send.tickUpdate(data);
chartWindow.webview.rpc?.send.tickUpdate(data);
}Embedded Webviews for Third-Party Content
Use <electrobun-webview> for isolated content like broker login pages:
<!-- In your main HTML -->
<electrobun-webview
id="broker-login"
src="https://kite.zerodha.com/connect/login"
sandbox
style="width: 100%; height: 500px;">
</electrobun-webview>The sandbox attribute prevents RPC injection, keeping the broker's login page isolated.
Service Architecture Pattern
// src/bun/services/market-data.ts
export class MarketDataService {
private ws: WebSocket | null = null;
private subscriptions = new Map<string, Set<(data: TickData) => void>>();
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private reconnectDelay = 1000;
constructor(private broker: BrokerConnection) {}
async connect() {
this.ws = new WebSocket(this.broker.getWebSocketUrl());
this.ws.onmessage = (event) => this.handleMessage(event);
this.ws.onclose = () => this.handleDisconnect();
this.ws.onerror = (err) => this.handleError(err);
this.reconnectAttempts = 0;
}
subscribe(symbol: string, callback: (data: TickData) => void) {
if (!this.subscriptions.has(symbol)) {
this.subscriptions.set(symbol, new Set());
this.ws?.send(JSON.stringify({ action: "subscribe", symbol }));
}
this.subscriptions.get(symbol)!.add(callback);
}
private handleMessage(event: MessageEvent) {
const data = this.parseTickData(event.data);
const callbacks = this.subscriptions.get(data.symbol);
if (callbacks) {
for (const cb of callbacks) cb(data);
}
}
private async handleDisconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error("Max reconnection attempts reached");
return;
}
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
this.reconnectAttempts++;
await Bun.sleep(delay);
await this.connect();
// Re-subscribe to all symbols after reconnect
for (const symbol of this.subscriptions.keys()) {
this.ws?.send(JSON.stringify({ action: "subscribe", symbol }));
}
}
}Application Menu for Trading
ApplicationMenu.setApplicationMenu([
{
label: "Trading App",
submenu: [
{ role: "about" },
{ type: "separator" },
{ label: "Preferences", action: "preferences", accelerator: "," },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "quit" },
],
},
{
label: "Trading",
submenu: [
{ label: "New Order", action: "new-order", accelerator: "n" },
{ label: "Cancel All Orders", action: "cancel-all", accelerator: "Shift+x" },
{ type: "separator" },
{ label: "Start Strategy", action: "start-strategy" },
{ label: "Stop All Strategies", action: "stop-all-strategies" },
{ type: "separator" },
{ label: "Square Off All", action: "square-off-all", accelerator: "Shift+q" },
],
},
{
label: "View",
submenu: [
{ label: "Dashboard", action: "view-dashboard", accelerator: "1" },
{ label: "Order Book", action: "view-orderbook", accelerator: "2" },
{ label: "Positions", action: "view-positions", accelerator: "3" },
{ label: "Charts", action: "view-charts", accelerator: "4" },
{ type: "separator" },
{ role: "toggleFullScreen" },
],
},
{
label: "Edit",
submenu: [
{ role: "undo" }, { role: "redo" },
{ type: "separator" },
{ role: "cut" }, { role: "copy" }, { role: "paste" }, { role: "selectAll" },
],
},
]);Tray for Background Trading
const tray = new Tray({
title: "Trading",
template: true, // Adapts to macOS light/dark mode
});
function updateTrayMenu(state: AppState) {
tray.setMenu([
{ label: `P&L: ${state.totalPnL >= 0 ? "+" : ""}${state.totalPnL.toFixed(2)}`, enabled: false },
{ label: `Open Positions: ${state.openPositions}`, enabled: false },
{ label: `Pending Orders: ${state.pendingOrders}`, enabled: false },
{ type: "separator" },
{ label: "Show Dashboard", action: "show-dashboard" },
{ label: "Quick Order", action: "quick-order" },
{ type: "separator" },
{
label: "Emergency",
submenu: [
{ label: "Cancel All Orders", action: "cancel-all" },
{ label: "Square Off All", action: "square-off" },
],
},
{ type: "separator" },
{ label: "Quit", action: "quit" },
]);
}Error Handling Strategy
// Wrap all RPC handlers with consistent error handling
function wrapHandler<P, R>(
name: string,
handler: (params: P) => Promise<R>
): (params: P) => Promise<R> {
return async (params: P) => {
try {
return await handler(params);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[RPC:${name}] Error:`, message);
// Log to DB for audit
db.run(
"INSERT INTO error_log (handler, message, params, timestamp) VALUES (?, ?, ?, ?)",
name, message, JSON.stringify(params), Date.now()
);
throw error; // Re-throw so webview gets the error
}
};
}Updater Integration
// Check for updates on startup (non-blocking)
async function checkUpdates() {
try {
const info = await Updater.checkForUpdate();
if (info.updateAvailable) {
mainWindow.webview.rpc?.send.connectionStatus({
broker: "app",
status: "update-available",
});
// Download in background
await Updater.downloadUpdate();
// Notify user, don't force-apply during trading hours
Utils.showNotification({
title: "Update Available",
body: `Version ${info.version} is ready to install.`,
});
}
} catch (e) {
console.error("Update check failed:", e);
}
}
// Only apply updates when user explicitly requests (not during trading)
async function applyUpdate() {
const positions = await broker.getPositions();
const openPositions = positions.filter(p => p.quantity !== 0);
if (openPositions.length > 0) {
Utils.showNotification({
title: "Cannot Update",
body: "Close all positions before updating.",
});
return;
}
await Updater.applyUpdate();
}Broker Integration Patterns for Electrobun Trading Apps
Abstract Broker Interface
Design a broker-agnostic interface so you can swap brokers without changing the rest of the app.
// src/bun/broker/interface.ts
export interface BrokerConnection {
// Authentication
authenticate(params: AuthParams): Promise<AuthResult>;
logout(): Promise<void>;
isAuthenticated(): boolean;
// Market Data
getLTP(symbols: string[]): Promise<Record<string, number>>;
getQuote(symbol: string): Promise<QuoteData>;
getOHLC(symbols: string[]): Promise<Record<string, OHLCData>>;
getHistorical(params: HistoricalParams): Promise<OHLC[]>;
// Orders
placeOrder(params: OrderParams): Promise<OrderResult>;
modifyOrder(params: ModifyOrderParams): Promise<ModifyResult>;
cancelOrder(orderId: string, variety?: string): Promise<CancelResult>;
getOrders(): Promise<Order[]>;
getOrderHistory(orderId: string): Promise<OrderUpdate[]>;
getTrades(): Promise<Trade[]>;
// Portfolio
getPositions(): Promise<Position[]>;
getHoldings(): Promise<Holding[]>;
getMargins(): Promise<MarginData>;
// WebSocket
connectWebSocket(): Promise<void>;
disconnectWebSocket(): void;
subscribe(symbols: string[], mode?: TickMode): void;
unsubscribe(symbols: string[]): void;
// Events
on(event: "tick", handler: (data: TickData) => void): void;
on(event: "order-update", handler: (data: OrderUpdate) => void): void;
on(event: "error", handler: (error: Error) => void): void;
on(event: "disconnect", handler: () => void): void;
on(event: "reconnect", handler: () => void): void;
off(event: string, handler: Function): void;
// Instrument search
searchInstruments(query: string): Promise<Instrument[]>;
}
// Common types
export interface AuthParams {
apiKey: string;
apiSecret: string;
totp?: string;
requestToken?: string;
}
export interface AuthResult {
success: boolean;
error?: string;
accessToken?: string;
}
export interface OrderParams {
symbol: string;
exchange: "NSE" | "BSE" | "NFO" | "MCX" | "BFO";
transactionType: "BUY" | "SELL";
quantity: number;
product: "CNC" | "MIS" | "NRML";
orderType: "MARKET" | "LIMIT" | "SL" | "SL-M";
price?: number;
triggerPrice?: number;
validity?: "DAY" | "IOC" | "TTL";
variety?: "regular" | "amo" | "co" | "iceberg";
tag?: string;
}
export interface Order {
orderId: string;
symbol: string;
exchange: string;
transactionType: "BUY" | "SELL";
quantity: number;
filledQuantity: number;
pendingQuantity: number;
price: number;
averagePrice: number;
triggerPrice: number;
orderType: string;
product: string;
variety: string;
status: OrderStatus;
statusMessage: string;
orderTimestamp: string;
exchangeTimestamp: string;
tag: string;
}
export type OrderStatus =
| "OPEN"
| "COMPLETE"
| "CANCELLED"
| "REJECTED"
| "TRIGGER_PENDING"
| "MODIFY_PENDING"
| "CANCEL_PENDING";
export interface Position {
symbol: string;
exchange: string;
product: string;
quantity: number;
buyQuantity: number;
sellQuantity: number;
buyPrice: number;
sellPrice: number;
averagePrice: number;
lastPrice: number;
pnl: number;
realizedPnl: number;
unrealizedPnl: number;
multiplier: number;
}
export interface Holding {
symbol: string;
exchange: string;
isin: string;
quantity: number;
averagePrice: number;
lastPrice: number;
pnl: number;
dayChange: number;
dayChangePercent: number;
}
export interface TickData {
symbol: string;
exchange: string;
lastPrice: number;
open: number;
high: number;
low: number;
close: number;
change: number;
changePercent: number;
volume: number;
buyQuantity: number;
sellQuantity: number;
ohlc: { open: number; high: number; low: number; close: number };
depth?: {
buy: { price: number; quantity: number; orders: number }[];
sell: { price: number; quantity: number; orders: number }[];
};
timestamp: number;
}
export type TickMode = "ltp" | "quote" | "full";
export interface OHLC {
timestamp: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
oi?: number;
}
export interface MarginData {
equity: {
available: number;
used: number;
total: number;
};
commodity: {
available: number;
used: number;
total: number;
};
}
export interface Instrument {
instrumentToken: number;
exchangeToken: number;
tradingSymbol: string;
name: string;
exchange: string;
segment: string;
instrumentType: string;
lotSize: number;
tickSize: number;
expiry?: string;
strike?: number;
}Zerodha/Kite Broker Implementation
// src/bun/broker/zerodha.ts
import type {
BrokerConnection, AuthParams, AuthResult, OrderParams,
OrderResult, Position, Holding, MarginData, TickData, OHLC
} from "./interface";
export class ZerodhaBroker implements BrokerConnection {
private baseUrl = "https://api.kite.trade";
private accessToken: string | null = null;
private apiKey: string = "";
private ws: WebSocket | null = null;
private listeners = new Map<string, Set<Function>>();
private reconnectAttempts = 0;
private subscribedSymbols = new Map<string, number>(); // symbol -> instrument_token
// --- Authentication ---
async authenticate(params: AuthParams): Promise<AuthResult> {
try {
this.apiKey = params.apiKey;
if (params.requestToken) {
// Exchange request token for access token
const response = await fetch(`${this.baseUrl}/session/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
api_key: params.apiKey,
request_token: params.requestToken,
checksum: await this.generateChecksum(params.apiKey, params.requestToken, params.apiSecret),
}),
});
const data = await response.json();
if (data.status === "success") {
this.accessToken = data.data.access_token;
return { success: true, accessToken: this.accessToken };
}
return { success: false, error: data.message };
}
return { success: false, error: "Request token required" };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : "Auth failed" };
}
}
isAuthenticated(): boolean {
return this.accessToken !== null;
}
async logout(): Promise<void> {
if (this.accessToken) {
await this.apiCall("DELETE", "/session/token", { access_token: this.accessToken });
this.accessToken = null;
}
this.disconnectWebSocket();
}
// --- API Call Helper ---
private async apiCall<T>(method: string, path: string, body?: any): Promise<T> {
const url = `${this.baseUrl}${path}`;
const headers: Record<string, string> = {
"X-Kite-Version": "3",
Authorization: `token ${this.apiKey}:${this.accessToken}`,
};
const options: RequestInit = { method, headers };
if (body && method !== "GET") {
headers["Content-Type"] = "application/x-www-form-urlencoded";
options.body = new URLSearchParams(body);
}
const response = await fetch(url, options);
if (response.status === 403) {
this.accessToken = null;
throw new Error("Session expired. Please re-authenticate.");
}
const data = await response.json();
if (data.status !== "success") {
throw new Error(data.message || `API error: ${response.status}`);
}
return data.data;
}
// --- Market Data ---
async getLTP(symbols: string[]): Promise<Record<string, number>> {
const instruments = symbols.map((s) => `i=${s}`).join("&");
const data = await this.apiCall<Record<string, { last_price: number }>>(
"GET", `/quote/ltp?${instruments}`
);
const result: Record<string, number> = {};
for (const [key, val] of Object.entries(data)) {
result[key] = val.last_price;
}
return result;
}
async getHistorical(params: {
instrumentToken: number; from: string; to: string; interval: string;
}): Promise<OHLC[]> {
const data = await this.apiCall<{ candles: number[][] }>(
"GET",
`/instruments/historical/${params.instrumentToken}/${params.interval}?from=${params.from}&to=${params.to}`
);
return data.candles.map(([ts, o, h, l, c, v]) => ({
timestamp: new Date(ts).toISOString(),
open: o, high: h, low: l, close: c, volume: v,
}));
}
// --- Orders ---
async placeOrder(params: OrderParams): Promise<{ orderId: string; status: string }> {
const data = await this.apiCall<{ order_id: string }>(
"POST", `/orders/${params.variety || "regular"}`, {
tradingsymbol: params.symbol,
exchange: params.exchange,
transaction_type: params.transactionType,
quantity: String(params.quantity),
product: params.product,
order_type: params.orderType,
price: params.price ? String(params.price) : undefined,
trigger_price: params.triggerPrice ? String(params.triggerPrice) : undefined,
validity: params.validity || "DAY",
tag: params.tag,
}
);
return { orderId: data.order_id, status: "OPEN" };
}
async cancelOrder(orderId: string, variety = "regular"): Promise<{ success: boolean }> {
await this.apiCall("DELETE", `/orders/${variety}/${orderId}`);
return { success: true };
}
async getOrders(): Promise<Order[]> {
return await this.apiCall("GET", "/orders");
}
async getPositions(): Promise<Position[]> {
const data = await this.apiCall<{ net: any[]; day: any[] }>("GET", "/portfolio/positions");
return data.net.map(this.mapPosition);
}
async getHoldings(): Promise<Holding[]> {
return await this.apiCall("GET", "/portfolio/holdings");
}
async getMargins(): Promise<MarginData> {
const data = await this.apiCall<any>("GET", "/user/margins");
return {
equity: {
available: data.equity?.available?.cash || 0,
used: data.equity?.utilised?.debits || 0,
total: data.equity?.net || 0,
},
commodity: {
available: data.commodity?.available?.cash || 0,
used: data.commodity?.utilised?.debits || 0,
total: data.commodity?.net || 0,
},
};
}
// --- WebSocket Market Data ---
async connectWebSocket(): Promise<void> {
const wsUrl = `wss://ws.kite.trade?api_key=${this.apiKey}&access_token=${this.accessToken}`;
this.ws = new WebSocket(wsUrl);
this.ws.binaryType = "arraybuffer";
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.emit("reconnect");
// Re-subscribe after reconnect
if (this.subscribedSymbols.size > 0) {
const tokens = Array.from(this.subscribedSymbols.values());
this.ws?.send(JSON.stringify({ a: "subscribe", v: tokens }));
}
};
this.ws.onmessage = (event) => {
if (typeof event.data === "string") {
// JSON message (order updates, etc.)
const data = JSON.parse(event.data);
if (data.type === "order") {
this.emit("order-update", data.data);
}
} else {
// Binary tick data
const ticks = this.parseBinaryTicks(event.data as ArrayBuffer);
for (const tick of ticks) {
this.emit("tick", tick);
}
}
};
this.ws.onclose = () => {
this.emit("disconnect");
this.reconnect();
};
this.ws.onerror = (err) => {
this.emit("error", new Error("WebSocket error"));
};
}
subscribe(symbols: string[], mode: TickMode = "quote"): void {
// Map symbols to instrument tokens (you'd look these up from instruments list)
const tokens = symbols.map((s) => this.subscribedSymbols.get(s)).filter(Boolean);
if (tokens.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ a: "subscribe", v: tokens }));
const modeMap = { ltp: "ltp", quote: "quote", full: "full" };
this.ws.send(JSON.stringify({ a: "mode", v: [modeMap[mode], tokens] }));
}
}
private async reconnect(): Promise<void> {
if (this.reconnectAttempts >= 10) {
this.emit("error", new Error("Max reconnection attempts reached"));
return;
}
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
await Bun.sleep(delay);
await this.connectWebSocket();
}
disconnectWebSocket(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// --- Binary tick parsing (Kite protocol) ---
private parseBinaryTicks(buffer: ArrayBuffer): TickData[] {
const view = new DataView(buffer);
const numberOfPackets = view.getInt16(0);
const ticks: TickData[] = [];
let offset = 2;
for (let i = 0; i < numberOfPackets; i++) {
const packetLength = view.getInt16(offset);
offset += 2;
const instrumentToken = view.getInt32(offset);
const lastPrice = view.getInt32(offset + 4) / 100;
// Simplified — full parsing depends on tick mode
ticks.push({
symbol: this.getSymbolByToken(instrumentToken),
exchange: "",
lastPrice,
open: 0, high: 0, low: 0, close: 0,
change: 0, changePercent: 0,
volume: 0, buyQuantity: 0, sellQuantity: 0,
ohlc: { open: 0, high: 0, low: 0, close: 0 },
timestamp: Date.now(),
});
offset += packetLength;
}
return ticks;
}
// --- Event Emitter ---
on(event: string, handler: Function): void {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
this.listeners.get(event)!.add(handler);
}
off(event: string, handler: Function): void {
this.listeners.get(event)?.delete(handler);
}
private emit(event: string, data?: any): void {
const handlers = this.listeners.get(event);
if (handlers) {
for (const handler of handlers) handler(data);
}
}
private getSymbolByToken(token: number): string {
for (const [symbol, t] of this.subscribedSymbols) {
if (t === token) return symbol;
}
return String(token);
}
private mapPosition(raw: any): Position {
return {
symbol: raw.tradingsymbol,
exchange: raw.exchange,
product: raw.product,
quantity: raw.quantity,
buyQuantity: raw.buy_quantity,
sellQuantity: raw.sell_quantity,
buyPrice: raw.buy_price,
sellPrice: raw.sell_price,
averagePrice: raw.average_price,
lastPrice: raw.last_price,
pnl: raw.pnl,
realizedPnl: raw.realised,
unrealizedPnl: raw.unrealised,
multiplier: raw.multiplier,
};
}
private async generateChecksum(apiKey: string, requestToken: string, apiSecret: string): Promise<string> {
const input = `${apiKey}${requestToken}${apiSecret}`;
const hash = new Bun.CryptoHasher("sha256");
hash.update(input);
return hash.digest("hex");
}
}Broker Factory
// src/bun/broker/factory.ts
import type { BrokerConnection } from "./interface";
import { ZerodhaBroker } from "./zerodha";
export type BrokerType = "zerodha" | "angel" | "fyers" | "upstox";
export function createBroker(type: BrokerType): BrokerConnection {
switch (type) {
case "zerodha":
return new ZerodhaBroker();
default:
throw new Error(`Broker '${type}' not implemented`);
}
}Order Manager
// src/bun/services/order-manager.ts
import type { BrokerConnection, OrderParams, Order } from "../broker/interface";
import { Database } from "bun:sqlite";
export class OrderManager {
private broker: BrokerConnection;
private db: Database;
private pendingOrders = new Map<string, OrderParams>();
constructor(broker: BrokerConnection, db: Database) {
this.broker = broker;
this.db = db;
// Listen for order updates
broker.on("order-update", (update) => this.handleOrderUpdate(update));
}
async place(params: OrderParams): Promise<{ orderId: string; status: string }> {
// Log intent
this.logOrder("PLACE_INTENT", params);
const result = await this.broker.placeOrder(params);
this.pendingOrders.set(result.orderId, params);
// Log result
this.logOrder("PLACE_RESULT", { ...params, orderId: result.orderId, status: result.status });
return result;
}
async cancel(orderId: string): Promise<{ success: boolean }> {
this.logOrder("CANCEL_INTENT", { orderId });
const result = await this.broker.cancelOrder(orderId);
this.pendingOrders.delete(orderId);
this.logOrder("CANCEL_RESULT", { orderId, ...result });
return result;
}
async cancelAll(): Promise<{ cancelled: number; failed: number }> {
const orders = await this.broker.getOrders();
const open = orders.filter((o) => o.status === "OPEN" || o.status === "TRIGGER_PENDING");
let cancelled = 0;
let failed = 0;
for (const order of open) {
try {
await this.broker.cancelOrder(order.orderId, order.variety);
cancelled++;
} catch {
failed++;
}
}
return { cancelled, failed };
}
async squareOffAll(): Promise<{ closed: number; failed: number }> {
const positions = await this.broker.getPositions();
const open = positions.filter((p) => p.quantity !== 0);
let closed = 0;
let failed = 0;
for (const pos of open) {
try {
await this.broker.placeOrder({
symbol: pos.symbol,
exchange: pos.exchange as any,
transactionType: pos.quantity > 0 ? "SELL" : "BUY",
quantity: Math.abs(pos.quantity),
product: pos.product as any,
orderType: "MARKET",
});
closed++;
} catch {
failed++;
}
}
return { closed, failed };
}
private handleOrderUpdate(update: any): void {
this.logOrder("ORDER_UPDATE", update);
if (update.status === "COMPLETE" || update.status === "CANCELLED" || update.status === "REJECTED") {
this.pendingOrders.delete(update.orderId);
}
}
private logOrder(action: string, data: any): void {
this.db.run(
"INSERT INTO order_log (timestamp, action, data) VALUES (?, ?, ?)",
[Date.now(), action, JSON.stringify(data)]
);
}
}GTT (Good Till Triggered) Orders
// Support for GTT orders — these persist on the broker's server
async placeGTT(params: {
symbol: string;
exchange: string;
transactionType: "BUY" | "SELL";
product: string;
triggerType: "single" | "two-leg";
triggerValue?: number;
limitPrice?: number;
quantity?: number;
upperTrigger?: number;
upperPrice?: number;
upperQuantity?: number;
lowerTrigger?: number;
lowerPrice?: number;
lowerQuantity?: number;
}): Promise<{ triggerId: number }> {
// Implementation depends on broker API
// Log to DB for tracking
this.logOrder("GTT_PLACE", params);
return await this.broker.placeGTT(params);
}Multi-Broker Support Pattern
// Support multiple broker connections simultaneously
class BrokerManager {
private brokers = new Map<string, BrokerConnection>();
addBroker(id: string, broker: BrokerConnection): void {
this.brokers.set(id, broker);
}
getBroker(id: string): BrokerConnection {
const broker = this.brokers.get(id);
if (!broker) throw new Error(`Broker ${id} not found`);
return broker;
}
async getAggregatedPositions(): Promise<Position[]> {
const allPositions: Position[] = [];
for (const [id, broker] of this.brokers) {
const positions = await broker.getPositions();
allPositions.push(...positions.map((p) => ({ ...p, brokerId: id })));
}
return allPositions;
}
}Broker Plugin System for Electrobun
Replicates OpenAlgo's 29-broker plugin architecture in TypeScript.
Plugin Interface
Every broker implements this interface. Add a new broker by creating a new directory with these files.
// src/bun/broker/types.ts
export interface BrokerPlugin {
readonly name: string;
readonly displayName: string;
readonly authType: "oauth" | "totp" | "api_key";
readonly exchanges: string[];
// Authentication
getLoginUrl(apiKey: string, redirectUrl: string): string;
handleCallback(params: OAuthCallbackParams): Promise<AuthTokens>;
refreshToken?(tokens: AuthTokens): Promise<AuthTokens>;
revokeToken(tokens: AuthTokens): Promise<void>;
isTokenValid(tokens: AuthTokens): boolean;
// Orders
placeOrder(params: BrokerOrderParams, tokens: AuthTokens): Promise<BrokerOrderResult>;
modifyOrder(params: BrokerModifyParams, tokens: AuthTokens): Promise<BrokerModifyResult>;
cancelOrder(orderId: string, variety: string, tokens: AuthTokens): Promise<{ success: boolean }>;
getOrders(tokens: AuthTokens): Promise<BrokerOrder[]>;
getOrderHistory(orderId: string, tokens: AuthTokens): Promise<BrokerOrderUpdate[]>;
getTrades(tokens: AuthTokens): Promise<BrokerTrade[]>;
// Portfolio
getPositions(tokens: AuthTokens): Promise<BrokerPosition[]>;
getHoldings(tokens: AuthTokens): Promise<BrokerHolding[]>;
getFunds(tokens: AuthTokens): Promise<BrokerFunds>;
// Market Data
getQuote(symbol: string, exchange: string, tokens: AuthTokens): Promise<BrokerQuote>;
getMultiQuotes(symbols: SymbolExchange[], tokens: AuthTokens): Promise<Record<string, BrokerQuote>>;
getDepth(symbol: string, exchange: string, tokens: AuthTokens): Promise<BrokerDepth>;
getHistory(params: BrokerHistoryParams, tokens: AuthTokens): Promise<BrokerOHLC[]>;
// Options
getOptionChain?(symbol: string, expiry: string, tokens: AuthTokens): Promise<BrokerOptionChain>;
getExpiries?(symbol: string, exchange: string, tokens: AuthTokens): Promise<string[]>;
// Instruments
searchInstruments(query: string, tokens: AuthTokens): Promise<BrokerInstrument[]>;
downloadMasterContract?(exchange: string, tokens: AuthTokens): Promise<BrokerInstrument[]>;
// WebSocket
createWebSocketAdapter(tokens: AuthTokens): BrokerWebSocketAdapter;
// Symbol Mapping
toOpenAlgoSymbol(brokerSymbol: string, exchange: string): string;
toBrokerSymbol(oaSymbol: string, exchange: string): string;
}
export interface AuthTokens {
accessToken: string;
feedToken?: string;
userId?: string;
expiresAt?: number;
}
export interface OAuthCallbackParams {
requestToken?: string;
authCode?: string;
apiKey: string;
apiSecret: string;
}
export interface BrokerWebSocketAdapter {
connect(): Promise<void>;
disconnect(): void;
subscribe(tokens: number[], mode: "ltp" | "quote" | "depth"): void;
unsubscribe(tokens: number[]): void;
on(event: "tick", handler: (data: NormalizedTick) => void): void;
on(event: "order", handler: (data: BrokerOrder) => void): void;
on(event: "error", handler: (error: Error) => void): void;
on(event: "disconnect", handler: () => void): void;
on(event: "reconnect", handler: () => void): void;
isConnected(): boolean;
}
// Normalized tick format (all brokers map to this)
export interface NormalizedTick {
instrumentToken: number;
symbol: string;
exchange: string;
lastPrice: number;
open: number;
high: number;
low: number;
close: number;
change: number;
changePercent: number;
volume: number;
oi: number;
buyQuantity: number;
sellQuantity: number;
depth?: {
buy: { price: number; quantity: number; orders: number }[];
sell: { price: number; quantity: number; orders: number }[];
};
timestamp: number;
}Plugin Directory Structure
src/bun/broker/
types.ts # Shared interfaces
registry.ts # Plugin registry
symbol-mapper.ts # Cross-broker symbol mapping
zerodha/
index.ts # BrokerPlugin implementation
auth.ts # OAuth2 flow
orders.ts # Order API calls
data.ts # Market data API
funds.ts # Funds & portfolio
mapping.ts # Symbol transformation
websocket.ts # WebSocket adapter
master-contract.ts # Instrument download
angel/
index.ts
...
dhan/
index.ts
...
{26 more brokers}/Plugin Registry
// src/bun/broker/registry.ts
import type { BrokerPlugin } from "./types";
class BrokerRegistry {
private plugins = new Map<string, BrokerPlugin>();
register(plugin: BrokerPlugin): void {
this.plugins.set(plugin.name, plugin);
}
get(name: string): BrokerPlugin {
const plugin = this.plugins.get(name);
if (!plugin) throw new Error(`Broker '${name}' not registered`);
return plugin;
}
getAll(): BrokerPlugin[] {
return Array.from(this.plugins.values());
}
has(name: string): boolean {
return this.plugins.has(name);
}
getAuthType(name: string): "oauth" | "totp" | "api_key" {
return this.get(name).authType;
}
list(): { name: string; displayName: string; authType: string }[] {
return this.getAll().map((p) => ({
name: p.name,
displayName: p.displayName,
authType: p.authType,
}));
}
}
export const brokerRegistry = new BrokerRegistry();
// Auto-register all brokers on import
export async function loadBrokerPlugins(): Promise<void> {
// Dynamic imports — only loads brokers you need
const brokers = [
() => import("./zerodha"),
() => import("./angel"),
() => import("./dhan"),
() => import("./fyers"),
() => import("./upstox"),
// Add more as implemented
];
for (const load of brokers) {
try {
const mod = await load();
brokerRegistry.register(mod.default);
} catch (e) {
console.warn(`Failed to load broker plugin:`, e);
}
}
}Example: Zerodha Plugin
// src/bun/broker/zerodha/index.ts
import type { BrokerPlugin, AuthTokens, OAuthCallbackParams, BrokerOrderParams, BrokerOrderResult } from "../types";
import { ZerodhaAuth } from "./auth";
import { ZerodhaOrders } from "./orders";
import { ZerodhaData } from "./data";
import { ZerodhaFunds } from "./funds";
import { ZerodhaMapping } from "./mapping";
import { ZerodhaWebSocket } from "./websocket";
const zerodha: BrokerPlugin = {
name: "zerodha",
displayName: "Zerodha (Kite)",
authType: "oauth",
exchanges: ["NSE", "BSE", "NFO", "MCX", "BFO", "CDS"],
getLoginUrl: ZerodhaAuth.getLoginUrl,
handleCallback: ZerodhaAuth.handleCallback,
revokeToken: ZerodhaAuth.revokeToken,
isTokenValid: ZerodhaAuth.isTokenValid,
placeOrder: ZerodhaOrders.place,
modifyOrder: ZerodhaOrders.modify,
cancelOrder: ZerodhaOrders.cancel,
getOrders: ZerodhaOrders.getAll,
getOrderHistory: ZerodhaOrders.getHistory,
getTrades: ZerodhaOrders.getTrades,
getPositions: ZerodhaFunds.getPositions,
getHoldings: ZerodhaFunds.getHoldings,
getFunds: ZerodhaFunds.getFunds,
getQuote: ZerodhaData.getQuote,
getMultiQuotes: ZerodhaData.getMultiQuotes,
getDepth: ZerodhaData.getDepth,
getHistory: ZerodhaData.getHistory,
getOptionChain: ZerodhaData.getOptionChain,
getExpiries: ZerodhaData.getExpiries,
searchInstruments: ZerodhaData.searchInstruments,
downloadMasterContract: ZerodhaData.downloadMasterContract,
createWebSocketAdapter: (tokens) => new ZerodhaWebSocket(tokens),
toOpenAlgoSymbol: ZerodhaMapping.toOpenAlgo,
toBrokerSymbol: ZerodhaMapping.toBroker,
};
export default zerodha;// src/bun/broker/zerodha/auth.ts
import type { AuthTokens, OAuthCallbackParams } from "../types";
const BASE_URL = "https://api.kite.trade";
const LOGIN_URL = "https://kite.zerodha.com/connect/login";
export const ZerodhaAuth = {
getLoginUrl(apiKey: string, redirectUrl: string): string {
return `${LOGIN_URL}?v=3&api_key=${apiKey}&redirect_url=${encodeURIComponent(redirectUrl)}`;
},
async handleCallback(params: OAuthCallbackParams): Promise<AuthTokens> {
const checksum = new Bun.CryptoHasher("sha256")
.update(`${params.apiKey}${params.requestToken}${params.apiSecret}`)
.digest("hex");
const response = await fetch(`${BASE_URL}/session/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
api_key: params.apiKey,
request_token: params.requestToken!,
checksum,
}),
});
const data = await response.json();
if (data.status !== "success") throw new Error(data.message);
return {
accessToken: data.data.access_token,
userId: data.data.user_id,
};
},
async revokeToken(tokens: AuthTokens): Promise<void> {
await fetch(`${BASE_URL}/session/token`, {
method: "DELETE",
headers: { Authorization: `token ${tokens.accessToken}` },
});
},
isTokenValid(tokens: AuthTokens): boolean {
if (!tokens.accessToken) return false;
if (tokens.expiresAt && Date.now() > tokens.expiresAt) return false;
return true;
},
};// src/bun/broker/zerodha/mapping.ts
// OpenAlgo uses "SBIN-EQ", Zerodha uses "SBIN"
export const ZerodhaMapping = {
toBroker(oaSymbol: string, exchange: string): string {
// Remove -EQ suffix for NSE/BSE equity
if ((exchange === "NSE" || exchange === "BSE") && oaSymbol.endsWith("-EQ")) {
return oaSymbol.replace("-EQ", "");
}
return oaSymbol;
},
toOpenAlgo(brokerSymbol: string, exchange: string): string {
// Add -EQ suffix for NSE/BSE equity
if ((exchange === "NSE" || exchange === "BSE") && !brokerSymbol.includes("-")) {
return `${brokerSymbol}-EQ`;
}
return brokerSymbol;
},
// Map OpenAlgo product types to Zerodha
mapProduct(oaProduct: string): string {
const map: Record<string, string> = {
CNC: "CNC", MIS: "MIS", NRML: "NRML",
};
return map[oaProduct] || oaProduct;
},
// Map OpenAlgo order types to Zerodha
mapOrderType(oaType: string): string {
const map: Record<string, string> = {
MARKET: "MARKET", LIMIT: "LIMIT", SL: "SL", "SL-M": "SL-M",
};
return map[oaType] || oaType;
},
};Symbol Mapping Database
// src/bun/broker/symbol-mapper.ts
import { Database } from "bun:sqlite";
export class SymbolMapper {
private db: Database;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.db.run(`
CREATE TABLE IF NOT EXISTS master_contract (
broker TEXT NOT NULL,
broker_symbol TEXT NOT NULL,
oa_symbol TEXT NOT NULL,
exchange TEXT NOT NULL,
instrument_token INTEGER,
name TEXT,
lot_size INTEGER DEFAULT 1,
tick_size REAL DEFAULT 0.05,
instrument_type TEXT,
expiry TEXT,
strike REAL,
PRIMARY KEY (broker, exchange, broker_symbol)
)
`);
this.db.run("CREATE INDEX IF NOT EXISTS idx_oa_symbol ON master_contract(oa_symbol, exchange)");
this.db.run("CREATE INDEX IF NOT EXISTS idx_token ON master_contract(broker, instrument_token)");
}
async loadMasterContract(broker: string, instruments: BrokerInstrument[]): Promise<number> {
const insert = this.db.prepare(`
INSERT OR REPLACE INTO master_contract
(broker, broker_symbol, oa_symbol, exchange, instrument_token, name, lot_size, tick_size, instrument_type, expiry, strike)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const batch = this.db.transaction((items: BrokerInstrument[]) => {
for (const inst of items) {
insert.run(
broker, inst.tradingSymbol, inst.oaSymbol, inst.exchange,
inst.instrumentToken, inst.name, inst.lotSize, inst.tickSize,
inst.instrumentType, inst.expiry || null, inst.strike || null
);
}
});
batch(instruments);
return instruments.length;
}
getBrokerSymbol(broker: string, oaSymbol: string, exchange: string): string | null {
const row = this.db.query(
"SELECT broker_symbol FROM master_contract WHERE broker = ? AND oa_symbol = ? AND exchange = ?"
).get(broker, oaSymbol, exchange) as any;
return row?.broker_symbol ?? null;
}
getOASymbol(broker: string, brokerSymbol: string, exchange: string): string | null {
const row = this.db.query(
"SELECT oa_symbol FROM master_contract WHERE broker = ? AND broker_symbol = ? AND exchange = ?"
).get(broker, brokerSymbol, exchange) as any;
return row?.oa_symbol ?? null;
}
getInstrumentToken(broker: string, symbol: string, exchange: string): number | null {
const row = this.db.query(
"SELECT instrument_token FROM master_contract WHERE broker = ? AND oa_symbol = ? AND exchange = ?"
).get(broker, symbol, exchange) as any;
return row?.instrument_token ?? null;
}
search(query: string, limit = 20): any[] {
return this.db.query(
"SELECT * FROM master_contract WHERE name LIKE ? OR oa_symbol LIKE ? LIMIT ?"
).all(`%${query}%`, `%${query}%`, limit);
}
}Adding a New Broker
To add a new broker, create a directory and implement the BrokerPlugin interface:
src/bun/broker/newbroker/
index.ts # Export default BrokerPlugin
auth.ts # Authentication
orders.ts # Order operations
data.ts # Market data
funds.ts # Funds & portfolio
mapping.ts # Symbol transformation
websocket.ts # WebSocket adapterThen register in registry.ts:
const brokers = [
// ... existing
() => import("./newbroker"),
];MIT License
Copyright (c) 2026 marketcalls
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Monitoring, Logging & Health Checks
Covers traffic logging, latency monitoring, health status, structured logging, and system metrics for Electrobun trading apps.
Structured Logger
// src/bun/services/logger.ts
import { Database } from "bun:sqlite";
type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
interface LogEntry {
timestamp: number;
level: LogLevel;
context: string; // e.g., "order-manager", "market-data", "broker:zerodha"
message: string;
meta?: Record<string, any>;
}
export class Logger {
private db: Database;
private buffer: LogEntry[] = [];
private flushInterval: Timer;
private minLevel: LogLevel;
private onLog?: (entry: LogEntry) => void;
private static LEVELS: Record<LogLevel, number> = {
debug: 0, info: 1, warn: 2, error: 3, fatal: 4,
};
constructor(db: Database, opts?: { minLevel?: LogLevel; flushMs?: number; onLog?: (entry: LogEntry) => void }) {
this.db = db;
this.minLevel = opts?.minLevel ?? "info";
this.onLog = opts?.onLog;
db.run(`
CREATE TABLE IF NOT EXISTS app_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL,
level TEXT NOT NULL,
context TEXT NOT NULL,
message TEXT NOT NULL,
meta TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
`);
db.run("CREATE INDEX IF NOT EXISTS idx_logs_ts ON app_logs(timestamp)");
db.run("CREATE INDEX IF NOT EXISTS idx_logs_level ON app_logs(level)");
db.run("CREATE INDEX IF NOT EXISTS idx_logs_ctx ON app_logs(context)");
this.flushInterval = setInterval(() => this.flush(), opts?.flushMs ?? 2000);
}
private shouldLog(level: LogLevel): boolean {
return Logger.LEVELS[level] >= Logger.LEVELS[this.minLevel];
}
log(level: LogLevel, context: string, message: string, meta?: Record<string, any>): void {
if (!this.shouldLog(level)) return;
const entry: LogEntry = { timestamp: Date.now(), level, context, message, meta };
this.buffer.push(entry);
// Always print errors immediately
if (level === "error" || level === "fatal") {
console.error(`[${level.toUpperCase()}] [${context}] ${message}`, meta ?? "");
}
// Forward to webview callback
this.onLog?.(entry);
// Auto-flush if buffer is large
if (this.buffer.length >= 100) this.flush();
}
debug(ctx: string, msg: string, meta?: Record<string, any>) { this.log("debug", ctx, msg, meta); }
info(ctx: string, msg: string, meta?: Record<string, any>) { this.log("info", ctx, msg, meta); }
warn(ctx: string, msg: string, meta?: Record<string, any>) { this.log("warn", ctx, msg, meta); }
error(ctx: string, msg: string, meta?: Record<string, any>) { this.log("error", ctx, msg, meta); }
fatal(ctx: string, msg: string, meta?: Record<string, any>) { this.log("fatal", ctx, msg, meta); }
private flush(): void {
if (this.buffer.length === 0) return;
const insert = this.db.prepare(
"INSERT INTO app_logs (timestamp, level, context, message, meta) VALUES (?, ?, ?, ?, ?)"
);
const batch = this.db.transaction((entries: LogEntry[]) => {
for (const e of entries) {
insert.run(e.timestamp, e.level, e.context, e.message, e.meta ? JSON.stringify(e.meta) : null);
}
});
batch(this.buffer);
this.buffer = [];
}
query(opts: { level?: LogLevel; context?: string; from?: number; to?: number; limit?: number }): LogEntry[] {
const conditions: string[] = [];
const params: any[] = [];
if (opts.level) { conditions.push("level = ?"); params.push(opts.level); }
if (opts.context) { conditions.push("context = ?"); params.push(opts.context); }
if (opts.from) { conditions.push("timestamp >= ?"); params.push(opts.from); }
if (opts.to) { conditions.push("timestamp <= ?"); params.push(opts.to); }
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const limit = opts.limit ?? 100;
return this.db.query(
`SELECT timestamp, level, context, message, meta FROM app_logs ${where} ORDER BY timestamp DESC LIMIT ?`
).all(...params, limit) as LogEntry[];
}
prune(olderThanDays: number): number {
const cutoff = Date.now() - olderThanDays * 86400000;
const result = this.db.run("DELETE FROM app_logs WHERE timestamp < ?", [cutoff]);
return result.changes;
}
destroy(): void {
clearInterval(this.flushInterval);
this.flush();
}
}Traffic Logger (API Request/Response Tracking)
// src/bun/services/traffic-logger.ts
import { Database } from "bun:sqlite";
interface TrafficEntry {
id?: number;
timestamp: number;
direction: "outbound" | "inbound"; // outbound = to broker, inbound = from broker
broker: string;
endpoint: string;
method: string;
status: number;
latencyMs: number;
requestSize: number;
responseSize: number;
error?: string;
}
export class TrafficLogger {
private db: Database;
private insertStmt: ReturnType<Database["prepare"]>;
constructor(db: Database) {
this.db = db;
db.run(`
CREATE TABLE IF NOT EXISTS traffic_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL,
direction TEXT NOT NULL,
broker TEXT NOT NULL,
endpoint TEXT NOT NULL,
method TEXT NOT NULL,
status INTEGER NOT NULL,
latency_ms REAL NOT NULL,
request_size INTEGER DEFAULT 0,
response_size INTEGER DEFAULT 0,
error TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
`);
db.run("CREATE INDEX IF NOT EXISTS idx_traffic_ts ON traffic_log(timestamp)");
db.run("CREATE INDEX IF NOT EXISTS idx_traffic_broker ON traffic_log(broker)");
this.insertStmt = db.prepare(`
INSERT INTO traffic_log (timestamp, direction, broker, endpoint, method, status, latency_ms, request_size, response_size, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
}
log(entry: TrafficEntry): void {
this.insertStmt.run(
entry.timestamp, entry.direction, entry.broker, entry.endpoint,
entry.method, entry.status, entry.latencyMs,
entry.requestSize, entry.responseSize, entry.error ?? null
);
}
// Instrumented fetch wrapper
createInstrumentedFetch(broker: string) {
return async (url: string, init?: RequestInit): Promise<Response> => {
const start = performance.now();
const method = init?.method ?? "GET";
const requestSize = init?.body ? new Blob([init.body]).size : 0;
let status = 0;
let responseSize = 0;
let error: string | undefined;
try {
const response = await fetch(url, init);
status = response.status;
// Clone to read size without consuming body
const clone = response.clone();
const body = await clone.arrayBuffer();
responseSize = body.byteLength;
return response;
} catch (e: any) {
status = 0;
error = e.message;
throw e;
} finally {
const latencyMs = performance.now() - start;
const endpoint = new URL(url).pathname;
this.log({
timestamp: Date.now(), direction: "outbound", broker, endpoint,
method, status, latencyMs, requestSize, responseSize, error,
});
}
};
}
getRecent(limit = 50, offset = 0): TrafficEntry[] {
return this.db.query(
"SELECT * FROM traffic_log ORDER BY timestamp DESC LIMIT ? OFFSET ?"
).all(limit, offset) as TrafficEntry[];
}
getByBroker(broker: string, limit = 50): TrafficEntry[] {
return this.db.query(
"SELECT * FROM traffic_log WHERE broker = ? ORDER BY timestamp DESC LIMIT ?"
).all(broker, limit) as TrafficEntry[];
}
getErrors(limit = 50): TrafficEntry[] {
return this.db.query(
"SELECT * FROM traffic_log WHERE status = 0 OR status >= 400 ORDER BY timestamp DESC LIMIT ?"
).all(limit) as TrafficEntry[];
}
prune(olderThanDays: number): number {
const cutoff = Date.now() - olderThanDays * 86400000;
return this.db.run("DELETE FROM traffic_log WHERE timestamp < ?", [cutoff]).changes;
}
}Latency Monitor
// src/bun/services/latency-monitor.ts
interface LatencyBucket {
endpoint: string;
count: number;
totalMs: number;
minMs: number;
maxMs: number;
p50Ms: number;
p95Ms: number;
p99Ms: number;
errorCount: number;
}
export class LatencyMonitor {
private buckets = new Map<string, number[]>();
private errors = new Map<string, number>();
private windowMs: number;
private lastReset: number;
constructor(windowMs = 60000) {
this.windowMs = windowMs;
this.lastReset = Date.now();
}
record(endpoint: string, latencyMs: number, isError = false): void {
this.maybeReset();
if (!this.buckets.has(endpoint)) this.buckets.set(endpoint, []);
this.buckets.get(endpoint)!.push(latencyMs);
if (isError) {
this.errors.set(endpoint, (this.errors.get(endpoint) ?? 0) + 1);
}
}
private maybeReset(): void {
if (Date.now() - this.lastReset > this.windowMs) {
this.buckets.clear();
this.errors.clear();
this.lastReset = Date.now();
}
}
private percentile(sorted: number[], p: number): number {
if (sorted.length === 0) return 0;
const idx = Math.ceil((p / 100) * sorted.length) - 1;
return sorted[Math.max(0, idx)];
}
getStats(): LatencyBucket[] {
const stats: LatencyBucket[] = [];
for (const [endpoint, latencies] of this.buckets) {
const sorted = [...latencies].sort((a, b) => a - b);
stats.push({
endpoint,
count: sorted.length,
totalMs: sorted.reduce((a, b) => a + b, 0),
minMs: sorted[0] ?? 0,
maxMs: sorted[sorted.length - 1] ?? 0,
p50Ms: this.percentile(sorted, 50),
p95Ms: this.percentile(sorted, 95),
p99Ms: this.percentile(sorted, 99),
errorCount: this.errors.get(endpoint) ?? 0,
});
}
return stats.sort((a, b) => b.count - a.count);
}
getSummary(): { totalRequests: number; avgLatencyMs: number; errorRate: number } {
let totalRequests = 0;
let totalLatency = 0;
let totalErrors = 0;
for (const [, latencies] of this.buckets) {
totalRequests += latencies.length;
totalLatency += latencies.reduce((a, b) => a + b, 0);
}
for (const [, count] of this.errors) {
totalErrors += count;
}
return {
totalRequests,
avgLatencyMs: totalRequests > 0 ? totalLatency / totalRequests : 0,
errorRate: totalRequests > 0 ? totalErrors / totalRequests : 0,
};
}
}Health Check Service
// src/bun/services/health.ts
interface HealthStatus {
overall: "healthy" | "degraded" | "unhealthy";
uptime: number;
memory: { heapUsed: number; heapTotal: number; rss: number };
components: ComponentHealth[];
timestamp: number;
}
interface ComponentHealth {
name: string;
status: "up" | "down" | "degraded";
latencyMs?: number;
message?: string;
lastCheck: number;
}
export class HealthService {
private startTime = Date.now();
private checks = new Map<string, () => Promise<ComponentHealth>>();
registerCheck(name: string, check: () => Promise<ComponentHealth>): void {
this.checks.set(name, check);
}
async getStatus(): Promise<HealthStatus> {
const components: ComponentHealth[] = [];
for (const [name, check] of this.checks) {
try {
const result = await check();
components.push(result);
} catch (e: any) {
components.push({ name, status: "down", message: e.message, lastCheck: Date.now() });
}
}
const hasDown = components.some((c) => c.status === "down");
const hasDegraded = components.some((c) => c.status === "degraded");
return {
overall: hasDown ? "unhealthy" : hasDegraded ? "degraded" : "healthy",
uptime: Date.now() - this.startTime,
memory: process.memoryUsage(),
components,
timestamp: Date.now(),
};
}
}
// Register health checks for trading app components
export function setupHealthChecks(
health: HealthService,
broker: { isConnected(): boolean },
wsAdapter: { isConnected(): boolean },
db: { query(sql: string): any }
): void {
// Broker API connectivity
health.registerCheck("broker-api", async () => ({
name: "broker-api",
status: broker.isConnected() ? "up" : "down",
message: broker.isConnected() ? "Connected" : "Disconnected",
lastCheck: Date.now(),
}));
// WebSocket feed
health.registerCheck("market-feed", async () => ({
name: "market-feed",
status: wsAdapter.isConnected() ? "up" : "down",
message: wsAdapter.isConnected() ? "Streaming" : "Disconnected",
lastCheck: Date.now(),
}));
// Database
health.registerCheck("database", async () => {
const start = performance.now();
try {
db.query("SELECT 1").get();
return {
name: "database",
status: "up",
latencyMs: performance.now() - start,
lastCheck: Date.now(),
};
} catch (e: any) {
return { name: "database", status: "down", message: e.message, lastCheck: Date.now() };
}
});
// Memory pressure
health.registerCheck("memory", async () => {
const mem = process.memoryUsage();
const heapPercent = mem.heapUsed / mem.heapTotal;
return {
name: "memory",
status: heapPercent > 0.9 ? "degraded" : "up",
message: `${(heapPercent * 100).toFixed(1)}% heap used (${(mem.heapUsed / 1024 / 1024).toFixed(1)}MB)`,
lastCheck: Date.now(),
};
});
}System Metrics Collector
// src/bun/services/metrics.ts
interface SystemMetrics {
timestamp: number;
cpu: { user: number; system: number };
memory: { heapUsed: number; heapTotal: number; rss: number; external: number };
ticks: { perSecond: number; totalToday: number };
orders: { placedToday: number; filledToday: number; rejectedToday: number };
websocket: { connected: boolean; reconnectCount: number; lastMessageAge: number };
}
export class MetricsCollector {
private tickCount = 0;
private ticksPerSecond = 0;
private orderStats = { placed: 0, filled: 0, rejected: 0 };
private wsReconnects = 0;
private lastTickTime = 0;
private tickWindow: number[] = [];
recordTick(): void {
this.tickCount++;
const now = Date.now();
this.tickWindow.push(now);
// Keep only last 5 seconds
const cutoff = now - 5000;
while (this.tickWindow.length > 0 && this.tickWindow[0] < cutoff) {
this.tickWindow.shift();
}
this.ticksPerSecond = this.tickWindow.length / 5;
this.lastTickTime = now;
}
recordOrder(status: "placed" | "filled" | "rejected"): void {
this.orderStats[status]++;
}
recordReconnect(): void {
this.wsReconnects++;
}
getMetrics(wsConnected: boolean): SystemMetrics {
const mem = process.memoryUsage();
const cpu = process.cpuUsage();
return {
timestamp: Date.now(),
cpu: { user: cpu.user / 1000, system: cpu.system / 1000 }, // μs to ms
memory: {
heapUsed: mem.heapUsed,
heapTotal: mem.heapTotal,
rss: mem.rss,
external: mem.external,
},
ticks: { perSecond: Math.round(this.ticksPerSecond), totalToday: this.tickCount },
orders: { ...this.orderStats },
websocket: {
connected: wsConnected,
reconnectCount: this.wsReconnects,
lastMessageAge: this.lastTickTime > 0 ? Date.now() - this.lastTickTime : -1,
},
};
}
resetDaily(): void {
this.tickCount = 0;
this.orderStats = { placed: 0, filled: 0, rejected: 0 };
this.wsReconnects = 0;
}
}Wiring Monitoring to RPC
// In src/bun/index.ts — RPC handlers for monitoring
const logger = new Logger(db, {
minLevel: "info",
onLog: (entry) => {
// Forward logs to webview in real-time
mainWindow.webview.rpc?.send.logMessage({
level: entry.level,
message: `[${entry.context}] ${entry.message}`,
context: entry.context,
});
},
});
const trafficLogger = new TrafficLogger(db);
const latencyMonitor = new LatencyMonitor(60000);
const health = new HealthService();
const metrics = new MetricsCollector();
// RPC request handlers
getHealthStatus: async () => {
return health.getStatus();
},
getLatencyStats: async () => {
return {
stats: latencyMonitor.getStats(),
summary: latencyMonitor.getSummary(),
};
},
getTrafficLogs: async ({ page, limit }) => {
return trafficLogger.getRecent(limit, (page - 1) * limit);
},
getSystemMetrics: async () => {
return metrics.getMetrics(wsAdapter.isConnected());
},
getLogs: async ({ level, context, limit }) => {
return logger.query({ level, context, limit });
},
// Push periodic metrics to UI
setInterval(() => {
const m = metrics.getMetrics(wsAdapter.isConnected());
mainWindow.webview.rpc?.send.metricsUpdate(m);
}, 5000);
// Daily maintenance
function scheduleDailyMaintenance() {
const now = new Date();
const nextMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0);
const msUntilMidnight = nextMidnight.getTime() - now.getTime();
setTimeout(() => {
logger.prune(30); // Keep 30 days of logs
trafficLogger.prune(7); // Keep 7 days of traffic
metrics.resetDaily();
scheduleDailyMaintenance(); // Reschedule
}, msUntilMidnight);
}
scheduleDailyMaintenance();RPC Schema Additions
// Add to your RPC schema for monitoring
type MonitoringRPC = {
bun: RPCSchema<{
requests: {
getHealthStatus: { params: {}; response: HealthStatus };
getLatencyStats: { params: {}; response: { stats: LatencyBucket[]; summary: { totalRequests: number; avgLatencyMs: number; errorRate: number } } };
getTrafficLogs: { params: { page: number; limit: number }; response: TrafficEntry[] };
getSystemMetrics: { params: {}; response: SystemMetrics };
getLogs: { params: { level?: string; context?: string; limit?: number }; response: LogEntry[] };
};
}>;
webview: RPCSchema<{
messages: {
metricsUpdate: SystemMetrics;
logMessage: { level: string; message: string; context?: string };
};
}>;
};OpenAlgo → Electrobun Migration Guide
Architecture Mapping
OpenAlgo is a Python (Flask) + React web app. Converting to Electrobun means:
| OpenAlgo (Web) | Electrobun (Desktop) |
|---|---|
| Flask backend (Python) | Bun main process (TypeScript) |
| React SPA (Vite) | Electrobun views (HTML/CSS/TS) |
| Flask-SocketIO | RPC messages (bun → webview) |
| WebSocket proxy server (port 8765) | Bun WebSocket client (direct to broker) |
REST API (/api/v1/*) | RPC request handlers + optional local HTTP server |
| ZeroMQ message bus | In-process event emitter (single Bun process) |
| SQLAlchemy ORM + SQLite/PostgreSQL | bun:sqlite with prepared statements |
| DuckDB (historify) | bun:sqlite or Bun FFI to DuckDB |
| Flask sessions + cookies | In-memory state (desktop app, single user) |
| CSRF tokens | Not needed (no web-accessible endpoints) |
| Gunicorn + eventlet | Not needed (Bun handles concurrency) |
| React Router (74 pages) | Multi-view RPC or single-view with client-side routing |
| Zustand stores | Webview-side state (vanilla TS or lightweight store) |
| Broker Python HTTP (httpx) | Bun fetch() with same HTTP APIs |
| subprocess.Popen (strategies) | Bun.spawn() for strategy execution |
| APScheduler | setInterval / setTimeout or Bun cron patterns |
| Telegram bot (python-telegram-bot) | Bun fetch() to Telegram Bot API |
| ngrok tunnels | Not needed (desktop app, direct broker WebSocket) |
What Gets Simpler
1. No web server needed — Desktop app, no HTTP endpoints to protect 2. No CSRF/CORS — No browser security model concerns 3. No session management — Single user, in-memory state 4. No ZeroMQ — Single Bun process handles everything 5. No WebSocket proxy — Bun connects directly to broker WebSocket 6. No rate limiting on API — Local app, no external access (unless you expose a local API) 7. No deployment infrastructure — Ship as a .app/.exe with Electrobun Updater
What Gets Different
1. Broker auth tokens — Store encrypted in SQLite (Bun-side), not Flask sessions 2. Market data flow — Broker WebSocket → Bun process → RPC messages to webview 3. Strategy execution — Bun.spawn() instead of subprocess.Popen(), same isolation model 4. Multi-broker — Plugin modules in TypeScript instead of Python 5. Paper trading — Same logic, different language (TS instead of Python)
Project Structure Mapping
openalgo/ → trading-app/
├── app.py → src/bun/index.ts (app entry)
├── broker/ → src/bun/broker/ (plugin system)
│ ├── zerodha/ → src/bun/broker/zerodha/
│ │ ├── api/order_api.py → src/bun/broker/zerodha/orders.ts
│ │ ├── api/auth_api.py → src/bun/broker/zerodha/auth.ts
│ │ ├── api/data.py → src/bun/broker/zerodha/data.ts
│ │ ├── api/funds.py → src/bun/broker/zerodha/funds.ts
│ │ ├── mapping/transform.py → src/bun/broker/zerodha/mapping.ts
│ │ └── streaming/adapter.py → src/bun/broker/zerodha/websocket.ts
│ └── {28 more brokers} → src/bun/broker/{brokers}/
├── blueprints/ → (merged into RPC handlers)
│ ├── auth.py → src/bun/services/auth.ts
│ ├── python_strategy.py → src/bun/services/strategy-runner.ts
│ └── flow.py → src/bun/services/flow-engine.ts
├── database/ → src/bun/db/
│ ├── auth_db.py → src/bun/db/auth.ts
│ ├── user_db.py → src/bun/db/users.ts
│ └── apilog_db.py → src/bun/db/logs.ts
├── services/ → src/bun/services/
│ ├── place_order_service.py → src/bun/services/order-service.ts
│ ├── quotes_service.py → src/bun/services/market-data.ts
│ └── ...50+ services → src/bun/services/...
├── sandbox/ → src/bun/sandbox/
│ ├── execution_engine.py → src/bun/sandbox/engine.ts
│ ├── fund_manager.py → src/bun/sandbox/funds.ts
│ └── position_manager.py → src/bun/sandbox/positions.ts
├── websocket_proxy/ → (eliminated: Bun connects directly)
├── restx_api/ → src/bun/api/ (local HTTP server, optional)
├── frontend/src/ → src/mainview/ (Electrobun views)
│ ├── pages/ → (multi-view or SPA within single view)
│ ├── components/ → src/mainview/components/
│ ├── hooks/ → src/mainview/hooks/
│ ├── stores/ → src/mainview/stores/
│ └── api/ → (replaced by RPC calls)
└── .env → src/bun/config.ts (encrypted in SQLite)RPC Schema for Full OpenAlgo Feature Set
import type { RPCSchema } from "electrobun/bun";
type OpenAlgoRPC = {
bun: RPCSchema<{
requests: {
// --- Authentication ---
login: { params: { username: string; password: string }; response: { success: boolean; error?: string } };
logout: { params: {}; response: { success: boolean } };
getSessionState: { params: {}; response: { user: string | null; broker: string | null; apiKey: string | null } };
brokerLogin: { params: { broker: string; apiKey: string; apiSecret: string; totp?: string }; response: { success: boolean; redirectUrl?: string } };
handleBrokerCallback: { params: { broker: string; requestToken: string }; response: { success: boolean } };
generateApiKey: { params: {}; response: { apiKey: string } };
// --- Orders ---
placeOrder: { params: OrderParams; response: OrderResult };
placeSmartOrder: { params: SmartOrderParams; response: OrderResult };
modifyOrder: { params: ModifyParams; response: { success: boolean } };
cancelOrder: { params: { orderId: string }; response: { success: boolean } };
cancelAllOrders: { params: {}; response: { cancelled: number; failed: number } };
closePosition: { params: ClosePositionParams; response: { success: boolean } };
closeAllPositions: { params: {}; response: { closed: number; failed: number } };
basketOrder: { params: { orders: OrderParams[] }; response: OrderResult[] };
splitOrder: { params: SplitOrderParams; response: OrderResult[] };
// --- Market Data ---
getQuote: { params: { symbol: string; exchange: string }; response: QuoteData };
getMultiQuotes: { params: { symbols: { symbol: string; exchange: string }[] }; response: Record<string, QuoteData> };
getDepth: { params: { symbol: string; exchange: string }; response: DepthData };
getHistory: { params: HistoryParams; response: OHLC[] };
subscribeMarketData: { params: { symbols: string[]; mode: "ltp" | "quote" | "depth" }; response: { success: boolean } };
unsubscribeMarketData: { params: { symbols: string[] }; response: { success: boolean } };
// --- Options ---
getOptionChain: { params: { symbol: string; expiry: string }; response: OptionChainData };
getOptionGreeks: { params: OptionGreeksParams; response: GreeksData };
getExpiries: { params: { symbol: string; exchange: string }; response: string[] };
// --- Portfolio ---
getPositions: { params: {}; response: Position[] };
getHoldings: { params: {}; response: Holding[] };
getOrders: { params: {}; response: Order[] };
getTrades: { params: {}; response: Trade[] };
getFunds: { params: {}; response: FundsData };
getOrderHistory: { params: { orderId: string }; response: OrderUpdate[] };
// --- Instruments ---
searchInstruments: { params: { query: string }; response: Instrument[] };
downloadMasterContract: { params: { exchange: string }; response: { success: boolean; count: number } };
// --- Strategies ---
getStrategies: { params: {}; response: Strategy[] };
createStrategy: { params: StrategyConfig; response: { id: string } };
deleteStrategy: { params: { id: string }; response: { success: boolean } };
startStrategy: { params: { id: string }; response: { success: boolean } };
stopStrategy: { params: { id: string }; response: { success: boolean } };
getStrategyLogs: { params: { id: string; lines?: number }; response: string[] };
// --- Python Strategies ---
getPythonStrategies: { params: {}; response: PythonStrategy[] };
createPythonStrategy: { params: { name: string; code: string }; response: { id: string } };
updatePythonStrategy: { params: { id: string; code: string }; response: { success: boolean } };
runPythonStrategy: { params: { id: string }; response: { pid: number } };
stopPythonStrategy: { params: { id: string }; response: { success: boolean } };
schedulePythonStrategy: { params: { id: string; schedule: ScheduleConfig }; response: { success: boolean } };
// --- Flow Workflows ---
getFlows: { params: {}; response: Flow[] };
getFlow: { params: { id: string }; response: Flow };
saveFlow: { params: { id: string; nodes: FlowNode[]; edges: FlowEdge[] }; response: { success: boolean } };
executeFlow: { params: { id: string }; response: { success: boolean } };
deleteFlow: { params: { id: string }; response: { success: boolean } };
// --- Sandbox ---
toggleSandbox: { params: { enabled: boolean }; response: { success: boolean } };
getSandboxPositions: { params: {}; response: SandboxPosition[] };
getSandboxOrders: { params: {}; response: SandboxOrder[] };
getSandboxPnL: { params: {}; response: SandboxPnLData };
resetSandbox: { params: {}; response: { success: boolean } };
// --- Telegram ---
configureTelegram: { params: { botToken: string; chatId: string }; response: { success: boolean } };
testTelegram: { params: {}; response: { success: boolean } };
// --- Settings ---
getSettings: { params: {}; response: AppSettings };
updateSettings: { params: Partial<AppSettings>; response: { success: boolean } };
setOrderMode: { params: { mode: "auto" | "semi_auto" }; response: { success: boolean } };
// --- Action Center (semi-auto) ---
getPendingOrders: { params: {}; response: PendingOrder[] };
approveOrder: { params: { id: string }; response: OrderResult };
rejectOrder: { params: { id: string }; response: { success: boolean } };
// --- Monitoring ---
getHealthStatus: { params: {}; response: HealthData };
getTrafficLogs: { params: { page: number; limit: number }; response: TrafficLog[] };
getLatencyStats: { params: {}; response: LatencyStats };
// --- Market Calendar ---
getMarketHolidays: { params: { year?: number }; response: Holiday[] };
getMarketTimings: { params: {}; response: MarketTiming[] };
isMarketOpen: { params: {}; response: { open: boolean; nextOpen?: string } };
// --- Historify ---
getHistorifyConfig: { params: {}; response: HistorifyConfig };
startHistorify: { params: HistorifyConfig; response: { success: boolean } };
getHistorifyData: { params: { symbol: string; interval: string; from: string; to: string }; response: OHLC[] };
// --- Admin ---
getUsers: { params: {}; response: User[] };
getFreezeQuantities: { params: {}; response: FreezeQty[] };
updateFreezeQuantity: { params: FreezeQty; response: { success: boolean } };
// --- Export ---
exportTrades: { params: { from: string; to: string; format: "csv" | "json" }; response: { filePath: string } };
exportPnL: { params: { from: string; to: string }; response: { filePath: string } };
};
messages: {
logMessage: { level: string; message: string; context?: string };
};
}>;
webview: RPCSchema<{
requests: {};
messages: {
// Real-time market data
tickUpdate: TickData;
depthUpdate: { symbol: string; exchange: string; depth: DepthData };
// Order/trade events
orderEvent: { type: "placed" | "modified" | "cancelled" | "filled" | "rejected"; data: Order };
tradeEvent: Trade;
positionUpdate: Position[];
// Strategy events
strategyLog: { strategyId: string; message: string; level: string; timestamp: number };
strategyStatus: { strategyId: string; status: "running" | "stopped" | "error"; pid?: number };
// Flow events
flowExecutionLog: { flowId: string; nodeId: string; status: string; output: any };
// Sandbox events
sandboxOrderEvent: { type: string; data: SandboxOrder };
sandboxPositionUpdate: SandboxPosition[];
// System events
connectionStatus: { service: string; status: "connected" | "disconnected" | "reconnecting" };
masterContractProgress: { exchange: string; progress: number; total: number };
notification: { title: string; body: string; type: "info" | "success" | "warning" | "error"; category: string };
// Action center (semi-auto)
pendingOrderCreated: PendingOrder;
// Telegram
telegramMessage: { chatId: string; message: string };
};
}>;
};Migration Priority Order
Phase 1: Core Trading (MVP)
1. User auth (local, single user — simplified from web version) 2. Broker plugin system (start with 2-3 brokers) 3. Order placement, modification, cancellation 4. Positions, holdings, order book, trade book 5. Market data via broker WebSocket → RPC messages to UI 6. Basic dashboard, order entry, position views
Phase 2: Data & Analytics
7. SQLite database (orders, trades, audit log) 8. Historical data fetching and caching 9. Option chain with live data 10. Options analytics (Greeks, IV, OI) 11. Funds and margin display
Phase 3: Automation
12. Webhook strategy system 13. Python strategy execution via Bun.spawn() 14. Strategy scheduling 15. Flow workflow engine (visual node editor)
Phase 4: Advanced Features
16. Sandbox/paper trading mode 17. Action center (semi-auto order approval) 18. Telegram bot integration 19. Local HTTP API server for external tools 20. Market calendar (holidays, timings)
Phase 5: Polish & Distribution
21. System tray with P&L summary 22. Health monitoring 23. Auto-updater via Electrobun Updater 24. Code signing and notarization 25. Multi-platform builds (macOS, Windows, Linux)
Key Decisions for Electrobun Conversion
UI Approach: Multi-View vs Single View SPA
Option A: Single main view with client-side routing (recommended)
- One
BrowserWindowwith one view - Use a lightweight client-side router in the webview
- Simpler, matches OpenAlgo's existing React SPA model
- Fewer RPC schemas to manage
Option B: Multi-window with separate views
- Separate windows for dashboard, order book, charts
- Better for multi-monitor trading setups
- More complex RPC wiring (broadcast to all windows)
Recommendation: Start with Option A (single view), add Option B for power users later.
Broker Auth: OAuth in Sandbox Webview
// Open broker OAuth in a sandboxed window
function openBrokerLogin(broker: string, oauthUrl: string): Promise<string> {
return new Promise((resolve, reject) => {
const loginWindow = new BrowserWindow({
title: `Login - ${broker}`,
url: oauthUrl,
sandbox: true,
frame: { x: 200, y: 200, width: 500, height: 700 },
navigationRules: `https://*.${broker}.com/*,https://*.kite.trade/*,^*`,
});
loginWindow.webview.on("will-navigate", (e) => {
const url = new URL(e.data.detail);
// Check for OAuth callback with request_token
if (url.searchParams.has("request_token")) {
resolve(url.searchParams.get("request_token")!);
loginWindow.close();
}
});
loginWindow.on("close", () => reject(new Error("Login cancelled")));
});
}Environment Variables → Encrypted Config
OpenAlgo uses .env with 100+ variables. In Electrobun, store config encrypted in SQLite:
// First run: prompt user for broker credentials
// Store encrypted in SQLite
// No .env file needed — desktop app manages its own config
const config = new SecureConfig(`${Utils.paths.userData}/config.db`, masterKey);
config.set("broker.zerodha.api_key", apiKey);
config.set("broker.zerodha.api_secret", apiSecret);Options Analytics for Electrobun Trading Apps
Covers option chain display, Greeks calculation, IV charting, OI analysis, max pain, straddle charts, volatility surface, and GEX.
Option Chain with Live Data
// src/bun/services/option-chain.ts
import type { BrokerPlugin, AuthTokens } from "../broker/types";
interface OptionStrike {
strikePrice: number;
call: OptionContract | null;
put: OptionContract | null;
}
interface OptionContract {
symbol: string;
ltp: number;
change: number;
changePercent: number;
volume: number;
oi: number;
oiChange: number;
bidPrice: number;
askPrice: number;
bidQty: number;
askQty: number;
iv: number;
delta: number;
gamma: number;
theta: number;
vega: number;
}
export class OptionChainService {
constructor(private broker: BrokerPlugin, private tokens: AuthTokens) {}
async getChain(symbol: string, expiry: string): Promise<OptionStrike[]> {
const raw = await this.broker.getOptionChain!(symbol, expiry, this.tokens);
return this.buildStrikeTable(raw);
}
async getExpiries(symbol: string, exchange: string): Promise<string[]> {
return this.broker.getExpiries!(symbol, exchange, this.tokens);
}
private buildStrikeTable(raw: any): OptionStrike[] {
const strikes = new Map<number, OptionStrike>();
for (const contract of raw.contracts || []) {
const strike = contract.strikePrice;
if (!strikes.has(strike)) {
strikes.set(strike, { strikePrice: strike, call: null, put: null });
}
const entry = strikes.get(strike)!;
const option: OptionContract = {
symbol: contract.symbol,
ltp: contract.lastPrice,
change: contract.change,
changePercent: contract.changePercent,
volume: contract.volume,
oi: contract.oi,
oiChange: contract.oiChange || 0,
bidPrice: contract.bidPrice,
askPrice: contract.askPrice,
bidQty: contract.bidQty,
askQty: contract.askQty,
iv: contract.iv || 0,
delta: 0, gamma: 0, theta: 0, vega: 0,
};
if (contract.optionType === "CE") entry.call = option;
else entry.put = option;
}
return Array.from(strikes.values()).sort((a, b) => a.strikePrice - b.strikePrice);
}
}Greeks Calculation (Black-Scholes)
// src/bun/services/greeks.ts
// Standard normal CDF approximation (Abramowitz & Stegun)
function normalCDF(x: number): number {
const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
const sign = x < 0 ? -1 : 1;
x = Math.abs(x) / Math.SQRT2;
const t = 1.0 / (1.0 + p * x);
const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
return 0.5 * (1 + sign * y);
}
function normalPDF(x: number): number {
return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
}
interface GreeksInput {
spotPrice: number;
strikePrice: number;
timeToExpiry: number; // In years (days/365)
riskFreeRate: number; // e.g., 0.07 for 7%
volatility: number; // e.g., 0.20 for 20% IV
optionType: "CE" | "PE";
}
interface GreeksOutput {
price: number;
delta: number;
gamma: number;
theta: number;
vega: number;
rho: number;
iv?: number;
}
export function calculateGreeks(input: GreeksInput): GreeksOutput {
const { spotPrice: S, strikePrice: K, timeToExpiry: T, riskFreeRate: r, volatility: sigma, optionType } = input;
if (T <= 0) {
// Expired
const intrinsic = optionType === "CE"
? Math.max(S - K, 0)
: Math.max(K - S, 0);
return { price: intrinsic, delta: optionType === "CE" ? (S > K ? 1 : 0) : (S < K ? -1 : 0), gamma: 0, theta: 0, vega: 0, rho: 0 };
}
const sqrtT = Math.sqrt(T);
const d1 = (Math.log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrtT);
const d2 = d1 - sigma * sqrtT;
const Nd1 = normalCDF(d1);
const Nd2 = normalCDF(d2);
const nd1 = normalPDF(d1);
const expRT = Math.exp(-r * T);
let price: number, delta: number, rho: number;
if (optionType === "CE") {
price = S * Nd1 - K * expRT * Nd2;
delta = Nd1;
rho = K * T * expRT * Nd2 / 100;
} else {
price = K * expRT * normalCDF(-d2) - S * normalCDF(-d1);
delta = Nd1 - 1;
rho = -K * T * expRT * normalCDF(-d2) / 100;
}
const gamma = nd1 / (S * sigma * sqrtT);
const theta = -(S * nd1 * sigma) / (2 * sqrtT) - r * K * expRT * (optionType === "CE" ? Nd2 : normalCDF(-d2));
const vega = S * nd1 * sqrtT / 100;
return {
price,
delta,
gamma,
theta: theta / 365, // Per day
vega,
rho,
};
}
// Implied Volatility via Newton-Raphson
export function calculateIV(
marketPrice: number,
spotPrice: number,
strikePrice: number,
timeToExpiry: number,
riskFreeRate: number,
optionType: "CE" | "PE"
): number {
let sigma = 0.3; // Initial guess
const maxIterations = 100;
const tolerance = 0.0001;
for (let i = 0; i < maxIterations; i++) {
const result = calculateGreeks({
spotPrice, strikePrice, timeToExpiry, riskFreeRate,
volatility: sigma, optionType,
});
const diff = result.price - marketPrice;
if (Math.abs(diff) < tolerance) break;
const vega = result.vega * 100; // Undo the /100 in vega calc
if (Math.abs(vega) < 1e-10) break;
sigma -= diff / vega;
sigma = Math.max(0.01, Math.min(5.0, sigma)); // Clamp to reasonable range
}
return sigma;
}
// Batch Greeks for option chain
export function enrichChainWithGreeks(
chain: OptionStrike[],
spotPrice: number,
timeToExpiry: number,
riskFreeRate: number
): OptionStrike[] {
return chain.map((strike) => {
if (strike.call) {
const iv = strike.call.iv > 0 ? strike.call.iv / 100 : calculateIV(
strike.call.ltp, spotPrice, strike.strikePrice, timeToExpiry, riskFreeRate, "CE"
);
const greeks = calculateGreeks({
spotPrice, strikePrice: strike.strikePrice, timeToExpiry, riskFreeRate,
volatility: iv, optionType: "CE",
});
strike.call = { ...strike.call, iv: iv * 100, delta: greeks.delta, gamma: greeks.gamma, theta: greeks.theta, vega: greeks.vega };
}
if (strike.put) {
const iv = strike.put.iv > 0 ? strike.put.iv / 100 : calculateIV(
strike.put.ltp, spotPrice, strike.strikePrice, timeToExpiry, riskFreeRate, "PE"
);
const greeks = calculateGreeks({
spotPrice, strikePrice: strike.strikePrice, timeToExpiry, riskFreeRate,
volatility: iv, optionType: "PE",
});
strike.put = { ...strike.put, iv: iv * 100, delta: greeks.delta, gamma: greeks.gamma, theta: greeks.theta, vega: greeks.vega };
}
return strike;
});
}Max Pain Calculation
// Max pain = strike price where total loss for option writers is minimum
export function calculateMaxPain(chain: OptionStrike[]): { maxPainStrike: number; data: MaxPainData[] } {
const strikes = chain.map((s) => s.strikePrice);
const data: MaxPainData[] = [];
for (const testStrike of strikes) {
let callWriterLoss = 0;
let putWriterLoss = 0;
for (const strike of chain) {
// Call writers lose when spot > strike
if (testStrike > strike.strikePrice && strike.call) {
callWriterLoss += (testStrike - strike.strikePrice) * strike.call.oi;
}
// Put writers lose when spot < strike
if (testStrike < strike.strikePrice && strike.put) {
putWriterLoss += (strike.strikePrice - testStrike) * strike.put.oi;
}
}
data.push({
strike: testStrike,
callWriterLoss,
putWriterLoss,
totalLoss: callWriterLoss + putWriterLoss,
});
}
const minLoss = data.reduce((min, d) => d.totalLoss < min.totalLoss ? d : min, data[0]);
return { maxPainStrike: minLoss.strike, data };
}
interface MaxPainData {
strike: number;
callWriterLoss: number;
putWriterLoss: number;
totalLoss: number;
}Put-Call Ratio (PCR)
export function calculatePCR(chain: OptionStrike[]): { oiPCR: number; volumePCR: number } {
let totalPutOI = 0, totalCallOI = 0;
let totalPutVol = 0, totalCallVol = 0;
for (const strike of chain) {
if (strike.put) { totalPutOI += strike.put.oi; totalPutVol += strike.put.volume; }
if (strike.call) { totalCallOI += strike.call.oi; totalCallVol += strike.call.volume; }
}
return {
oiPCR: totalCallOI > 0 ? totalPutOI / totalCallOI : 0,
volumePCR: totalCallVol > 0 ? totalPutVol / totalCallVol : 0,
};
}GEX (Gamma Exposure)
// GEX estimates the aggregate gamma exposure of market makers
export function calculateGEX(chain: OptionStrike[], spotPrice: number): GEXData[] {
return chain.map((strike) => {
const callGamma = strike.call ? strike.call.gamma * strike.call.oi * 100 * spotPrice * spotPrice * 0.01 : 0;
const putGamma = strike.put ? strike.put.gamma * strike.put.oi * 100 * spotPrice * spotPrice * 0.01 : 0;
// Call OI = positive gamma (dealers hedging calls), Put OI = negative gamma
const netGEX = callGamma - putGamma;
return {
strike: strike.strikePrice,
callGEX: callGamma,
putGEX: -putGamma,
netGEX,
};
});
}
interface GEXData {
strike: number;
callGEX: number;
putGEX: number;
netGEX: number;
}Straddle/Strangle Pricing
export function calculateStraddle(chain: OptionStrike[], atmStrike: number): StraddleData | null {
const strike = chain.find((s) => s.strikePrice === atmStrike);
if (!strike || !strike.call || !strike.put) return null;
return {
strike: atmStrike,
callPrice: strike.call.ltp,
putPrice: strike.put.ltp,
straddlePrice: strike.call.ltp + strike.put.ltp,
upperBreakeven: atmStrike + strike.call.ltp + strike.put.ltp,
lowerBreakeven: atmStrike - (strike.call.ltp + strike.put.ltp),
totalIV: (strike.call.iv + strike.put.iv) / 2,
totalOI: strike.call.oi + strike.put.oi,
};
}
interface StraddleData {
strike: number;
callPrice: number;
putPrice: number;
straddlePrice: number;
upperBreakeven: number;
lowerBreakeven: number;
totalIV: number;
totalOI: number;
}Wiring Options to Webview
// In RPC handlers (src/bun/index.ts)
const optionService = new OptionChainService(broker, tokens);
// RPC request handlers:
getOptionChain: async ({ symbol, expiry }) => {
const chain = await optionService.getChain(symbol, expiry);
const spotLTP = await broker.getQuote(symbol, "NSE", tokens);
const daysToExpiry = Math.max(1, daysBetween(new Date(), new Date(expiry)));
const enriched = enrichChainWithGreeks(chain, spotLTP.lastPrice, daysToExpiry / 365, 0.07);
const maxPain = calculateMaxPain(enriched);
const pcr = calculatePCR(enriched);
const gex = calculateGEX(enriched, spotLTP.lastPrice);
return { chain: enriched, maxPain, pcr, gex, spotPrice: spotLTP.lastPrice };
},
getExpiries: async ({ symbol, exchange }) => {
return optionService.getExpiries(symbol, exchange);
},Electrobun Skill for Claude Code
A comprehensive Claude Code skill for building large-scale algo trading desktop applications with Electrobun.
What's Included
Core Electrobun Guides
| File | Description |
|---|---|
SKILL.md | Main skill entry point — project structure, RPC patterns, core rules |
api-reference.md | Complete Electrobun API reference (BrowserWindow, BrowserView, RPC, Tray, Menus, Utils, Events, etc.) |
architecture.md | Application architecture, multi-window patterns, service design, error handling |
broker-integration.md | Abstract broker interface, Zerodha/Kite implementation, order management, WebSocket binary parsing |
security.md | Encryption at rest, keychain integration, sandboxing, navigation rules, rate limiting, audit logging |
websockets-realtime.md | Market data streaming, reconnection, local API server, tick aggregation, connection monitoring |
storage.md | SQLite setup (WAL mode), schema migrations, prepared queries, OHLC caching, config management |
performance.md | Object pools, throttled broadcasting, ring buffers, virtual scrolling, memory monitoring |
OpenAlgo Migration & Advanced Features
| File | Description |
|---|---|
openalgo-migration.md | Full OpenAlgo → Electrobun migration guide with architecture mapping, RPC schema, 5-phase plan |
broker-plugin-system.md | 29-broker plugin architecture, plugin registry, symbol mapping database, Zerodha example |
options-analytics.md | Black-Scholes Greeks, implied volatility, max pain, put-call ratio, GEX, straddle pricing |
strategy-execution.md | Python strategy subprocess execution, visual flow engine, webhooks, action center, Telegram, market calendar |
sandbox-paper-trading.md | Paper trading engine, virtual capital management, margin simulation, live/sandbox order routing |
monitoring-logging.md | Structured logging, API traffic tracking, latency percentiles, health checks, system metrics |
Installation
As a project skill (recommended)
Clone into your project's .claude/skills/ directory:
cd your-electrobun-project
mkdir -p .claude/skills
git clone https://github.com/marketcalls/electrobun-skill.git .claude/skills/electrobunAs a personal skill (available in all projects)
mkdir -p ~/.claude/skills
git clone https://github.com/marketcalls/electrobun-skill.git ~/.claude/skills/electrobunUsage
Once installed, the skill is available in Claude Code:
- Auto-invocation — Claude automatically loads the skill when it detects you're building an Electrobun app
- Manual invocation — Type
/electrobunto explicitly load the skill context - With arguments — Type
/electrobun dashboardor/electrobun order-managerfor targeted guidance
What It Covers
Electrobun APIs
- BrowserWindow, BrowserView, RPC (defineRPC), Electroview
- ApplicationMenu, ContextMenu, Tray, GlobalShortcut
- Utils (file dialogs, clipboard, notifications, paths)
- Screen, Session, Updater, BuildConfig, Events
- Webview tags, draggable regions, navigation rules, sandboxing
- CLI commands, build configuration, bundling & distribution
Algo Trading Best Practices
- Architecture — Bun process for all trading logic, webview for UI only
- Security — AES-256-GCM encryption, TOTP/2FA, navigation lockdown, input validation
- Broker Integration — Abstract interface pattern, Zerodha/Kite implementation with binary WebSocket parsing
- Real-Time Data — WebSocket reconnection with state reconciliation, throttled UI updates
- Storage — SQLite with WAL mode, schema migrations, OHLC caching, audit trails
- Performance — Object pooling, ring buffers, typed arrays, virtual scrolling, batch DB writes
- Risk Management — Rate limiting, circuit breakers, position size limits, duplicate prevention
Requirements
- Claude Code CLI
- Bun runtime
- Electrobun framework
Resources
License
MIT