docs: generic integration patterns

This commit is contained in:
2026-05-24 01:12:17 -07:00
parent c3ad56cda8
commit 57a6aaa9ac
+211
View File
@@ -0,0 +1,211 @@
# 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:
```go
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`:
```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
1. **Create `pkg/provider/<venue_key>/`** with a package that
implements `provider.Provider` and any optional interfaces the
venue supports.
2. **Construct from env.** Use `pkg/provider/envconfig` so
secrets are sourced from `BROKER_<VENUE>_API_KEY`,
`BROKER_<VENUE>_API_SECRET`, etc. — never compiled in. In
production secrets are pulled from KMS into env at process
start.
3. **Register at boot.** In `cmd/brokerd`, register the venue
with `provider.Registry.Register(name, instance)` behind a
build tag or env-driven feature flag so deployments enable
only the venues they are licensed to use.
4. **Declare capabilities.** Call `router.SetCapability` with
asset classes, order types, regions, and min/max sizes so the
SOR can pre-filter.
5. **Declare fees.** Call `router.SetFees` with the venue's
maker/taker bps. The SOR will weight net price against them.
6. **Write a contract test** under `pkg/provider/<venue_key>/`
that drives the adapter against the venue's sandbox / paper
environment via `httptest` recordings. 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** (`MaxOrderValue` USD).
- **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:
```go
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.