8.6 KiB
brokerd — integration patterns
brokerd is a venue-agnostic execution engine. It accepts an
order intent (symbol + side + quantity), scores every venue that
can fill it on net price, runs the trade through a pre-trade risk
gate, and settles the resulting fill against a shared capital pool.
The same binary trades spot, derivatives, fixed income, and
on-chain assets — each surface is reached through a uniform venue
adapter and a uniform order request.
This document describes the architectural patterns. It does not
name individual counterparties; concrete adapters in
pkg/provider/<name>/ are pluggable backends behind the same
interface and are configured at deploy time.
1. Smart order router (SOR)
pkg/router is the front door. The request shape is intentionally
small:
RouteRequest{Symbol, Side, Qty}
The router consults its provider.Registry, asks every registered
adapter for a quote, and ranks the responses by net execution
price:
net_price(buy) = ask + (taker_fee_bps / 10_000) * ask
net_price(sell) = bid - (taker_fee_bps / 10_000) * bid
score = net_price // lower wins for buy, higher for sell
Fee schedules per adapter are registered with
router.SetFees(name, makerBps, takerBps). Capabilities (assets
supported, order types, min/max sizes, region restrictions) are
registered with router.SetCapability(*ProviderCapability).
FindBestProvider returns the winning venue; GetAllRoutes
returns the full ranked slate for transparency and audit.
For institutional flow, pkg/router/twap.go slices a parent
order into a time-weighted schedule across the same SOR. Order
splitting across two or more venues for a single parent order is
supported when one venue cannot fill the full quantity at the
posted top-of-book size.
2. Venue adapter abstraction
Every external trading venue — spot exchanges, OTC liquidity
providers, regulated US exchanges, international ECNs, on-chain
liquidity pools, fixed-income desks — implements one interface in
pkg/venue/venue.go:
type Venue interface {
Name() string
GetQuote(ctx, symbol) (*Quote, error)
PlaceOrder(ctx, *PlaceOrderRequest) (*Order, error)
CancelOrder(ctx, accountID, orderID) error
GetOrderBook(ctx, symbol, depth) (*OrderBook, error)
StreamQuotes(ctx, symbols) (<-chan *Quote, error)
}
Venue is the narrow surface the SOR, the market-data aggregator,
and the arbitrage detector consume. Capability beyond the narrow
surface — bracket orders, fractional shares, ACATS, journal
transfers, fixed-income trading, crypto deposits — is expressed
through the optional interfaces in pkg/provider/provider.go
(TradingExtended, JournalManager, ACATSManager,
FixedIncomeTrader, CryptoDataProvider, etc.). A venue
implements only the optional interfaces that apply to it; callers
type-assert before invoking.
Adding a new venue
- Create
pkg/provider/<venue_key>/with a package that implementsprovider.Providerand any optional interfaces the venue supports. - Construct from env. Use
pkg/provider/envconfigso secrets are sourced fromBROKER_<VENUE>_API_KEY,BROKER_<VENUE>_API_SECRET, etc. — never compiled in. In production secrets are pulled from KMS into env at process start. - Register at boot. In
cmd/brokerd, register the venue withprovider.Registry.Register(name, instance)behind a build tag or env-driven feature flag so deployments enable only the venues they are licensed to use. - Declare capabilities. Call
router.SetCapabilitywith asset classes, order types, regions, and min/max sizes so the SOR can pre-filter. - Declare fees. Call
router.SetFeeswith the venue's maker/taker bps. The SOR will weight net price against them. - Write a contract test under
pkg/provider/<venue_key>/that drives the adapter against the venue's sandbox / paper environment viahttptestrecordings. No live keys in CI.
The adapter is self-contained: the SOR, risk engine, settlement
pool, and market-data feed do not learn anything venue-specific —
they see only Venue, Quote, Order.
3. Risk engine — pre-trade gate
pkg/risk runs before any order reaches a venue adapter. It
enforces, in order:
- Symbol allow/block lists (global + per-account).
- Per-order notional ceiling (
MaxOrderValueUSD). - Open-order count ceiling per account.
- Position-value ceiling per account.
- Daily-volume ceiling per account (rolling 24h window).
- Rate limit per account (orders per minute).
- Provider whitelist per global config — only adapters explicitly allowed for an account/jurisdiction can be selected by the SOR.
- Cooldown after significant loss to dampen runaway algorithmic flow.
Account limits override global limits where set. Every decision
returns a typed reason on rejection (RejectReason) so the API
surface can return structured 4xx responses and the audit log can
attribute every block.
4. Settlement pool
pkg/settlement separates execution from funding. A user
can buy before their fiat funding settles by drawing on a shared
pool of pre-funded capital, sized by KYC tier:
| Tier | Default instant-buy ceiling (USD) |
|---|---|
| Basic | 250 |
| Standard | 5,000 |
| Enhanced | 25,000 |
| Institutional | 250,000 |
Lifecycle events (ach_initiated, ach_pending, ach_cleared,
ach_failed, margin_call, liquidated) drive the state
machine. The pool tracks outstanding prefunded credit per
account, reconciles against incoming fiat settlements, and emits
margin calls / liquidations when an account's outstanding draw
breaches its tier ceiling for too long. pkg/settlement/margin.go
holds the margin maintenance math; pkg/settlement/pool.go holds
the pool-capital accounting.
5. Market-data aggregation
pkg/marketdata builds an in-process consolidated book by
fanning every adapter's StreamQuotes output into one feed keyed
by symbol. Two derived structures are maintained continuously:
- Consolidated BBO — best bid and best ask across all
connected venues, with the source venue attributed at each
price level (
PriceLevel{Provider, Price, Size}). - Ticker cache — most-recent quote per symbol, fanned out to subscribers via per-symbol channels.
The SOR reads the consolidated book at decision time so it never
makes a routing decision on a stale single-venue quote.
pkg/marketdata/arbitrage.go runs a separate scan over the same
feed: when best_bid(venue_A) > best_ask(venue_B) + threshold_bps
for the same symbol, an ArbitrageOpportunity is emitted (sized
by the smaller of the two top-of-book sizes). The detector is an
observer only — it does not auto-trade; downstream policy decides.
6. Funding rails — deposit and withdraw
pkg/funding delegates fiat and on-chain movement to the
processor registry in hanzoai/commerce. The request shape is
uniform regardless of method:
DepositRequest{
AccountID, Provider, Amount /* cents */, Currency,
PaymentMethod, // card | bank_transfer | crypto
Token, // card nonce
TxHash, Chain, Address, // on-chain
}
WithdrawRequest{
AccountID, Provider, Amount, Currency,
PaymentMethod, // bank_transfer | crypto
RelationshipID, // bank linkage id
DestAddress, Chain, // on-chain
}
The processor registry resolves Provider to a concrete
payment-processor adapter (card networks, bank-rail processors,
on-chain custody / wallet services) and returns a typed result
including a settlement ETA. Funding events are pushed into the
same settlement pool that the execution path draws from — there
is one capital ledger, not two.
7. End-to-end request flow
client → API (pkg/api)
→ risk.Engine.Check() — pre-trade gate
→ router.FindBestProvider(symbol, side) — net-price ranking
→ venue.PlaceOrder() — winning adapter
→ settlement.Pool.Reserve() — capital draw
→ audit.Log() — content-addressed record
→ ws / webhook fan-out — fill notifications
Every layer is independently testable; every layer is independently swappable. New venues, new payment processors, new risk policies, new settlement tiers all plug into the same boundaries without touching the others.