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.
Stage Contracts¶
- 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.
For account-adjustment batch policy hooks, see Account Adjustments.
Concurrency and Policy State¶
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.
Language Interfaces¶
Go Interface¶
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.
Python Interface¶
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
PolicyDecision | Iterable[PolicyReject] | tuple[Mutation, ...] | None
Policies can register:
Mutation(commit=callable, rollback=callable)
JavaScript / TypeScript Interface¶
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,
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++ Interface¶
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&) const- return the account blocks to raise, optionally pushing group-tagged outcomes - account adjustment:
PolicyDecision ApplyAccountAdjustment(const accountadjustment::Context&, param::AccountId, const accountadjustment::AccountAdjustment&, tx::Mutations&, AccountOutcomes&) const- validate one adjustment, optionally registering mutations and pushing outcomes
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 through the native runtime. 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 catches a callback exception inside the C
trampoline and the invoking C++ engine method rethrows it after the native call
returns, so no exception crosses the C ABI. Exceptions represent API failures;
expected business outcomes are returned as Reject / PolicyDecision values.
Rust Interface¶
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) - start-stage callback receives:
&PreTradeContext,&Order - main-stage callback receives:
&PreTradeContext,&Order,&mut Mutations - account-adjustment callback receives:
&AccountAdjustmentContext,AccountId,&A,&mut Mutations
Example: Custom Main-Stage Policy¶
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,
) []reject.AccountBlock {
return nil
}
func (p *NotionalCapPolicy) ApplyAccountAdjustment(
accountadjustment.Context,
param.AccountID,
model.AccountAdjustment,
tx.Mutations,
pretrade.AccountOutcomes,
) []reject.Reject {
return nil
}
Python
import typing
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
@typing.override
def name(self) -> str:
return "NotionalCapPolicy"
@typing.override
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()
@typing.override
def apply_execution_report(
self,
ctx: openpit.pretrade.PostTradeContext,
report: openpit.ExecutionReport,
) -> openpit.pretrade.PostTradeResult | None:
_ = ctx, report
return None
JavaScript
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++
// Computes settlement notional from a per-unit price and an instrument
// quantity (notional = price * quantity), crossing the exact-decimal C ABI so
// the result is bit-for-bit identical across language bindings. Returns
// nullopt when the engine reports the multiplication as a value error, which
// the caller turns into an explicit reject rather than an exception.
[[nodiscard]] std::optional<openpit::param::Volume> CalculateNotional(
const openpit::param::Price& price,
const openpit::param::Quantity& quantity) {
OpenPitParamVolume raw{};
OpenPitParamError* error = nullptr;
if (!openpit_param_price_calculate_volume(price.Raw(), quantity.Raw(), &raw,
&error)) {
if (error != nullptr) {
openpit_destroy_param_error(error);
}
return std::nullopt;
}
return openpit::param::Volume::FromRaw(raw);
}
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.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 and quantity could not be used to evaluate notional"));
return;
}
}
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) const {
static_cast<void>(context);
static_cast<void>(report);
static_cast<void>(adjustments);
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
}
}
Rollback on Main-Stage Error¶
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
Example: Rollback Safety Pattern¶
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,
) []reject.AccountBlock {
return nil
}
func (p *ReserveThenValidatePolicy) ApplyAccountAdjustment(
accountadjustment.Context,
param.AccountID,
model.AccountAdjustment,
tx.Mutations,
pretrade.AccountOutcomes,
) []reject.Reject {
return nil
}
Python
import typing
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
@typing.override
def name(self) -> str:
return "ReserveThenValidatePolicy"
@typing.override
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])
@typing.override
def apply_execution_report(
self,
ctx: openpit.pretrade.PostTradeContext,
report: openpit.ExecutionReport,
) -> openpit.pretrade.PostTradeResult | None:
_ = ctx, report
return None
JavaScript
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++
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) const {
static_cast<void>(context);
static_cast<void>(report);
static_cast<void>(adjustments);
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
}
}
Custom Order and Execution Report Models¶
Go Custom Models¶
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 (p *StrategyTagPolicy) Close() {}
func (p *StrategyTagPolicy) Name() string { return "StrategyTagPolicy" }
func (p *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 (p *StrategyTagPolicy) PerformPreTradeCheck(
pretrade.Context,
StrategyOrder,
tx.Mutations,
pretrade.Result,
) []reject.Reject {
return nil
}
func (p *StrategyTagPolicy) ApplyExecutionReport(
pretrade.PostTradeContext,
StrategyReport,
pretrade.PostTradeAdjustments,
) []reject.AccountBlock {
return nil
}
func (p *StrategyTagPolicy) ApplyAccountAdjustment(
accountadjustment.Context,
param.AccountID,
model.AccountAdjustment,
tx.Mutations,
pretrade.AccountOutcomes,
) []reject.Reject {
return 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¶
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):
@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 []
engine = (
openpit.Engine.builder()
.no_sync()
.pre_trade(StrategyTagPolicy())
.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()
JS Custom Models¶
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¶
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++
// 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) const {
static_cast<void>(context);
static_cast<void>(adjustments);
*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("AAPL", "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("AAPL", "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 Custom Models¶
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 full derive setup, manual trait implementations, and wrapper composition patterns.
Blocking an Account from a Policy¶
A policy can block an account (kill switch) directly from a callback through the
context's account control handle. Once blocked, the engine rejects every later
start stage for that account with ACCOUNT BLOCKED, without involving any
policy start-check. Blocking is owned by the engine: the policy only asks the
handle to record the block.
Context availability differs by stage:
- account-adjustment context:
account controlis always present. - pre-trade context (start and main stage):
account controlis optional. It is present only when the engine exposes the account-block facility for the order's account; otherwise it is absent.
A policy may either block immediately, or capture the handle into a mutation rollback/commit closure to block on a deferred failure.
Language Surfaces¶
- Go: the account-adjustment context exposes the same facility; block through
ctx.AccountControl().Block(reject.NewAccountBlock(...)). - Python:
ctx.account_controlis anopenpit.AccountControl | Nonein pre-trade callbacks and anopenpit.AccountControlin the account-adjustment callback. Block withctx.account_control.block(openpit.pretrade.AccountBlock(policy=..., code=openpit.pretrade.RejectCode.ACCOUNT_BLOCKED, reason=..., details=...)). - JavaScript:
ctx.accountControlis optional in pre-trade callbacks and always present in the account-adjustment callback. Block withctx.accountControl.block(new AccountBlock(...)). - C++:
context.AccountControl()returns astd::optional<accounts::AccountControl>in the pre-trade context and anaccounts::AccountControldirectly in the account-adjustment context. Block from the latter withcontext.AccountControl().Block(accounts::AccountBlock(RejectCode::AccountBlocked, policy, reason, details)). - Rust:
ctx.account_controlis anOption<AccountControl<..>>inPreTradeContextand anAccountControl<..>inAccountAdjustmentContext. Block withaccount_control.block(AccountBlock::new(policy, code, reason, details))usingopenpit::pretrade::{AccountBlock, RejectCode}.
Example: Block an Account from an Adjustment Callback¶
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,
) []reject.AccountBlock {
return nil
}
func (p *BlockOnAdjustmentPolicy) ApplyAccountAdjustment(
ctx accountadjustment.Context,
_ param.AccountID,
_ model.AccountAdjustment,
_ tx.Mutations,
_ pretrade.AccountOutcomes,
) []reject.Reject {
ctx.AccountControl().Block(reject.NewAccountBlock(
reject.CodeAccountBlocked,
p.Name(),
"blocked via account control",
"custom policy blocked the account from a callback",
))
return 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,
) -> None:
del account_id, adjustment
# The adjustment context always exposes the account-block facility.
control: openpit.AccountControl = ctx.account_control
control.block(
openpit.pretrade.AccountBlock(
policy=self.name,
code=openpit.pretrade.RejectCode.ACCOUNT_BLOCKED,
reason="blocked via account_control",
details="custom policy blocked the account from a callback",
)
)
return None
engine = (
openpit.Engine.builder().no_sync().pre_trade(policy=BlockOnAdjustmentPolicy()).build()
)
# Driving an adjustment triggers the block.
engine.apply_account_adjustment(
account_id=openpit.param.AccountId.from_int(99224416),
adjustments=[
openpit.AccountAdjustment(
operation=openpit.AccountAdjustmentBalanceOperation(asset="USD")
)
],
)
# 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_BLOCKED
JavaScript
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 } 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,
) {
void accountId;
void adjustment;
// The adjustment context always exposes the account-block facility.
ctx.accountControl.block(
new AccountBlock(
"BlockOnAdjustmentPolicy",
"AccountBlocked",
"blocked via accountControl",
"custom policy blocked the account from a callback",
),
);
return null;
},
};
const engine = Engine.builder()
.preTrade(blockOnAdjustmentPolicy)
.build();
// Driving an adjustment triggers the block.
engine.applyAccountAdjustment(99224416, [{ operation: { asset: "USD" } }]);
// 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 receives an accountadjustment::Context
whose AccountControl() is always present. The policy records the kill switch
through it; the outcome is the same as the other bindings: once the account is
blocked, every later start stage for it is rejected with ACCOUNT_BLOCKED,
without involving any policy start-check.
C++
// BlockOnAdjustmentPolicy blocks the adjusted account from its callback.
class BlockOnAdjustmentPolicy {
public:
[[nodiscard]] std::string_view Name() const noexcept {
return "BlockOnAdjustmentPolicy";
}
[[nodiscard]] openpit::pretrade::PolicyDecision 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);
// The adjustment context always exposes the account-block facility.
context.AccountControl().Block(openpit::accounts::AccountBlock(
openpit::pretrade::RejectCode::AccountBlocked, std::string(Name()),
"blocked via account control",
"custom policy blocked the account from a callback"));
return {}; // Accept the adjustment; the block is the side effect.
}
};
// 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("AAPL", "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);
// Driving an adjustment triggers the block.
openpit::accountadjustment::BalanceOperation balanceOp;
balanceOp.asset = "USD";
openpit::accountadjustment::AccountAdjustment adjustment;
adjustment.operation =
openpit::accountadjustment::Operation::OfBalance(std::move(balanceOp));
openpit::accountadjustment::Amount amount;
amount.balance = openpit::param::AdjustmentAmount::OfAbsolute(
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());
// 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<Vec<AccountOutcomeEntry>, Rejects> {
ctx.account_control.block(AccountBlock::new(
"BlockOnAdjustmentPolicy",
RejectCode::AccountBlocked,
"blocked via account control",
"custom policy blocked the account from a callback",
));
Ok(Vec::new())
}
}
Post-Trade Context¶
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). - Python:
apply_execution_report(self, ctx: openpit.pretrade.PostTradeContext, report). - JavaScript:
applyExecutionReport(ctx: PostTradeContext, report). - C++:
ApplyExecutionReport(const pretrade::PostTradeContext&, const R&, pretrade::PostTradeAdjustments&). - Rust:
apply_execution_report(&self, ctx: &PostTradeContext<..>, report: &R).
Unlike the pre-trade and account-adjustment contexts, the post-trade context
carries no account control handle: a post-trade kill switch is reported
through the hook's return value (AccountBlock / PostTradeResult), as
described in Policies.
Reading the Account Group¶
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.
Related Pages¶
- 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 Groups: account-group registry and the lazy
account groupaccessor on the policy contexts - Custom Go Types: Go ClientEngine and typed model composition
- 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