-
Notifications
You must be signed in to change notification settings - Fork 1
Policies
OpenPit exposes two policy stages:
-
Start stage: cheap checks that must see every request and do not need rollback support. -
Main stage: deeper checks that may emit multiple rejects or register reversible mutations.
Exact interface names differ by SDK and are listed in the language-specific sections below.
Built-in policies use an axis-based configuration model. Each policy has one or more axes - dimensions along which a limit or barrier is applied. An axis becomes active only when a barrier is explicitly configured for it.
Common axes across built-in policies:
- Broker axis: applies to all orders regardless of account or asset.
- Asset axis: applies to all orders with a given settlement asset.
- Account axis: applies to all orders from a given account.
- Account+Asset axis: applies to orders from a specific account and settlement asset pair.
An axis without a matching barrier never rejects the order. At least one axis must be configured; constructors return an error if all axes are empty.
The sync policy is chosen on the engine builder. It controls how stateful built-in policies synchronize their internal storage and defines the engine handle's threading capability (see Threading Contract for the per-mode contract).
| Mode | Behavior | Use case |
|---|---|---|
| Full sync | Thread-safe handle; concurrent invocation on the same handle is safe. | Engine is shared across threads, or migration patterns make sequential pinning impractical. |
| No sync | Single-threaded handle; calls must stay on the OS thread that created the engine. | Single-threaded embedding; zero synchronization overhead. |
| Account sync | Sequential cross-thread access; caller pins each account to one processing chain. | Account-sharded workloads where one worker / queue owns each account. |
Storage owned by a built-in or custom policy is always account-keyed
when used through binding-facing storage contracts: the top-level key
carries an AccountId, and the per-account value can be any structure the
policy needs. The choice of sync mode does not change this rule.
Method names per language:
Go
openpit.NewEngineBuilder().FullSync()openpit.NewEngineBuilder().NoSync()openpit.NewEngineBuilder().AccountSync()
If you are unsure, start with FullSync(): goroutines migrate between OS
threads at any await point, so even a single goroutine calling the engine
sequentially can wake up on a different OS thread than the one it suspended on.
FullSync() is the safe default.
AccountSync() only requires that calls for the same account are never
concurrent - it does not require a fixed OS thread. The standard Go pattern is
a sharded worker pool: hash the account ID to one of N channels; one goroutine
drains each channel, so the same account always lands on the same worker.
const shards = 256
type task struct {
accountID string
order model.Order
}
workers := make([]chan task, shards)
for i := range workers {
ch := make(chan task, 1024)
workers[i] = ch
go func(ch <-chan task) {
for t := range ch {
req, _, _ := engine.ExecutePreTrade(t.order)
if req != nil {
req.Close()
}
}
}(ch)
}
func dispatch(t task) {
workers[fnv32(t.accountID)%shards] <- t
}Different shards run in parallel; all orders for the same account always go to the same shard and are processed sequentially.
Python
openpit.Engine.builder().full_sync()openpit.Engine.builder().no_sync()openpit.Engine.builder().account_sync()
Prefer no_sync() when you do not explicitly work with multiple threads
yourself - it has zero synchronization overhead and is the right default for
embeddings that drive the engine from a single thread (synchronous code or one
asyncio loop). Use full_sync() only when you actually share the engine
across threads concurrently. Use account_sync() for sharded sequential
workloads where each account is pinned to one processing chain.
Rust
Engine::builder::<OrderOperation, (), ()>().full_sync()Engine::builder::<OrderOperation, (), ()>().no_sync()Engine::builder::<OrderOperation, (), ()>().account_sync()
The choice depends on the embedding; the SDK does not impose a default.
The engine maintains an internal blocked-accounts registry next to the
policy chain. When a policy emits a reject with scope = account or signals
a kill switch from apply execution report, the engine records the affected
account and short-circuits every subsequent pre-trade request for that
account before any policy runs.
The block is owned by the engine, not by the policy. A policy must not keep its own "this account is blocked" flag and must not re-check the condition on every order - returning the reject once is the entire contract. Once a block is recorded, the engine guarantees the policy is not invoked again for that account until the block is cleared.
- A
start stagecallback returns rejects that includescope = account. - A
main stagecallback returns rejects that includescope = account. -
apply execution reportreturns a non-empty list ofAccountBlockvalues (a kill switch).
Caution: returning a Reject with scope = account from any pre-trade
stage triggers an irreversible account block, cleared only by rebuilding the
engine. A policy that intends to reject only the current order, for example
for a transient condition that may resolve on its own, must use
scope = order instead.
Only the first AccountBlock recorded for an account is kept. Later
recordings for the same account are no-ops - first cause wins. When
apply execution report is dispatched across policies, the engine records
only the first AccountBlock from the first policy that returned a non-empty
list. Remaining blocks, whether from the same policy or from later policies,
are still returned to the caller in PostTradeResult account blocks but do
not affect the registry.
- If the offending order or report exposes an
account id, only that account is blocked, across every settlement asset and instrument. - If the order or report cannot produce an
account id, the engine activates a global block and rejects every subsequent pre-trade request, regardless of account.
- For blocks triggered by start-stage or main-stage rejects, future rejects on
that account carry
code = AccountBlockedand replay thepolicy,reason, anddetailsfrom the original reject. - For blocks triggered by
apply execution report, future rejects replay thecode,reason, anddetailsfrom theAccountBlockthe policy emitted (for examplePnlKillSwitchTriggered). - While any block is active and an order arrives without an
account id, the engine rejects it withMissingRequiredFieldand the order scope.
A blocked account stays blocked until the engine is rebuilt. There is no implicit unblock: rerunning the original check, replaying the order, retrying the request, or letting time pass does not lift the block.
The current built-in policies are all start-stage policies.
Validates basic order structure before more expensive checks. Does not validate price, side, instrument, or account.
What it controls:
- Rejects zero quantity
trade amount - Rejects zero volume
trade amount
Required fields to populate:
-
Order:trade amount -
Execution Report: none
Rejects:
- Missing
trade amountCode:MissingRequiredField, Scope:orderReason:failed to access required field - Zero quantity
Code:
InvalidFieldValue, Scope:orderReason:order quantity must be non-zero - Zero volume
Code:
InvalidFieldValue, Scope:orderReason:order volume must be non-zero
Go
engine, err := openpit.NewEngineBuilder().
NoSync().
Builtin(
policies.BuildOrderValidation(),
).
Build()Python
import openpit
import openpit.pretrade.policies
engine = (
openpit.Engine.builder()
.no_sync()
.builtin(
openpit.pretrade.policies.build_order_validation(),
)
.build()
)Rust
use openpit::pretrade::policies::OrderValidationPolicy;
use openpit::{Engine, OrderOperation};
let engine = Engine::builder::<OrderOperation, (), ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;Counts start-stage attempts inside a time window. Flood semantics: rejected attempts count against the limit to prevent bypass by retrying.
What it controls (by axis):
- Broker axis: global limit across all orders. Uses an approximate fixed-window counter.
- Asset axis: limit per settlement asset. Uses an approximate fixed-window counter.
- Account axis: limit per account. Uses a precise sliding-window log.
- Account+Asset axis: limit per account and settlement asset pair. Uses a precise sliding-window log.
Caution: RateLimitAccountBarrier and RateLimitAccountAssetBarrier
reject with scope = account on breach. The engine records the affected
account in its blocked-accounts registry on the first breach, which turns
what would otherwise be a window-bound rate limit into a permanent account
block cleared only by rebuilding the engine. Choose these axes only when that
escalation is intended; use the broker or asset axes (both reject with
scope = order) when a transient cap is enough.
All configured axes are incremented and checked on every call. Checks run in order: broker -> asset -> account -> account+asset.
Limit parameters (RateLimit):
| Field | Type | Description |
|---|---|---|
max orders |
integer | maximum attempts in the window |
window |
duration | length of the time window |
Required fields to populate:
-
Order:instrument(when asset or account+asset axes are configured),account id(when account or account+asset axes are configured) -
Execution Report: none
Rejects:
- Code:
RateLimitExceeded, Scope:orderReason:rate limit exceeded: broker barrier - Code:
RateLimitExceeded, Scope:orderReason:rate limit exceeded: asset barrier - Code:
RateLimitExceeded, Scope:accountReason:rate limit exceeded: account barrier - Code:
RateLimitExceeded, Scope:accountReason:rate limit exceeded: account+asset barrier - Code:
MissingRequiredField, Scope:order(when a required field cannot be read)
Go
engine, err := openpit.NewEngineBuilder().
NoSync().
Builtin(
policies.BuildRateLimit().
BrokerBarrier(
policies.RateLimitBrokerBarrier{
Limit: policies.RateLimit{
MaxOrders: 100,
Window: time.Second,
},
},
),
).
Build()Python
import datetime
import openpit
import openpit.pretrade.policies
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=100,
window=datetime.timedelta(seconds=1),
),
),
)
)
.build()
)Rust
use std::time::Duration;
use openpit::pretrade::policies::{
RateLimit,
RateLimitBrokerBarrier,
RateLimitPolicy,
};
use openpit::{
Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact,
};
type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = RateLimitPolicy::new(
Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 100,
window: Duration::from_secs(1),
},
}),
[], // asset barriers
[], // account barriers
[], // account+asset barriers
builder.storage_builder(),
)?;
let engine = builder.pre_trade(policy).build()?;Limits the size of a single order by quantity and notional value. Prevents fat-finger errors.
What it controls (by axis):
- Broker axis: global hard cap applied to all orders. Additive - checked in addition to other axes.
- Asset axis: limit per settlement asset.
- Account+Asset axis: limit per account and settlement asset pair.
There is no per-account (without asset) axis.
Override rule for the asset chain: if an account+asset barrier is configured for a given pair, it overrides the asset barrier for that account. If no account+asset barrier exists but an asset barrier does, the asset barrier applies. If neither is configured, the axis does not check the order. The broker axis is always additive regardless of the asset chain outcome.
When both an asset-chain axis and the broker axis reject the same order, the asset-chain reject is returned first.
Limit parameters (OrderSizeLimit):
| Field | Type | Description |
|---|---|---|
max quantity |
Quantity | maximum quantity per order |
max notional |
Volume | maximum notional value per order |
For a volume-based trade amount, notional is taken directly. For a
quantity-based trade amount, notional is derived as |price| * quantity.
price must be provided when this conversion is needed.
Required fields to populate:
-
Order:instrument,trade amount,account id(when account+asset axes are configured),price(when quantity-to-notional conversion is needed) -
Execution Report: none
Rejects:
- Quantity exceeded
Code:
OrderQtyExceedsLimitReason:order quantity exceeded - Notional exceeded
Code:
OrderNotionalExceedsLimitReason:order notional exceeded - Both exceeded
Code:
OrderExceedsLimitReason:order size exceeded - Missing
priceor failed size translation Code:OrderValueCalculationFailedReason:order value calculation failed - Missing required field
Code:
MissingRequiredField
Go
usd, err := param.NewAsset("USD")
maxQty, err := param.NewQuantityFromString("100")
maxNotional, err := param.NewVolumeFromString("50000")
engine, err := openpit.NewEngineBuilder().
NoSync().
Builtin(
policies.BuildOrderSizeLimit().
AssetBarriers(
policies.OrderSizeAssetBarrier{
SettlementAsset: usd,
Limit: policies.OrderSizeLimit{
MaxQuantity: maxQty,
MaxNotional: maxNotional,
},
},
).
BrokerBarrier(
policies.OrderSizeBrokerBarrier{
Limit: policies.OrderSizeLimit{
MaxQuantity: maxQty,
MaxNotional: maxNotional,
},
},
),
).
Build()Python
import openpit
import openpit.pretrade.policies
engine = (
openpit.Engine.builder()
.no_sync()
.builtin(
openpit.pretrade.policies.build_order_size_limit()
.asset_barriers(
openpit.pretrade.policies.OrderSizeAssetBarrier(
limit=openpit.pretrade.policies.OrderSizeLimit(
max_quantity=openpit.param.Quantity(100),
max_notional=openpit.param.Volume(50000),
),
settlement_asset="USD",
),
)
.broker_barrier(
openpit.pretrade.policies.OrderSizeBrokerBarrier(
limit=openpit.pretrade.policies.OrderSizeLimit(
max_quantity=openpit.param.Quantity(10000),
max_notional=openpit.param.Volume(5000000),
),
),
)
)
.build()
)Rust
use openpit::param::{Asset, Quantity, Volume};
use openpit::pretrade::policies::{
OrderSizeAssetBarrier,
OrderSizeLimit,
OrderSizeLimitPolicy,
};
use openpit::{Engine, OrderOperation};
let policy = OrderSizeLimitPolicy::new(
None,
[OrderSizeAssetBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("100")?,
max_notional: Volume::from_str("50000")?,
},
settlement_asset: Asset::new("USD")?,
}],
[],
)?;
let engine = Engine::builder::<OrderOperation, (), ()>()
.no_sync()
.pre_trade(policy)
.build()?;Tracks accumulated realized P&L and blocks new orders when it moves outside
configured bounds. After a kill switch triggers, the engine blocks the
affected account id from every subsequent pre-trade request across all
settlement assets - see Account Blocking by Engine
for how the block is recorded and cleared.
What it controls (by axis):
- Broker barrier: P&L bounds applied to all accounts with a given settlement asset. If no broker barrier is configured for an order's settlement asset and no account barrier matches, the order passes without P&L tracking.
-
Account+Asset barrier: P&L bounds for a specific account and settlement
asset pair. Accepts an
initial pnlto resume tracking from a previously accumulated value.
Both barriers are checked on every order; the broker barrier is checked first.
Barrier parameters (PnlBoundsBrokerBarrier):
| Field | Type | Required | Description |
|---|---|---|---|
settlement asset |
Asset | yes | settlement asset for P&L tracking |
lower bound |
optional P&L | no | loss limit (typically negative) |
upper bound |
optional P&L | no | profit-taking limit (typically positive) |
At least one bound must be set. The constructor does not validate bound signs or
the order of lower bound <= upper bound.
P&L accumulation uses the account id from the execution report (not from the
original order). Each report contributes pnl + fee to the accumulated total.
P&L overflow causes a permanent block.
Post-trade behavior:
- Accepts two valid ways to pass trading result:
- Separate:
pnlis net trading P&L before fees,feeis provided separately. The engine appliesfeeas a negative P&L contribution. - Combined:
pnlalready includes fees,feereturnsNoneor zero. In that case the fee is not counted twice.
- Separate:
Required fields to populate:
-
Order:instrument,account id -
Execution Report:instrument,account id,pnl,fee
Rejects:
- First detection via broker barrier
Code:
PnlKillSwitchTriggered, Scope:accountReason:pnl kill switch triggered: broker barrier - First detection via account+asset barrier
Code:
PnlKillSwitchTriggered, Scope:accountReason:pnl kill switch triggered: account+asset barrier - All subsequent checks after kill switch has triggered
Code:
PnlKillSwitchTriggered, Scope:accountReason:pnl kill switch triggered: account blocked - Missing required field
Code:
MissingRequiredField
Go
usd, err := param.NewAsset("USD")
lowerBound, err := param.NewPnlFromString("-1000")
upperBound, err := param.NewPnlFromString("500")
engine, err := openpit.NewEngineBuilder().
NoSync().
Builtin(
policies.BuildPnlBoundsKillswitch().
BrokerBarriers(
policies.PnlBoundsBrokerBarrier{
SettlementAsset: usd,
LowerBound: optional.Some(lowerBound),
UpperBound: optional.Some(upperBound),
},
),
).
Build()Python
import openpit
import openpit.pretrade.policies
engine = (
openpit.Engine.builder()
.no_sync()
.builtin(
openpit.pretrade.policies.build_pnl_bounds_killswitch()
.broker_barriers(
openpit.pretrade.policies.PnlBoundsBrokerBarrier(
settlement_asset="USD",
lower_bound=openpit.param.Pnl(-1000),
upper_bound=openpit.param.Pnl(500),
),
)
)
.build()
)Rust
use openpit::param::{Asset, Pnl};
use openpit::pretrade::policies::{
PnlBoundsBrokerBarrier,
PnlBoundsKillSwitchPolicy,
};
use openpit::{
Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact,
};
type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = PnlBoundsKillSwitchPolicy::new(
[PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD")?,
lower_bound: Some(Pnl::from_str("-1000")?),
upper_bound: Some(Pnl::from_str("500")?),
}],
[],
builder.storage_builder(),
)?;
let engine = builder.pre_trade(policy).build()?;Custom policy interfaces, callbacks, and language-specific examples: Policy API.
- Pre-trade Pipeline: Request and reservation semantics
- Policy API: Custom policy interfaces and examples
- Domain Types: Value types used by built-in and custom policies
- Reject Codes: Standard business reject codes
- Architecture: Public integration model
- Storage: Built-in synchronization-aware key-value storage