EnginePolicies

Your first policy

Add a deterministic quantity guard and record its frame and execution decisions.

This walkthrough adds an L3 policy that rejects automated place/modify requests with an invalid quantity or a quantity greater than two. It observes frames without proposing orders. Explicit close and cancel requests are allowed by this policy.

1. Prepare a historical run

Use a short historical experiment with an existing entry strategy. Keep its instrument and execution settings fixed. You need an enabled L3 source and the injected modules.policies framework; no AI credentials or additional algorithms are needed for this example.

2. Paste this into L3

The following is a complete L3 script. Put it in the L3 editor, leaving your entry strategy in its own source field.

export const config = { enabled: true };

const stackKey = "quantityTutorial";
const maximumQuantity = 2;

const quantityPolicy = {
  id: "quantity-limit",
  description: "Limit the quantity of each place or modify request",
  intents: ["block"],

  async onFrame({ policyId }) {
    return {
      policyId,
      status: "watching",
      reason: "Waiting for an execution request",
    };
  },

  async onIntent({ policyId }, request) {
    if (request.source === "manual") {
      return { policyId, allowed: true, reason: "Manual request audited" };
    }
    if (!["place_order", "modify_order"].includes(request.kind)) {
      return { policyId, allowed: true, reason: "Outside quantity guard scope" };
    }

    const quantity = request.options?.order?.quantity;
    const allowed = typeof quantity === "number"
      && Number.isFinite(quantity)
      && quantity > 0
      && quantity <= maximumQuantity;

    return {
      policyId,
      allowed,
      reason: allowed ? "Quantity within limit" : "Invalid or excessive quantity",
      audit: { quantity: quantity ?? null, maximumQuantity },
    };
  },
};

const policies = [quantityPolicy];

export async function app(args) {
  const framework = args.modules?.policies;
  if (!framework) throw new Error("Policy framework is unavailable");

  const stack = framework.getPolicyStackState(args.context, stackKey);
  const results = await framework.runPolicyFrames(policies, args, stack);
  const final = await framework.resolveFrameActions(results, args);
  stack.lastFrameResults = results;
  stack.lastResolution = {
    results,
    final: framework.summarizeResolvedPolicyAction(final),
    resolvedAt: new Date().toISOString(),
  };

  if (final) {
    const executed = await framework.executeResolvedAction(final, args);
    stack.lastExecution = {
      executed,
      executedAt: new Date().toISOString(),
      final: framework.summarizeResolvedPolicyAction(final),
    };
  }
}

export async function intent(args) {
  const framework = args.modules?.policies;
  if (!framework) return false;

  const stack = framework.getPolicyStackState(args.context, stackKey);
  const checks = await framework.runPolicyIntentChecks(policies, args, stack);
  const allowed = framework.resolveIntentChecks(checks, args.intent);
  stack.lastIntentCheck = {
    allowed,
    checks,
    checkedAt: new Date().toISOString(),
    intent: framework.summarizeExecutionIntent(args.intent),
  };
  args.context.log?.("quantity-policy", stack.lastIntentCheck);
  return allowed;
}

The frame hook records watching, with no action intent. The module-level intent export runs the policy's onIntent when the engine checks an execution request. The shared runner and resolver remain useful when you add an action-producing policy later.

3. Check the outcome

Run your entry strategy with requests on either side of the limit. Inspect quantity-policy logs and context.l3State.quantityTutorial.

Ordinary automated requestPolicy result
Place or modify with quantity 1 or 2Allowed
Place or modify with quantity 3Rejected
Place or modify with missing, zero, negative, non-finite, or string quantityRejected
Explicit close or cancelAllowed by this policy

If there are no order requests, expect frame results but no lastIntentCheck. Permission also does not guarantee execution: automation controls, other policies, and the simulator or broker can affect the outcome.

Boundaries of this example

This guard checks individual request quantities, not accumulated exposure. Multiple allowed requests could exceed two units in total. A place_order request used to reduce a position is still a place request and receives the same quantity check.

The shared simulated close path delegates to order placement after its close-kind check. Its closing order can therefore be rejected by the quantity guard even though the explicit close_position check was allowed. See Working with L3.

Actions submitted with source l3 bypass the L3 intent hook. If you add a policy that submits orders itself, validate its quantity before returning its action intent. Manual requests follow the runtime's manual-override behavior. See Execution flow.

Next, learn the policy contract or turn this guard into a reusable module.

On this page