Qoc

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

Full interface — implement all methods for order-routing connectors
typescript
// 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

src/connectors/my-venue/index.ts
typescript
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

Point Qoc at your adapter file
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.