Skip to content

Policy API

Eugene Palchukovsky edited this page Jun 4, 2026 · 11 revisions

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 request step 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 stage or execute request while 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 uses model.AccountAdjustment);
  • adapters with payload validation: pretrade.NewSafeClientPreTradePolicy;
  • adapters without validation, for SDK-controlled paths: pretrade.NewUnsafeFastClientPreTradePolicy;
  • built-in native policies are registered via the Builtin builder 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)

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 name is 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
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
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.Order into a custom struct to add project-specific fields.
  • Embed model.ExecutionReport into a custom struct to add project-specific fields.
  • Implement pretrade.ClientPreTradePolicy[Order, Report] - all four callbacks receive the typed project struct, not the generic model.Order; account adjustment uses model.AccountAdjustment regardless 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()

Rust Custom Models

Rust uses capability traits (Has*) and can compose OrderOperation with project-only fields plus Deref to inherit required capabilities.

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 control is always present.
  • pre-trade context (start and main stage): account control is 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

  • Python: ctx.account_control is an openpit.AccountControl | None in pre-trade callbacks and an openpit.AccountControl in the account-adjustment callback. Block with ctx.account_control.block(openpit.pretrade.AccountBlock(policy=..., code=openpit.pretrade.RejectCode.ACCOUNT_BLOCKED, reason=..., details=...)).
  • Rust: ctx.account_control is an Option<AccountControl<..>> in PreTradeContext and an AccountControl<..> in AccountAdjustmentContext. Block with account_control.block(AccountBlock::new(policy, code, reason, details)) using openpit::pretrade::{AccountBlock, RejectCode}.
  • Go: the account-adjustment context exposes the same facility; block through the context handle with an AccountBlock carrying reject.CodeAccountBlocked.

Example: Block an Account from an Adjustment Callback

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

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).
  • 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

Clone this wiki locally