Skip to content

Custom Rust Types

Eugene Palchukovsky edited this page May 21, 2026 · 5 revisions

Custom Rust Types

The Rust SDK works with caller-defined order and execution-report types. Policies depend on capability traits such as HasInstrument, HasTradeAmount, HasOrderPrice, HasPnl, and HasFee, so any type that provides the required traits can be passed as the Order or ExecutionReport type parameter to Engine::builder::<Order, ExecutionReport, AccountAdjustment>().

Two authoring styles are supported:

  • manual Has* implementations;
  • derive-based wrapper composition through RequestFields.

Why Capability Traits Instead of One Giant Record

OpenPit is aimed at latency-sensitive trading code, not one universal order or report structure that carries every conceivable field. The capability-trait approach exists so that:

  • policies declare only the fields they actually need;
  • host applications carry only the data their code path uses;
  • custom project fields live next to SDK fields without bloating the core model;
  • wrapper composition stays cheap and explicit;
  • the SDK remains modular instead of centralizing every extension into one ever-growing public record type.

In practice this means a policy that only needs HasTradeAmount and HasOrderPrice does not force the caller to provide HasPnl, HasFee, position fields, or execution-report data just to satisfy a monolithic model. That is the trade-off: more explicit composition in exchange for tighter contracts and less unnecessary data.

Supported Derive Setup

The supported way to use derive macros is through the main crate feature:

openpit = { version = "X.X", features = ["derive"] }

This keeps openpit and openpit-derive in lockstep and exposes RequestFields from the main crate namespace.

Direct dependency on openpit-derive is technically possible, but it is not the supported integration path.

Manual Field Implementations

Manual implementations are the most explicit option. They are appropriate when:

  • you only need a small number of traits,
  • your type layout does not follow the standard wrapper pattern,
  • a field requires custom conversion logic.

Example:

use openpit::{HasInstrument, Instrument, RequestFieldAccessError};

struct MyOrder {
    instrument: Instrument,
}

impl HasInstrument for MyOrder {
    fn instrument(&self) -> Result<&Instrument, RequestFieldAccessError> {
        // Expose the project field through the capability trait expected by policies.
        Ok(&self.instrument)
    }
}

For optional reference returns or conversions, implement the trait manually. This is the correct approach for cases such as:

  • converting Option<T> into Option<&T>,
  • validating a field before returning it,
  • computing a value instead of reading a field directly.

Derive-Based Wrapper Composition

RequestFields reduces boilerplate for wrapper stacks that follow the SDK's composition style.

Typical shape:

use openpit::param::{AccountId, Price, TradeAmount};
use openpit::{
    HasAccountId, HasInstrument, HasOrderPrice, HasTradeAmount, Instrument,
    RequestFieldAccessError, RequestFields,
};

#[derive(RequestFields)]
struct WithMyOperation<T> {
    // Preserve capabilities already provided by outer wrappers.
    inner: T,
    // Map SDK capability traits onto the embedded standard operation record.
    #[openpit(
        HasInstrument(instrument -> Result<&Instrument, RequestFieldAccessError>),
        HasAccountId(account_id -> Result<AccountId, RequestFieldAccessError>),
        HasTradeAmount(trade_amount -> Result<TradeAmount, RequestFieldAccessError>),
        HasOrderPrice(price -> Result<Option<Price>, RequestFieldAccessError>)
    )]
    operation: openpit::OrderOperation,
}

Account-adjustment wrappers follow the same composition style as order/report wrappers. Example:

use openpit::param::{AdjustmentAmount, Asset, PositionSize};
use openpit::{
    HasAccountAdjustmentBalance, HasAccountAdjustmentHeld, HasAccountAdjustmentIncoming,
    HasBalanceAsset, RequestFieldAccessError, RequestFields,
};

struct BalanceContext {
    asset: Asset,
}

impl HasBalanceAsset for BalanceContext {
    fn balance_asset(&self) -> Result<&Asset, RequestFieldAccessError> {
        Ok(&self.asset)
    }
}

#[derive(RequestFields)]
struct WithAccountAdjustmentAmount<T> {
    // Keep balance-level capabilities from the outer context available.
    #[openpit(inner, HasBalanceAsset(balance_asset -> Result<&Asset, RequestFieldAccessError>))]
    inner: T,
    // Expose the standard account-adjustment amount fields through capability traits.
    #[openpit(
        HasAccountAdjustmentBalance(
            balance -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>
        ),
        HasAccountAdjustmentHeld(
            held -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>
        ),
        HasAccountAdjustmentIncoming(
            incoming -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>
        )
    )]
    amount: openpit::AccountAdjustmentAmount,
}

let wrapper = WithAccountAdjustmentAmount {
    inner: BalanceContext {
        asset: Asset::new("USD")?,
    },
    amount: openpit::AccountAdjustmentAmount {
        balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str("100")?)),
        held: Some(AdjustmentAmount::Delta(PositionSize::from_str("-20")?)),
        incoming: Some(AdjustmentAmount::Delta(PositionSize::from_str("5")?)),
    },
};

In this pattern:

  • #[openpit(Trait(method -> ReturnType))] on a field generates a direct impl for that trait,
  • #[openpit(HasSomething(-> ReturnType))] is the shorthand form when the derive can infer the method name from a Has* trait,
  • #[openpit(inner, ...)] marks the passthrough field and generates delegated impls for the listed traits.

That means a wrapper can add new fields while preserving all previously available capabilities from the inner type.

Selecting the Inner Field

RequestFields does not infer passthrough from the field name alone. Passthrough implementations are generated only for traits listed on a field marked with #[openpit(inner, ...)].

If your wrapper uses a different field name, mark it explicitly and list the traits that should delegate to that field:

use openpit::{
    HasInstrument, Instrument, RequestFieldAccessError, RequestFields,
};

#[derive(RequestFields)]
struct WithMyOperation<T> {
    // Explicitly declare which traits should passthrough
    // to the non-standard inner field.
    #[openpit(inner, HasInstrument(instrument -> Result<&Instrument, RequestFieldAccessError>))]
    base: T,
}

Without the trait list on the inner field, no passthrough impls are generated.

Attribute Syntax

RequestFields accepts only namespaced attributes:

#[openpit(
    HasInstrument(instrument -> Result<&Instrument, RequestFieldAccessError>),
    HasAccountId(account_id -> Result<AccountId, RequestFieldAccessError>)
)]

and:

#[openpit(inner, HasInstrument(-> Result<&Instrument, RequestFieldAccessError>))]

Legacy syntax:

#[request_fields(...)]

is rejected on purpose. The derive emits a compile-time error that points to #[openpit(...)].

How Passthrough Works

RequestFields is for wrapper composition, not for arbitrary structural introspection.

The derive generates implementations for the exact trait paths listed in #[openpit(...)]. For direct field mappings it calls the named method on that field. For #[openpit(inner, ...)] mappings it generates passthrough impls with the corresponding where InnerType: Trait bound.

Method inference in the Trait(-> ReturnType) form only works for Has* trait names. For any other trait name, spell out the method explicitly.

Compatibility is guaranteed for the supported path where RequestFields is re-exported from openpit under the derive feature and both crates move in lockstep.

Choosing Between Manual and Derive

Prefer manual implementations when:

  • the type is not a wrapper,
  • field access needs custom logic,
  • only one or two traits are needed.

Prefer RequestFields when:

  • you are building With* wrappers,
  • you want to compose multiple layers,
  • you want trait passthrough through the wrapper stack with minimal boilerplate.

Related Pages

Clone this wiki locally