-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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.
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=...). -
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. -
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.
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 |
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, and ConfigureError in Rust).
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"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");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"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");- 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