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.
Warning
No data or trading instrument with the same ID may be active in the
strategy when you create the new one (including instruments that are
still in PENDING). Creating a duplicate stops the strategy.
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.
Warning
All of the following must hold, or the strategy will be stopped:
- The instrument is known by the strategy.
- The instrument was dynamically created.
- The instrument is not a trading instrument.
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
Note
Risk parameters can be specified in USD or in native currency. At least one of these must be set:
-
At least one of
maxPositionormaxUsdPosition. -
For spot instruments, at least one of
maxCounterPositionormaxUsdCounterPosition. -
For instruments with isolated positions, at least one of
maxNetPositionormaxUsdNetPosition.
Create the instrument through the strategy context:
TradingInstrument myTradingInstrument = context.createTradingInstrument(myTradingInstrumentParams);
MarketDataState and TradingInstrumentState both start as PENDING.
Warning
No data or trading instrument with the same ID may be active in the
strategy when you create the new one (including instruments that are
still in PENDING). Creating a duplicate stops the strategy.
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.
Warning
All of the following must hold, or the strategy will be stopped:
- The instrument is known by the strategy.
- The instrument was dynamically created.
- The instrument is not a data-only instrument.
- The instrument has no open orders.
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:
Warning
The Trading Adapter publishes its supported wallet types as capabilities. Configuring a trading instrument with a wallet type the exchange does not support keeps the strategy from starting.
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
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();
Note
For non-trigger orders, isTriggered() always returns false.
Cancel all open orders
The Strategy API supports a bulk cancel-all on an account, by triggering the exchange to cancel everything open.
Warning
The bulk cancel does not guarantee every order is cancelled. Some cancels can fail, and a strategy may place new orders after the request is triggered but before it lands at the exchange.
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:
Callback after completion
When the request completes,
onAccountCancelAllOrdersResponse(Account account, OrderError orderError)
fires:
Accountis the account the request was sent for.- If the request succeeded,
OrderErrorisnull. - Otherwise,
OrderErrordescribes 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.
Note
IP address and network connection are not exposed in the SDK. They are technical limits unlikely to be restrictive in practice, and the Trading Adapter has its own mitigations for them.
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
RateLimitLoad accountLoad = account.getRateLimitLoad();
Instrument-level load
RateLimitLoad instrumentLoad = tradingInstrument.getRateLimitLoad();
Each call returns a RateLimitLoad carrying one value per request type:
class RateLimitLoad {
double place;
double amend;
double replace;
double cancel;
}
Tip
If a given limit doesn't apply at the exchange (e.g. cancels are not limited, or there are no per-instrument limits), the corresponding value is always zero.
Note
The values are indicative estimates, updated at a fixed interval. They are not precise real-time measurements.