-
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. CustomPolicy catches an exception in the
C trampoline and the invoking C++ method rethrows it after the native call
returns, so no exception crosses the C ABI. Exceptions represent API failures;
rejects remain values. In SafeSlow mode, an order payload mismatch is reported
as a value reject (see below).
Derive the project types from the concrete openpit::model::Order and
openpit::model::ExecutionReport models, then add the project fields. The
concrete bases already implement the polymorphic EngineRaw() contract, so the
engine can consume the standard fields while the adapter recovers the project
type. Financial fields use the openpit::param value types, never double.
// A desk order carries the standard model fields plus a strategy tag the
// engine does not own. The inherited EngineRaw() submits the standard view.
struct StrategyOrder : public openpit::model::Order {
std::string strategyTag;
};
// A desk report carries the standard model fields plus the venue execution id.
// Its inherited EngineRaw() lets ApplyExecutionReport preserve the dynamic
// type.
struct StrategyReport : public openpit::model::ExecutionReport {
std::string venueExecId;
};The inherited model keeps the standard groups (operation, margin, position,
financial impact, and fill details) on the same object as the project fields.
Its EngineRaw() override produces the C ABI view consumed by the engine.
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:
explicit StrategyTagPolicy(std::shared_ptr<std::string> appliedVenueExecId)
: m_appliedVenueExecId(std::move(appliedVenueExecId)) {}
[[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 openpit::pretrade::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: typed metadata arrives next to the normalized standard fields.
[[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>(adjustments);
*m_appliedVenueExecId = report.venueExecId;
return {};
}
private:
std::shared_ptr<std::string> m_appliedVenueExecId;
};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. PolicyAdapter is the unified
wrapper for a policy with a main hook: it also forwards any start, dry-run,
post-trade, and account-adjustment hooks implemented by that same policy
instance. StartPolicyAdapter remains available for start-only policies.
// 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 StrategyAdapter =
openpit::pretrade::PolicyAdapterWithSafeSlowArgType<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 | empty account-block list; hook not called | 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 StrategyAdapterFast =
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.
const auto appliedVenueExecId = std::make_shared<std::string>();
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<StrategyAdapter> policy(
"StrategyTagPolicy",
StrategyAdapter{StrategyTagPolicy{appliedVenueExecId}});
builder.Add(policy);
const openpit::Engine engine = builder.Build();The engine accepts the derived order and execution report directly. Their
inherited EngineRaw() implementations supply the standard C ABI views, while
the adapter recovers the original dynamic types for policy callbacks.
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.operation = std::move(op);
order.strategyTag = "alpha";
// Submit the typed order; inherited EngineRaw() supplies the standard view,
// while the adapter preserves the StrategyOrder dynamic type.
openpit::pretrade::ExecuteResult result = engine.ExecutePreTrade(order);
if (result.Passed()) {
result.reservation->Commit();
}
StrategyOrder blockedOrder = order;
blockedOrder.strategyTag = "blocked";
const openpit::pretrade::StartResult blocked =
engine.StartPreTrade(std::move(blockedOrder));
assert(!blocked.Passed());
assert(blocked.rejects.front().code ==
openpit::pretrade::RejectCode::ComplianceRestriction);
StrategyOrder missingTagOrder = order;
missingTagOrder.strategyTag.clear();
const openpit::pretrade::ExecuteResult missingTag =
engine.ExecutePreTrade(std::move(missingTagOrder));
assert(!missingTag.Passed());
assert(missingTag.rejects.front().code ==
openpit::pretrade::RejectCode::MissingRequiredField);
StrategyReport report;
openpit::model::ExecutionReportOperation reportOp;
reportOp.instrument = openpit::model::Instrument("AAPL", "USD");
reportOp.accountId = openpit::param::AccountId::FromUint64(1);
reportOp.side = openpit::model::Side::Buy;
report.operation = std::move(reportOp);
report.venueExecId = "EX-1";
const openpit::PostTradeResult post = engine.ApplyExecutionReport(report);
assert(post.accountBlocks.empty());
assert(*appliedVenueExecId == "EX-1");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. -
StartPreTrade(order)copies an lvalue or moves an rvalue into the deferred request, preserving its concrete type untilRequest::Execute(). Type-erased code passes astd::shared_ptr<const openpit::Order>explicitly; the request retains that owner, so execution never depends on caller lifetime. - Callback exceptions are captured inside the C trampoline and rethrown from
the invoking C++ engine method after the native call returns. Use that channel
for API failures; use
Reject/PolicyDecisionfor expected business outcomes.SafeSlowalready turns an order payload mismatch into a value reject and skips a mismatched report hook.
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