Skip to content

Custom Go Types

Eugene V. Palchukovsky edited this page Apr 28, 2026 · 1 revision

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.ClientCheckPreTradeStartPolicy[Order, Report] CheckPreTradeStart(Context, Order), ApplyExecutionReport(Report)
pretrade.ClientPreTradePolicy[Order, Report] PerformPreTradeCheck(Context, Order, tx.Mutations), ApplyExecutionReport(Report)
accountadjustment.ClientPolicy[Adjustment] ApplyAccountAdjustment(Context, param.AccountID, Adjustment, tx.Mutations)

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 (StrategyTagPolicy) ApplyExecutionReport(StrategyReport) bool {
    return false
}

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
}

Step 3 — build and use the engine

builder, err := NewClientPreTradeEngineBuilder[StrategyOrder, StrategyReport]()
if err != nil {
    return err
}
builder.CheckPreTradeStartPolicy(&StrategyTagPolicy{})
engine, err := builder.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

ClientEngine follows the same threading contract as Engine: no concurrent calls, single-threaded-per-call semantics, thread-portable sequential usage.

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