Custom brokers
Implement a venue connector adapter to integrate any trading venue into your Unified Trading Account.
A custom broker connector lets you plug any trading venue into Qoc's Unified Trading Account by implementing a typed adapter interface that handles quotes, positions, order placement, cancellation, and fill streaming.
The connector interface
Every connector — built-in or custom — must implement the ConnectorAdapter interface. The interface is intentionally narrow: Qoc only asks a connector for what it needs. You do not need to implement methods you don't use (for example, a cash-only connector can return an empty array from getPositions for options).
Connectors run inside the Qoc process (or container) and are loaded from the path specified in desk.toml. They are isolated from each other but share the UTA event bus for fill delivery.
ConnectorAdapter interface
// types supplied by the qoc SDK package
import type {
Quote,
Position,
Order,
OrderAck,
Fill,
ConnectorContext,
} from "@qoc-app/sdk";
export interface ConnectorAdapter {
/** Called once at startup; use to authenticate and subscribe to feeds. */
connect(ctx: ConnectorContext): Promise<void>;
/** Called on graceful shutdown; close websockets, flush buffers. */
disconnect(): Promise<void>;
/** Return a single best-bid/ask quote for the given symbol. */
getQuote(symbol: string): Promise<Quote>;
/** Return all open positions for this connector. */
getPositions(): Promise<Position[]>;
/** Return available buying power in USD. */
getBuyingPower(): Promise<number>;
/**
* Submit an order. Return an acknowledgement with the venue's order ID.
* Do NOT wait for a fill — emit fills via ctx.emitFill().
*/
placeOrder(order: Order): Promise<OrderAck>;
/** Cancel an open order by venue order ID. */
cancelOrder(venueOrderId: string): Promise<void>;
}Minimal adapter skeleton
import type {
ConnectorAdapter,
ConnectorContext,
Quote,
Position,
Order,
OrderAck,
} from "@qoc-app/sdk";
export class MyVenueConnector implements ConnectorAdapter {
private ctx!: ConnectorContext;
private ws!: WebSocket;
async connect(ctx: ConnectorContext): Promise<void> {
this.ctx = ctx;
const apiKey = ctx.secret("api_key");
// open websocket, authenticate, subscribe to fill stream
this.ws = new WebSocket("wss://api.my-venue.example/stream");
this.ws.onmessage = (msg) => this.handleMessage(msg);
ctx.logger.info("MyVenueConnector connected");
}
async disconnect(): Promise<void> {
this.ws?.close();
}
async getQuote(symbol: string): Promise<Quote> {
const resp = await fetch(
"https://api.my-venue.example/quote/" + symbol,
{ headers: { Authorization: "Bearer " + this.ctx.secret("api_key") } }
);
const data = await resp.json();
return { symbol, bid: data.bid, ask: data.ask, timestamp: data.ts };
}
async getPositions(): Promise<Position[]> {
// fetch and map to Position[]
return [];
}
async getBuyingPower(): Promise<number> {
// fetch buying power
return 0;
}
async placeOrder(order: Order): Promise<OrderAck> {
// submit order, return ack with venueOrderId
return { venueOrderId: "placeholder-id", status: "acknowledged" };
}
async cancelOrder(venueOrderId: string): Promise<void> {
// send cancel request
}
private handleMessage(msg: MessageEvent): void {
// parse fill events and emit them
const fill = parseFill(msg.data);
if (fill) this.ctx.emitFill(fill);
}
}
function parseFill(data: unknown) {
// venue-specific parse logic
return null;
}
export default MyVenueConnector;Registering the connector in desk.toml
[[connector]]
name = "my-venue"
type = "custom"
adapter = "./src/connectors/my-venue/index.ts"
enabled = true
[connector.auth]
api_key = { env = "MY_VENUE_API_KEY" }Use ctx.logger for structured logs
Log via ctx.logger.info / warn / error rather than console.log. Structured logs appear in qoc logs --connector <name> with the connector name tagged automatically, making debugging across multiple connectors much easier.
placeOrder must be non-blocking
placeOrder should return as soon as the venue acknowledges receipt — do not await the fill. Emit fills asynchronously via ctx.emitFill(). Blocking on the fill inside placeOrder will stall the entire guard-dispatch pipeline.