Skip to content

Custom JS Types

JavaScript policies already receive the typed OpenPit SDK surface directly: Order, ExecutionReport, Context, PolicyReject, and the other callback types are regular JS/TS objects exposed by the binding. Intersect an SDK model with application fields and use it as a Policy type parameter; callbacks then receive a fresh clone of the submitted object with those fields preserved.

When to Use

Use the plain JS policy interface when:

  • you want a custom pre-trade or post-trade policy in JavaScript or TypeScript;
  • you need project-specific fields next to the standard Order / ExecutionReport fields;
  • you want callback typing directly from the package exports.

The standard fields remain validated by OpenPit. Host-specific fields stay opaque to the engine and are available only to custom policy code.

Building Blocks

The custom-type seam uses the ordinary JS package surface:

Piece Role
Order / ExecutionReport Validated SDK models that application fields can intersect or extend.
Policy<OrderModel, ExecutionReportModel> Keeps the application payload types visible in every applicable callback.
Engine.builder().preTrade(policy) Registers the typed policy directly; no adapter or separate client engine is required.

Use an SDK wrapper when the application wants model methods and setters. A plain object with the same standard groups is also accepted at the engine boundary, but an intersection with Order or ExecutionReport is the most direct way to type custom policy callbacks.

Example

Complete typed policy and engine

import { Engine } from "@openpit/engine";
import { Order } from "@openpit/engine/model";
import { TradeAmount } from "@openpit/engine/param";
import {
  type Context,
  type Policy,
  type PolicyPreTradeResult,
  type PolicyReject,
} from "@openpit/engine/pretrade";

type DeskOrder = Order & { deskId: string };

// Rejects SELL orders for this desk; accepts everything else. The callback
// reads both the validated OpenPit fields and application-owned metadata.
const sellGate: Policy<DeskOrder> = {
  name: "sell-gate",
  policyGroupId: 0,

  checkPreTradeStart(
    ctx: Context,
    order: DeskOrder,
  ): Iterable<PolicyReject> {
    void ctx;
    if (order.deskId === "cash-equities" && order.operation?.side === "SELL") {
      return [
        {
          code: "InvalidFieldValue",
          reason: "sells are disabled for this desk",
          details: "sell-gate",
          scope: "order",
        },
      ];
    }
    return [];
  },

  performPreTradeCheck(
    ctx: Context,
    order: DeskOrder,
  ): PolicyPreTradeResult | null {
    void ctx;
    void order;
    return null;
  },
};

const engine = Engine.builder().preTrade(sellGate).build();

const order: DeskOrder = Object.assign(new Order(), {
  deskId: "cash-equities",
});
order.operation = {
  underlyingAsset: "AAPL",
  settlementAsset: "USD",
  accountId: 99224416,
  side: "BUY",
  tradeAmount: TradeAmount.quantity("100"),
  price: "185.00",
};

const result = engine.executePreTrade(order);
if (result.ok) {
  // The policy accepted; finalize the reservation it produced.
  const reservation = result.reservation;
  if (reservation === undefined) {
    throw new Error("accepted execute result is missing its reservation");
  }
  reservation.commit();
} else {
  for (const reject of result.rejects) {
    console.log(
      `rejected by ${reject.policy} [${reject.code}]: ${reject.reason}`,
    );
  }
}

Lifecycle / Payload Contract

  • The application model must include the standard Order or ExecutionReport surface so the engine can validate it.
  • Policy<OrderModel, ExecutionReportModel> keeps host fields typed in every applicable callback. The default model parameters remain Order and ExecutionReport.
  • Each policy receives its own fresh clone, so one callback cannot mutate the object observed by a later policy.
  • Cloning preserves application-owned string keys, symbol keys, prototypes, cycles, and shared references. Standard SDK fields are normalized and validated independently.
  • Input payloads are borrowed for the duration of the call. Only lifecycle handles returned by the engine (Request, Reservation, and Mutation) are single-use and consumed by their terminal operation.

Threading

The WebAssembly engine is single-threaded in JS, so a custom policy runs synchronously on the caller's thread. There is no user-selectable sync mode or off-thread callback execution. Use one engine per worker or isolate when the application needs parallelism.

  • Policy API: full callback contract and rollback patterns
  • Getting Started: first engine construction and end-to-end flow
  • Policies: built-in controls and custom policy registration