-
Notifications
You must be signed in to change notification settings - Fork 1
Custom Go Types
Go's ClientEngine accepts any order, execution-report, and
account-adjustment type that satisfies a small set of interfaces. Policy
callbacks receive the original typed value, so project-specific fields are
always available without a side-channel or a cast from interface{}.
Use ClientEngine and its generic builder when:
- the 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.
If model.Order and model.ExecutionReport cover the integration, the
plain Engine and EngineBuilder are simpler and have lower overhead.
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 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 adjustments always use model.AccountAdjustment, regardless of the
client type - the adjustment payload is not routed through the client typing
system.
| 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 |
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
}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,
) []reject.AccountBlock {
return nil
}
func (StrategyTagPolicy) ApplyAccountAdjustment(
accountadjustment.Context,
param.AccountID,
model.AccountAdjustment,
tx.Mutations,
) []reject.Reject {
return nil
}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()The SDK allocates a cgo.Handle wrapping the client value at call entry and
releases it synchronously:
-
StartPreTrade- the handle is released whenExecuteorCloseis called on the returned*ClientRequest, not whenStartPreTradeitself returns. Policy callbacks during the main stage still have access to the original payload. -
ExecutePreTrade- the handle is released beforeExecutePreTradereturns. -
ApplyExecutionReport- the handle is released beforeApplyExecutionReportreturns.
All policy callbacks run synchronously within the same call, so policies can read the typed payload freely without extending its lifetime.
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, this validation can be skipped:
builder, err := NewClientPreTradeEngineBuilder[StrategyOrder, StrategyReport](
UnsafeFastClientPayloadCallbacks(),
)A missing or mismatched payload then panics instead of producing a reject. Use this only when the submission path is fully controlled by the caller.
Custom Go types follow the OpenPit threading contract: concurrent calls
on the same engine handle are safe under FullSync(), forbidden under
NoSync(), and under AccountSync() safe only when the caller
guarantees that calls for the same account are never concurrent. If the
custom type holds state shared across SDK calls (atomics, a
mutex-protected struct, etc.), align that state's thread-safety with the
sync mode chosen 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.
- 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