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, or limit-mode 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 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
RateLimitUpdateorOrderSizeLimitUpdatewhen the broker axis itself changes.optional.Noneleaves it unchanged,optional.Some[*policies.RateLimitBrokerBarrier](nil)clears the rate-limit broker barrier, andoptional.Some(&barrier)replaces it. For order-size use the matchingoptional.Some[*policies.OrderSizeBrokerBarrier](nil). - Python:
broker=Noneleaves the broker barrier unchanged; passclear_broker=Trueto remove it.brokerandclear_broker=Truecannot be provided together. - JavaScript: omit
brokerto leave it unchanged; passclearBroker: trueto remove it.brokerandclearBroker: truecannot be provided together. - C++: use
RateLimitBrokerBarrierUpdate::Unchanged(),Clear(), orSet(barrier)for the rate-limit broker axis, and the matchingOrderSizeBrokerBarrierUpdatetype for order size. Vector axes usestd::nulloptfor 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 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.Errorandconfigure.ErrorKind. - Python:
PolicyConfigureErrorandConfigureErrorKind. - JS:
PolicyConfigureErrorandConfigureErrorKind. - C++:
openpit::ConfigureErrorandopenpit::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.
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"
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"
JavaScript
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"
C++
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";
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");
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.
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"
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"
JavaScript
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"
C++
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";
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");
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. 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.
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"
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"
JavaScript
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"
C++
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";
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");
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.
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"
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"
JavaScript
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"
C++
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";
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");
Related Pages¶
- Policies: built-in controls and the policy catalog
- Policy API: custom policy interfaces and rollback patterns
- Pre-trade Pipeline: request and reservation semantics
- Spot Funds: per-account funds policy and its settings
- Reject Codes: standard business reject codes