Skip to content

Custom Cpp Types

Eugene Palchukovsky edited this page Jun 27, 2026 · 1 revision

Custom C++ Types

The C++ SDK lets a project carry its own order and execution-report payload types through the pre-trade engine. A client payload type derives from the polymorphic base openpit::Order or openpit::ExecutionReport; the adapter templates in openpit/adapters.hpp recover the concrete type at the policy boundary and hand the typed value to the policy callback. Project-specific fields (strategy tag, desk identifier, venue annotation) are therefore always available to the policy without a side-channel or a manual cast from an opaque handle.

When to Use

Use a custom payload type when:

  • the order or report carries fields the engine model does not own (strategy tag, exchange annotation, client metadata);
  • you want those fields delivered to a policy callback with their concrete type, not as a base reference;
  • you prefer a typed policy object over reading raw payload groups out of openpit::model::Order.

If openpit::model::Order and openpit::model::ExecutionReport already cover the integration, the built-in policy configurations and the plain pipeline are simpler and carry no downcast cost. See Policies and Pre-trade Pipeline.

Building Blocks

The custom-type seam is three cooperating pieces from openpit/adapters.hpp, openpit/model.hpp, and openpit/pretrade/custom_policy.hpp.

Piece Role Header
openpit::Order / openpit::ExecutionReport Polymorphic bases the client payload derives from; the adapter downcasts to recover the concrete type. openpit/model.hpp
StartPolicyAdapter / PolicyAdapter Bridge a typed client policy to the start-stage and main-stage callback signatures the engine expects. openpit/adapters.hpp
CastMode::SafeSlow / CastMode::UnsafeFast The cast strategy each adapter uses to turn the base reference back into the client type. openpit/adapters.hpp
CustomPolicy<Handler> Owning RAII policy that registers the adapter on the engine builder through the C ABI custom-policy vtable. openpit/pretrade/custom_policy.hpp

Client policy surface

A client policy is a plain C++ object. The adapters detect each hook by its signature, so a policy implements only the hooks it needs:

  • std::string_view Name() const - stable policy name (required by the adapters).
  • std::optional<Reject> CheckPreTradeStart(const ClientOrder&) const - start-stage check; return an engaged Reject to reject, std::nullopt to accept.
  • void PerformPreTradeCheck(const ClientOrder&, const Context&, tx::Mutations&, Result&, PolicyDecision&) const - main-stage check; push zero or more rejects into the PolicyDecision, and optionally register mutations or push lock prices / outcomes into the collectors.
  • std::vector<accounts::AccountBlock> ApplyExecutionReport(const PostTradeContext&, const ClientReport&, PostTradeAdjustments&) const - post-trade hook; return the account blocks (kill-switch state) raised, and optionally push group-tagged outcomes into the collector.

Callbacks run on the engine hot path and must not let exceptions escape across the C ABI boundary. Rejects are values, not exceptions; in SafeSlow mode a payload type mismatch is itself reported as a value reject (see below).

Step 1 - define the custom payload types

Derive the project order type from openpit::Order and the report type from openpit::ExecutionReport, then add the project fields. Financial fields use the openpit::param value types, never double.

// A desk order carries the standard model order plus a strategy tag the
// engine model does not own.
struct StrategyOrder : public openpit::Order {
  openpit::model::Order base;
  std::string strategyTag;
};

// A desk report carries the standard model report plus the venue execution id.
struct StrategyReport : public openpit::ExecutionReport {
  openpit::model::ExecutionReport base;
  std::string venueExecId;
};

The embedded openpit::model::Order keeps the standard groups (operation, margin, position) available through base; Raw() on it produces the C ABI view the engine consumes when the order is submitted.

Step 2 - write the typed policy

The policy works in terms of StrategyOrder and StrategyReport. Each callback receives the concrete client type directly - no cast in the policy body.

class StrategyTagPolicy {
 public:
  [[nodiscard]] std::string_view Name() const noexcept {
    return "StrategyTagPolicy";
  }

  // Start stage: reject a blocked strategy tag before the order enters the
  // pipeline. The typed StrategyOrder gives direct access to the project field.
  [[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", order.strategyTag);
    }
    return std::nullopt;
  }

  // Main stage: push a reject into the decision; an empty decision accepts.
  void PerformPreTradeCheck(const StrategyOrder& order, const 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.strategyTag.empty()) {
      decision.Push(openpit::pretrade::Reject(
          std::string(Name()), openpit::pretrade::RejectScope::Order,
          openpit::pretrade::RejectCode::MissingRequiredField,
          "strategy tag is required", "strategyTag"));
    }
  }

  // Post-trade: return the account blocks to raise; an empty vector trips none.
  [[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>(report);
    static_cast<void>(adjustments);
    return {};
  }
};

Step 3 - bridge with an adapter (choose a cast mode)

The adapter templates wrap the client policy and expose the callback signatures the engine drives. They downcast the polymorphic openpit::Order / openpit::ExecutionReport reference back to the client type. There is intentionally no default cast strategy - the policy author chooses one explicitly through the convenience aliases.

// SafeSlow: the adapter uses dynamic_cast and produces a deterministic
// type-mismatch reject (start/main stage) or no account blocks (report stage)
// when the arriving payload is not the client type. Safe default at the boundary.
using StrategyMainAdapter =
    openpit::pretrade::PolicyAdapterWithSafeSlowArgType<StrategyTagPolicy,
                                                        StrategyOrder,
                                                        StrategyReport>;
using StrategyStartAdapter =
    openpit::pretrade::StartPolicyAdapterWithSafeSlowArgType<StrategyTagPolicy,
                                                             StrategyOrder,
                                                             StrategyReport>;

SafeSlow versus UnsafeFast

The cast mode is the CastMode enum template parameter; the aliases pick it for you.

Mode Cast Order mismatch Report mismatch Use when
CastMode::SafeSlow dynamic_cast deterministic type-mismatch reject callback returns false dynamic boundaries; safe default
CastMode::UnsafeFast static_cast undefined behavior undefined behavior closed systems with compile-time payload pairing

UnsafeFast skips the RTTI check entirely. Select it only when the submission path is fully controlled by the caller and the payload type that reaches a given policy is guaranteed at compile time, because a wrong wiring is undefined behavior rather than a reject:

// UnsafeFast: direct static_cast, no runtime type check. A mismatched payload
// is undefined behavior, so this is only for closed, statically paired wiring.
using StrategyMainAdapterFast =
    openpit::pretrade::PolicyAdapterWithUnsafeFastArgType<StrategyTagPolicy,
                                                          StrategyOrder,
                                                          StrategyReport>;

Step 4 - compose a typed engine

Wrap the adapter in a CustomPolicy, name it, register it on the builder with Add, and build the engine. The builder keeps its own reference to the policy; the caller keeps ownership of the CustomPolicy handle until at least registration completes.

openpit::EngineBuilder builder(openpit::SyncPolicy::None);

// The CustomPolicy adopts the adapter (which owns the client policy) and wires
// the detected hooks into the C ABI custom-policy vtable.
openpit::pretrade::CustomPolicy<StrategyMainAdapter> mainPolicy(
    "StrategyTagPolicy", StrategyMainAdapter{StrategyTagPolicy{}});
builder.Add(mainPolicy);

const openpit::Engine engine = builder.Build();

Step 5 - submit a custom order

The engine pre-trade entry points still take an openpit::model::Order, built from the embedded base of the custom payload, so the C ABI view is the standard one. The policy callbacks, in contrast, receive the full client type recovered by the adapter.

StrategyOrder order;
openpit::model::OrderOperation op;
op.instrument = openpit::model::Instrument("AAPL", "USD");
op.accountId = openpit::param::AccountId::FromUint64(1);
op.side = openpit::model::Side::Buy;
op.tradeAmount = openpit::model::TradeAmount::OfQuantity(
    openpit::param::Quantity::FromString("1"));
op.price = openpit::param::Price::FromString("100");
order.base.operation = std::move(op);
order.strategyTag = "alpha";

// The standard model view drives the pipeline; the policy still sees the typed
// StrategyOrder through the adapter.
openpit::pretrade::ExecuteResult result = engine.ExecutePreTrade(order.base);
if (result.Passed()) {
  result.reservation->Commit();
}

Lifecycle and Payload Contract

The custom payload type and the policy object follow the SDK ownership rules:

  • The CustomPolicy<Adapter> is a move-only owning RAII handle. The adapter it holds owns the client policy by value. Registration on the builder retains its own reference, so the engine keeps the policy alive after Build(); the caller's handle may be released after registration completes.
  • Policy callbacks are synchronous and run within the engine call that triggered them. The payload reference handed to a callback is valid only for that call - do not retain it. The same holds for the Context, which is non-owning and callback-scoped.
  • Callbacks must not throw across the C ABI boundary. SafeSlow already turns a payload type mismatch into a value reject instead of an exception; keep any other failure on the value-reject channel as well.

Why a Polymorphic Base Instead of One Record

OpenPit targets latency-sensitive trading code, not a single universal order record carrying every conceivable field. The polymorphic-base seam exists so that:

  • a policy declares the exact client payload type it consumes;
  • the host carries only the fields its code path uses, next to the standard model groups;
  • the downcast cost is paid once, at the policy boundary, and only in SafeSlow mode;
  • the core model stays fixed while projects extend their payloads freely.

The trade-off is explicit adapter wiring (a cast mode and the client type parameters) in exchange for typed callbacks and a stable core model.

Threading Addendum

Custom C++ types follow the OpenPit threading contract: concurrent calls on the same engine handle are safe under SyncPolicy::Full, forbidden under SyncPolicy::None, and under SyncPolicy::Account safe only when the caller guarantees that calls for the same account are never concurrent. If a custom payload or policy holds state shared across SDK calls, align that state's thread-safety with the sync policy chosen on the builder. See Threading Contract.

Related Pages

Clone this wiki locally