mirror of
https://github.com/luxfi/broker.git
synced 2026-09-24 00:25:18 +00:00
docs: generic integration patterns
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user