Dynamic Policy Reconfiguration¶
Retune a policy while the engine is running, without rebuilding it.
The engine owns synchronization. Reconfiguration is a rare write against settings read on every order: reads stay on the hot-path optimal; the small coordination cost is paid only on the write.
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; a spot-funds override change applies from the next order onward. The one deliberate exception is force-setting a P&L accumulator - an 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=...). - 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. - C++:
std::nulloptleaves an axis unchanged; pass an engagedstd::optionalto replace it. For vector axes, an engaged empty vector clears that axis. - Rust: call
settings.set_broker(None)inside the configure closure.
initial pnl is mandatory only when constructing a P&L account barrier, where
it seeds P&L accumulated before engine startup. Runtime barrier replacement has
no initial pnl and never re-seeds state: it always evaluates the engine's
current live accumulated P&L. If tightened bounds are already breached by that
P&L, subsequent orders fail; otherwise behavior continues normally. To set the
live accumulator deliberately - instead of retuning the bounds around it - use
the explicit call in Force-set Accumulated P&L.
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 when the name is unknown, the
named policy has a different settings type than the call targets, or the new
values fail validation. The failure surfaces as the language's idiomatic error
type (a *configure.Error in Go, PolicyConfigureError carrying a
ConfigureErrorKind in Python, openpit::ConfigureError in C++, and
ConfigureError in Rust).
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"
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::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"
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{"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, "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"
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 = "USD";
seed.operation = aa::Operation::OfBalance(std::move(balanceOp));
aa::Amount seedAmount;
seedAmount.balance =
AdjustmentAmount::OfAbsolute(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("AAPL", "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<()>>,
>;
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: (),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
realized_pnl: 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"
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 = "USD";
seed.operation = aa::Operation::OfBalance(std::move(balanceOp));
aa::Amount seedAmount;
seedAmount.balance =
AdjustmentAmount::OfAbsolute(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("AAPL", "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<()>>,
>;
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: (),
operation: AccountAdjustmentBalanceOperation {
asset: Asset::new("USD")?,
average_entry_price: None,
realized_pnl: 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