Qoc

Custom guards

Write a custom pre-trade guard in TypeScript to enforce strategy-specific risk rules that run before every order is dispatched.


Custom guards let you encode strategy-specific pre-trade constraints — sector concentration, delta limits, correlation caps — as pure TypeScript functions that integrate seamlessly with Qoc's built-in guard pipeline.

Guard contract

A guard is a synchronous, pure function. It receives the proposed order and a read-only snapshot of the current UTA book. It must return either { result: 'allow' } or { result: 'block', reason: string }. It cannot perform I/O, mutate state, or call external services.

Guards that throw are treated as blocks with the error message as the reason, ensuring a failing guard never silently allows an order through.

GuardAdapter interface

Types from @qoc-app/sdk
typescript
import type { Order, BookSnapshot } from "@qoc-app/sdk";

export type GuardResult =
  | { result: "allow" }
  | { result: "block"; reason: string };

export interface GuardAdapter {
  /** Unique key — must match the key field in desk.toml */
  readonly key: string;
  /** One-sentence description shown in guard evaluation logs */
  readonly description: string;
  /** Pure evaluation function — no I/O, no side effects */
  evaluate(order: Order, book: BookSnapshot): GuardResult;
}

Example: max-sector-concentration guard

src/guards/max-sector-concentration.ts
typescript
import type { GuardAdapter, GuardResult, Order, BookSnapshot } from "@qoc-app/sdk";

interface Config {
  max_pct: number;
  sector_map: Record<string, string>; // symbol -> GICS sector
}

export class MaxSectorConcentrationGuard implements GuardAdapter {
  readonly key = "max-sector-concentration";
  readonly description =
    "Block any order that would push a single GICS sector above max_pct of NAV.";

  private readonly maxPct: number;
  private readonly sectorMap: Record<string, string>;

  constructor(config: Config) {
    this.maxPct = config.max_pct;
    this.sectorMap = config.sector_map;
  }

  evaluate(order: Order, book: BookSnapshot): GuardResult {
    const sector = this.sectorMap[order.symbol];
    if (!sector) {
      return { result: "allow" }; // unknown symbol -> pass through
    }

    // compute current sector exposure
    let sectorNotional = 0;
    for (const pos of book.positions) {
      if (this.sectorMap[pos.symbol] === sector) {
        sectorNotional += pos.quantity * pos.mark;
      }
    }

    // add the proposed order's notional
    const orderNotional = order.quantity * (order.limit_price ?? book.nav * 0.01);
    const postFillNotional = sectorNotional + orderNotional;
    const postFillPct = (postFillNotional / book.nav) * 100;

    if (postFillPct > this.maxPct) {
      return {
        result: "block",
        reason:
          "Post-fill " + sector + " sector exposure would be " +
          postFillPct.toFixed(1) + "% of NAV (limit " + this.maxPct + "%). " +
          "Reduce order size or trim existing " + sector + " positions first.",
      };
    }

    return { result: "allow" };
  }
}

export default MaxSectorConcentrationGuard;

Registering the guard in desk.toml

Custom guards sit in the [[guard]] array alongside built-in guards
toml
[[guard]]
key     = "max-sector-concentration"
adapter = "./src/guards/max-sector-concentration.ts"
enabled = true
max_pct = 30.0

  [guard.sector_map]
  AAPL  = "Information Technology"
  MSFT  = "Information Technology"
  NVDA  = "Information Technology"
  JPM   = "Financials"
  GS    = "Financials"
  XOM   = "Energy"

Guard ordering and the pipeline

Custom guards run in the position they occupy in the [[guard]] array. Place expensive guards after cheap structural ones (buying-power, market-hours) so they only run when basic checks pass.

The evaluate function receives a BookSnapshot that is frozen at the time of approval — it will not change during pipeline execution even if a concurrent fill arrives. This guarantees deterministic evaluation.

BookSnapshot fields available to guards

FieldTypeDescription
navnumberTotal NAV in USD at time of evaluation
cashnumberTotal undeployed cash in USD
buying_powernumberAvailable buying power net of open orders
positionsPosition[]All open positions across all connectors
open_ordersOrder[]All approved but unfilled orders
realized_pnl_todaynumberRealized P&L since midnight UTC

Guards must be synchronous

The evaluate method signature is synchronous. Any async operation (file read, HTTP call, database query) will cause a runtime error. Load external data in a constructor or a separate initialization step before guards are registered.

Test guards in isolation with qoc run

Run qoc run guard max-sector-concentration --order ./orders/proposed/<file>.toml to evaluate a single guard against a specific proposed order without running the full pipeline. The output shows the allow/block result and the book snapshot used.