Skip to content

Custom Rust Types

Eugene V. Palchukovsky edited this page Apr 26, 2026 · 3 revisions

Custom Rust Types

Pit's Rust SDK can work 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 used with Engine<O, R>.

This page documents the two supported authoring styles for custom Rust types:

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

Why Pit Uses Capability Traits Instead of One Giant Record

Pit targets latency-sensitive and resource-conscious systems. The SDK is meant to fit into highly optimized trading code, not force every integration through one giant order or report structure with every possible field.

The capability-trait approach exists so that:

  • policies declare only the fields they actually need,
  • host applications can carry only the data their own code path uses,
  • custom project fields can live next to SDK fields without bloating the core model,
  • wrapper composition stays cheap and explicit,
  • the SDK can remain 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 trade-off is the reason for the extra machinery around Has* traits and RequestFields: more explicit composition in exchange for tighter contracts, less unnecessary data, and better fit for optimized systems.

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::{
    HasAccountAdjustmentPending, HasAccountAdjustmentReserved, HasAccountAdjustmentTotal,
    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(
        HasAccountAdjustmentTotal(
            total -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>
        ),
        HasAccountAdjustmentReserved(
            reserved -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>
        ),
        HasAccountAdjustmentPending(
            pending -> Result<Option<AdjustmentAmount>, RequestFieldAccessError>
        )
    )]
    amount: openpit::AccountAdjustmentAmount,
}

let wrapper = WithAccountAdjustmentAmount {
    inner: BalanceContext {
        asset: Asset::new("USD")?,
    },
    amount: openpit::AccountAdjustmentAmount {
        total: Some(AdjustmentAmount::Absolute(PositionSize::from_str("100")?)),
        reserved: Some(AdjustmentAmount::Delta(PositionSize::from_str("-20")?)),
        pending: 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