Skip to content

Custom Go Types

Eugene Palchukovsky edited this page May 16, 2026 · 4 revisions

Custom Go Types

Go's ClientEngine accepts any order, execution-report, and account-adjustment type that satisfies a small set of interfaces. Policies receive the original typed value in every callback, so project-specific fields are always available without a separate side-channel or a cast from interface{}.

When to Use

Use ClientEngine and its generic builder when:

  • your order type carries fields the engine does not own (strategy tag, exchange annotation, client metadata),
  • you want those fields delivered to policy callbacks without manual bookkeeping,
  • you prefer typed callbacks over casting raw payload handles.

When you only use model.Order and model.ExecutionReport directly, the plain Engine and EngineBuilder are simpler and have lower overhead.

Building Blocks

Payload interfaces

Each payload interface requires a single method that returns the standard engine view. model.Order, model.ExecutionReport, and model.AccountAdjustment each implement their own interface and can be embedded in a project struct to satisfy it automatically.

Interface Required method Package
pretrade.ClientOrder EngineOrder() model.Order pretrade
pretrade.ClientExecutionReport EngineExecutionReport() model.ExecutionReport pretrade
accountadjustment.ClientAccountAdjustment EngineAccountAdjustment() model.AccountAdjustment accountadjustment

Policy interfaces

Policy interfaces are parameterized over the concrete payload types so callbacks receive the typed value directly.

Interface Typed callbacks
pretrade.ClientPreTradePolicy[Order, Report] CheckPreTradeStart(Context, Order), PerformPreTradeCheck(Context, Order, tx.Mutations), ApplyExecutionReport(Report), ApplyAccountAdjustment(accountadjustment.Context, param.AccountID, model.AccountAdjustment, tx.Mutations)

Account-adjustment uses model.AccountAdjustment regardless of the client type because the adjustment payload is not routed through the client typing system.

Engine builders

Builder Custom types
NewClientPreTradeEngineBuilder[Order, Report]() order and report; adjustment stays on model.AccountAdjustment
NewClientAccountAdjustmentEngineBuilder[Adjustment]() adjustment only; order and report stay on model.Order / model.ExecutionReport
NewClientEngineBuilder[Order, Report, Adjustment]() all three

Example

Step 1 — define the custom types

Embed the SDK model type to inherit its payload interface automatically:

type StrategyOrder struct {
    model.Order           // EngineOrder() is promoted automatically
    StrategyTag string
}

type StrategyReport struct {
    model.ExecutionReport // EngineExecutionReport() is promoted automatically
    VenueExecID string
}

Step 2 — write the start-stage policy

The policy is parameterized over both custom types. Callbacks receive the typed value directly — no cast required:

type StrategyTagPolicy struct{}

func (StrategyTagPolicy) Close() {}
func (StrategyTagPolicy) Name() string { return "StrategyTagPolicy" }

func (p StrategyTagPolicy) CheckPreTradeStart(
    _ pretrade.Context,
    order StrategyOrder,
) []reject.Reject {
    if order.StrategyTag == "blocked" {
        return reject.NewSingleItemList(
            reject.CodeComplianceRestriction,
            p.Name(),
            "strategy blocked",
            fmt.Sprintf("strategy tag %q is not allowed", order.StrategyTag),
            reject.ScopeOrder,
        )
    }
    return nil
}

func (StrategyTagPolicy) PerformPreTradeCheck(
    pretrade.Context,
    StrategyOrder,
    tx.Mutations,
) []reject.Reject {
    return nil
}

func (StrategyTagPolicy) ApplyExecutionReport(StrategyReport) bool {
    return false
}

func (StrategyTagPolicy) ApplyAccountAdjustment(
    accountadjustment.Context,
    param.AccountID,
    model.AccountAdjustment,
    tx.Mutations,
) []reject.Reject {
    return nil
}

Step 3 — build and use the engine

engine, err := NewClientPreTradeEngineBuilder[StrategyOrder, StrategyReport]().
    FullSync().
    PreTrade(&StrategyTagPolicy{}).
    Build()
if err != nil {
    return err
}
defer engine.Stop()

order := StrategyOrder{Order: model.NewOrder(), StrategyTag: "alpha"}
request, rejects, err := engine.StartPreTrade(order)
if err != nil || rejects != nil {
    // handle error or reject
}
defer request.Close()

reservation, rejects, err := request.Execute()
if err != nil || rejects != nil {
    // handle error or reject
}
reservation.CommitAndClose()

Lifecycle / Payload Contract

The SDK allocates a cgo.Handle wrapping the client value at call entry and releases it synchronously:

  • StartPreTrade — handle is released when Execute or Close is called on the returned *ClientRequest, not when StartPreTrade returns. This means policy callbacks during the main stage still have access to the original payload.
  • ExecutePreTrade — handle is released before ExecutePreTrade returns.
  • ApplyExecutionReport — handle is released before ApplyExecutionReport returns.

All policy callbacks are invoked synchronously within the same call, so policies can read the typed payload freely without extending its lifetime.

UnsafeFastClientPayloadCallbacks

By default each callback adapter validates that the arriving payload type matches the builder's declared types. When only one payload type flows through the engine you can skip this validation:

builder, err := NewClientPreTradeEngineBuilder[StrategyOrder, StrategyReport](
    UnsafeFastClientPayloadCallbacks(),
)

A missing or mismatched payload then panics instead of returning a reject. Use this only when the submission path is fully controlled by the caller.

Threading Addendum

Custom Go types follow the OpenPit threading contract: concurrent calls on the same engine handle are safe under FullSync() and forbidden under NoSync() or AccountSync(). If your custom type holds state shared across SDK calls (e.g. atomics or a mutex-protected struct), align that state's thread-safety with the sync mode you pick on the engine builder. See Threading Contract.

Payload handles (cgo.Handle) are always released within the same logical call that created them. No cross-goroutine ownership of payload handles occurs, and callers never need to extend payload lifetime beyond the submitting call.

Related Pages

  • Getting Started: first engine construction and end-to-end flow
  • Policy API: custom policy hooks, language interfaces, and rollback patterns
  • Custom Rust Types: Rust capability traits and derive-based composition

Clone this wiki locally