GitHubBlog

Search Documentation

Search for a page in the docs

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.ts and factory.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:

  1. Add its stable id to BrokerEngine in packages/uta-protocol/src/brokers/preset-catalog.ts.
  2. Import the implementation in services/uta/src/domain/trading/brokers/registry.ts.
  3. Add a minimal BROKER_ENGINE_REGISTRY entry:
'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:

FieldWhy it matters
idStable value stored as UTAConfig.presetId; renaming it breaks existing derived UTA ids.
zodSchemaValidates the form-shaped presetConfig; field descriptions drive the wizard.
engineSelects the internal implementation without serializing the engine class into account records.
toEngineConfigTranslates user-facing modes/fields into the engine's internal config.
writeOnlyFieldsKeeps secrets out of normal account reads and renders password inputs.
fingerprintFieldsDefines physical account identity for deterministic UTA id derivation; exclude cosmetic fields.
modes / isPaperDescribes live vs paper/demo/testnet behavior for the UI and E2E safety filters.
guardCategorySelects 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:

CodeBehavior
CONFIG, AUTHPermanent; disables the account until configuration changes.
CONNECTINGTransient readiness state; data is pending while connection/recovery continues.
NETWORK, EXCHANGE, MARKET_CLOSED, UNKNOWNTransient/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