Custom Brokers
Adding a new broker to OpenAlice requires three things: implement the IBroker interface, declare config schema and UI field descriptors, and register in the broker registry. Zero changes to the framework needed.
Preset wrapper. Brokers live at the engine layer (the
IBrokerimplementation + registry entry described below). The user-facing layer is a preset insrc/domain/trading/brokers/preset-catalog.tsthat wraps the broker with a Zod schema, an optional mode dropdown, password masking, and atoEngineConfig()translator. Once your broker is registered, add one preset entry so users can pick it from the new-account wizard. The CCXT engine demonstrates the pattern best — one broker class (CcxtBroker), many presets (okx,bybit,hyperliquid,bitget,ccxt-custom).
IBroker Interface
Every broker implements this interface:
interface IBroker {
readonly id: string // Unique account ID
readonly label: string // Display name
// Lifecycle
init(): Promise<void>
close(): Promise<void>
// Contract search
searchContracts(pattern: string): Promise<ContractDescription[]>
getContractDetails(query: Contract): Promise<ContractDetails | null>
// Trading
placeOrder(contract: Contract, order: Order, tpsl?: TpSlParams): Promise<PlaceOrderResult>
modifyOrder(orderId: string, changes: Partial<Order>): Promise<PlaceOrderResult>
cancelOrder(orderId: string, orderCancel?: OrderCancel): Promise<PlaceOrderResult>
closePosition(contract: Contract, quantity?: Decimal): Promise<PlaceOrderResult>
// Queries
getAccount(): Promise<AccountInfo>
getPositions(): Promise<Position[]>
getOrders(orderIds: string[]): Promise<OpenOrder[]>
getOrder(orderId: string): Promise<OpenOrder | null>
getQuote(contract: Contract): Promise<Quote>
getMarketClock(): Promise<MarketClock>
// Capabilities
getCapabilities(): AccountCapabilities
// Identity
getNativeKey(contract: Contract): string
resolveNativeKey(nativeKey: string): Contract
}
All types (Contract, Order, Execution, OrderState) come from @traderalice/ibkr — this is the single source of truth for the type system.
Config Schema & UI Fields
Each broker declares its config shape and how the Web UI should render the config form:
class MyBroker implements IBroker {
// Zod schema for the engine-level config — populated by the
// preset's toEngineConfig() from the user-facing presetConfig.
static configSchema = z.object({
apiKey: z.string(),
apiSecret: z.string(),
sandbox: z.boolean().default(false),
})
// UI field descriptors for dynamic form rendering
static configFields: BrokerConfigField[] = [
{ name: 'apiKey', type: 'password', label: 'API Key', required: true, sensitive: true },
{ name: 'apiSecret', type: 'password', label: 'API Secret', required: true, sensitive: true },
{ name: 'sandbox', type: 'boolean', label: 'Sandbox Mode', default: false },
]
// Factory: construct from AccountConfig
static fromConfig(config: AccountConfig): IBroker {
const parsed = MyBroker.configSchema.parse(config.brokerConfig)
return new MyBroker(config.id, config.label ?? config.id, parsed)
}
}
Register in the Broker Registry
Add one entry to src/domain/trading/brokers/registry.ts:
import { MyBroker } from './my-broker/MyBroker.js'
export const BROKER_REGISTRY: Record<string, BrokerRegistryEntry> = {
// ... existing brokers
'my-broker': {
configSchema: MyBroker.configSchema,
configFields: MyBroker.configFields,
fromConfig: MyBroker.fromConfig,
name: 'My Broker',
description: 'Description for the account management UI.',
badge: 'MB',
badgeColor: 'text-blue-400',
subtitleFields: [{ field: 'sandbox', label: 'Sandbox' }],
guardCategory: 'securities', // or 'crypto'
},
}
The BrokerRegistryEntry includes:
| Field | Description |
|---|---|
configSchema | Zod schema for validation |
configFields | UI field descriptors for dynamic form rendering |
fromConfig | Factory function from AccountConfig |
name | Display name |
description | Short description |
badge / badgeColor | 2-3 char badge for the UI |
subtitleFields | Fields shown in the account card subtitle |
guardCategory | "crypto" or "securities" — determines available guard types |
Financial Precision
Use decimal.js for all quantity calculations. The Order.totalQuantity field is a Decimal, and Position.quantity must also be a Decimal. Never use JavaScript floating-point for financial math.
Error Handling
Throw BrokerError with appropriate codes:
| Code | Permanent? | Description |
|---|---|---|
CONFIG | Yes | Invalid configuration |
AUTH | Yes | Authentication failure |
NETWORK | No | Network/timeout issues |
EXCHANGE | No | Exchange-level rejections |
MARKET_CLOSED | No | Market is closed |
UNKNOWN | No | Unclassified errors |
Permanent errors disable the account. Transient errors trigger auto-recovery with exponential backoff.
Reference
Study the existing implementations for patterns:
- AlpacaBroker — Simple REST API broker, good starting point
- CcxtBroker — Adapter pattern over the CCXT library
- IbkrBroker — Complex callback-based SDK bridged to Promise-based interface