-
Notifications
You must be signed in to change notification settings - Fork 1
Custom Cpp 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.
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.
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 |
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 engagedRejectto reject,std::nulloptto accept. -
void PerformPreTradeCheck(const ClientOrder&, const Context&, tx::Mutations&, Result&, PolicyDecision&) const- main-stage check; push zero or more rejects into thePolicyDecision, 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).
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.
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 {};
}
};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>;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>;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();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();
}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 afterBuild(); 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.
SafeSlowalready turns a payload type mismatch into a value reject instead of an exception; keep any other failure on the value-reject channel as well.
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
SafeSlowmode; - 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.
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.
- Getting Started: first engine construction and end-to-end flow
- Policies: built-in policy configurations and registration
- Pre-trade Pipeline: start/main stages and reservation finalization
- Threading Contract: sync policies and callback concurrency rules