---
title: "Non-Mutating Dry-Run"
description: "The normal pre-trade pipeline mutates by design. The start stage spends rate-limit budget on every call, even rejected ones. The main stage creates."
---

<!-- markdownlint-disable MD033 -->
# Non-Mutating Dry-Run

**Probe the pre-trade pipeline without changing any engine state.**

The normal pre-trade pipeline mutates by design. The start stage spends
rate-limit budget on every call, even rejected ones. The main stage creates
reservations and holds. There is no "undo path" that restores the engine to
its exact pre-call state: rollback cancels the reservation but the budget is
already spent.

A dry-run is a completely separate, explicitly non-mutating path in the engine.
It runs the same policy checks against the current engine state and reports
what the equivalent real call *would* produce - verdict, would-be lock, and
would-be account adjustments - while touching nothing. No rate-limit budget is
spent, no reservation or hold is applied, and no account block is recorded.
Repeating a dry-run is idempotent: a second call with the same order and state
produces the same answer.

## What a Dry-Run Returns

The engine returns an inert report object (not a transaction). There is nothing
to commit or roll back.

<!-- markdownlint-disable MD013 -->
| Accessor | Meaning |
| --- | --- |
| `is pass` | `true` when all stages would have admitted the order |
| `rejects` | The rejects the pipeline would have collected, or empty/nil |
| `lock` | The [`PreTradeLock`](Pre-Trade-Lock.md) the main stage would have produced |
| `account adjustments` | Per-asset `held`/`available` outcomes the main stage would have reported |
| `account block` | The account block a real call would have recorded in the engine's blocked-accounts registry |
<!-- markdownlint-enable MD013 -->

`lock` and `account adjustments` are empty when the start stage would have
rejected (the main stage never runs in that case, exactly as in the real
pipeline). `account block` is reported here but is **not** recorded in the
engine.

## Entry Points

Two entry points mirror the real pre-trade API:

- `start pre trade dry run` - runs only the start
  stage in dry-run mode. Use this when you want a fast admission check without
  computing a lock or adjustments.
- `execute pre trade dry run` - runs both stages in
  dry-run mode. Returns the full report including would-be lock and
  adjustments.

## Examples

### Read the Dry-Run Verdict

<details><summary>Go</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_wiki_test.go -->
```go
report, err := engine.ExecutePreTradeDryRun(order)
if err != nil {
  panic(err)
}
defer report.Close()

pass, err := report.IsPass()
if err != nil {
  panic(err)
}
if pass {
  fmt.Println("order would be admitted")
} else {
  rejects, err := report.Rejects()
  if err != nil {
    panic(err)
  }
  for _, r := range rejects {
    fmt.Printf(
      "would reject by %s [%d]: %s (%s)\n",
      r.Policy,
      r.Code,
      r.Reason,
      r.Details,
    )
  }
}
```

</details>

<details><summary>Python</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/python/tests/integration/test_examples_wiki.py -->
```python
report = engine.execute_pre_trade_dry_run(order=order)
if report.is_pass:
    print("order would be admitted")
else:
    for reject in report.rejects:
        print(
            f"would reject by {reject.policy} "
            f"[{reject.code}]: {reject.reason}: {reject.details}"
        )
```

</details>

<details><summary>JavaScript</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/dry_run.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { TradeAmount } from "@openpit/engine/param";
import { type OrderInit } from "@openpit/engine/model";
import { buildOrderValidation } from "@openpit/engine/pretrade/policies";

const engine = Engine.builder().builtin(buildOrderValidation()).build();
const order: OrderInit = {
  operation: {
    underlyingAsset: "AAPL",
    settlementAsset: "USD",
    accountId: 99_224_416n,
    side: "BUY",
    tradeAmount: TradeAmount.quantity("1"),
    price: "100",
  },
};

const report = engine.executePreTradeDryRun(order);
if (report.isPass) {
  console.log("order would be admitted");
} else {
  for (const reject of report.rejects) {
    console.log(
      `would reject by ${reject.policy} [${reject.code}]: ${reject.reason}: ${reject.details}`,
    );
  }
}
```

</details>

<details><summary>C++</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/non_mutating_dry_run_test.cpp -->
```cpp
const openpit::pretrade::DryRunReport report =
    engine.ExecutePreTradeDryRun(order);
if (report.Passed()) {
  std::cout << "order would be admitted\n";
} else {
  for (const openpit::pretrade::Reject& reject : report.Rejects()) {
    std::cout << "would reject by " << reject.policy << " ["
              << static_cast<int>(reject.code) << "]: " << reject.reason
              << " (" << reject.details << ")\n";
  }
}
```

</details>

<details><summary>Rust</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_wiki.rs -->
```rust
let report = engine.execute_pre_trade_dry_run(order);
if report.is_pass() {
    println!("order would be admitted");
} else {
    for reject in report.rejects().unwrap().iter() {
        eprintln!(
            "would reject by {} [{}]: {} ({})",
            reject.policy, reject.code, reject.reason, reject.details
        );
    }
}
```

</details>

### Use the Dry-Run Before a Real Call

A common pattern: probe first, then commit only when the probe passes.

<details><summary>Go</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_wiki_test.go -->
```go
// Probe without spending any budget or creating any reservation.
probe, err := engine.ExecutePreTradeDryRun(order)
if err != nil {
  panic(err)
}
defer probe.Close()

pass, err := probe.IsPass()
if err != nil {
  panic(err)
}
if !pass {
  return // would have been rejected - skip the real call
}

// The real call now runs with fresh state; the probe had no effect.
reservation, rejects, err := engine.ExecutePreTrade(order)
if err != nil {
  panic(err)
}
if rejects != nil {
  return
}
defer reservation.Close()
reservation.Commit()
```

</details>

<details><summary>Python</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/python/tests/integration/test_examples_wiki.py -->
```python
# Probe without spending any budget or creating any reservation.
probe = engine.execute_pre_trade_dry_run(order=order)
if not probe.is_pass:
    pass  # would have been rejected - skip the real call
else:
    # The real call now runs with fresh state; the probe had no effect.
    result = engine.execute_pre_trade(order=order)
    if result:
        result.reservation.commit()
```

</details>

<details><summary>JavaScript</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/dry_run.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { TradeAmount } from "@openpit/engine/param";
import { type OrderInit } from "@openpit/engine/model";
import {
  buildRateLimit,
  RateLimit,
  RateLimitBrokerBarrier,
} from "@openpit/engine/pretrade/policies";

const engine = Engine.builder()
  .builtin(
    buildRateLimit().brokerBarrier(
      new RateLimitBrokerBarrier(new RateLimit(1, 60_000)),
    ),
  )
  .build();
const order = (): OrderInit => ({
  operation: {
    underlyingAsset: "AAPL",
    settlementAsset: "USD",
    accountId: 99_224_416n,
    side: "BUY",
    tradeAmount: TradeAmount.quantity("1"),
    price: "100",
  },
});

// Probe without spending any budget or creating any reservation.
const probe = engine.executePreTradeDryRun(order());
if (!probe.isPass) {
  // would have been rejected - skip the real call
} else {
  // The real call now runs with fresh state; the probe had no effect.
  const result = engine.executePreTrade(order());
  if (result.ok) {
    const reservation = result.reservation;
    if (reservation === undefined) {
      throw new Error("accepted execute result is missing its reservation");
    }
    reservation.commit();
  }
}
```

</details>

<details><summary>C++</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/non_mutating_dry_run_test.cpp -->
```cpp
// Probe without spending any budget or creating any reservation.
const openpit::pretrade::DryRunReport probe =
    engine.ExecutePreTradeDryRun(order);
if (!probe.Passed()) {
  return;  // would have been rejected - skip the real call
}

// The real call now runs with fresh state; the probe had no effect.
openpit::pretrade::ExecuteResult result = engine.ExecutePreTrade(order);
if (result.Passed()) {
  result.reservation->Commit();
}
```

</details>

<details><summary>Rust</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_wiki.rs -->
```rust
// Probe without spending any budget or creating any reservation.
let probe = engine.execute_pre_trade_dry_run(order.clone());
if probe.is_pass() {
    // The real call now runs with fresh state; the probe had no effect.
    engine.execute_pre_trade(order)?.commit();
}
```

</details>

## Custom Policy Dry-Run Hooks

Side-effect-free policies get dry-run support for free. The engine calls the
dry-run hooks instead of the normal ones on the dry-run path; the default
implementations simply delegate to the normal hooks.

A policy whose normal hook produces an immediate side effect - spending budget,
applying a hold - **must** override the dry-run variant to be read-only.

<!-- markdownlint-disable MD013 -->
| Normal hook | Dry-run hook | Default |
| --- | --- | --- |
| `check pre trade start` | `check pre trade start dry run` | delegates to normal |
| `perform pre trade check` | `perform pre trade check dry run` | delegates to normal |
<!-- markdownlint-enable MD013 -->

Built-in policies with side effects already override their dry-run variants:
the rate-limit policy reads the window counter without spending budget; the
spot-funds policy computes the would-be reserve without applying any hold.

### Example: Read-Only Custom Start-Stage Hook

<details><summary>Go</summary>

The `DryRunPolicy` interface carries the two dry-run hooks. When a policy
satisfies `DryRunPolicy`, the engine uses the dry-run methods instead of the
normal ones for the dry-run path.

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_wiki_test.go -->
<!-- markdownlint-disable MD013 -->
```go
type MyCountingPolicy struct {
  count int
}

func (*MyCountingPolicy) Close()                              {}
func (*MyCountingPolicy) Name() string                       { return "MyCountingPolicy" }
func (*MyCountingPolicy) PolicyGroupID() model.PolicyGroupID { return model.DefaultPolicyGroupID }

func (p *MyCountingPolicy) CheckPreTradeStart(
  _ pretrade.Context,
  _ model.Order,
) []reject.Reject {
  p.count++ // side effect: spends the real counter budget
  return nil
}

func (*MyCountingPolicy) PerformPreTradeCheck(
  _ pretrade.Context,
  _ model.Order,
  _ tx.Mutations,
  _ pretrade.Result,
) []reject.Reject {
  return nil
}

func (*MyCountingPolicy) ApplyExecutionReport(
  _ pretrade.PostTradeContext,
  _ model.ExecutionReport,
  _ pretrade.PostTradeAdjustments,
  _ pretrade.PostTradePnls,
) []reject.AccountBlock {
  return nil
}

func (*MyCountingPolicy) ApplyAccountAdjustment(
  _ accountadjustment.Context,
  _ param.AccountID,
  _ model.AccountAdjustment,
  _ tx.Mutations,
  _ pretrade.AccountOutcomes,
) (pretrade.PolicyAccountAdjustmentResult, []reject.Reject) {
  return pretrade.PolicyAccountAdjustmentResult{}, nil
}

// CheckPreTradeStartDryRun is the read-only variant: no counter increment.
func (*MyCountingPolicy) CheckPreTradeStartDryRun(
  _ pretrade.Context,
  _ model.Order,
) []reject.Reject {
  return nil // read current state without spending the budget
}

// PerformPreTradeCheckDryRun is the read-only variant.
func (*MyCountingPolicy) PerformPreTradeCheckDryRun(
  _ pretrade.Context,
  _ model.Order,
  _ tx.Mutations,
  _ pretrade.Result,
) []reject.Reject {
  return nil
}
```
<!-- markdownlint-enable MD013 -->

</details>

<details><summary>Python</summary>

Defining `check_pre_trade_start_dry_run` or
`perform_pre_trade_check_dry_run` on the policy class opts that hook into
read-only mode on the dry-run path. Methods that are absent fall back to the
normal hooks.

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/python/tests/integration/test_examples_wiki.py -->
```python
class MyCountingPolicy(openpit.pretrade.Policy):
    def __init__(self) -> None:
        self._count = 0

    @property
    def name(self) -> str:
        return "MyCountingPolicy"

    def check_pre_trade_start(
        self,
        ctx: openpit.pretrade.Context,
        order: openpit.Order,
    ) -> openpit.pretrade.Rejects:
        self._count += 1  # side effect: spends the real counter budget
        return ()  # pass

    def check_pre_trade_start_dry_run(
        self,
        ctx: openpit.pretrade.Context,
        order: openpit.Order,
    ) -> openpit.pretrade.Rejects:
        return ()  # read-only: no increment
```

</details>

<details><summary>JavaScript</summary>

Define `checkPreTradeStartDryRun` or `performPreTradeCheckDryRun` to provide a
read-only variant of a stateful hook. When either method is absent, the JS
binding falls back to the corresponding normal hook.

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/dry_run.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { TradeAmount } from "@openpit/engine/param";
import { type OrderInit } from "@openpit/engine/model";
import { type Policy } from "@openpit/engine/pretrade";

const counterState: { acceptedOrders: number; limit: number } = {
  acceptedOrders: 0,
  limit: 10,
};

const readOnlyStartPolicy: Policy = {
  name: "ReadOnlyStartPolicy",

  checkPreTradeStart() {
    counterState.acceptedOrders += 1;
    return [];
  },

  checkPreTradeStartDryRun() {
    if (counterState.acceptedOrders >= counterState.limit) {
      return [
        {
          code: "RateLimitExceeded",
          reason: "counter budget exhausted",
          details: "the read-only start check observed a full counter",
          scope: "order",
        },
      ];
    }
    return [];
  },

  performPreTradeCheck() {
    return null;
  },
};

const engine = Engine.builder().preTrade(readOnlyStartPolicy).build();
const order: OrderInit = {
  operation: {
    underlyingAsset: "AAPL",
    settlementAsset: "USD",
    accountId: 99_224_416n,
    side: "BUY",
    tradeAmount: TradeAmount.quantity("1"),
    price: "100",
  },
};

const probe = engine.startPreTradeDryRun(order);
if (!probe.isPass || counterState.acceptedOrders !== 0) {
  throw new Error("the dry-run start hook must remain read-only");
}

const real = engine.executePreTrade(order);
if (!real.ok || Number(counterState.acceptedOrders) !== 1) {
  throw new Error("the real start hook must apply its side effect");
}
const reservation = real.reservation;
if (reservation === undefined) {
  throw new Error("accepted execute result is missing its reservation");
}
reservation.rollback();
```

</details>

<details><summary>C++</summary>

Define `CheckPreTradeStartDryRun` or `PerformPreTradeCheckDryRun` on the
handler type passed to `CustomPolicy<Handler>`. Hooks that are absent fall back
to their normal counterparts.

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/non_mutating_dry_run_test.cpp -->
```cpp
class MyCountingPolicy {
 public:
  explicit MyCountingPolicy(std::uint64_t* count) : m_count(count) {}

  [[nodiscard]] std::optional<openpit::pretrade::Reject> CheckPreTradeStart(
      const openpit::Order& order) const {
    static_cast<void>(order);
    ++*m_count;  // side effect: spends the real counter budget
    return std::nullopt;
  }

  void PerformPreTradeCheck(const openpit::pretrade::Context& context,
                            openpit::pretrade::PolicyDecision& decision) const {
    static_cast<void>(context);
    static_cast<void>(decision);
  }

  // CheckPreTradeStartDryRun is the read-only variant: no counter increment.
  [[nodiscard]] std::optional<openpit::pretrade::Reject>
  CheckPreTradeStartDryRun(const openpit::Order& order) const {
    static_cast<void>(order);
    return std::nullopt;  // read current state without spending the budget
  }

  // PerformPreTradeCheckDryRun is the read-only variant.
  void PerformPreTradeCheckDryRun(
      const openpit::pretrade::Context& context,
      openpit::pretrade::PolicyDecision& decision) const {
    static_cast<void>(context);
    static_cast<void>(decision);
  }

 private:
  std::uint64_t* m_count;
};
```

</details>

<details><summary>Rust</summary>

Override `check_pre_trade_start_dry_run` and/or
`perform_pre_trade_check_dry_run` on the `PreTradePolicy` trait. Methods that
are not overridden delegate to the normal hooks automatically.

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_wiki.rs -->
```rust
use openpit::pretrade::{
    PolicyPreTradeResult, PostTradeContext, PreTradeContext, PreTradePolicy, Rejects,
};
use openpit::Mutations;

struct MyCountingPolicy {
    count: std::cell::Cell<u64>,
}

impl<O, R, A, Sync> PreTradePolicy<O, R, A, Sync> for MyCountingPolicy
where
    Sync: openpit::SyncMode,
{
    fn name(&self) -> &str {
        "MyCountingPolicy"
    }

    fn check_pre_trade_start(
        &self,
        _ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
        _order: &O,
    ) -> Result<(), Rejects> {
        self.count.set(self.count.get() + 1); // side effect
        Ok(())
    }

    // Dry-run variant: read-only, no counter increment.
    fn check_pre_trade_start_dry_run(
        &self,
        _ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
        _order: &O,
    ) -> Result<(), Rejects> {
        Ok(()) // inspect state without spending budget
    }

    fn perform_pre_trade_check(
        &self,
        _ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
        _order: &O,
        _mutations: &mut Mutations,
    ) -> Result<Option<PolicyPreTradeResult>, Rejects> {
        Ok(None)
    }

    fn apply_execution_report(
        &self,
        _ctx: &PostTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
        _report: &R,
    ) -> Option<openpit::PostTradeResult> {
        None
    }
}
```

</details>

## Related Pages

- [Pre-trade Pipeline](Pre-trade-Pipeline.md): Lifecycle, result handling, and
  finalization rules
- [Policies](Policies.md): Built-in controls and custom policy hooks
- [Policy API](Policy-API.md): Custom policy interfaces and rollback patterns
- [Pre-Trade Lock](Pre-Trade-Lock.md): Persist the reservation lock and replay it
  on execution reports
- [Reject Codes](Reject-Codes.md): Standard business reject codes
