Advanced SDK Features

Beyond the basics in Basics, the Strategy SDK has four features worth their own page: dynamic instruments, margin trading, trigger orders, and rate-limit load.


Dynamic instruments

A dynamic instrument is one created or destroyed at runtime, after strategy construction. Dynamic instruments outlive the current strategy run: they are resubscribed when the strategy restarts.

To get the strategy's current instruments:

List<DataInstrument> myDataInstruments = context.getCurrentDataInstruments();
List<TradingInstrument> myTradingInstruments = context.getCurrentTradingInstruments();

These lists include instruments still being created or torn down.

To check whether an instrument was created dynamically:

boolean isDataDynamicallyCreated = myDataInstrument.isDynamicallyCreated();
boolean isTradingDynamicallyCreated = myTradingInstrument.isDynamicallyCreated();

Creating data instruments

A data instrument is fully specified by a DataInstrumentParams. The fields match those used in the static strategy configuration:

DataInstrumentParams myDataInstrumentParams =
    DataInstrumentParams.builder()
        .id(0L)                    // required
        .active(true)              // optional
        .maxTimeWithoutUpdates(42) // optional
        .maxTimeInError(42)        // optional
        .maxTimeWithoutTrades(42)  // optional
        .maxSnapshotDepth(42)      // optional
        .build();

Create the instrument through the strategy context:

DataInstrument myDataInstrument = context.createDataInstrument(myDataInstrumentParams);

The MarketDataState of a freshly created instrument is PENDING.

Callbacks after creation

The onMarketDataState(DataInstrument) callback fires with state READY. Public market-data callbacks then arrive as usual.

The onMarketDataState(DataInstrument) callback fires with state FAILED.

Deleting data instruments

context.deleteDataInstrument(myInstrumentId);

After deletion is requested, no more market-data callbacks for that instrument arrive.

When unsubscription completes, onMarketDataState(DataInstrument) fires with state GONE.

Creating trading instruments

TradingInstrumentParams myTradingInstrumentParams =
    TradingInstrumentParams.builder()
        .id(0L)                                                         // required
        .accountId(0L)                                                  // required
        .active(true)                                                   // optional
        .maxTimeWithoutUpdates(42)                                      // optional
        .maxTimeInError(42)                                             // optional
        .maxTimeWithoutTrades(42)                                       // optional
        .maxSnapshotDepth(42)                                           // optional
        .minOrderSize(Quantity.ZERO)                                    // optional
        .maxOrderCount(TwoSides.of(0, 0))                               // optional
        .maxPosition(TwoSides.of(Quantity.ZERO, Quantity.ZERO))         // optional
        .maxNetPosition(TwoSides.of(Quantity.ZERO, Quantity.ZERO))      // optional
        .maxCounterPosition(TwoSides.of(Price.ZERO, Price.ZERO))        // optional
        .maxUsdPosition(TwoSides.of(Price.ZERO, Price.ZERO))            // optional
        .maxUsdNetPosition(TwoSides.of(Price.ZERO, Price.ZERO))         // optional
        .maxUsdCounterPosition(TwoSides.of(Price.ZERO, Price.ZERO))     // optional
        .build();

Risk-limit requirements

Create the instrument through the strategy context:

TradingInstrument myTradingInstrument = context.createTradingInstrument(myTradingInstrumentParams);

MarketDataState and TradingInstrumentState both start as PENDING.

Callbacks after creation

The instrument is fully operational only when both market-data and order-entry subscriptions succeed.

onMarketDataState(DataInstrument) fires with state READY. Public market-data callbacks arrive as usual.

onOrderEntryState(Account, Set<TradingInstrument>) fires. The set contains only the new trading instrument; the trading state is READY. The strategy can place orders against the instrument and will receive order-entry callbacks as usual.

Both onMarketDataState(DataInstrument) and onOrderEntryState(Account, Set<TradingInstrument>) fire with state FAILED.

Deleting trading instruments

context.deleteTradingInstrument(myInstrumentId, myAccountId);

After deletion is requested:

  • No more market-data or order-entry callbacks arrive for the instrument.
  • The strategy can no longer place orders against it.

TradingInstrumentState of an instrument pending deletion is PENDING_DELETE.

Callbacks after deletion

onMarketDataState(DataInstrument) fires with state GONE.

onOrderEntryState(Account, Set<TradingInstrument>) fires with state GONE.


Margin trading

The Strategy Server supports trading spot instruments on margin. The trading-instrument parameter walletType selects spot trading (SPOT) or cross-margin trading (CROSS_MARGIN).

Configuring CROSS_MARGIN means:

  • Orders placed through the instrument are flagged for margin trading.
  • Risk limits are evaluated against the cross-margin wallet, using the wallet's marginBalance.

Wallet types by exchange

Wallet-type support varies by exchange:

Exchange Supported Wallet Types Notes
Binance SPOT, CROSS_MARGIN Separate spot and cross-margin wallets
FTX ACCOUNT Single wallet account for both spot instruments and futures

Accessing wallet types

Account.getWallet(WalletType) returns the wallet of the requested type:

// Margin-less spot wallet
Wallet spotWallet = account.getWallet(WalletType.SPOT);

// Cross-margin wallet
Wallet crossMarginWallet = account.getWallet(WalletType.CROSS_MARGIN);

// Isolated-margin wallet (per instrument)
Wallet isolatedWallet = account.getIsolatedMarginWallet(instrumentId);

For convenience, the wallet configured on a trading instrument is reachable directly:

Wallet wallet = tradingInstrument.getWallet();

Margin balance calculation

The marginBalance field gives the net asset value of the wallet:

marginBalance = (availableMargin + blockedMargin) - (borrowed + interest)

Order parameters for margin trading

Parameter Description
autoBorrow Takes on an automatic loan if available margin is insufficient to place an order. The loan is taken on as soon as the order is placed, interest accrues immediately, and the loan is repaid if the order is cancelled before execution or explicitly repaid.
autoRepay Explicitly repays borrowed amount and interest with the proceeds of order execution.

Trigger orders

A trigger order activates only after a price condition is met. The SDK supports four variants.

Supported order types

Behaviour

  • After the trigger condition fires, the order becomes a limit or market order respectively.

Trigger condition

  • For BID: activated when price rises to or above the trigger price.
  • For ASK: activated when price falls to or below the trigger price.

Note

  • For STOP_LIMIT, the limit price is usually equal to or less aggressive than the trigger price.

Behaviour

  • After the trigger condition fires, the order becomes a limit or market order respectively.

Trigger condition

  • For BID: activated when price falls to or below the trigger price.
  • For ASK: activated when price rises to or above the trigger price.

Note

  • For LIMIT_IF_TOUCHED, the limit price is usually equal to or more aggressive than the trigger price.

Checking exchange support

Confirm the trigger type is supported on the target exchange:

final boolean isSupported = myInstrument.getTradingCapabilities()
    .orderTypes()
    .contains(OrderType.STOP_LIMIT);

Specifying trigger price

For every trigger order type, set triggerPrice on the OrderParams:

OrderParams params = OrderParams.builder()
    .orderType(OrderType.STOP_LIMIT)
    .triggerPrice(Price.of("50000.00"))
    // ... other parameters
    .build();

Querying trigger status

To see whether the order is still untriggered or already triggered (and possibly resting in the book):

final boolean isTriggered = order.isTriggered();

Cancel all open orders

The Strategy API supports a bulk cancel-all on an account, by triggering the exchange to cancel everything open.

Checking exchange support

Get the account either through a parameter or from a TradingInstrument:

Account account = tradingInstrument.getAccount();

Then check the capability:

final boolean isSupported = account.getTradingCapabilities().cancelAllOrdersOfAccount();

Triggering cancel-all-orders

final DiscardReason discardReason = account.cancelAllOrders();

The call returns a DiscardReason:

DiscardReason Description
CAPABILITY_NOT_AVAILABLE Not supported by the exchange
ADAPTER_NOT_AVAILABLE The adapter is currently not available
ORDER_PENDING Already triggered and a response has not yet arrived
NOT_DISCARDED The request was successfully sent

Callback after completion

When the request completes, onAccountCancelAllOrdersResponse(Account account, OrderError orderError) fires:

  • Account is the account the request was sent for.
  • If the request succeeded, OrderError is null.
  • Otherwise, OrderError describes the failure.

@Override
public void onAccountCancelAllOrdersResponse(Account account, OrderError orderError) {
  if (orderError == null) {
    log.info("Cancel all orders succeeded for account: {}", account.getId());
  } else {
    log.error("Cancel all orders failed for account: {} with error: {}",
        account.getId(), orderError);
  }
}

Rate limit load

Each exchange has its own rate-limiting rules, typically multiple limits applied to different aspects of an order request.

Aspects exchanges limit

Common ones include:

  • Request type (place, amend/modify, cancel, replace, others).
  • Instrument.
  • Instrument group.
  • (Sub) account.
  • Master account.
  • IP address.
  • Network connection.

Reading the load

The SDK exposes the load for the three most useful scopes: request type, instrument, and account. The load is a value between 0 (no usage) and 1 (limit fully reached).

Account-level load

Retrieve account-level rate limit load
RateLimitLoad accountLoad = account.getRateLimitLoad();

Instrument-level load

Retrieve instrument-level rate limit load
RateLimitLoad instrumentLoad = tradingInstrument.getRateLimitLoad();

Each call returns a RateLimitLoad carrying one value per request type:

RateLimitLoad structure
class RateLimitLoad {

  double place;
  double amend;
  double replace;
  double cancel;
}