Skip to content

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-trade composes start stage and execute request into 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 raises RuntimeError.

Reservation

The reservation handle is returned after execute request passes (see the return-type table above for the per-language name).

  • commit keeps the reserved state.
  • rollback cancels 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 asset or instrument.underlying asset: asset that was traded
  • settlement asset or instrument.settlement asset: asset in which P&L and fees are measured
  • pnl: realized P&L contribution
  • fee: fee or rebate contribution
  • lock: 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 with MissingRequiredField.

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.