Strategy Development
This page covers the development patterns you use most often when building strategies with the Strategy SDK: declaring parameters, placing orders, handling market-data callbacks, handling order-entry callbacks, and reading rate-limit load.
Strategy parameters
A parameter is a configurable field on a strategy. Parameters let you change behaviour without modifying or rebuilding code: each strategy instance is constructed from a JSON config that supplies values for them.
Basic parameter declaration
Annotate a field on your strategy class with @Parameter:
class MyStrategy implements Strategy {
@Parameter
Integer myIntParameter;
}
{
"instanceName": "my_strategy_instance",
"strategyName": "MyStrategy",
"parameters": {
"myIntParameter": 42
}
}
Optional parameters
A parameter can be optional, with a default applied when the config omits it:
class MyStrategy implements Strategy {
@Parameter(optional = true)
Integer myOptionalIntParameter = 10;
}
{
"instanceName": "my_strategy_instance",
"strategyName": "MyStrategy",
"parameters": {}
}
Custom parameter names
By default, the parameter name is the Java field name. Override it with
name:
class MyStrategy implements Strategy {
@Parameter(name = "myRenamedIntParameter")
Integer myIntParameter;
}
{
"instanceName": "my_strategy_instance",
"strategyName": "MyStrategy",
"parameters": {
"myRenamedIntParameter": 42
}
}
Composite parameters
Parameters can nest. Annotate fields on your own classes with @Parameter
to build a hierarchy:
class MyStrategy implements Strategy {
@Parameter
MyCompositeParameter myCompositeParameter;
class MyCompositeParameter {
@Parameter
Integer myIntParameter;
@Parameter
MyNestedParameter myNestedParameter;
}
class MyNestedParameter {
@Parameter
Integer myIntParameter;
}
}
{
"instanceName": "my_strategy_instance",
"strategyName": "MyStrategy",
"parameters": {
"myCompositeParameter": {
"myIntParameter": 42,
"myNestedParameter": {
"myIntParameter": 42
}
}
}
}
Supported parameter types
Boxed primitives
BooleanIntegerLongFloatDoubleString
Decimal types
BigDecimalPriceQuantity
SDK types
DataInstrumentTradingInstrumentAccountTwoSides<T>, whereTis any other supported type
Collections
List<T>, whereTis any other supported type
Custom classes
- Your own classes containing any of the supported types, with each field
annotated
@Parameter
Placing orders
Basic placement
A strategy typically has a TradingInstrument parameter. To place an
order, build an OrderParams with side, price, and quantity, and pass it
to TradingInstrument.placeOrder.
Side, price, and quantity are all you need for a good-till-cancel limit
order: type defaults to LIMIT and timeInForce to GOOD_TILL_CANCEL.
Everything else on OrderParams is for more sophisticated orders.
A buy of quantity 1 at price 10:
@Parameter
TradingInstrument tradingInstrument;
...
@Override
void onInstrumentSnapshot(DataInstrument instrument) {
Order order = tradingInstrument.placeOrder(
OrderParams.builder()
.side(OrderSide.BID)
.price(Price.valueOf(10.0))
.quantity(Quantity.valueOf(1))
.build());
}
Note
Right after placing an order, you can check whether it has already been
discarded. Otherwise the strategy will receive an
Strategy.onOrderUpdate(Order order) callback when the order is
confirmed. If the order is rejected, the strategy receives an
Strategy.onOrderError(Order order, OrderError orderError) callback
instead. The error may come from the Trading Adapter (a rate limit, for
example) or from the exchange (insufficient balance).
OrderParams fields
side and quantity are the only fields the builder enforces. Everything
else either has a default or applies only to a particular order type.
Warning
An order that cannot meet minQuantity is discarded with
DiscardReason.MIN_ORDER_SIZE. Placing with useSpare against an
exchange whose Trading Adapter does not support spare requests stops the
strategy. Out-of-range values for baseRiskRatio or counterRiskRatio
throw.
Defensive vs aggressive orders
By default, orders are defensive. The exchange rejects them if they would match immediately, which is the post-only behaviour you want for most resting orders:
@Parameter
TradingInstrument tradingInstrument;
...
@Override
void onInstrumentSnapshot(DataInstrument instrument) {
Order order = tradingInstrument.placeOrder(
OrderParams.builder()
.side(OrderSide.BID)
.price(Price.valueOf(10.0))
.quantity(Quantity.valueOf(1))
.build());
}
For an aggressive order that may match immediately, set postOnly to
false and use a price that crosses:
@Parameter
TradingInstrument tradingInstrument;
...
@Override
void onInstrumentSnapshot(DataInstrument instrument) {
Order order = tradingInstrument.placeOrder(
OrderParams.builder()
.side(OrderSide.BID)
.postOnly(false)
.price(Price.valueOf(10.0))
.quantity(Quantity.valueOf(1))
.build());
}
Market data callbacks
Market-data callbacks notify the strategy about changes in public market data for subscribed instruments. They drive any strategy that reacts to order-book updates, trades, or other market events in real time.
onMarketDataState
Called when the market-data connection state for an instrument changes. Tells the strategy whether data is flowing or whether the exchange feed has broken.
States:
-
READY: market data is being delivered. -
ERROR: market data is currently not being delivered due to an error. -
PENDING: market data is not yet available; the subscription is in flight. -
FAILED: the market-data subscription failed. -
GONE: market data is no longer available because the instrument was unsubscribed. Only dynamic instruments reach this state.
onInstrumentSnapshot
Called after a new order-book snapshot is available for the instrument.
A snapshot is the full state of the order book at a point in time, all bid and ask levels included. It fires when the system receives a full order-book update from the exchange, or when a new book with a consolidated top-of-book becomes available.
onInstrumentTopOfBook
Called after a new top-of-book (best bid/ask) update is available.
Top-of-book is the highest bid and lowest ask, with the quantity at each. This is the most frequently updated market data and the relevant signal for most latency-sensitive strategies.
Tip
This callback can fire much more often than onInstrumentSnapshot for
most instruments. Use it when you need to react fast and don't need the
full book.
onInstrumentTrades
Called after one or more public trades on the exchange.
Each trade is an executed transaction between a buyer and seller at a specific price and quantity.
Note
Multiple trades may arrive in a single callback. Call
instrument.getLastTrades() to retrieve all trades since the previous
callback.
onInstrumentLiquidations
Called after a liquidation event on the instrument.
Liquidations occur in derivatives markets when a position's margin falls below the maintenance requirement and the exchange force-closes the position. The callback gives visibility into forced liquidations, which often indicate market stress or volatility.
Note
Only relevant for derivatives instruments (futures, perpetual swaps, options). Spot instruments don't have liquidations.
Order entry callbacks
Order-entry callbacks notify the strategy about changes to private data. Their shape follows what most crypto exchanges actually publish.
onOrderUpdate
Called whenever an order changes.
Examples:
- Order placed (state moves from
PENDING_NEWtoCONFIRMED). - Order canceled (state moves to
CANCELED). - Order received a fill (filled/remaining quantity changes; state may move
to
FILLED).
onOrderError
Called when an order request (place, amend/modify, or cancel) fails or is rejected.
The error can come from:
- The Trading Adapter (e.g. rate limit exceeded).
- The exchange (e.g. insufficient balance).
The OrderError object carries the error type and message.
onOrderFill
Called whenever an order receives a new fill. Fills are reported one at a
time. To retrieve the latest fill, call getLastFill() on the order.
onOrderFillUpdate
Some exchanges don't publish all fill information at once. Size and price are always available right away; fees and the final trade ID may arrive a moment later.
To avoid stalling, the system publishes the initial (incomplete) fill via
onOrderFill, then publishes the late-arriving fields through an
onOrderFillUpdate callback that signals: "this is not a new fill, this is
an update to a fill you've already seen."
onInstrumentPosition
Derivatives. A position is a number of contracts. When that number changes (because of a trade), the strategy is notified. A position carries unrealised PnL; closing it produces realised PnL, which shows up in the account wallet.
Spot. Spot instruments don't carry a real position. Trades affect the
wallet balance directly. To make spot and derivatives feel similar to a
strategy, the wallet entry of the traded base underlying is treated as if
it were a position. Spot trading triggers onInstrumentPosition whenever
the corresponding wallet entry changes.
onOrderEntryState
Each account has its own private connection to the exchange, managed by the Trading Adapter. This callback notifies the strategy about that connection's state.
States:
onAccountCancelAllOrdersResponse
Some exchanges support cancelling every open order on an account in one
request. A strategy triggers this by calling cancelAllOrders() on the
Account. The response comes back through this callback:
-
If the exchange confirmed the request,
OrderErrorisnull. Each affected order also produces anonOrderUpdatereporting the cancellation. -
If the request failed,
OrderErrorcarries the details.
This is the same shape as onOrderError, but scoped to the whole account
rather than one order.
Warning
Cancel-all also cancels orders placed by other strategies on the same account.
Callback ordering
Some callbacks are related. A trade typically triggers at least three:
-
onOrderUpdate: the order's filled/remaining quantity changes. -
onOrderFill: each trade has at least one fill (multiple fills if the trade touched multiple price levels, hence multiple callbacks). -
onInstrumentPosition: a trade changes the position.
All three trace back to a single transaction on the exchange. The callbacks are modelled on what the exchange actually publishes, so the exchange's ordering is mostly passed straight through. The trade-off: ordering can vary by exchange and by case.
Note
The system makes no guarantee about the order of these callbacks, with
one exception. For derivatives, onInstrumentPosition is guaranteed to
fire first. The system tracks the real position locally from all three
events, so no matter which arrives first the position is known exactly.
The same is not true for spot trading: the wallet is affected by
fees, transfers, and fundings as well as trades.