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
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
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
[[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
| Field | Type | Description |
|---|---|---|
| nav | number | Total NAV in USD at time of evaluation |
| cash | number | Total undeployed cash in USD |
| buying_power | number | Available buying power net of open orders |
| positions | Position[] | All open positions across all connectors |
| open_orders | Order[] | All approved but unfilled orders |
| realized_pnl_today | number | Realized 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.