Custom Brokers
Broker execution no longer lives in Alice's main src/domain tree. It runs in
the separate UTA service, while the account-wizard contract lives in the shared
UTA protocol package.
That split is load-bearing:
- engine implementation:
services/uta/src/domain/trading/brokers/ - engine registry and factory:
services/uta/src/domain/trading/brokers/registry.tsandfactory.ts - user-facing preset catalog:
packages/uta-protocol/src/brokers/preset-catalog.ts - canonical broker types:
packages/uta-protocol/src/types/broker.ts
Adding a genuinely new broker engine normally touches both the service and the
shared package. Adding another exchange/account form backed by an existing
engine may need only a new preset plus tests. The CCXT family demonstrates the
many-presets-to-one-engine pattern: Binance, OKX, Bybit, Bitget, Hyperliquid,
and CCXT Custom all resolve through CcxtBroker.
1. Implement the engine
Create the implementation under the UTA service, for example:
services/uta/src/domain/trading/brokers/my-broker/
├── MyBroker.ts
├── MyBroker.spec.ts
└── index.ts
Implement the canonical IBroker contract from @traderalice/uta-protocol.
Do not copy the interface into your module; it evolves as contract search,
sub-accounts, historical bars, and broker capabilities grow.
The required core includes lifecycle, contract search, order operations, account/position/order reads, quotes, capabilities, and contract identity. Optional hooks cover features such as contract expansion, catalog refresh, sub-accounts, broad open-order observation, and historical bars.
import { z } from 'zod'
import type { IBroker } from '@traderalice/uta-protocol'
export class MyBroker implements IBroker {
static configSchema = z.object({
apiKey: z.string().min(1),
apiSecret: z.string().min(1),
sandbox: z.boolean().default(true),
})
static fromConfig(config: {
id: string
label?: string
brokerConfig: Record<string, unknown>
}): MyBroker {
const parsed = MyBroker.configSchema.parse(config.brokerConfig)
return new MyBroker({
id: config.id,
label: config.label ?? config.id,
...parsed,
})
}
// Implement the current IBroker contract from uta-protocol.
}
Use Decimal for quantities and calculations. Shared order and contract types
come from @traderalice/ibkr; UTA-specific types such as Position,
AccountInfo, Quote, and BrokerError come from
@traderalice/uta-protocol.
2. Register the engine
If this is a new engine family:
- Add its stable id to
BrokerEngineinpackages/uta-protocol/src/brokers/preset-catalog.ts. - Import the implementation in
services/uta/src/domain/trading/brokers/registry.ts. - Add a minimal
BROKER_ENGINE_REGISTRYentry:
'my-broker': {
configSchema: MyBroker.configSchema,
fromConfig: MyBroker.fromConfig,
},
The engine registry is intentionally not a UI catalog. It validates the
engine-shaped config and constructs an IBroker. Labels, password fields,
mode choices, categories, and account-card metadata belong to the preset.
If an existing engine can support the account type, skip this step and point the new preset at that engine.
3. Add the user-facing preset
Add a BrokerPresetDef to
packages/uta-protocol/src/brokers/preset-catalog.ts, then append it to
BROKER_PRESET_CATALOG in the intended UI order.
export const MY_BROKER_PRESET: BrokerPresetDef = {
id: 'my-broker',
label: 'My Broker',
description: 'Short account-picker description.',
category: 'recommended',
defaultName: 'my-broker-paper',
badge: 'MB',
badgeColor: 'text-blue-400',
engine: 'my-broker',
guardCategory: 'securities',
modes: [
{ id: 'paper', label: 'Paper Trading' },
{ id: 'live', label: 'Live Trading' },
],
zodSchema: z.object({
mode: z.enum(['paper', 'live']).default('paper').describe('Mode'),
apiKey: z.string().min(1).describe('API Key'),
apiSecret: z.string().min(1).describe('Secret Key'),
}),
subtitleFields: [{ field: 'mode', prefix: 'My Broker · ' }],
writeOnlyFields: ['apiKey', 'apiSecret'],
fingerprintFields: ['mode', 'apiKey'],
toEngineConfig: (data) => ({
apiKey: data.apiKey,
apiSecret: data.apiSecret,
sandbox: data.mode === 'paper',
}),
}
The preset is the persisted/user-facing contract:
| Field | Why it matters |
|---|---|
id | Stable value stored as UTAConfig.presetId; renaming it breaks existing derived UTA ids. |
zodSchema | Validates the form-shaped presetConfig; field descriptions drive the wizard. |
engine | Selects the internal implementation without serializing the engine class into account records. |
toEngineConfig | Translates user-facing modes/fields into the engine's internal config. |
writeOnlyFields | Keeps secrets out of normal account reads and renders password inputs. |
fingerprintFields | Defines physical account identity for deterministic UTA id derivation; exclude cosmetic fields. |
modes / isPaper | Describes live vs paper/demo/testnet behavior for the UI and E2E safety filters. |
guardCategory | Selects the crypto or securities guard family. |
Keep preset ids and fingerprint semantics stable after release. If one engine backs several account types, create several presets rather than leaking a generic engine config into the UI.
4. Test both halves
At minimum:
- add focused engine specs beside the implementation;
- add a minimal valid preset sample to
services/uta/src/domain/trading/brokers/presets.spec.ts; - verify the preset schema →
toEngineConfig→ engine schema round trip; - verify paper/demo/testnet classification without touching a live account;
- add contract, capability, and error-path tests for every supported feature.
Before calling a broker supported, run the applicable demo-account scenarios
from OpenAlice's docs/uta-live-testing.md. Unit tests cannot reproduce venue
amendment rules, partial fills, external orders, restart recovery, or the
quality of real broker error messages.
Financial precision and errors
Use decimal.js for quantity and financial math. Position monetary fields are
decimal strings; convert them deliberately and never introduce binary
floating-point into execution calculations.
Throw BrokerError with a current canonical code:
| Code | Behavior |
|---|---|
CONFIG, AUTH | Permanent; disables the account until configuration changes. |
CONNECTING | Transient readiness state; data is pending while connection/recovery continues. |
NETWORK, EXCHANGE, MARKET_CLOSED, UNKNOWN | Transient/runtime failure classes handled by UTA recovery and callers. |
Study AlpacaBroker for a straightforward REST implementation,
CcxtBroker for one engine behind several presets, and IbkrBroker for a
callback SDK bridged into the async interface.
Next Steps
- Unified Trading Account — Understand the service boundary your broker plugs into.
- Trading Setup — See preset configuration and account lifecycle.
- Orders & Execution — Match broker capabilities to execution behavior.
- Custom Guards — Add account-specific push-time constraints.