-
Notifications
You must be signed in to change notification settings - Fork 1
Policy API
OpenPit exposes custom policy hooks for two stages:
-
Start stage: cheap checks that must run for every request. -
Main stage: deeper checks that can emit one or more rejects and register reversible mutations.
Behavioral contract first, then language-specific examples.
- Start stage returns one reject outcome or pass-through.
- Main stage can collect multiple rejects and register reversible mutations.
- Main-stage context provides read-only access to request data.
- Main-stage mutations are committed only when the full
execute requeststep succeeds. - A registered mutation's commit and rollback callbacks are finalizers and have no right to fail; a failure arms an engine kill switch whose reach follows the registering policy. See Account Blocking - Mutation Finalizer Contract.
For account-adjustment batch policy hooks, see Account Adjustments.
Every stage hook receives a context as its first argument. How long that context stays usable is language-specific:
- Go: the context itself is non-owning and callback-scoped - valid only for
that call; do not retain it or use it after the hook returns. An
AccountControlobtained from it remains valid through the reservation's commit or rollback, or through finalization of a drop-copy operation, and may be captured by a mutation callback for deferred blocking; do not use it after that transaction completes. See Custom Go Types. - Python: the context owns its state, so holding a reference is safe; the
AccountControlit exposes still records a block only within its own pre-trade transaction, from the callback through that request's commit or rollback. A block recorded after that transaction completes is unspecified. - JavaScript: the context owns its state and may be retained; once the owning
operation is finalized, its
AccountControland drop-copy mutation recorder throwLifecycleErrorrather than writing into a finished transaction. - C++: the context is non-owning and callback-scoped, and is neither copyable nor movable - valid only for that call. See Custom Cpp Types.
- Rust: the hook borrows the context, so the compiler already bounds its use to the call.
For drop copy, account-control operations requested through these contexts are applied in order before the accepted operation is returned. They are not retained as caller rollback callbacks and are not undone by a later operation rollback.
Drop copy runs the same start and main policy hooks in registration order. The context flag tells a policy that ordinary risk or compliance rejects are non-enforcing because the order already happened. A policy should branch on the flag only when necessary to preserve bookkeeping - for example, Spot Funds forces TrackOnly. Pure admission policies such as order validation and order size limits do no work in drop-copy mode.
The context surfaces use each language's native naming:
| Language | Detect drop copy | Register a start-stage mutation |
|---|---|---|
| Go | ctx.IsDropCopy() |
ctx.RecordDropCopyStartMutation(commit, rollback) |
| Python | ctx.is_drop_copy |
ctx.record_drop_copy_start_mutation(mutation) |
| JavaScript | ctx.isDropCopy |
ctx.recordDropCopyStartMutation(mutation) |
| C++ | ctx.IsDropCopy() |
ctx.RecordDropCopyStartMutation(commit, rollback) |
| Rust | ctx.is_drop_copy() |
ctx.record_drop_copy_start_mutation(mutation) |
Start-stage hooks have no ordinary mutation return channel, so stateful custom policies use this recorder only during an active drop-copy callback. Apply tentative state before registering a start-stage or main-stage mutation. Both recorders take the same infallible commit and rollback pair as an ordinary main-stage mutation: the commit callback runs when the caller commits the returned operation, and the rollback callback runs when the caller rolls it back or releases it unfinalized. If evaluation fails, the engine runs every collected rollback in reverse registration order before returning the rejects, and no operation is produced. Main-stage policies keep using their ordinary mutation channel.
There is no public record drop-copy account block hook. A custom policy asks
for a block in the normal way by returning an account-scoped reject. Drop copy
does not use that reject to deny the historical order. Ordered account-control
operations are applied before the accepted operation is returned and remain in
effect even if the caller later rolls the operation back. To abort the
bookkeeping because the policy cannot calculate or apply the historical effect,
return a standardized code for which
the binding's evaluation-failure classifier is true; see
Reject Codes.
Custom policy state must not be read or mutated in parallel with engine calls on the same engine instance.
- Unsafe pattern: one thread executes
start stageorexecute requestwhile another thread reads or mutates fields used by the same policy callbacks. - If shared access is unavoidable, synchronization is fully owned by the host application (locks, serialized access, actor loop, etc.).
- Preferred pattern: keep policy state mutations inside engine calls and
feed external corrections through
apply account adjustments.
A callback must not re-enter the engine that invoked it. The call is already inside that engine's storage access, so re-entering it aliases state the sync mode cannot protect.
In JavaScript the rule is enforced across engine methods, retained accounts
and configure facades, requests, reservations, and drop-copy operations. They
throw LifecycleError when the same engine is re-entered synchronously from one
of its own policy or mutation callbacks. The guard also covers the implicit
rollback that runs when a Reservation or DropCopyOperation is released
without commit or rollback, so a mutation rollback closure cannot reach
back into its own engine during release. Driving a different engine instance
from a callback remains allowed. Match on the error class rather than its
message.
In Python and C++, a nested engine or market-data call made from inside a callback does not disturb the exception owned by the operation that encloses it. Each operation owns its own callback-exception slot. A policy may catch a nested lookup failure without losing the enclosing operation's failure, and a failing account-group lookup is reported by the call that asked for it rather than being read as "this account has no group". Python keeps the first callback exception raised within one operation; remaining callbacks still run, but a later exception does not replace the first.
A callback that raises or throws is an evaluation failure, not a verdict, and
drop copy handles it exactly like an ordinary pre-trade call: the engine
compensates the mutations it collected and reports the failure through the
binding's own channel. Python rethrows the original callback exception. C++
rethrows the original exception too, once cleanup has finished. JavaScript
throws PolicyCallbackError with the original value as cause, whatever its
class. An operation that an engine defect abandoned reports that
InternalError instead of wrapping it.
A mutation finalizer is the one callback that has no right to fail. When one
fails during a caller-driven commit or rollback it still reaches the same
binding-native channel as the equivalent reservation callback, and the void call
still returns normally - but the failure is not dropped: the engine arms a kill
switch, and for a mutation registered by a custom policy that block covers every
account, not only the pipeline's. Every mutation registered through a binding is
a custom-policy mutation. Implicit rollback on release performs the same cleanup
without intentionally throwing, because no caller is left to receive an error;
the kill switch is then the only channel. The full contract, including the cause
the engine records and how a policy author should avoid it, is in
Account Blocking - Mutation Finalizer Contract.
Go does not let a panic cross the cgo boundary. A panic from either pre-trade
hook - including a drop-copy hook - from either dry-run hook, or from the
account-adjustment hook becomes an order-level SystemUnavailable reject whose
details contain the recovered value. A panic
from ApplyExecutionReport becomes a SystemUnavailable account block
that the engine records for that account. A panic from policy teardown has no
result channel left, so it is contained and reported to the process-wide handler
installed with openpit.SetTeardownFailureHandler; without one it leaves no
trace.
A native Rust callback that panics unwinds to the caller unchanged; the SDK adds no containment around policy or mutation callbacks in any pipeline, including drop copy. Rust policy and mutation callbacks must therefore not panic. The foreign bindings above convert their own callback failures before this boundary, so their documented channels are unaffected.
A Rust panic at the JavaScript WebAssembly boundary surfaces as
InternalError, not a bare trap. It permanently poisons the loaded module
instance: every later call that would reach core state returns the same
InternalError instead of running. That covers the whole state-bearing
surface - Engine, Request, Reservation, Accounts, Configurator,
AccountControl, Context, MarketDataService, and ReferenceBook. The
instance never recovers: discard every handle from it and reload the module
before continuing.
The Go SDK exposes:
- unified interface
pretrade.Policy- all stage hooks and the account-adjustment callback in one interface; - for custom order/report types,
pretrade.ClientPreTradePolicy[Order, Report]- the same four callbacks, but order and report arrive as the typed project struct (account-adjustment still usesmodel.AccountAdjustment); - adapters with payload validation:
pretrade.NewSafeClientPreTradePolicy; - adapters without validation, for SDK-controlled paths:
pretrade.NewUnsafeFastClientPreTradePolicy; - built-in native policies are registered via the
Builtinbuilder method.
The post-trade hook returns account blocks and may push group-tagged
account-adjustment and account-PnL outcomes into pretrade.PostTradeAdjustments
and pretrade.PostTradePnls. The three channels are independent.
Python exposes a unified policy class over record-style openpit.Order and
openpit.ExecutionReport:
- unified class:
openpit.pretrade.Policy- all stage hooks and account-adjustment callback with default no-op implementations
Business outcomes are returned, not raised:
- start stage returns
Iterable[PolicyReject] - main stage returns
PolicyPreTradeResult - account adjustment returns
PolicyAccountAdjustmentResult; its independentrejects,mutations,account_blocks, andaccount_adjustmentschannels may be omitted or empty for a no-op
Policies can register:
Mutation(commit=callable, rollback=callable)
JavaScript exposes one structural policy interface over the submitted order and execution-report model types:
- unified interface:
Policy<OrderModel, ExecutionReportModel>; - start stage:
checkPreTradeStart(ctx, order); - optional read-only start dry-run:
checkPreTradeStartDryRun(ctx, order); - main stage:
performPreTradeCheck(ctx, order); - optional read-only main dry-run:
performPreTradeCheckDryRun(ctx, order); - post-trade:
applyExecutionReport(ctx, report); - account adjustment:
applyAccountAdjustment(ctx, accountId, adjustment).
Business outcomes are returned as PolicyReject, PolicyPreTradeResult,
PolicyAccountAdjustmentResult, PolicyDecision, and mutation values. A thrown callback exception is an API
failure and is re-thrown to the caller. Each policy receives a fresh clone of
the submitted order or report, including host-specific fields; account
adjustments use the concrete OpenPit AccountAdjustment model.
C++ has no single policy base class. A direct CustomPolicy<Handler> receives
its name in the constructor, and the handler exposes one or more stage hooks
detected at compile time; any other hook is registered as null and treated as
"accept by default":
- start stage:
std::optional<Reject> CheckPreTradeStart(const Order&) const- return aRejectorstd::nullopt - optional read-only start dry-run:
std::optional<Reject> CheckPreTradeStartDryRun(const Order&) const - main stage:
void PerformPreTradeCheck(const Context&, tx::Mutations&, Result&, PolicyDecision&) const- report zero or more rejects throughPushReject(decision, ...), and optionally register mutations or push lock prices / outcomes into the collectors - optional read-only main dry-run:
void PerformPreTradeCheckDryRun(const Context&, tx::Mutations&, Result&, PolicyDecision&) const - post-trade:
std::vector<accounts::AccountBlock> ApplyExecutionReport(const PostTradeContext&, const ExecutionReport&, PostTradeAdjustments&, PostTradePnls&) const- return the account blocks raised, optionally pushing group-tagged adjustment and account-PnL outcomes into the two collectors - account adjustment:
PolicyAccountAdjustmentResult ApplyAccountAdjustment(const accountadjustment::Context&, param::AccountId, const accountadjustment::AccountAdjustment&, tx::Mutations&, AccountOutcomes&) const- validate one adjustment, optionally registering mutations and pushing outcomes; return accepted account blocks through the result'saccountBlocks
Those are the direct CustomPolicy<Handler> signatures. A typed
PolicyAdapter client policy additionally exposes Name() and prepends
const ClientOrder& to the main-stage and main dry-run signatures, substitutes
const ClientOrder& in the start and start dry-run signatures, and substitutes
const ClientReport& in the post-trade signature. Missing dry-run hooks fall
back to the corresponding normal hook. The adapter performs the selected
checked or unchecked cast before invoking a hook. The
name passed to CustomPolicy must match the client policy's Name() so engine
registration and adapter-produced rejects carry one stable identity.
The handler is wrapped in an adapter that fixes the cast mode and the concrete
order/report types. PolicyAdapter registers its main hook plus every optional
start, dry-run, post-trade, and account-adjustment hook exposed by the same
client-policy instance; StartPolicyAdapter is the start-only compatibility
wrapper:
-
StartPolicyAdapterWithSafeSlowArgType<Policy, Order, Report>andPolicyAdapterWithSafeSlowArgType<Policy, Order, Report>- recover the typed order/report from the context with a checked cast, turning a payload mismatch into a value reject; - the
...WithUnsafeFastArgTypevariants skip the check on SDK-controlled paths.
The adapter is registered through openpit::pretrade::CustomPolicy<Adapter> on
the EngineBuilder. CustomPolicy contains callback exceptions inside the SDK
boundary; the invoking C++ engine method, drop copy included, rethrows the
original exception after the call returns. Exceptions represent API
failures; expected business outcomes are returned as Reject /
PolicyDecision values.
Rust exposes a unified trait for custom policies and caller-defined order contracts:
- unified trait:
PreTradePolicy<Order, ExecutionReport, AccountAdjustment = ()>- all stage hooks and account-adjustment callback with default no-op
implementations (only
nameis required)
- all stage hooks and account-adjustment callback with default no-op
implementations (only
- start-stage callback receives:
&PreTradeContext,&Order - main-stage callback receives:
&PreTradeContext,&Order,&mut Mutations - account-adjustment callback receives:
&AccountAdjustmentContext,AccountId,&A,&mut Mutations
Go
package main
import (
"fmt"
"go.openpit.dev/openpit/accountadjustment"
"go.openpit.dev/openpit/model"
"go.openpit.dev/openpit/param"
"go.openpit.dev/openpit/pretrade"
"go.openpit.dev/openpit/reject"
"go.openpit.dev/openpit/tx"
)
type NotionalCapPolicy struct {
// Policy-local config: reject any order above this absolute notional.
MaxAbsNotional param.Volume
}
func (p *NotionalCapPolicy) Close() {}
func (p *NotionalCapPolicy) Name() string { return "NotionalCapPolicy" }
func (p *NotionalCapPolicy) PolicyGroupID() model.PolicyGroupID {
return model.DefaultPolicyGroupID
}
func (p *NotionalCapPolicy) CheckPreTradeStart(
pretrade.Context,
model.Order,
) []reject.Reject {
return nil
}
func (p *NotionalCapPolicy) PerformPreTradeCheck(
_ pretrade.Context,
order model.Order,
_ tx.Mutations,
_ pretrade.Result,
) []reject.Reject {
operation, ok := order.Operation().Get()
if !ok {
return reject.NewSingleItemList(
reject.CodeMissingRequiredField,
p.Name(),
"required order field missing",
"operation is not set",
reject.ScopeOrder,
)
}
// Translate the public order surface into one number that this policy
// can reason about: requested notional.
tradeAmount, ok := operation.TradeAmount().Get()
if !ok {
return reject.NewSingleItemList(
reject.CodeMissingRequiredField,
p.Name(),
"required order field missing",
"trade_amount is not set",
reject.ScopeOrder,
)
}
var requestedNotional param.Volume
if tradeAmount.IsVolume() {
requestedNotional = tradeAmount.MustVolume()
} else {
price, ok := operation.Price().Get()
if !ok {
return reject.NewSingleItemList(
reject.CodeOrderValueCalculationFailed,
p.Name(),
"order value calculation failed",
"price not provided for evaluating notional",
reject.ScopeOrder,
)
}
notional, err := price.CalculateVolume(tradeAmount.MustQuantity())
if err != nil {
return reject.NewSingleItemList(
reject.CodeOrderValueCalculationFailed,
p.Name(),
"order value calculation failed",
"price and quantity could not be used to evaluate notional",
reject.ScopeOrder,
)
}
requestedNotional = notional
}
if requestedNotional.Compare(p.MaxAbsNotional) > 0 {
// Business validation failures should become explicit rejects.
return reject.NewSingleItemList(
reject.CodeRiskLimitExceeded,
p.Name(),
"strategy cap exceeded",
fmt.Sprintf(
"requested notional %v, max allowed: %v",
requestedNotional, p.MaxAbsNotional,
),
reject.ScopeOrder,
)
}
// This policy only validates. It does not reserve mutable state.
return nil
}
func (p *NotionalCapPolicy) ApplyExecutionReport(
pretrade.PostTradeContext,
model.ExecutionReport,
pretrade.PostTradeAdjustments,
pretrade.PostTradePnls,
) []reject.AccountBlock {
return nil
}
func (p *NotionalCapPolicy) ApplyAccountAdjustment(
accountadjustment.Context,
param.AccountID,
model.AccountAdjustment,
tx.Mutations,
pretrade.AccountOutcomes,
) (pretrade.PolicyAccountAdjustmentResult, []reject.Reject) {
return pretrade.PolicyAccountAdjustmentResult{}, nil
}Python
import openpit
class NotionalCapPolicy(openpit.pretrade.Policy):
def __init__(self, max_abs_notional: openpit.param.Volume) -> None:
# Policy-local config: reject any order above this absolute notional.
self._max_abs_notional = max_abs_notional
@property
def name(self) -> str:
return "NotionalCapPolicy"
def perform_pre_trade_check(
self,
ctx: openpit.pretrade.Context,
order: openpit.Order,
) -> openpit.pretrade.PolicyPreTradeResult:
assert order.operation is not None
# Translate the public order surface into one number that this policy
# can reason about: requested notional.
trade_amount = order.operation.trade_amount
if trade_amount.is_volume:
requested_notional = trade_amount.as_volume
else:
assert trade_amount.is_quantity
assert order.operation.price is not None
requested_notional = order.operation.price.calculate_volume(
trade_amount.as_quantity
)
if requested_notional > self._max_abs_notional:
# Business validation failures should become explicit rejects,
# not exceptions.
return openpit.pretrade.PolicyPreTradeResult.reject(
rejects=[
openpit.pretrade.PolicyReject(
code=openpit.pretrade.RejectCode.RISK_LIMIT_EXCEEDED,
reason="strategy cap exceeded",
details=(
"requested notional "
f"{requested_notional}, "
f"max allowed: {self._max_abs_notional}"
),
scope=openpit.pretrade.RejectScope.ORDER,
)
]
)
# This policy only validates. It does not reserve mutable state.
return openpit.pretrade.PolicyPreTradeResult.accept()
def apply_execution_report(
self,
ctx: openpit.pretrade.PostTradeContext,
report: openpit.ExecutionReport,
) -> openpit.pretrade.PostTradeResult | None:
_ = ctx, report
return NoneJavaScript
import { Order } from "@openpit/engine/model";
import { Volume } from "@openpit/engine/param";
import {
type Context,
type Policy,
type PolicyPreTradeResult,
type PolicyReject,
} from "@openpit/engine/pretrade";
// Reject any order above this absolute notional. Implemented against the
// public `Policy` interface; the callbacks read the typed `Order` view.
function notionalCapPolicy(maxAbsNotional: Volume): Policy {
const name = "NotionalCapPolicy";
return {
name,
checkPreTradeStart(ctx: Context, order: Order): Iterable<PolicyReject> {
void ctx;
void order;
return [];
},
performPreTradeCheck(
ctx: Context,
order: Order,
): PolicyPreTradeResult | null {
void ctx;
const operation = order.operation;
if (operation === undefined) {
return {
rejects: [
{
code: "MissingRequiredField",
reason: "required order field missing",
details: "operation is not set",
scope: "order",
},
],
};
}
// Translate the public order surface into one number this policy can
// reason about: requested notional.
const tradeAmount = operation.tradeAmount;
if (tradeAmount === undefined) {
return {
rejects: [
{
code: "MissingRequiredField",
reason: "required order field missing",
details: "tradeAmount is not set",
scope: "order",
},
],
};
}
let requestedNotional: Volume;
if (tradeAmount.isVolume) {
requestedNotional = tradeAmount.asVolume!;
} else {
const price = operation.price;
if (price === undefined) {
return {
rejects: [
{
code: "OrderValueCalculationFailed",
reason: "order value calculation failed",
details: "price not provided for evaluating notional",
scope: "order",
},
],
};
}
requestedNotional = price.calculateVolume(tradeAmount.asQuantity!);
}
if (requestedNotional.compare(maxAbsNotional) > 0) {
// Business validation failures should become explicit rejects.
return {
rejects: [
{
code: "RiskLimitExceeded",
reason: "strategy cap exceeded",
details: `requested notional ${requestedNotional.toString()}, max allowed: ${maxAbsNotional.toString()}`,
scope: "order",
},
],
};
}
// This policy only validates. It does not reserve mutable state.
return null;
},
};
}C++
#include <openpit/openpit.hpp>
// Computes settlement notional with the exact domain value types. A boundary
// failure is an API error and therefore propagates as `openpit::Error`.
[[nodiscard]] openpit::param::Volume CalculateNotional(
const openpit::param::Price& price,
const openpit::param::Quantity& quantity) {
return price.CalculateVolume(quantity);
}
class NotionalCapPolicy {
public:
// Policy-local config: reject any order above this absolute notional.
explicit NotionalCapPolicy(openpit::param::Volume maxAbsNotional)
: m_maxAbsNotional(maxAbsNotional) {}
[[nodiscard]] std::string_view Name() const noexcept {
return "NotionalCapPolicy";
}
void PerformPreTradeCheck(const openpit::model::Order& order,
const openpit::pretrade::Context& context,
openpit::tx::Mutations& mutations,
openpit::pretrade::Result& result,
openpit::pretrade::PolicyDecision& decision) const {
static_cast<void>(context);
static_cast<void>(mutations);
static_cast<void>(result);
if (!order.operation.has_value()) {
openpit::pretrade::PushReject(
decision,
openpit::pretrade::Reject(
std::string(Name()), openpit::pretrade::RejectScope::Order,
openpit::pretrade::RejectCode::MissingRequiredField,
"required order field missing", "operation is not set"));
return;
}
const openpit::model::OrderOperation& operation = *order.operation;
// Translate the public order surface into one number that this policy can
// reason about: requested notional.
if (!operation.tradeAmount.has_value()) {
openpit::pretrade::PushReject(
decision,
openpit::pretrade::Reject(
std::string(Name()), openpit::pretrade::RejectScope::Order,
openpit::pretrade::RejectCode::MissingRequiredField,
"required order field missing", "trade_amount is not set"));
return;
}
const openpit::model::TradeAmount& tradeAmount = *operation.tradeAmount;
// A volume trade amount is already the notional; a quantity trade amount
// must be priced into a notional (notional = price * quantity).
std::optional<openpit::param::Volume> requestedNotional =
tradeAmount.AsVolume();
if (!requestedNotional.has_value()) {
const std::optional<openpit::param::Quantity> quantity =
tradeAmount.AsQuantity();
if (!operation.price.has_value()) {
openpit::pretrade::PushReject(
decision,
openpit::pretrade::Reject(
std::string(Name()), openpit::pretrade::RejectScope::Order,
openpit::pretrade::RejectCode::OrderValueCalculationFailed,
"order value calculation failed",
"price not provided for evaluating notional"));
return;
}
requestedNotional = CalculateNotional(*operation.price, *quantity);
}
if (*requestedNotional > m_maxAbsNotional) {
// Business validation failures should become explicit rejects.
openpit::pretrade::PushReject(
decision,
openpit::pretrade::Reject(
std::string(Name()), openpit::pretrade::RejectScope::Order,
openpit::pretrade::RejectCode::RiskLimitExceeded,
"strategy cap exceeded",
"requested notional " + requestedNotional->ToString() +
", max allowed: " + m_maxAbsNotional.ToString()));
return;
}
// This policy only validates. It does not reserve mutable state.
}
[[nodiscard]] std::vector<openpit::accounts::AccountBlock>
ApplyExecutionReport(
const openpit::pretrade::PostTradeContext& context,
const openpit::ExecutionReport& report,
openpit::pretrade::PostTradeAdjustments& adjustments,
openpit::pretrade::PostTradePnls& pnls) const {
static_cast<void>(context);
static_cast<void>(report);
static_cast<void>(adjustments);
static_cast<void>(pnls);
return {};
}
private:
openpit::param::Volume m_maxAbsNotional;
};Rust
use openpit::param::{TradeAmount, Volume};
use openpit::pretrade::{
PolicyPreTradeResult, PostTradeContext, PreTradeContext, PreTradePolicy, Reject,
RejectCode, RejectScope, Rejects,
};
use openpit::Mutations;
use openpit::{HasOrderPrice, HasTradeAmount};
struct NotionalCapPolicy {
// Policy-local config: reject any order above this absolute notional.
max_abs_notional: Volume,
}
impl<O, R, A, Sync> PreTradePolicy<O, R, A, Sync> for NotionalCapPolicy
where
O: HasTradeAmount + HasOrderPrice,
Sync: openpit::SyncMode,
{
fn name(&self) -> &str {
"NotionalCapPolicy"
}
fn perform_pre_trade_check(
&self,
_ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
order: &O,
_mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
// Translate the public order surface into one number that this policy
// can reason about: requested notional.
let trade_amount = match order.trade_amount() {
Ok(trade_amount) => trade_amount,
Err(error) => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::MissingRequiredField,
"required order field missing",
error.to_string(),
)));
}
};
let price = match order.price() {
Ok(price) => price,
Err(error) => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::MissingRequiredField,
"required order field missing",
error.to_string(),
)));
}
};
let requested_notional = match (trade_amount, price) {
(TradeAmount::Volume(volume), _) => volume,
(TradeAmount::Quantity(quantity), Some(price)) => {
match price.calculate_volume(quantity) {
Ok(v) => v,
Err(_) => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::OrderValueCalculationFailed,
"order value calculation failed",
"price and quantity could not be used to evaluate notional",
)));
}
}
}
(TradeAmount::Quantity(_), None) => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::OrderValueCalculationFailed,
"order value calculation failed",
"price not provided for evaluating cash flow/notional/volume",
)));
}
_ => {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::UnsupportedOrderType,
"unsupported order type",
"custom trade amount variant is not supported by this policy",
)));
}
};
if requested_notional > self.max_abs_notional {
// Business validation failures should become explicit rejects.
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::RiskLimitExceeded,
"strategy cap exceeded",
format!(
"requested notional {}, max allowed: {}",
requested_notional, self.max_abs_notional
),
)));
}
Ok(None)
}
fn apply_execution_report(
&self,
_ctx: &PostTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_report: &R,
) -> Option<openpit::PostTradeResult> {
None
}
}If at least one main-stage policy rejects, the engine does not return a reservation and rolls back all registered mutations in reverse order.
Rollback order is deterministic:
- registration order for commit
- reverse registration order for rollback
This pattern is useful when one policy updates intermediate in-memory state and the same policy decides that the request must be rejected.
Go
package main
import (
"fmt"
"go.openpit.dev/openpit/accountadjustment"
"go.openpit.dev/openpit/model"
"go.openpit.dev/openpit/param"
"go.openpit.dev/openpit/pretrade"
"go.openpit.dev/openpit/reject"
"go.openpit.dev/openpit/tx"
)
type ReserveThenValidatePolicy struct {
reserved param.Volume
limit param.Volume
}
func (p *ReserveThenValidatePolicy) Close() {}
func (p *ReserveThenValidatePolicy) Name() string {
return "ReserveThenValidatePolicy"
}
func (p *ReserveThenValidatePolicy) PolicyGroupID() model.PolicyGroupID {
return model.DefaultPolicyGroupID
}
func (p *ReserveThenValidatePolicy) CheckPreTradeStart(
pretrade.Context,
model.Order,
) []reject.Reject {
return nil
}
func (p *ReserveThenValidatePolicy) PerformPreTradeCheck(
_ pretrade.Context,
_ model.Order,
mutations tx.Mutations,
_ pretrade.Result,
) []reject.Reject {
// Pretend that this request needs a temporary reservation of 100.
// We apply it eagerly because downstream logic wants to observe the
// tentative state immediately.
prevReserved := p.reserved
nextReserved, _ := param.NewVolumeFromString("100")
p.reserved = nextReserved
_ = mutations.Push(
func() {
// Commit is empty: state was applied eagerly.
},
func() {
p.reserved = prevReserved
},
)
if p.reserved.Compare(p.limit) > 0 {
// Return the reject after the rollback mutation is registered.
// The engine will restore the previous state automatically.
return reject.NewSingleItemList(
reject.CodeRiskLimitExceeded,
p.Name(),
"temporary reservation exceeds limit",
fmt.Sprintf("reserved %v, limit: %v", nextReserved, p.limit),
reject.ScopeOrder,
)
}
return nil
}
func (p *ReserveThenValidatePolicy) ApplyExecutionReport(
pretrade.PostTradeContext,
model.ExecutionReport,
pretrade.PostTradeAdjustments,
pretrade.PostTradePnls,
) []reject.AccountBlock {
return nil
}
func (p *ReserveThenValidatePolicy) ApplyAccountAdjustment(
accountadjustment.Context,
param.AccountID,
model.AccountAdjustment,
tx.Mutations,
pretrade.AccountOutcomes,
) (pretrade.PolicyAccountAdjustmentResult, []reject.Reject) {
return pretrade.PolicyAccountAdjustmentResult{}, nil
}Python
import openpit
class ReserveThenValidatePolicy(openpit.pretrade.Policy):
def __init__(self) -> None:
self._reserved = openpit.param.Volume(0.0)
self._limit = openpit.param.Volume(50.0)
@property
def name(self) -> str:
return "ReserveThenValidatePolicy"
def perform_pre_trade_check(
self,
ctx: openpit.pretrade.Context,
order: openpit.Order,
) -> openpit.pretrade.PolicyPreTradeResult:
assert order.operation is not None
# Pretend that this request needs a temporary reservation of 100.
# We apply it eagerly because downstream logic wants to observe the
# tentative state immediately.
prev_reserved = self._reserved
next_reserved = openpit.param.Volume(100.0)
self._reserved = next_reserved
rollback = openpit.Mutation(
commit=lambda: None, # Commit is empty: state was applied eagerly.
rollback=lambda: setattr(self, "_reserved", prev_reserved),
)
if next_reserved > self._limit:
# Return the reject together with the rollback mutation.
# The engine will restore the previous state automatically.
return openpit.pretrade.PolicyPreTradeResult.reject(
rejects=[
openpit.pretrade.PolicyReject(
code=openpit.pretrade.RejectCode.RISK_LIMIT_EXCEEDED,
reason="temporary reservation exceeds limit",
details=(
f"reserved {next_reserved}, "
f"limit: {self._limit}"
),
scope=openpit.pretrade.RejectScope.ORDER,
)
],
mutations=[rollback],
)
return openpit.pretrade.PolicyPreTradeResult.accept(mutations=[rollback])
def apply_execution_report(
self,
ctx: openpit.pretrade.PostTradeContext,
report: openpit.ExecutionReport,
) -> openpit.pretrade.PostTradeResult | None:
_ = ctx, report
return NoneJavaScript
import { Order } from "@openpit/engine/model";
import { Volume } from "@openpit/engine/param";
import {
type Context,
type Policy,
type PolicyPreTradeResult,
} from "@openpit/engine/pretrade";
// Updates intermediate in-memory state and may then reject the same request.
function reserveThenValidatePolicy(): Policy {
// Policy-local state, captured by the hook closure.
let reserved = Volume.fromString("0");
const limit = Volume.fromString("50");
return {
name: "ReserveThenValidatePolicy",
checkPreTradeStart() {
return [];
},
performPreTradeCheck(
ctx: Context,
order: Order,
): PolicyPreTradeResult | null {
void ctx;
void order;
// Pretend that this request needs a temporary reservation of 100. We
// apply it eagerly because downstream logic wants to observe the
// tentative state immediately.
const prevReserved = reserved;
const nextReserved = Volume.fromString("100");
reserved = nextReserved;
// Commit is empty: state was applied eagerly. Rollback restores the
// previous value if any policy rejects; the engine runs it
// automatically in reverse registration order.
const rollback = {
commit: () => {},
rollback: () => {
reserved = prevReserved;
},
};
if (nextReserved.compare(limit) > 0) {
// Return the reject together with the rollback mutation.
return {
rejects: [
{
code: "RiskLimitExceeded",
reason: "temporary reservation exceeds limit",
details: `reserved ${nextReserved.toString()}, limit: ${limit.toString()}`,
scope: "order",
},
],
mutations: [rollback],
};
}
return { mutations: [rollback] };
},
};
}C++
#include <openpit/openpit.hpp>
#include <memory>
#include <string_view>
class ReserveThenValidatePolicy {
public:
ReserveThenValidatePolicy() = default;
[[nodiscard]] std::string_view Name() const noexcept {
return "ReserveThenValidatePolicy";
}
void PerformPreTradeCheck(const openpit::model::Order& order,
const openpit::pretrade::Context& context,
openpit::tx::Mutations& mutations,
openpit::pretrade::Result& result,
openpit::pretrade::PolicyDecision& decision) const {
static_cast<void>(order);
static_cast<void>(context);
static_cast<void>(mutations);
static_cast<void>(result);
// Pretend that this request needs a temporary reservation of 100. We apply
// it eagerly because downstream logic wants to observe the tentative state
// immediately.
const openpit::param::Volume prevReserved = m_reserved;
const openpit::param::Volume nextReserved =
openpit::param::Volume::FromString("100");
m_reserved = nextReserved;
if (m_reserved > m_limit) {
// The decision is rejected, so the engine will not apply this request:
// restore the previous state before returning the reject.
m_reserved = prevReserved;
openpit::pretrade::PushReject(
decision,
openpit::pretrade::Reject(
std::string(Name()), openpit::pretrade::RejectScope::Order,
openpit::pretrade::RejectCode::RiskLimitExceeded,
"temporary reservation exceeds limit",
"reserved " + nextReserved.ToString() +
", limit: " + m_limit.ToString()));
}
}
[[nodiscard]] std::vector<openpit::accounts::AccountBlock>
ApplyExecutionReport(
const openpit::pretrade::PostTradeContext& context,
const openpit::ExecutionReport& report,
openpit::pretrade::PostTradeAdjustments& adjustments,
openpit::pretrade::PostTradePnls& pnls) const {
static_cast<void>(context);
static_cast<void>(report);
static_cast<void>(adjustments);
static_cast<void>(pnls);
return {};
}
private:
mutable openpit::param::Volume m_reserved =
openpit::param::Volume::FromString("0");
openpit::param::Volume m_limit =
openpit::param::Volume::FromString("50");
};Rust
use std::cell::RefCell;
use std::rc::Rc;
use openpit::param::Volume;
use openpit::pretrade::{
PolicyPreTradeResult, PostTradeContext, PreTradeContext, PreTradePolicy, Reject,
RejectCode, RejectScope, Rejects,
};
use openpit::{Mutation, Mutations};
struct ReserveThenValidatePolicy {
reserved: Rc<RefCell<Volume>>,
next: Volume,
limit: Volume,
}
impl<O, R, A, Sync> PreTradePolicy<O, R, A, Sync> for ReserveThenValidatePolicy
where
Sync: openpit::SyncMode,
{
fn name(&self) -> &str {
"ReserveThenValidatePolicy"
}
fn perform_pre_trade_check(
&self,
_ctx: &PreTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_order: &O,
mutations: &mut Mutations,
) -> Result<Option<PolicyPreTradeResult>, Rejects> {
let prev = *self.reserved.borrow();
let rollback_reserved = Rc::clone(&self.reserved);
let next = self.next;
*self.reserved.borrow_mut() = next;
mutations.push(Mutation::new(
|| {
// Commit is empty: state was applied eagerly.
},
move || {
*rollback_reserved.borrow_mut() = prev;
},
));
if next > self.limit {
return Err(Rejects::from(Reject::new(
<Self as PreTradePolicy<O, R, A, Sync>>::name(self),
RejectScope::Order,
RejectCode::RiskLimitExceeded,
"temporary reservation exceeds limit",
format!("reserved {}, limit: {}", next, self.limit),
)));
}
Ok(None)
}
fn apply_execution_report(
&self,
_ctx: &PostTradeContext<<Sync as openpit::SyncMode>::StorageLockingPolicyFactory>,
_report: &R,
) -> Option<openpit::PostTradeResult> {
None
}
}Go uses ClientEngine and typed policy interfaces to work with project-specific
order and report types:
- Embed
model.Orderinto a custom struct to add project-specific fields. - Embed
model.ExecutionReportinto a custom struct to add project-specific fields. - Implement
pretrade.ClientPreTradePolicy[Order, Report]- all four callbacks receive the typed project struct, not the genericmodel.Order; account adjustment usesmodel.AccountAdjustmentregardless of client type. - Build the engine with
NewClientPreTradeEngineBuilder[Order, Report](), which returns a*ClientEngine[Order, Report, ...]. The client engine wraps each submitted value in a cgo handle and routes it to the typed policy callbacks.
Go
package main
import (
"fmt"
"log"
"go.openpit.dev/openpit"
"go.openpit.dev/openpit/accountadjustment"
"go.openpit.dev/openpit/model"
"go.openpit.dev/openpit/param"
"go.openpit.dev/openpit/pretrade"
"go.openpit.dev/openpit/reject"
"go.openpit.dev/openpit/tx"
)
// StrategyOrder carries project-specific metadata alongside the standard order.
type StrategyOrder struct {
model.Order
StrategyTag string
}
// StrategyReport carries project-specific metadata alongside
// the standard report.
type StrategyReport struct {
model.ExecutionReport
VenueExecID string
}
// StrategyTagPolicy rejects orders from blocked strategy tags.
type StrategyTagPolicy struct{}
func (*StrategyTagPolicy) Close() {}
func (*StrategyTagPolicy) Name() string { return "StrategyTagPolicy" }
func (*StrategyTagPolicy) PolicyGroupID() model.PolicyGroupID {
return model.DefaultPolicyGroupID
}
func (p *StrategyTagPolicy) CheckPreTradeStart(
_ pretrade.Context,
order StrategyOrder,
) []reject.Reject {
if order.StrategyTag == "blocked" {
return reject.NewSingleItemList(
reject.CodeComplianceRestriction,
p.Name(),
"strategy blocked",
fmt.Sprintf("strategy tag %q is not allowed", order.StrategyTag),
reject.ScopeOrder,
)
}
return nil
}
func (*StrategyTagPolicy) PerformPreTradeCheck(
pretrade.Context,
StrategyOrder,
tx.Mutations,
pretrade.Result,
) []reject.Reject {
return nil
}
func (*StrategyTagPolicy) ApplyExecutionReport(
pretrade.PostTradeContext,
StrategyReport,
pretrade.PostTradeAdjustments,
pretrade.PostTradePnls,
) []reject.AccountBlock {
return nil
}
func (*StrategyTagPolicy) ApplyAccountAdjustment(
accountadjustment.Context,
param.AccountID,
model.AccountAdjustment,
tx.Mutations,
pretrade.AccountOutcomes,
) (pretrade.PolicyAccountAdjustmentResult, []reject.Reject) {
return pretrade.PolicyAccountAdjustmentResult{}, nil
}
func main() {
engine, err := openpit.NewClientPreTradeEngineBuilder[
StrategyOrder, StrategyReport,
]().
FullSync().
PreTrade(&StrategyTagPolicy{}).
Build()
if err != nil {
log.Fatal(err)
}
defer engine.Stop()
order := StrategyOrder{Order: model.NewOrder(), StrategyTag: "alpha"}
request, rejects, err := engine.StartPreTrade(order)
if err != nil {
log.Fatal(err)
}
if rejects != nil {
for _, r := range rejects {
fmt.Printf("rejected by %s: %s\n", r.Policy, r.Reason)
}
return
}
defer request.Close()
reservation, rejects, err := request.Execute()
if err != nil {
log.Fatal(err)
}
if rejects != nil {
for _, r := range rejects {
fmt.Printf("rejected by %s: %s\n", r.Policy, r.Reason)
}
return
}
defer reservation.Close()
reservation.Commit()
}Python custom models inherit from openpit.Order or openpit.ExecutionReport.
The original subclass instance reaches policy callbacks unchanged. Policies
access project-specific attributes by casting the received base type.
Python
import typing
import openpit
class StrategyOrder(openpit.Order):
def __init__(
self,
*,
operation: openpit.OrderOperation,
strategy_tag: str,
) -> None:
super().__init__(operation=operation)
# Project-specific metadata carried alongside the standard order fields.
self.strategy_tag = strategy_tag
class StrategyReport(openpit.ExecutionReport):
def __init__(
self,
*,
operation: openpit.ExecutionReportOperation,
financial_impact: openpit.FinancialImpact,
venue_exec_id: str,
) -> None:
super().__init__(operation=operation, financial_impact=financial_impact)
# Project-specific metadata alongside the standard report fields.
self.venue_exec_id = venue_exec_id
class StrategyTagPolicy(openpit.pretrade.Policy):
def __init__(self) -> None:
self.last_venue_exec_id: str | None = None
@property
def name(self) -> str:
return "StrategyTagPolicy"
def check_pre_trade_start(
self,
ctx: openpit.pretrade.Context,
order: openpit.Order,
) -> list[openpit.pretrade.PolicyReject]:
# The original subclass instance reaches the callback unchanged.
strategy_order = typing.cast(StrategyOrder, order)
if strategy_order.strategy_tag == "blocked":
return [
openpit.pretrade.PolicyReject(
code=openpit.pretrade.RejectCode.COMPLIANCE_RESTRICTION,
reason="strategy blocked",
details=(
"strategy tag "
f"{strategy_order.strategy_tag!r}"
" is not allowed"
),
scope=openpit.pretrade.RejectScope.ORDER,
)
]
return []
def apply_execution_report(
self,
ctx: openpit.pretrade.PostTradeContext,
report: openpit.ExecutionReport,
) -> openpit.pretrade.PostTradeResult | None:
del ctx
strategy_report = typing.cast(StrategyReport, report)
self.last_venue_exec_id = strategy_report.venue_exec_id
return None
policy = StrategyTagPolicy()
engine = (
openpit.Engine.builder()
.no_sync()
.pre_trade(policy)
.build()
)
order = StrategyOrder(
operation=openpit.OrderOperation(
instrument=openpit.Instrument("AAPL", "USD"),
account_id=openpit.param.AccountId.from_int(99224416),
side=openpit.param.Side.BUY,
trade_amount=openpit.param.TradeAmount.quantity(10),
price=openpit.param.Price(25),
),
strategy_tag="alpha",
)
start_result = engine.start_pre_trade(order=order)
if not start_result:
messages = ", ".join(
f"{r.policy} [{r.code}]: {r.reason}: {r.details}"
for r in start_result.rejects
)
raise RuntimeError(messages)
execute_result = start_result.request.execute()
if not execute_result:
messages = ", ".join(
f"{r.policy} [{r.code}]: {r.reason}: {r.details}"
for r in execute_result.rejects
)
raise RuntimeError(messages)
execute_result.reservation.commit()
order.strategy_tag = "blocked"
blocked = engine.start_pre_trade(order=order)
assert not blocked
assert blocked.rejects[0].code == openpit.pretrade.RejectCode.COMPLIANCE_RESTRICTION
report = StrategyReport(
operation=openpit.ExecutionReportOperation(
instrument=openpit.Instrument("AAPL", "USD"),
account_id=openpit.param.AccountId.from_int(99224416),
side=openpit.param.Side.BUY,
),
financial_impact=openpit.FinancialImpact(
pnl=openpit.param.Pnl(0),
fee=openpit.param.Fee(0),
),
venue_exec_id="venue-123",
)
engine.apply_execution_report(report=report)
assert policy.last_venue_exec_id == "venue-123"JavaScript composes host metadata with the OpenPit wrapper types. The engine
validates the standard Order / ExecutionReport fields separately, while each
custom policy callback receives a fresh clone that preserves the submitted
object's custom fields, prototype, symbols, and cycles. Parameterize Policy
with the application model types to keep those fields typed in callbacks.
JavaScript
import { Engine } from "@openpit/engine";
import { ExecutionReport, Order } from "@openpit/engine/model";
import { TradeAmount } from "@openpit/engine/param";
import { type Policy } from "@openpit/engine/pretrade";
type StrategyOrder = Order & {
strategyTag: string;
};
type StrategyReport = ExecutionReport & {
venueExecId: string;
};
let appliedVenueExecId: string | undefined;
const strategyTagPolicy: Policy<StrategyOrder, StrategyReport> = {
name: "StrategyTagPolicy",
checkPreTradeStart(_ctx, order) {
if (order.strategyTag === "blocked") {
return [
{
code: "ComplianceRestriction",
reason: "strategy blocked",
details: `strategy tag ${order.strategyTag} is not allowed`,
scope: "order",
},
];
}
return [];
},
performPreTradeCheck() {
return null;
},
applyExecutionReport(_ctx, report) {
appliedVenueExecId = report.venueExecId;
return null;
},
};
const strategyOrder: StrategyOrder = Object.assign(new Order(), {
strategyTag: "alpha",
});
strategyOrder.operation = {
underlyingAsset: "AAPL",
settlementAsset: "USD",
accountId: 99_224_416n,
side: "BUY",
tradeAmount: TradeAmount.quantity("10"),
price: "25",
};
const strategyReport: StrategyReport = Object.assign(new ExecutionReport(), {
venueExecId: "venue-42",
});
strategyReport.operation = {
underlyingAsset: "AAPL",
settlementAsset: "USD",
accountId: 99_224_416n,
side: "BUY",
};
strategyReport.financialImpact = { pnl: "5", fee: "0.25" };
const engine = Engine.builder().preTrade(strategyTagPolicy).build();
const start = engine.startPreTrade(strategyOrder);
if (!start.ok) {
throw new Error("strategy order must pass the start stage");
}
const request = start.request;
if (request === undefined) {
throw new Error("accepted start result is missing its request");
}
const execute = request.execute();
if (!execute.ok) {
throw new Error("strategy order must pass the main stage");
}
const reservation = execute.reservation;
if (reservation === undefined) {
throw new Error("accepted execute result is missing its reservation");
}
reservation.commit();
engine.applyExecutionReport(strategyReport);
console.log(strategyOrder.strategyTag, appliedVenueExecId);C++ custom models derive from openpit::model::Order or
openpit::model::ExecutionReport and add project-specific fields. A typed
policy receives the concrete type through the SafeSlow adapter, which recovers
it from the context order with a checked cast; the adapter then drives the
engine builder through openpit::pretrade::CustomPolicy.
C++
#include <openpit/openpit.hpp>
#include <string>
// StrategyOrder carries project-specific metadata alongside the standard order.
struct StrategyOrder : public openpit::model::Order {
std::string strategyTag;
};
// StrategyReport carries project-specific metadata alongside the standard
// report.
struct StrategyReport : public openpit::model::ExecutionReport {
std::string venueExecId;
};
// StrategyTagPolicy rejects orders from blocked strategy tags.
class StrategyTagPolicy {
public:
explicit StrategyTagPolicy(std::shared_ptr<std::string> appliedVenueExecId)
: m_appliedVenueExecId(std::move(appliedVenueExecId)) {}
[[nodiscard]] std::string_view Name() const noexcept {
return "StrategyTagPolicy";
}
[[nodiscard]] std::optional<openpit::pretrade::Reject> CheckPreTradeStart(
const StrategyOrder& order) const {
if (order.strategyTag == "blocked") {
return openpit::pretrade::Reject(
std::string(Name()), openpit::pretrade::RejectScope::Order,
openpit::pretrade::RejectCode::ComplianceRestriction,
"strategy blocked",
"strategy tag \"" + order.strategyTag + "\" is not allowed");
}
return std::nullopt;
}
[[nodiscard]] std::vector<openpit::accounts::AccountBlock>
ApplyExecutionReport(
const openpit::pretrade::PostTradeContext& context,
const StrategyReport& report,
openpit::pretrade::PostTradeAdjustments& adjustments,
openpit::pretrade::PostTradePnls& pnls) const {
static_cast<void>(context);
static_cast<void>(adjustments);
static_cast<void>(pnls);
*m_appliedVenueExecId = report.venueExecId;
return {};
}
private:
std::shared_ptr<std::string> m_appliedVenueExecId;
};
using StrategyStartAdapter =
openpit::pretrade::StartPolicyAdapterWithSafeSlowArgType<StrategyTagPolicy,
StrategyOrder,
StrategyReport>;
const auto appliedVenueExecId = std::make_shared<std::string>();
openpit::pretrade::CustomPolicy<StrategyStartAdapter> policy(
"StrategyTagPolicy",
StrategyStartAdapter{StrategyTagPolicy{appliedVenueExecId}});
openpit::EngineBuilder builder(openpit::SyncPolicy::Full);
builder.Add(policy);
openpit::Engine engine = builder.Build();
StrategyOrder order;
openpit::model::OrderOperation op;
op.instrument = openpit::model::Instrument(::openpit::param::Asset("AAPL"),
::openpit::param::Asset("USD"));
op.accountId = openpit::param::AccountId::FromUint64(99224416);
op.side = openpit::model::Side::Buy;
op.tradeAmount =
openpit::model::TradeAmount::OfQuantity(
openpit::param::Quantity::FromString("10"));
op.price = openpit::param::Price::FromString("25");
order.operation = std::move(op);
order.strategyTag = "alpha";
openpit::pretrade::StartResult start = engine.StartPreTrade(order);
assert(start.Passed());
openpit::pretrade::ExecuteResult execute = start.request->Execute();
assert(execute.Passed());
execute.reservation->Commit();
StrategyReport report;
openpit::model::ExecutionReportOperation reportOp;
reportOp.instrument = openpit::model::Instrument(
::openpit::param::Asset("AAPL"), ::openpit::param::Asset("USD"));
reportOp.accountId = openpit::param::AccountId::FromUint64(99224416);
reportOp.side = openpit::model::Side::Buy;
report.operation = std::move(reportOp);
report.venueExecId = "venue-42";
const openpit::PostTradeResult post = engine.ApplyExecutionReport(report);
assert(post.accountBlocks.empty());
assert(*appliedVenueExecId == "venue-42");Rust uses capability traits (Has*) and can compose OrderOperation with
project-only fields plus Deref to inherit required capabilities.
Rust
use std::ops::Deref;
use openpit::{ExecutionReportOperation, OrderOperation};
struct StrategyOrder {
inner: OrderOperation,
strategy_tag: String,
}
impl Deref for StrategyOrder {
type Target = OrderOperation;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
struct StrategyReport {
inner: ExecutionReportOperation,
venue_exec_id: String,
}
impl Deref for StrategyReport {
type Target = ExecutionReportOperation;
fn deref(&self) -> &Self::Target {
&self.inner
}
}See Custom Rust Types for the available derive setup, manual trait implementations, and wrapper-composition patterns.
An account-adjustment policy can accept a modification and report an account
block in the same callback result. The engine commits the accepted batch, then
records every reported block before apply account adjustments returns. Thus a
caller that receives an account block already has a blocked account: every
later start stage for it is rejected with ACCOUNT BLOCKED, without involving
any policy start-check.
Pre-trade contexts still expose an optional account control handle when the
engine provides the account-block facility for an order's account. A pre-trade
policy may use that handle immediately or from a mutation commit closure. An
account-adjustment policy reports its block through its return value instead;
it does not mutate account blocking as an unreported callback side effect.
- Go: return
pretrade.PolicyAccountAdjustmentResultwithAccountBlocks: []reject.AccountBlock{...}and no rejects. - Python: return
openpit.pretrade.PolicyAccountAdjustmentResult(account_blocks=(openpit.pretrade.AccountBlock(...),)). - JavaScript: return a
PolicyAccountAdjustmentResultobject with anaccountBlocksarray. - C++: return
openpit::pretrade::PolicyAccountAdjustmentResultand append anaccounts::AccountBlocktoaccountBlocks. - Rust: return
Ok(PolicyAccountAdjustmentResult { account_adjustments: Vec::new(), account_blocks: vec![AccountBlock::new(...)] }).
Go
type BlockOnAdjustmentPolicy struct{}
func (*BlockOnAdjustmentPolicy) Close() {}
func (*BlockOnAdjustmentPolicy) Name() string {
return "BlockOnAdjustmentPolicy"
}
func (*BlockOnAdjustmentPolicy) PolicyGroupID() model.PolicyGroupID {
return model.DefaultPolicyGroupID
}
func (*BlockOnAdjustmentPolicy) CheckPreTradeStart(
pretrade.Context,
model.Order,
) []reject.Reject {
return nil
}
func (*BlockOnAdjustmentPolicy) PerformPreTradeCheck(
pretrade.Context,
model.Order,
tx.Mutations,
pretrade.Result,
) []reject.Reject {
return nil
}
func (*BlockOnAdjustmentPolicy) ApplyExecutionReport(
pretrade.PostTradeContext,
model.ExecutionReport,
pretrade.PostTradeAdjustments,
pretrade.PostTradePnls,
) []reject.AccountBlock {
return nil
}
func (p *BlockOnAdjustmentPolicy) ApplyAccountAdjustment(
_ accountadjustment.Context,
_ param.AccountID,
_ model.AccountAdjustment,
_ tx.Mutations,
_ pretrade.AccountOutcomes,
) (pretrade.PolicyAccountAdjustmentResult, []reject.Reject) {
return pretrade.PolicyAccountAdjustmentResult{
AccountBlocks: []reject.AccountBlock{reject.NewAccountBlock(
reject.CodeAccountBlocked,
p.Name(),
"blocked by account-adjustment policy",
"custom policy reported an account block from a callback",
)},
}, nil
}Python
import openpit
class BlockOnAdjustmentPolicy(openpit.pretrade.Policy):
@property
def name(self) -> str:
return "BlockOnAdjustmentPolicy"
def apply_account_adjustment(
self,
_ctx: openpit.AccountAdjustmentContext,
account_id: openpit.param.AccountId,
adjustment: openpit.AccountAdjustment,
) -> openpit.pretrade.PolicyAccountAdjustmentResult:
del account_id, adjustment
return openpit.pretrade.PolicyAccountAdjustmentResult(
account_blocks=(
openpit.pretrade.AccountBlock(
policy=self.name,
code=openpit.pretrade.RejectCode.ACCOUNT_BLOCKED,
reason="blocked by account-adjustment policy",
details="custom policy reported an account block from a callback",
),
),
)
engine = (
openpit.Engine.builder().no_sync().pre_trade(policy=BlockOnAdjustmentPolicy()).build()
)
# The accepted adjustment reports a block that the engine has already recorded.
result = engine.apply_account_adjustment(
account_id=openpit.param.AccountId.from_int(99224416),
adjustments=[
openpit.AccountAdjustment(
operation=openpit.AccountAdjustmentBalanceOperation(asset="USD")
)
],
)
assert result.ok
assert len(result.account_blocks) == 1
# A later order on the same account is rejected with ACCOUNT_BLOCKED, without
# any start-check involvement.
blocked = engine.start_pre_trade(
order=openpit.Order(
operation=openpit.OrderOperation(
instrument=openpit.Instrument("AAPL", "USD"),
account_id=openpit.param.AccountId.from_int(99224416),
side=openpit.param.Side.BUY,
trade_amount=openpit.param.TradeAmount.quantity(10),
price=openpit.param.Price(25),
),
)
)
assert not blocked.ok
assert blocked.rejects[0].code == openpit.pretrade.RejectCode.ACCOUNT_BLOCKEDJavaScript
import { Engine } from "@openpit/engine";
import { type AccountAdjustmentContext } from "@openpit/engine/accountadjustment";
import { type AccountAdjustment } from "@openpit/engine/model";
import { AccountId, TradeAmount } from "@openpit/engine/param";
import {
type Policy,
type PolicyAccountAdjustmentResult,
} from "@openpit/engine/pretrade";
import { AccountBlock } from "@openpit/engine/reject";
const blockOnAdjustmentPolicy: Policy = {
name: "BlockOnAdjustmentPolicy",
checkPreTradeStart() {
return [];
},
performPreTradeCheck() {
return null;
},
applyAccountAdjustment(
_ctx: AccountAdjustmentContext,
_accountId: AccountId,
_adjustment: AccountAdjustment,
): PolicyAccountAdjustmentResult {
return {
accountBlocks: [new AccountBlock(
"BlockOnAdjustmentPolicy",
"AccountBlocked",
"blocked by account-adjustment policy",
"custom policy reported an account block from a callback",
)],
};
},
};
const engine = Engine.builder()
.preTrade(blockOnAdjustmentPolicy)
.build();
// The accepted adjustment reports a block that the engine has already recorded.
const adjustmentResult = engine.applyAccountAdjustment(99224416, [
{ operation: { asset: "USD" } },
]);
if (!adjustmentResult.ok || adjustmentResult.accountBlocks.length !== 1) {
throw new Error("accepted adjustment must report one account block");
}
// A later order on the same account is rejected with AccountBlocked, without
// any start-check involvement.
const blocked = engine.startPreTrade({
operation: {
underlyingAsset: "AAPL",
settlementAsset: "USD",
accountId: 99224416,
side: "BUY",
tradeAmount: TradeAmount.quantity("10"),
price: "25",
},
});
if (blocked.ok) {
throw new Error("order must be blocked");
}
if (blocked.rejects[0]!.code !== "AccountBlocked") {
throw new Error("expected AccountBlocked");
}In C++, the account-adjustment hook returns the block in its policy result.
The outcome is the same as the other bindings: before the accepted batch result
is returned, the engine records the block, and every later start stage for that
account is rejected with ACCOUNT_BLOCKED without involving a policy
start-check.
C++
#include <openpit/openpit.hpp>
#include <string>
#include <string_view>
// BlockOnAdjustmentPolicy accepts the adjustment and reports an account block.
class BlockOnAdjustmentPolicy {
public:
[[nodiscard]] std::string_view Name() const noexcept {
return "BlockOnAdjustmentPolicy";
}
[[nodiscard]] openpit::pretrade::PolicyAccountAdjustmentResult
ApplyAccountAdjustment(
const openpit::accountadjustment::Context& context,
openpit::param::AccountId accountId,
const openpit::accountadjustment::AccountAdjustment& adjustment,
openpit::tx::Mutations& mutations,
openpit::pretrade::AccountOutcomes& outcomes) const {
static_cast<void>(accountId);
static_cast<void>(adjustment);
static_cast<void>(mutations);
static_cast<void>(outcomes);
static_cast<void>(context);
openpit::pretrade::PolicyAccountAdjustmentResult result;
result.accountBlocks.emplace_back(
openpit::pretrade::RejectCode::AccountBlocked, std::string(Name()),
"blocked by account-adjustment policy",
"custom policy reported an account block from a callback");
return result;
}
};
// Builds the canonical single-leg order for `accountId`.
[[nodiscard]] openpit::model::Order AccountOrder(std::uint64_t accountId) {
openpit::model::Order order;
openpit::model::OrderOperation op;
op.instrument = openpit::model::Instrument(::openpit::param::Asset("AAPL"),
::openpit::param::Asset("USD"));
op.accountId = ::openpit::param::AccountId::FromUint64(accountId);
op.side = openpit::model::Side::Buy;
op.tradeAmount =
openpit::model::TradeAmount::OfQuantity(
openpit::param::Quantity::FromString("10"));
op.price = openpit::param::Price::FromString("25");
order.operation = std::move(op);
return order;
}
openpit::EngineBuilder builder(openpit::SyncPolicy::None);
openpit::pretrade::CustomPolicy<BlockOnAdjustmentPolicy> policy(
"BlockOnAdjustmentPolicy", BlockOnAdjustmentPolicy{});
builder.Add(policy);
openpit::Engine engine = builder.Build();
const openpit::param::AccountId accountId =
openpit::param::AccountId::FromUint64(99224416);
// The accepted adjustment reports a block that the engine has already recorded.
openpit::accountadjustment::BalanceOperation balanceOp;
balanceOp.asset = ::openpit::param::Asset("USD");
openpit::accountadjustment::AccountAdjustment adjustment;
adjustment.operation =
openpit::accountadjustment::Operation::OfBalance(std::move(balanceOp));
openpit::accountadjustment::Amount amount;
amount.balance = openpit::param::AdjustmentAmount::Absolute(
openpit::param::PositionSize::FromString("0"));
adjustment.amount = std::move(amount);
const openpit::AdjustmentResult adjustmentResult = engine.ApplyAccountAdjustment(
accountId,
std::vector<openpit::accountadjustment::AccountAdjustment>{adjustment});
assert(adjustmentResult.Passed());
assert(adjustmentResult.accountBlocks.size() == 1);
// A later order on the same account is rejected with ACCOUNT_BLOCKED, without
// any start-check involvement.
openpit::pretrade::StartResult blocked =
engine.StartPreTrade(AccountOrder(99224416));Rust
struct BlockOnAdjustmentPolicy;
impl<Order, ExecutionReport, AccountAdjustment, Sync>
PreTradePolicy<Order, ExecutionReport, AccountAdjustment, Sync>
for BlockOnAdjustmentPolicy
where
Sync: openpit::SyncMode,
{
fn name(&self) -> &str {
"BlockOnAdjustmentPolicy"
}
fn apply_account_adjustment(
&self,
_ctx: &AccountAdjustmentContext<
<Sync as openpit::SyncMode>::StorageLockingPolicyFactory,
>,
_account_id: AccountId,
_adjustment: &AccountAdjustment,
_mutations: &mut Mutations,
) -> Result<PolicyAccountAdjustmentResult, Rejects> {
Ok(PolicyAccountAdjustmentResult {
account_adjustments: Vec::new(),
account_blocks: vec![AccountBlock::new(
"BlockOnAdjustmentPolicy",
RejectCode::AccountBlocked,
"blocked by account-adjustment policy",
"custom policy reported an account block from a callback",
)],
})
}
}The post-trade hook apply execution report receives a post-trade context
as its first argument. It carries the realized outcome of an execution report
back into policy state and exposes a lazy account-group accessor for the
report's account.
- Go:
ApplyExecutionReport(ctx pretrade.PostTradeContext, report, adjustments, pnls) []reject.AccountBlock; push account adjustments and account PnL into the two collectors. - Python:
apply_execution_report(self, ctx: openpit.pretrade.PostTradeContext, report) -> PostTradeResult | None; returnaccount_blocks,account_pnls, andaccount_adjustmentsin that result. - JavaScript:
applyExecutionReport(ctx: PostTradeContext, report) -> PostTradeResult | null | undefined; returnaccountBlocks,accountPnls, andaccountAdjustmentsin that result. - C++:
std::vector<accounts::AccountBlock> ApplyExecutionReport(const pretrade::PostTradeContext&, const R&, pretrade::PostTradeAdjustments&, pretrade::PostTradePnls&) const. - Rust:
apply_execution_report(&self, ctx: &PostTradeContext<..>, report: &R) -> Option<PostTradeResult>; returnaccount_blocks,account_pnls, andaccount_adjustmentsin that result.
Unlike the pre-trade and account-adjustment contexts, the post-trade context
carries no account control handle. Go and C++ report post-trade account blocks
through the hook return and push account-adjustment and account-PnL outcomes
into their collectors. Python, JavaScript, and Rust return all three independent
channels as fields of PostTradeResult. The engine merges those per-policy
results into the result returned to the caller, as described in
Policies.
The post-trade context exposes the report account's
account group id through account_group() (Rust) /
AccountGroup() (Go, returning an optional.Option) / account_group
(Python). The pre-trade and account-adjustment contexts expose the same
accessor for their bound account. The lookup is performed once and cached for
the lifetime of the context, so a policy can branch on the account's group
cheaply:
- pre-trade context: the order's account; the accessor yields nothing when the order carries no account;
- account-adjustment context: the account being adjusted;
- post-trade context: the execution report's account.
This is the account group, distinct from the per-policy policy group id
used by Pre-Trade Lock; see
Account Groups.
- Policies: built-in controls and policy catalog
- Dynamic Policy Reconfiguration: retune built-in policies at runtime without rebuilding the engine
- Pre-trade Pipeline: request and reservation semantics
-
Pre-Trade Lock: emit
lock_pricesfrom a policy'sPolicyPreTradeResultand reconcile them on execution reports - Account Adjustments: batch rollback semantics
- Account Blocking: the mutation finalizer contract, the engine-wide block, and the admin blocking API
-
Account Groups: account-group registry and the lazy
account groupaccessor on the policy contexts - Custom Go Types: Go ClientEngine and typed model composition
- Custom Python Types: Python model subclasses and custom-policy callback typing
- Custom JS Types: JavaScript/TypeScript custom order and execution-report fields
- Custom Cpp Types: C++ typed model composition and adapters
- Custom Rust Types: Rust model composition patterns