Pre-trade Pipeline¶
OpenPit models pre-trade admission as two explicit stages plus a post-trade
update step. Prose uses the conceptual step names start stage, execute
request, finalize reservation, and apply execution report; exact API
names stay inside the code blocks and type tables.
Non-trade operation (NTO) batch validation is handled by apply account
adjustments and documented in Account Adjustments.
Lifecycle¶
start stage
-> start-stage policies
-> either reject immediately or return a deferred request
execute request
-> main-stage policies
-> either return one or more rejects or return a reservation
finalize reservation
-> finalize the reserved state exactly once
apply execution report
-> update cumulative policy state from realized outcomes
Stage Semantics¶
- Start stage: runs fast admission checks in registration order and aggregates rejects from all registered policies.
- Main stage: runs all registered main-stage policies, aggregates rejects, and rolls back collected mutations when any reject is produced.
- Reservation: state that must be committed or rolled back explicitly by the caller.
- Post-trade: feeds realized outcomes back into policies through an execution report.
- Shortcut:
execute pre-tradecomposesstart stageandexecute requestinto one call; it does not introduce a new lifecycle. - Context seam: start-stage and main-stage callbacks both receive
Pre-trade context. Operation payload (order) stays a separate callback argument.
Public Return Types¶
| Step | Go | Python | Rust |
|---|---|---|---|
| Start stage | (*pretrade.Request, []reject.Reject, error) |
StartResult |
Result<PreTradeRequest<_>, Rejects> |
| Main stage | (*pretrade.Reservation, []reject.Reject, error) |
ExecuteResult |
Result<PreTradeReservation, Rejects> |
| Post-trade | PostTradeResult |
PostTradeResult |
PostTradeResult |
The Rust SDK uses Result for business rejects. The Python SDK returns result
objects for business rejects and reserves exceptions for invalid inputs or API
misuse.
Request¶
Pre-trade request is the deferred, single-use handle returned after the
start stage passes. The main stage consumes it.
- In Rust,
Request::execute(self)consumes the request by type. - In Python, calling
request.execute()twice raisesRuntimeError.
Reservation¶
The reservation handle is returned after execute request passes (see the
return-type table above for the per-language name).
commitkeeps the reserved state.rollbackcancels it.- Dropping a Rust reservation without finalization rolls it back automatically.
- In Python, finalizing the same reservation twice raises
RuntimeError.
The reservation also carries a pre-trade lock: the reservation-time context a policy needs to reconcile later. If a registered policy (such as Spot Funds) reconciles fills against what it reserved, read the lock off the reservation, persist it, and attach it to every execution report for that order - see Execution Reports.
Rejects¶
Every reject carries the same public fields across the SDKs:
| Field | Meaning |
|---|---|
policy |
Stable policy name that produced the reject |
code |
Stable machine-readable reject code |
reason |
Short human-readable reject summary |
details |
Case-specific explanation |
scope |
order or account |
scope = account means the caller should treat the condition as broader than
one request, for example an active kill switch.
Execution Reports¶
Execution reports carry realized post-trade outcomes back into the engine. The common record-style surface is:
underlying assetorinstrument.underlying asset: asset that was tradedsettlement assetorinstrument.settlement asset: asset in which P&L and fees are measuredpnl: realized P&L contributionfee: fee or rebate contributionlock: the pre-trade lock captured at reservation time; attach the order's stored lock so reconciling policies can settle the reservation. Omitting it can block the account withMissingRequiredField.
PostTradeResult account blocks is non-empty when at least one
registered policy reports a blocked state after the report is applied.
Example: Handle a Start-Stage Reject¶
Go
request, rejects, err := engine.StartPreTrade(order)
if err != nil {
panic(err)
}
if rejects != nil {
for _, r := range rejects {
fmt.Printf(
"rejected by %s [%d]: %s (%s)\n",
r.Policy,
r.Code,
r.Reason,
r.Details,
)
}
return
}
defer request.Close()
Python
# Start stage returns either a reject or a deferred request handle.
start_result = engine.start_pre_trade(order=order)
if not start_result:
for reject in start_result.rejects:
print(
f"rejected by {reject.policy} "
f"[{reject.code}]: {reject.reason}: {reject.details}"
)
else:
# Keep the request object if later code wants to enter the main stage.
request = start_result.request
C++
// Start stage returns either a reject or a deferred request handle.
openpit::pretrade::StartResult startResult = engine.StartPreTrade(order);
if (!startResult.Passed()) {
for (const openpit::pretrade::Reject& reject : startResult.rejects) {
std::cout << "rejected by " << reject.policy << " ["
<< static_cast<int>(reject.code) << "]: " << reject.reason
<< ": " << reject.details << '\n';
}
} else {
// Keep the request if later code wants to enter the main stage.
openpit::pretrade::Request request = std::move(*startResult.request);
}
Rust
// Start stage returns either a reject or a deferred request handle.
match engine.start_pre_trade(order) {
Ok(request) => {
// Keep the request object if later code wants to enter the main stage.
let _request = request;
}
Err(rejects) => {
for reject in rejects.iter() {
eprintln!(
"rejected by {} [{}]: {} ({})",
reject.policy,
reject.code,
reject.reason,
reject.details
);
}
}
}
Example: Execute the Main Stage and Finalize the Reservation¶
Go
request, rejects, err := engine.StartPreTrade(order)
if err != nil {
panic(err)
}
if rejects != nil {
panic("start stage rejected")
}
defer request.Close()
reservation, rejects, err := request.Execute()
if err != nil {
panic(err)
}
if rejects != nil {
for _, r := range rejects {
fmt.Printf(
"rejected by %s [%d]: %s (%s)\n",
r.Policy,
r.Code,
r.Reason,
r.Details,
)
}
return
}
defer reservation.Close()
// Commit only after the caller knows the reservation should become durable.
reservation.Commit()
Python
start_result = engine.start_pre_trade(order=order)
# Main stage consumes the deferred request and returns reservation or rejects.
execute_result = start_result.request.execute()
if execute_result:
# Commit only after the caller knows the reservation should become durable.
execute_result.reservation.commit()
else:
for reject in execute_result.rejects:
print(
f"rejected by {reject.policy} "
f"[{reject.code}]: {reject.reason}: {reject.details}"
)
C++
openpit::pretrade::StartResult startResult = engine.StartPreTrade(order);
// Main stage consumes the deferred request and returns reservation or rejects.
openpit::pretrade::ExecuteResult executeResult = startResult.request->Execute();
if (executeResult.Passed()) {
// Commit only after the caller knows the reservation should become durable.
executeResult.reservation->Commit();
} else {
for (const openpit::pretrade::Reject& reject : executeResult.rejects) {
std::cout << "rejected by " << reject.policy << " ["
<< static_cast<int>(reject.code) << "]: " << reject.reason
<< ": " << reject.details << '\n';
}
}
Rust
let request = engine.start_pre_trade(order).expect("start stage must pass");
// Main stage consumes the deferred request and returns reservation or rejects.
match request.execute() {
Ok(mut reservation) => {
// Commit only after the caller knows the reservation should become durable.
reservation.commit()
}
Err(rejects) => {
for reject in rejects.iter() {
eprintln!(
"rejected by {} [{}]: {} ({})",
reject.policy,
reject.code,
reject.reason,
reject.details
);
}
}
}
Example: Shortcut for Start + Main Stages¶
Go
// The shortcut runs start stage and main stage as one convenience call.
reservation, rejects, err := engine.ExecutePreTrade(order)
if err != nil {
panic(err)
}
if rejects != nil {
for _, r := range rejects {
fmt.Printf(
"rejected by %s [%d]: %s (%s)\n",
r.Policy,
r.Code,
r.Reason,
r.Details,
)
}
return
}
defer reservation.Close()
// Finalization is still explicit even when the two stages are composed.
reservation.Commit()
Python
# The shortcut runs start stage and main stage as one convenience call.
execute_result = engine.execute_pre_trade(order=order)
if execute_result:
# Finalization is still explicit even when the two stages are composed.
execute_result.reservation.commit()
else:
for reject in execute_result.rejects:
print(
f"rejected by {reject.policy} "
f"[{reject.code}]: {reject.reason}: {reject.details}"
)
C++
// The shortcut runs start stage and main stage as one convenience call.
openpit::pretrade::ExecuteResult executeResult = engine.ExecutePreTrade(order);
if (executeResult.Passed()) {
// Finalization is still explicit even when the two stages are composed.
executeResult.reservation->Commit();
} else {
for (const openpit::pretrade::Reject& reject : executeResult.rejects) {
std::cout << "rejected by " << reject.policy << " ["
<< static_cast<int>(reject.code) << "]: " << reject.reason
<< ": " << reject.details << '\n';
}
}
Rust
// The shortcut runs start stage and main stage as one convenience call.
match engine.execute_pre_trade(order) {
Ok(mut reservation) => {
// Finalization is still explicit even when the two stages are composed.
reservation.commit()
}
Err(rejects) => {
for reject in rejects.iter() {
eprintln!(
"rejected by {} [{}]: {} ({})",
reject.policy,
reject.code,
reject.reason,
reject.details
);
}
}
}
Example: Apply Post-Trade Feedback¶
Go
// Execution reports feed realized outcomes back into cumulative policy state.
result, err := engine.ApplyExecutionReport(report)
if err != nil {
panic(err)
}
if len(result.AccountBlocks) > 0 {
fmt.Println("halt new orders until the blocked state is cleared")
}
Python
# Execution reports feed realized outcomes back into cumulative policy state.
result = engine.apply_execution_report(report=report)
if result.account_blocks:
print("halt new orders until the blocked state is cleared")
C++
// Execution reports feed realized outcomes back into cumulative policy state.
const openpit::PostTradeResult result = engine.ApplyExecutionReport(report);
if (!result.accountBlocks.empty()) {
std::cout << "halt new orders until the blocked state is cleared" << '\n';
}
Rust
// Execution reports feed realized outcomes back into cumulative policy state.
let result = engine.apply_execution_report(&report);
if !result.account_blocks.is_empty() {
eprintln!("halt new orders until the blocked state is cleared");
}
Caller Responsibilities¶
- Finalize every successful reservation exactly once.
- Keep the reservation alive until the host knows whether the order should keep consuming reserved state.
- Feed realized outcomes back through execution reports so cumulative controls stay aligned with external truth.
Related Pages¶
- Getting Started: First end-to-end setup
- Account Adjustments: Non-trade (NTO) batch semantics and API
- Policies: Built-in controls and custom policy hooks
- Pre-Trade Lock: Persist the reservation lock and replay it on execution reports
- Reject Codes: Standard business reject codes
- Architecture: Public integration model