---
title: "Dynamic Policy Reconfiguration"
description: "The engine owns synchronization. Ordinary setting lookups stay on the hot path, but a Spot Funds P&L barrier retune or account-group membership change is."
---

<!-- markdownlint-disable MD010 MD013 MD033 -->
# Dynamic Policy Reconfiguration

**Retune a policy while the engine is running, without rebuilding it.**

The engine owns synchronization. Ordinary setting lookups stay on the hot path,
but a Spot Funds P&L barrier retune or account-group membership change is a rare
global transition: concurrent account-state operations can wait across all
accounts until it completes.

A settings retune never resets live counters or accumulated state. A rate-limit
barrier key that survives a replacement keeps counting in the same window.
Rate-limit, order-size, and Spot Funds pricing, slippage, limit-mode, or
position-limit changes apply from the next order onward. Spot Funds P&L barrier
retunes are immediate: the same call sweeps the affected known accounts, and a
halted or breached account is blocked before configuration returns.
Force-setting a P&L accumulator is immediate too; it is the explicit call
covered in [Force-set Accumulated P&L](#force-set-accumulated-pl) below.

## How It Works

A built-in policy holds its runtime-tunable configuration in a settings cell.
Registering a built-in through the normal builder path auto-captures that cell,
keyed by the policy's name - there is no separate "register as configurable"
step. Updating through the engine and reading from the policy touch one shared
value: a published update is visible to the running policy on its next check.

## The Retune API

Obtain the configurator from the engine and call the typed method for the policy
you want to retune:

- **Go**: `engine.Configure().RateLimit(name, broker, assets, accounts,
  accountAssets)`.
- **Python**: `engine.configure().rate_limit(name, broker=...)`.
- **JavaScript**: `engine.configure().rateLimit(name, { broker: ... })`.
- **C++**: `engine.Configure().RateLimit(name, broker, assets, accounts,
  accountAssets)`.
- **Rust**: `engine.configure().rate_limit(name, |settings| ...)`.

The same surface exposes `pnl bounds killswitch`, `order size limit`, and
`spot funds` for the other built-ins.

Broker barriers have one extra distinction because the compatibility retune
calls use a missing value to mean "leave unchanged":

- **Go**: use `RateLimitUpdate` or `OrderSizeLimitUpdate` when the broker axis
  itself changes. `optional.None` leaves it unchanged,
  `optional.Some[*policies.RateLimitBrokerBarrier](nil)` clears the rate-limit
  broker barrier, and `optional.Some(&barrier)` replaces it. For order-size
  use the matching `optional.Some[*policies.OrderSizeBrokerBarrier](nil)`.
- **Python**: `broker=None` leaves the broker barrier unchanged; pass
  `clear_broker=True` to remove it. `broker` and `clear_broker=True` cannot be
  provided together.
- **JavaScript**: omit `broker` to leave it unchanged; pass
  `clearBroker: true` to remove it. `broker` and `clearBroker: true` cannot be
  provided together.
- **C++**: use `RateLimitBrokerBarrierUpdate::Unchanged()`, `Clear()`, or
  `Set(barrier)` for the rate-limit broker axis, and the matching
  `OrderSizeBrokerBarrierUpdate` type for order size. Vector axes use
  `std::nullopt` for unchanged and an engaged empty vector for clear.
- **Rust**: call `settings.set_broker(None)` inside the configure closure.

The singular C++ Spot Funds global P&L barrier follows the same pattern through
`SpotFundsPnlBoundsGlobalBarrierUpdate::Unchanged()`, `Clear()`, and
`Set(barrier)`. Stale `std::nullopt` and direct-barrier calls fail to compile
rather than silently changing meaning.

For the standalone P&L bounds kill switch, `initial pnl` is mandatory when an
account+asset barrier is constructed because it seeds P&L accumulated before
engine startup. Runtime barrier replacement has no `initial pnl` and never
re-seeds state. The replacement is applied to the current live accumulator on
the next policy check; if tightened bounds are already breached, subsequent
orders fail.

Spot Funds follows a different runtime contract. Its barriers carry a mandatory
currency and at least one bound, and never seed account state. For an account
with a known effective currency, a mismatching account-specific barrier blocks
the account. Mismatching account-group and global barriers are skipped; if no
fallback matches and there is no account barrier, the account has no control.
Without an effective currency, no level is filtered by currency and the first
in-scope barrier is effective. Bounds are plain
numbers in the barrier's own currency and are never FX-converted. The same
configuration call bounds its
candidate sweep by the barrier axes that changed. A global change sweeps
accounts known through the Spot Funds P&L ledger, holdings, explicit account
barriers, registered account-group membership, or explicit account currency.
An active in-flight holdings mutation also keeps its account in the holdings
candidate set after the provisional row is physically pruned while rollback
can still restore it. Finalized pruned history is excluded; this is not a
historical seen registry.
An account/group-only change sweeps the changed account targets and members of
the changed groups. Every candidate is then resolved through the cascade with
its own effective currency. A retune that changes only a barrier's currency is
an ordinary effective-barrier change. An account-specific mismatch blocks its
account; group/global barriers evaluate accounts they newly match, while
accounts they no longer match lose that fallback control without being
unblocked. Accounts without an effective currency still select the first
in-scope barrier and are evaluated when that barrier changes. An unset ledger is
evaluated as implicit zero without
storing or publishing an outcome. An entirely unseen future account is checked
on its first policy access. An already halted or out-of-bounds candidate gets
an individual block before the call returns. The retune result contains
per-account outcomes - an account plus its block - only for blocks newly
recorded by that call. It is not the current set of blocked accounts: an
account already blocked for an earlier cause is absent because the Engine
preserves its first cause. To set the live accumulator deliberately - instead
of retuning the bounds around it - use the explicit call described in
[Spot Funds runtime reconfiguration](Spot-Funds.md#runtime-reconfiguration).

Spot Funds P&L barrier retunes and account-group membership changes use one
global state-transition coordinator. While either rare administrative
operation waits for current state readers and publishes its transition, new
account-state writes pause for every account, not only the changed accounts.
The coordinator yields briefly and then waits in short sleeps; this global
pause is the intentional coordination cost of making the transition atomic.

**Warning:** direct currency writes on an account or account group are
unchecked and re-evaluate nothing. Stored PnL and cost basis accumulated under
a different effective currency lose their meaning; the SDK guarantees nothing
about them and does not convert, detect, or report the change. Barrier
selection itself stays deterministic: the cascade is re-resolved with the new
effective currency on the next policy access, and an existing block is neither
released nor re-recorded. Account-group membership changes remain the
exception: they resolve the effective currency on each side of the transition
and re-evaluate when the effective barrier changes or an unchanged account
barrier flips currency-match status. An otherwise unchanged group or global
barrier does not trigger another evaluation.

Spot Funds P&L bounds are optional. A plain Spot Funds policy starts with no
barriers and can receive its first barrier through the runtime configurator.
Every barrier carries a currency, so a policy can hold group or global barriers
that no current account matches; those are configuration without control until
an account resolves to that currency. An account barrier remains control for its
named account: a known-currency mismatch blocks rather than becoming inactive.
The global PATCH value uses each binding's existing tri-state container with
cardinality at most one; account-group and account axes use iterables. An
omitted axis is unchanged, a supplied value replaces the axis wholesale, and an
engaged empty iterable clears it. Clearing all three axes disables only bounds
checks and fail-closed account blocking. Account and position PnL calculation,
state, and publication continue. Only the dedicated construction-time Spot
Funds P&L builder requires at least one barrier. Clearing a barrier returns no
block for an account when the call records no new individual block - for
example because no fallback becomes effective, the state is inside that
fallback, or the account already has a stored block. A newly exposed fallback
is evaluated for candidates selected by the changed axes in the same call.
Clearing never releases an existing engine-level account block.

### Policy Names

The `name` selects which registered policy to retune. Each built-in registers
under a fixed name:

| Policy                      | Name                          |
|-----------------------------|-------------------------------|
| Rate limit                  | `RateLimitPolicy`             |
| Order-size limit            | `OrderSizeLimitPolicy`        |
| P&L bounds kill-switch      | `PnlBoundsKillSwitchPolicy`   |
| Spot funds                  | `SpotFundsPolicy`             |

### Error Handling

A retune fails without touching the live settings in four cases: the name is
unknown, the named policy has a different settings type than the call targets,
the new values fail validation, or configuration is re-entered from a policy
configuration callback (`NestedConfiguration`). Each binding reports those
four kinds through its idiomatic error surface:

- Go: `*configure.Error` and `configure.ErrorKind`.
- Python: `PolicyConfigureError` and `ConfigureErrorKind`.
- JS: `PolicyConfigureError` and `ConfigureErrorKind`.
- C++: `openpit::ConfigureError` and `openpit::ConfigureErrorKind`.
- Rust: `ConfigureError`.

## Retune a Built-in Policy

The example below registers a rate-limit policy with a generous broker limit,
admits three orders, then tightens the limit at runtime. The next order is
rejected against the new limit, proving the live policy reads the retuned value.

`order` is the AAPL/USD order built in [Getting Started](Getting-Started.md).

<details><summary>Go</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_wiki_test.go -->
```go
// Register the rate-limit policy through Builtin so the engine keeps a
// handle to its settings; built-in policies are configurable by name.
engine, err := openpit.NewEngineBuilder().
	NoSync().
	Builtin(
		policies.BuildRateLimit().BrokerBarrier(
			policies.RateLimitBrokerBarrier{
				Limit: policies.RateLimit{
					MaxOrders: 5,
					Window:    60 * time.Second,
				},
			},
		),
	).
	Build()
if err != nil {
	return err
}
defer engine.Stop()

// The generous limit of 5 admits the first three orders.
for i := 0; i < 3; i++ {
	reservation, rejects, err := engine.ExecutePreTrade(order)
	if err != nil {
		return err
	}
	if rejects != nil {
		return fmt.Errorf("unexpected rejects: %v", rejects)
	}
	reservation.CommitAndClose()
}

// Tighten the broker limit to 2 at runtime, without rebuilding the engine.
// Built-in policies register under their type name (policies.RateLimitPolicyName).
err = engine.Configure().RateLimit(
	policies.RateLimitPolicyName,
	&policies.RateLimitBrokerBarrier{
		Limit: policies.RateLimit{MaxOrders: 2, Window: 60 * time.Second},
	},
	nil,
	nil,
	nil,
)
if err != nil {
	return err
}

// The next order would have passed under the old limit of 5; the new limit
// of 2 rejects it, proving the live policy reads the retuned value.
_, rejects, err := engine.ExecutePreTrade(order)
if err != nil {
	return err
}
fmt.Println(rejects[0].Reason) // "rate limit exceeded: broker barrier"
```

</details>

<details><summary>Python</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/python/tests/integration/test_examples_wiki.py -->
```python
import datetime

import openpit
import openpit.pretrade.policies

# Register the rate-limit policy through builtin so the engine keeps a
# handle to its settings; built-in policies are configurable by name.
engine = (
    openpit.Engine.builder()
    .no_sync()
    .builtin(
        openpit.pretrade.policies.build_rate_limit().broker_barrier(
            openpit.pretrade.policies.RateLimitBrokerBarrier(
                limit=openpit.pretrade.policies.RateLimit(
                    max_orders=5,
                    window=datetime.timedelta(seconds=60),
                ),
            ),
        )
    )
    .build()
)

# The generous limit of 5 admits the first three orders.
for _ in range(3):
    execute_result = engine.execute_pre_trade(order=order)
    assert execute_result.ok
    execute_result.reservation.commit()

# Tighten the broker limit to 2 at runtime, without rebuilding the engine.
# Built-in policies register under their type name (RateLimitBuilder.NAME).
engine.configure().rate_limit(
    openpit.pretrade.policies.RateLimitBuilder.NAME,
    broker=openpit.pretrade.policies.RateLimitBrokerBarrier(
        limit=openpit.pretrade.policies.RateLimit(
            max_orders=2,
            window=datetime.timedelta(seconds=60),
        ),
    ),
)

# The next order would have passed under the old limit of 5; the new limit
# of 2 rejects it, proving the live policy reads the retuned value.
execute_result = engine.execute_pre_trade(order=order)
assert not execute_result.ok
assert execute_result.rejects[0].reason == "rate limit exceeded: broker barrier"
```

</details>

<details><summary>JavaScript</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/configure.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,
  RateLimitBuilder,
} from "@openpit/engine/pretrade/policies";

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

// Register the rate-limit policy through builtin so the engine keeps a
// handle to its settings; built-in policies are configurable by name.
const engine = Engine.builder()
  .builtin(
    buildRateLimit().brokerBarrier(
      new RateLimitBrokerBarrier(new RateLimit(5, 60_000)),
    ),
  )
  .build();

// The generous limit of 5 admits the first three orders.
for (let i = 0; i < 3; i += 1) {
  const result = engine.executePreTrade(order());
  if (!result.ok) {
    throw new Error("unexpected rejects");
  }
  const reservation = result.reservation;
  if (reservation === undefined) {
    throw new Error("accepted execute result is missing its reservation");
  }
  reservation.commit();
}

// Tighten the broker limit to 2 at runtime, without rebuilding the engine.
// Built-in policies register under their type name (RateLimitBuilder.NAME).
engine.configure().rateLimit(RateLimitBuilder.NAME, {
  broker: new RateLimitBrokerBarrier(new RateLimit(2, 60_000)),
});

// The next order would have passed under the old limit of 5; the new limit
// of 2 rejects it, proving the live policy reads the retuned value.
const rejected = engine.executePreTrade(order());
console.log(rejected.rejects[0]!.reason); // "rate limit exceeded: broker barrier"
```

</details>

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

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/dynamic_policy_reconfiguration_test.cpp -->
```cpp
namespace policies = openpit::pretrade::policies;

// Register the rate-limit policy through the builder so the engine keeps a
// handle to its settings; built-in policies are configurable by name.
openpit::EngineBuilder builder(openpit::SyncPolicy::None);
builder.Add(
    policies::RateLimitPolicy{}.BrokerBarrier(policies::RateLimitBrokerBarrier(
        policies::RateLimit(/*maxOrders=*/5,
                            /*windowNanoseconds=*/60'000'000'000))));
openpit::Engine engine = builder.Build();

// The generous limit of 5 admits the first three orders.
for (int i = 0; i < 3; ++i) {
  openpit::pretrade::ExecuteResult result = engine.ExecutePreTrade(order);
  if (result.Passed()) {
    result.reservation->Commit();
  }
}

// Tighten the broker limit to 2 at runtime, without rebuilding the engine.
// Built-in policies register under their type name (RateLimitPolicyName).
engine.Configure().RateLimit(
    policies::RateLimitPolicyName,
    policies::RateLimitBrokerBarrierUpdate::Set(
        policies::RateLimitBrokerBarrier(policies::RateLimit(
            /*maxOrders=*/2,
            /*windowNanoseconds=*/60'000'000'000))));

// The next order would have passed under the old limit of 5; the new limit
// of 2 rejects it, proving the live policy reads the retuned value.
const openpit::pretrade::ExecuteResult rejected =
    engine.ExecutePreTrade(order);
std::cout << rejected.rejects[0].reason << "\n";
```

</details>

<details><summary>Rust</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_wiki.rs -->
```rust
use std::time::Duration;

use openpit::pretrade::policies::{
    RateLimit,
    RateLimitBrokerBarrier,
    RateLimitPolicy,
    RateLimitPolicyError,
    RateLimitSettings,
};
use openpit::storage::NoLocking;
use openpit::{Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact};

type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;

// Register the rate-limit policy so the engine keeps a handle to its
// settings cell; built-in policies are configurable by name.
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = RateLimitPolicy::new(
    RateLimitSettings::new(
        Some(RateLimitBrokerBarrier {
            limit: RateLimit {
                max_orders: 5,
                window: Duration::from_secs(60),
            },
        }),
        [],
        [],
        [],
    )?,
    builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;

// The generous limit of 5 admits the first three orders. `order` is the
// AAPL/USD order built in Getting Started.
for _ in 0..3 {
    engine.execute_pre_trade(order())?.commit();
}

// Tighten the broker limit to 2 at runtime, without rebuilding the engine.
// Built-in policies register under their own name (RateLimitPolicy::NAME).
let name = RateLimitPolicy::<NoLocking>::NAME;
engine
    .configure()
    .rate_limit::<RateLimitPolicyError>(name, |settings| {
        settings.set_broker(Some(RateLimitBrokerBarrier {
            limit: RateLimit {
                max_orders: 2,
                window: Duration::from_secs(60),
            },
        }))
    })?;

// The next order would have passed under the old limit of 5; the new limit
// of 2 rejects it, proving the live policy reads the retuned value.
let rejects = engine.execute_pre_trade(order())
    .err()
    .expect("order beyond the tightened limit must be rejected");
assert_eq!(rejects[0].reason, "rate limit exceeded: broker barrier");
```

</details>

## Force-set Accumulated P&L

A bounds retune never moves booked P&L: it changes the thresholds and re-reads
the engine's current live accumulator. Sometimes you need the opposite - to
seed or correct the live accumulator itself, for example to reconcile against an
external ledger of record after a restart or a manual position transfer.

Force-setting the P&L is an absolute assignment (upsert) of the live accumulated
P&L for one `(account, settlement asset)` entry. It creates the entry if absent,
exactly as a construction-time seed would, and the new value is evaluated against
the live bounds on the next order.

One caveat follows from the kill-switch semantics. Forcing the accumulator past
a bound trips the kill switch, and a tripped kill switch latches an
engine-level account block. That block is a separate concern owned by the
engine; force-setting the accumulator back inside the bound does not clear it -
only an explicit admin unblock does. So this call can move an account into a
blocked state but never out of one.

The example below registers a kill-switch policy with a broker barrier whose
lower bound is -100 USD, admits an order while the account's P&L is 0, then
force-sets the account's accumulated P&L to -150 USD. The next order for that
account is rejected against the breached lower bound.

`order` is the AAPL/USD order built in [Getting Started](Getting-Started.md).

<details><summary>Go</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_wiki_test.go -->
```go
usd, err := param.NewAsset("USD")
lowerBound, err := param.NewPnlFromString("-100")

// Register the kill-switch policy through Builtin so the engine keeps a
// handle to its accumulator; built-in policies are configurable by name.
engine, err := openpit.NewEngineBuilder().
	NoSync().
	Builtin(
		policies.BuildPnlBoundsKillSwitch().BrokerBarriers(
			policies.PnlBoundsBrokerBarrier{
				SettlementAsset: usd,
				LowerBound:      optional.Some(lowerBound),
			},
		),
	).
	Build()
if err != nil {
	return err
}
defer engine.Stop()

// With no P&L history the order passes against the lower bound of -100.
reservation, rejects, err := engine.ExecutePreTrade(order)
if err != nil {
	return err
}
if rejects != nil {
	return fmt.Errorf("unexpected rejects: %v", rejects)
}
reservation.CommitAndClose()

// Force-set the account's accumulated P&L to -150 USD, below the bound.
// Built-in policies register under their type name (policies.PnlBoundsKillSwitchPolicyName).
forced, err := param.NewPnlFromString("-150")
if err != nil {
	return err
}
err = engine.Configure().SetAccountPnl(
	policies.PnlBoundsKillSwitchPolicyName,
	account,
	usd,
	forced,
)
if err != nil {
	return err
}

// The next order for that account breaches the lower bound and is rejected;
// the breach also latches an engine-level block on the account.
_, rejects, err = engine.ExecutePreTrade(order)
if err != nil {
	return err
}
fmt.Println(rejects[0].Reason) // "pnl kill switch triggered: broker barrier"
```

</details>

<details><summary>Python</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/python/tests/integration/test_examples_wiki.py -->
```python
import openpit
import openpit.pretrade.policies

# Register the kill-switch policy through builtin so the engine keeps a
# handle to its accumulator; built-in policies are configurable by name.
engine = (
    openpit.Engine.builder()
    .no_sync()
    .builtin(
        openpit.pretrade.policies.build_pnl_bounds_killswitch().broker_barriers(
            openpit.pretrade.policies.PnlBoundsBrokerBarrier(
                settlement_asset=openpit.param.Asset("USD"),
                lower_bound=openpit.param.Pnl(-100),
            ),
        )
    )
    .build()
)

# With no P&L history the order passes against the lower bound of -100.
execute_result = engine.execute_pre_trade(order=order)
assert execute_result.ok
execute_result.reservation.commit()

# Force-set the account's accumulated P&L to -150 USD, below the bound.
# Built-in policies register under their type name (PnlBoundsKillswitchBuilder.NAME).
engine.configure().set_account_pnl(
    openpit.pretrade.policies.PnlBoundsKillswitchBuilder.NAME,
    account=account,
    settlement_asset=openpit.param.Asset("USD"),
    pnl=openpit.param.Pnl(-150),
)

# The next order for that account breaches the lower bound and is rejected;
# the breach also latches an engine-level block on the account.
execute_result = engine.execute_pre_trade(order=order)
assert not execute_result.ok
assert execute_result.rejects[0].reason == "pnl kill switch triggered: broker barrier"
```

</details>

<details><summary>JavaScript</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/configure.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { TradeAmount } from "@openpit/engine/param";
import { type OrderInit } from "@openpit/engine/model";
import {
  buildPnlBoundsKillswitch,
  PnlBoundsBrokerBarrier,
  PnlBoundsKillswitchBuilder,
} from "@openpit/engine/pretrade/policies";

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

// Register the kill-switch policy through builtin so the engine keeps a
// handle to its accumulator; built-in policies are configurable by name.
const engine = Engine.builder()
  .builtin(
    buildPnlBoundsKillswitch().brokerBarriers([
      new PnlBoundsBrokerBarrier("USD", "-100", undefined),
    ]),
  )
  .build();

// With no P&L history the order passes against the lower bound of -100.
const first = engine.executePreTrade(order());
if (!first.ok) {
  throw new Error("unexpected rejects");
}
const firstReservation = first.reservation;
if (firstReservation === undefined) {
  throw new Error("accepted execute result is missing its reservation");
}
firstReservation.commit();

// Force-set the account's accumulated P&L to -150 USD, below the bound.
// Built-in policies register under their type name (PnlBoundsKillswitchBuilder.NAME).
engine.configure().setAccountPnl(PnlBoundsKillswitchBuilder.NAME, {
  account: accountId,
  settlementAsset: "USD",
  pnl: "-150",
});

// The next order for that account breaches the lower bound and is rejected;
// the breach also latches an engine-level block on the account.
const rejected = engine.startPreTrade(order());
console.log(rejected.rejects[0]!.reason); // "pnl kill switch triggered: broker barrier"
```

</details>

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

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/dynamic_policy_reconfiguration_test.cpp -->
```cpp
namespace policies = openpit::pretrade::policies;

const openpit::param::AccountId account =
    openpit::param::AccountId::FromUint64(99224416);

// Register the kill-switch policy through the builder so the engine keeps a
// handle to its accumulator; built-in policies are configurable by name.
openpit::EngineBuilder builder(openpit::SyncPolicy::None);
policies::PnlBoundsBrokerBarrier barrier{openpit::param::Asset("USD")};
barrier.lowerBound = openpit::param::Pnl::FromString("-100");
builder.Add(policies::PnlBoundsKillSwitchPolicy{}.BrokerBarrier(barrier));
openpit::Engine engine = builder.Build();

// With no P&L history the order passes against the lower bound of -100.
openpit::pretrade::ExecuteResult first = engine.ExecutePreTrade(order);
if (first.Passed()) {
  first.reservation->Commit();
}

// Force-set the account's accumulated P&L to -150 USD, below the bound.
// Built-in policies register under their type name
// (PnlBoundsKillSwitchPolicyName).
engine.Configure().SetAccountPnl(
    policies::PnlBoundsKillSwitchPolicyName, account,
    openpit::param::Asset("USD"),
    openpit::param::Pnl::FromString("-150"));

// The next order for that account breaches the lower bound and is rejected;
// the breach also latches an engine-level block on the account.
const openpit::pretrade::ExecuteResult rejected =
    engine.ExecutePreTrade(order);
std::cout << rejected.rejects[0].reason << "\n";
```

</details>

<details><summary>Rust</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_wiki.rs -->
```rust
use openpit::param::{Asset, Pnl};
use openpit::pretrade::policies::{
    PnlBoundsBrokerBarrier,
    PnlBoundsKillSwitchPolicy,
    PnlBoundsKillSwitchSettings,
};
use openpit::storage::NoLocking;
use openpit::{Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact};

type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;

// Register the kill-switch policy so the engine keeps a handle to its
// accumulator; built-in policies are configurable by name.
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = PnlBoundsKillSwitchPolicy::new(
    PnlBoundsKillSwitchSettings::new(
        [PnlBoundsBrokerBarrier {
            settlement_asset: Asset::new("USD")?,
            lower_bound: Some(Pnl::from_str("-100")?),
            upper_bound: None,
        }],
        [],
    )?,
    builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;

// With no P&L history the order passes against the lower bound of -100.
// `order` is the AAPL/USD order built in Getting Started.
engine.execute_pre_trade(order())?.commit();

// Force-set the account's accumulated P&L to -150 USD, below the bound.
// Built-in policies register under their own name (PnlBoundsKillSwitchPolicy::NAME).
let name = PnlBoundsKillSwitchPolicy::<NoLocking>::NAME;
engine
    .configure()
    .set_account_pnl(name, account, Asset::new("USD")?, Pnl::from_str("-150")?)?;

// The next order for that account breaches the lower bound and is rejected;
// the breach also latches an engine-level block on the account.
let rejects = engine.execute_pre_trade(order())
    .err()
    .expect("order beyond the breached bound must be rejected");
assert_eq!(rejects[0].reason, "pnl kill switch triggered: broker barrier");
```

</details>

## Spot Funds: Global Limit Mode

The global limit mode is the base tier of the three-tier enforcement cascade
described in [Spot Funds - Funds Limit Mode](Spot-Funds.md#funds-limit-mode).
Switching it at runtime takes effect on the next pre-trade check with no engine
rebuild.

The example below seeds 1 000 USD - not enough for a 10-unit AAPL buy at 200
(notional 2 000). In the default Enforce mode the order is rejected. Switching
to TrackOnly lets the same order through and records the reservation against
the deficit. Switching back to Enforce rejects again.

<details><summary>Go</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_wiki_test.go -->
```go
engine, err := openpit.NewEngineBuilder().
	NoSync().
	Builtin(policies.BuildSpotFunds()).
	Build()
if err != nil {
	return err
}
defer engine.Stop()

accountID := param.NewAccountIDFromUint64(99224416)
// Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
usd, _ := param.NewAsset("USD")
total, _ := param.NewPositionSizeFromString("1000")
seed, _ := model.NewAccountAdjustmentFromValues(model.AccountAdjustmentValues{
	BalanceOperation: optional.Some(
		model.NewAccountAdjustmentBalanceOperationFromValues(
			model.AccountAdjustmentBalanceOperationValues{Asset: optional.Some(usd)},
		),
	),
	Amount: optional.Some(
		model.NewAccountAdjustmentAmountFromValues(model.AccountAdjustmentAmountValues{
			Balance: optional.Some(param.NewAbsoluteAdjustmentAmount(total)),
		}),
	),
})
if _, err := engine.ApplyAccountAdjustment(
	accountID, []model.AccountAdjustment{seed},
); err != nil {
	return err
}

aapl, _ := param.NewAsset("AAPL")
price, _ := param.NewPriceFromString("200")
qty, _ := param.NewQuantityFromString("10")
order := model.NewOrder()
op := order.EnsureOperationView()
op.SetInstrument(param.NewInstrument(aapl, usd))
op.SetAccountID(accountID)
op.SetSide(param.SideBuy)
op.SetTradeAmount(param.NewQuantityTradeAmount(qty))
op.SetPrice(price)

// Default Enforce: 2 000 notional exceeds 1 000 available - rejected.
_, rejects, err := engine.ExecutePreTrade(order)
if err != nil {
	return err
}
fmt.Println(rejects[0].Reason) // "spot funds insufficient"

// Switch to TrackOnly: the same order now passes and reserves against deficit.
if err := engine.Configure().SpotFundsGlobalLimitMode(
	policies.SpotFundsPolicyName,
	policies.SpotFundsLimitModeTrackOnly,
); err != nil {
	return err
}
reservation, _, err := engine.ExecutePreTrade(order)
if err != nil {
	return err
}
reservation.CommitAndClose() // available: 1 000 - 2 000 = -1 000

// Restore Enforce: available is negative - still rejected.
if err := engine.Configure().SpotFundsGlobalLimitMode(
	policies.SpotFundsPolicyName,
	policies.SpotFundsLimitModeEnforce,
); err != nil {
	return err
}
_, rejects, err = engine.ExecutePreTrade(order)
if err != nil {
	return err
}
fmt.Println(rejects[0].Reason) // "spot funds insufficient"
```

</details>

<details><summary>Python</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/python/tests/integration/test_examples_wiki.py -->
```python
import openpit
import openpit.pretrade.policies

engine = (
    openpit.Engine.builder()
    .no_sync()
    .builtin(openpit.pretrade.policies.build_spot_funds())
    .build()
)

account_id = openpit.param.AccountId.from_int(99224416)
# Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
seed = openpit.AccountAdjustment(
    operation=openpit.AccountAdjustmentBalanceOperation(asset="USD"),
    amount=openpit.AccountAdjustmentAmount(
        balance=openpit.param.AdjustmentAmount.absolute(
            openpit.param.PositionSize(1000)
        )
    ),
)
engine.apply_account_adjustment(account_id=account_id, adjustments=[seed])

order = openpit.Order(
    operation=openpit.OrderOperation(
        instrument=openpit.Instrument("AAPL", "USD"),
        account_id=account_id,
        side=openpit.param.Side.BUY,
        trade_amount=openpit.param.TradeAmount.quantity("10"),
        price=openpit.param.Price("200"),
    ),
)

# Default Enforce: 2 000 notional exceeds 1 000 available - rejected.
result = engine.execute_pre_trade(order=order)
assert not result.ok
assert result.rejects[0].reason == "spot funds insufficient"

# Switch to TrackOnly: the same order now passes and reserves against deficit.
engine.configure().spot_funds(
    openpit.pretrade.policies.SpotFundsBuilder.NAME,
    global_limit_mode=openpit.pretrade.policies.SpotFundsLimitMode.TRACK_ONLY,
)
result = engine.execute_pre_trade(order=order)
assert result.ok
result.reservation.commit()  # available: 1 000 - 2 000 = -1 000

# Restore Enforce: available is negative - still rejected.
engine.configure().spot_funds(
    openpit.pretrade.policies.SpotFundsBuilder.NAME,
    global_limit_mode=openpit.pretrade.policies.SpotFundsLimitMode.ENFORCE,
)
result = engine.execute_pre_trade(order=order)
assert not result.ok
assert result.rejects[0].reason == "spot funds insufficient"
```

</details>

<details><summary>JavaScript</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/configure.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { TradeAmount } from "@openpit/engine/param";
import { type OrderInit } from "@openpit/engine/model";
import { AdjustmentAmount } from "@openpit/engine/param";
import {
  buildSpotFunds,
  SpotFundsBuilder,
  SpotFundsLimitMode,
} from "@openpit/engine/pretrade/policies";

const accountId = 99_224_416n;
const engine = Engine.builder().builtin(buildSpotFunds()).build();

// Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
engine.applyAccountAdjustment(accountId, [
  {
    operation: { asset: "USD" },
    amount: { balance: AdjustmentAmount.absolute("1000") },
  },
]);

const order = (): OrderInit => ({
  operation: {
    underlyingAsset: "AAPL",
    settlementAsset: "USD",
    accountId,
    side: "BUY",
    tradeAmount: TradeAmount.quantity("10"),
    price: "200",
  },
});

// Default Enforce: 2 000 notional exceeds 1 000 available - rejected.
let result = engine.executePreTrade(order());
console.log(result.rejects[0]!.reason); // "spot funds insufficient"

// Switch to TrackOnly: the same order now passes and reserves against deficit.
engine.configure().spotFunds(SpotFundsBuilder.NAME, {
  globalLimitMode: SpotFundsLimitMode.TrackOnly,
});
result = engine.executePreTrade(order());
if (!result.ok) {
  throw new Error("unexpected rejects");
}
const reservation = result.reservation;
if (reservation === undefined) {
  throw new Error("accepted execute result is missing its reservation");
}
reservation.commit(); // available: 1 000 - 2 000 = -1 000

// Restore Enforce: available is negative - still rejected.
engine.configure().spotFunds(SpotFundsBuilder.NAME, {
  globalLimitMode: SpotFundsLimitMode.Enforce,
});
result = engine.executePreTrade(order());
console.log(result.rejects[0]!.reason); // "spot funds insufficient"
```

</details>

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

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/dynamic_policy_reconfiguration_test.cpp -->
```cpp
namespace aa = openpit::accountadjustment;
namespace policies = openpit::pretrade::policies;
using openpit::param::AccountId;
using openpit::param::AdjustmentAmount;
using openpit::param::PositionSize;
using openpit::param::Price;
using openpit::param::Quantity;

openpit::EngineBuilder builder(openpit::SyncPolicy::None);
builder.Add(policies::SpotFundsPolicy{});
openpit::Engine engine = builder.Build();

const AccountId account = AccountId::FromUint64(99224416);
// Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
aa::AccountAdjustment seed;
aa::BalanceOperation balanceOp;
balanceOp.asset = ::openpit::param::Asset("USD");
seed.operation = aa::Operation::OfBalance(std::move(balanceOp));
aa::Amount seedAmount;
seedAmount.balance =
    AdjustmentAmount::Absolute(PositionSize::FromString("1000"));
seed.amount = std::move(seedAmount);

const openpit::AdjustmentResult seedResult =
    engine.ApplyAccountAdjustment(account,
                                  std::vector<aa::AccountAdjustment>{seed});
if (!seedResult.Passed()) {
  return;
}

openpit::model::Order order;
openpit::model::OrderOperation op;
op.instrument = openpit::model::Instrument(::openpit::param::Asset("AAPL"),
                                           ::openpit::param::Asset("USD"));
op.accountId = account;
op.side = openpit::model::Side::Buy;
op.tradeAmount =
    openpit::model::TradeAmount::OfQuantity(Quantity::FromString("10"));
op.price = Price::FromString("200");
order.operation = std::move(op);

// Default Enforce: 2 000 notional exceeds 1 000 available - rejected.
openpit::pretrade::ExecuteResult rejected = engine.ExecutePreTrade(order);
std::cout << rejected.rejects[0].reason << "\n";

// Switch to TrackOnly: the same order now passes and reserves against deficit.
engine.Configure().SpotFundsGlobalLimitMode(
    policies::SpotFundsPolicyName, policies::SpotFundsLimitMode::TrackOnly);
openpit::pretrade::ExecuteResult accepted = engine.ExecutePreTrade(order);
if (accepted.Passed()) {
  accepted.reservation->Commit();  // available: 1 000 - 2 000 = -1 000
}

// Restore Enforce: available is negative - still rejected.
engine.Configure().SpotFundsGlobalLimitMode(
    policies::SpotFundsPolicyName, policies::SpotFundsLimitMode::Enforce);
rejected = engine.ExecutePreTrade(order);
std::cout << rejected.rejects[0].reason << "\n";
```

</details>

<details><summary>Rust</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_wiki.rs -->
```rust
use openpit::param::{
    AccountId, AdjustmentAmount, Asset, PositionSize, Price, Quantity, Side, TradeAmount,
};
use openpit::pretrade::policies::{SpotFundsConfigError, SpotFundsPolicy, SpotFundsSettings};
use openpit::pretrade::SpotFundsLimitMode;
use openpit::{
    AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
    Engine, FullSync, Instrument, OrderOperation, SpotFundsMarketData, SpotFundsPricingSource,
    WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
    WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};

type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
    WithAccountAdjustmentBounds<
        WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
    >,
>;

let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
    SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
    None::<SpotFundsMarketData<FullSync>>,
    builder.storage_builder(),
);

let account = AccountId::from_u64(99224416);
// Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
let seed = WithAccountAdjustmentAmount {
    inner: WithAccountAdjustmentBounds {
        inner: WithAccountAdjustmentBalanceOperation {
            inner: AccountAdjustmentAmount::default(),
            operation: AccountAdjustmentBalanceOperation {
                asset: Asset::new("USD")?,
                average_entry_price: None,
            },
        },
        bounds: AccountAdjustmentBounds::default(),
    },
    amount: AccountAdjustmentAmount {
        balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str("1000")?)),
        held: None,
        incoming: None,
    },
};
engine.apply_account_adjustment(account, &[seed])?;

let order = OrderOperation {
    instrument: Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?),
    account_id: account,
    side: Side::Buy,
    trade_amount: TradeAmount::Quantity(Quantity::from_str("10")?),
    price: Some(Price::from_str("200")?),
};

// Default Enforce: 2 000 notional exceeds 1 000 available - rejected.
let rejects = engine
    .execute_pre_trade(order.clone())
    .err()
    .expect("enforce must reject insufficient funds");
assert_eq!(rejects[0].reason, "spot funds insufficient");

// Switch to TrackOnly: the same order now passes and reserves against deficit.
let name = SpotFundsPolicy::<FullSync, FullSync>::NAME;
engine
    .configure()
    .spot_funds::<SpotFundsConfigError>(name, |settings| {
        settings.set_global_limit_mode(SpotFundsLimitMode::TrackOnly);
        Ok(())
    })?;
engine.execute_pre_trade(order.clone())?.commit(); // available: -1 000

// Restore Enforce: available is negative - still rejected.
engine
    .configure()
    .spot_funds::<SpotFundsConfigError>(name, |settings| {
        settings.set_global_limit_mode(SpotFundsLimitMode::Enforce);
        Ok(())
    })?;
let rejects = engine
    .execute_pre_trade(order)
    .err()
    .expect("enforce must reject negative available");
assert_eq!(rejects[0].reason, "spot funds insufficient");
```

</details>

## Spot Funds: Per-Account Limit Mode

A per-account override pins the mode for one account regardless of the global
setting. Passing `None` (Go: `optional.None`; Python: `mode=None`) clears the
override so the cascade falls through to the account-group or global tier.

The example below keeps the global mode at Enforce and pins one account to
TrackOnly so that account's under-funded orders pass while the default
rejection logic stays in effect for all other accounts. Clearing the pin
restores the cascade fall-through and the account is gated again.

<details><summary>Go</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_wiki_test.go -->
```go
engine, err := openpit.NewEngineBuilder().
	NoSync().
	Builtin(policies.BuildSpotFunds()).
	Build()
if err != nil {
	return err
}
defer engine.Stop()

accountID := param.NewAccountIDFromUint64(99224416)
// Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
usd, _ := param.NewAsset("USD")
total, _ := param.NewPositionSizeFromString("1000")
seed, _ := model.NewAccountAdjustmentFromValues(model.AccountAdjustmentValues{
	BalanceOperation: optional.Some(
		model.NewAccountAdjustmentBalanceOperationFromValues(
			model.AccountAdjustmentBalanceOperationValues{Asset: optional.Some(usd)},
		),
	),
	Amount: optional.Some(
		model.NewAccountAdjustmentAmountFromValues(model.AccountAdjustmentAmountValues{
			Balance: optional.Some(param.NewAbsoluteAdjustmentAmount(total)),
		}),
	),
})
if _, err := engine.ApplyAccountAdjustment(
	accountID, []model.AccountAdjustment{seed},
); err != nil {
	return err
}

aapl, _ := param.NewAsset("AAPL")
price, _ := param.NewPriceFromString("200")
qty, _ := param.NewQuantityFromString("10")
order := model.NewOrder()
op := order.EnsureOperationView()
op.SetInstrument(param.NewInstrument(aapl, usd))
op.SetAccountID(accountID)
op.SetSide(param.SideBuy)
op.SetTradeAmount(param.NewQuantityTradeAmount(qty))
op.SetPrice(price)

// Global Enforce: under-funded buy is rejected.
_, rejects, err := engine.ExecutePreTrade(order)
if err != nil {
	return err
}
fmt.Println(rejects[0].Reason) // "spot funds insufficient"

// Pin this account to TrackOnly: per-account override wins over global Enforce.
if err := engine.Configure().SpotFundsAccountLimitMode(
	policies.SpotFundsPolicyName,
	accountID,
	optional.Some(policies.SpotFundsLimitModeTrackOnly),
); err != nil {
	return err
}
reservation, _, err := engine.ExecutePreTrade(order)
if err != nil {
	return err
}
reservation.CommitAndClose() // reservation recorded despite insufficient funds

// Clear the per-account override: cascade falls back to global Enforce.
if err := engine.Configure().SpotFundsAccountLimitMode(
	policies.SpotFundsPolicyName,
	accountID,
	optional.None[policies.SpotFundsLimitMode](),
); err != nil {
	return err
}
_, rejects, err = engine.ExecutePreTrade(order)
if err != nil {
	return err
}
fmt.Println(rejects[0].Reason) // "spot funds insufficient"
```

</details>

<details><summary>Python</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/python/tests/integration/test_examples_wiki.py -->
```python
import openpit
import openpit.pretrade.policies

engine = (
    openpit.Engine.builder()
    .no_sync()
    .builtin(openpit.pretrade.policies.build_spot_funds())
    .build()
)

account_id = openpit.param.AccountId.from_int(99224416)
# Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
seed = openpit.AccountAdjustment(
    operation=openpit.AccountAdjustmentBalanceOperation(asset="USD"),
    amount=openpit.AccountAdjustmentAmount(
        balance=openpit.param.AdjustmentAmount.absolute(
            openpit.param.PositionSize(1000)
        )
    ),
)
engine.apply_account_adjustment(account_id=account_id, adjustments=[seed])

order = openpit.Order(
    operation=openpit.OrderOperation(
        instrument=openpit.Instrument("AAPL", "USD"),
        account_id=account_id,
        side=openpit.param.Side.BUY,
        trade_amount=openpit.param.TradeAmount.quantity("10"),
        price=openpit.param.Price("200"),
    ),
)

# Global Enforce: under-funded buy is rejected.
result = engine.execute_pre_trade(order=order)
assert not result.ok
assert result.rejects[0].reason == "spot funds insufficient"

# Pin this account to TrackOnly: per-account override wins over global Enforce.
engine.configure().spot_funds(
    openpit.pretrade.policies.SpotFundsBuilder.NAME,
    account_limit_modes=[
        openpit.pretrade.policies.SpotFundsLimitModeAccountEntry(
            account_id=account_id,
            mode=openpit.pretrade.policies.SpotFundsLimitMode.TRACK_ONLY,
        )
    ],
)
result = engine.execute_pre_trade(order=order)
assert result.ok
result.reservation.commit()  # reservation recorded despite insufficient funds

# Clear the per-account override: cascade falls back to global Enforce.
engine.configure().spot_funds(
    openpit.pretrade.policies.SpotFundsBuilder.NAME,
    account_limit_modes=[
        openpit.pretrade.policies.SpotFundsLimitModeAccountEntry(
            account_id=account_id,
            mode=None,
        )
    ],
)
result = engine.execute_pre_trade(order=order)
assert not result.ok
assert result.rejects[0].reason == "spot funds insufficient"
```

</details>

<details><summary>JavaScript</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/configure.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { TradeAmount } from "@openpit/engine/param";
import { type OrderInit } from "@openpit/engine/model";
import { AdjustmentAmount } from "@openpit/engine/param";
import {
  buildSpotFunds,
  SpotFundsBuilder,
  SpotFundsLimitMode,
} from "@openpit/engine/pretrade/policies";

const accountId = 99_224_416n;
const engine = Engine.builder().builtin(buildSpotFunds()).build();

// Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
engine.applyAccountAdjustment(accountId, [
  {
    operation: { asset: "USD" },
    amount: { balance: AdjustmentAmount.absolute("1000") },
  },
]);

const order = (): OrderInit => ({
  operation: {
    underlyingAsset: "AAPL",
    settlementAsset: "USD",
    accountId,
    side: "BUY",
    tradeAmount: TradeAmount.quantity("10"),
    price: "200",
  },
});

// Global Enforce: under-funded buy is rejected.
let result = engine.executePreTrade(order());
console.log(result.rejects[0]!.reason); // "spot funds insufficient"

// Pin this account to TrackOnly: per-account override wins over global Enforce.
engine.configure().spotFunds(SpotFundsBuilder.NAME, {
  accountLimitModes: [
    { accountId, mode: SpotFundsLimitMode.TrackOnly },
  ],
});
result = engine.executePreTrade(order());
if (!result.ok) {
  throw new Error("unexpected rejects");
}
const reservation = result.reservation;
if (reservation === undefined) {
  throw new Error("accepted execute result is missing its reservation");
}
reservation.commit(); // reservation recorded despite insufficient funds

// Clear the per-account override: cascade falls back to global Enforce.
engine.configure().spotFunds(SpotFundsBuilder.NAME, {
  accountLimitModes: [{ accountId, mode: null }],
});
result = engine.executePreTrade(order());
console.log(result.rejects[0]!.reason); // "spot funds insufficient"
```

</details>

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

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/dynamic_policy_reconfiguration_test.cpp -->
```cpp
namespace aa = openpit::accountadjustment;
namespace policies = openpit::pretrade::policies;
using openpit::param::AccountId;
using openpit::param::AdjustmentAmount;
using openpit::param::PositionSize;
using openpit::param::Price;
using openpit::param::Quantity;

openpit::EngineBuilder builder(openpit::SyncPolicy::None);
builder.Add(policies::SpotFundsPolicy{});
openpit::Engine engine = builder.Build();

const AccountId account = AccountId::FromUint64(99224416);
// Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
aa::AccountAdjustment seed;
aa::BalanceOperation balanceOp;
balanceOp.asset = ::openpit::param::Asset("USD");
seed.operation = aa::Operation::OfBalance(std::move(balanceOp));
aa::Amount seedAmount;
seedAmount.balance =
    AdjustmentAmount::Absolute(PositionSize::FromString("1000"));
seed.amount = std::move(seedAmount);

const openpit::AdjustmentResult seedResult =
    engine.ApplyAccountAdjustment(account,
                                  std::vector<aa::AccountAdjustment>{seed});
if (!seedResult.Passed()) {
  return;
}

openpit::model::Order order;
openpit::model::OrderOperation op;
op.instrument = openpit::model::Instrument(::openpit::param::Asset("AAPL"),
                                           ::openpit::param::Asset("USD"));
op.accountId = account;
op.side = openpit::model::Side::Buy;
op.tradeAmount =
    openpit::model::TradeAmount::OfQuantity(Quantity::FromString("10"));
op.price = Price::FromString("200");
order.operation = std::move(op);

// Global Enforce: under-funded buy is rejected.
openpit::pretrade::ExecuteResult rejected = engine.ExecutePreTrade(order);
std::cout << rejected.rejects[0].reason << "\n";

// Pin this account to TrackOnly: per-account override wins over global
// Enforce.
engine.Configure().SpotFundsAccountLimitMode(
    policies::SpotFundsPolicyName, account,
    policies::SpotFundsLimitMode::TrackOnly);
openpit::pretrade::ExecuteResult accepted = engine.ExecutePreTrade(order);
if (accepted.Passed()) {
  // Reservation recorded despite insufficient funds.
  accepted.reservation->Commit();
}

// Clear the per-account override: cascade falls back to global Enforce.
engine.Configure().SpotFundsAccountLimitMode(
    policies::SpotFundsPolicyName, account, std::nullopt);
rejected = engine.ExecutePreTrade(order);
std::cout << rejected.rejects[0].reason << "\n";
```

</details>

<details><summary>Rust</summary>

<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_wiki.rs -->
```rust
use openpit::param::{
    AccountId, AdjustmentAmount, Asset, PositionSize, Price, Quantity, Side, TradeAmount,
};
use openpit::pretrade::policies::{SpotFundsConfigError, SpotFundsPolicy, SpotFundsSettings};
use openpit::pretrade::SpotFundsLimitMode;
use openpit::{
    AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
    Engine, FullSync, Instrument, OrderOperation, SpotFundsMarketData, SpotFundsPricingSource,
    WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
    WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};

type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
    WithAccountAdjustmentBounds<
        WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
    >,
>;

let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
    SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
    None::<SpotFundsMarketData<FullSync>>,
    builder.storage_builder(),
);

let account = AccountId::from_u64(99224416);
// Seed 1 000 USD - not enough for 10 AAPL @ 200 (= 2 000 notional).
let seed = WithAccountAdjustmentAmount {
    inner: WithAccountAdjustmentBounds {
        inner: WithAccountAdjustmentBalanceOperation {
            inner: AccountAdjustmentAmount::default(),
            operation: AccountAdjustmentBalanceOperation {
                asset: Asset::new("USD")?,
                average_entry_price: None,
            },
        },
        bounds: AccountAdjustmentBounds::default(),
    },
    amount: AccountAdjustmentAmount {
        balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str("1000")?)),
        held: None,
        incoming: None,
    },
};
engine.apply_account_adjustment(account, &[seed])?;

let order = OrderOperation {
    instrument: Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?),
    account_id: account,
    side: Side::Buy,
    trade_amount: TradeAmount::Quantity(Quantity::from_str("10")?),
    price: Some(Price::from_str("200")?),
};

// Global Enforce: under-funded buy is rejected.
let rejects = engine
    .execute_pre_trade(order.clone())
    .err()
    .expect("enforce must reject insufficient funds");
assert_eq!(rejects[0].reason, "spot funds insufficient");

// Pin this account to TrackOnly: per-account override wins over global Enforce.
let name = SpotFundsPolicy::<FullSync, FullSync>::NAME;
engine
    .configure()
    .spot_funds::<SpotFundsConfigError>(name, |settings| {
        settings.set_account_limit_mode(account, Some(SpotFundsLimitMode::TrackOnly));
        Ok(())
    })?;
engine.execute_pre_trade(order.clone())?.commit(); // reservation recorded

// Clear the per-account override: cascade falls back to global Enforce.
engine
    .configure()
    .spot_funds::<SpotFundsConfigError>(name, |settings| {
        settings.set_account_limit_mode(account, None);
        Ok(())
    })?;
let rejects = engine
    .execute_pre_trade(order)
    .err()
    .expect("enforce must reject after pin is cleared");
assert_eq!(rejects[0].reason, "spot funds insufficient");
```

</details>

## Spot Funds: Position Limit

No binding's builder takes a [position limit](Spot-Funds.md#position-limit); the
bindings pin, replace, or clear one at runtime per `(account, asset)` pair:

- **Go**: `engine.Configure().SpotFundsPositionLimit(name, accountID, asset,
  limit)`; `optional.None` clears.
- **Python**: `engine.configure().spot_funds(name, position_limits=[...])` with
  `SpotFundsPositionLimitEntry` items; each entry requires `limit`, and
  `limit=None` clears.
- **JavaScript**: `engine.configure().spotFunds(name, {
  positionLimits: [...] })` with `SpotFundsPositionLimitEntry` items; each
  entry requires `limit`, and `limit: null` clears.
- **C++**: `engine.Configure().SpotFundsPositionLimit(name, accountId, asset,
  limit)`; `std::nullopt` clears.
- **C**: `openpit_engine_configure_spot_funds_position_limit`;
  `has_limit = false` clears.
- **Rust**: `settings.set_position_limit(account_id, asset, limit)` inside
  `engine.configure().spot_funds(name, |settings| ...)`; `None` clears. The
  same setter also works on `SpotFundsSettings` before `SpotFundsPolicy::new`.

A change applies from the next order and does not re-evaluate open
reservations.

## Related Pages

- [Policies](Policies.md): built-in controls and the policy catalog
- [Policy API](Policy-API.md): custom policy interfaces and rollback patterns
- [Pre-trade Pipeline](Pre-trade-Pipeline.md): request and reservation semantics
- [Spot Funds](Spot-Funds.md): per-account funds policy and its settings
- [Reject Codes](Reject-Codes.md): standard business reject codes
