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

  • Boolean
  • Integer
  • Long
  • Float
  • Double
  • String

Decimal types

  • BigDecimal
  • Price
  • Quantity

SDK types

  • DataInstrument
  • TradingInstrument
  • Account
  • TwoSides<T>, where T is any other supported type

Collections

  • List<T>, where T is 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:

Basic order placement

@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());
}

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.

Field Type Required Default Description
side OrderSide Required none BID or ASK. See Order Side.
quantity Quantity Required none Total order quantity.
price Price Optional null Order price. Null for MARKET_PEGGED and PRIMARY_PEGGED.
type OrderType Optional LIMIT See Order Type.
timeInForce TimeInForce Optional GOOD_TILL_CANCEL See Time in Force.
postOnly boolean Optional true Rejects the order if it would match immediately.
triggerPrice Price Optional null Required for trigger order types. See Trigger orders.
pegOffsetValue Price Optional null Offset added to the reference price. Only for MARKET_PEGGED and PRIMARY_PEGGED.
displayQuantity Quantity Optional null Quantity visible in the public book. Smaller than quantity makes an iceberg; zero makes a fully hidden order.
minQuantity Quantity Optional Quantity.ZERO Lower bound on order size. The effective bound is the larger of this and the instrument's minimum order size.
closeOnly Boolean Optional false Closes the position completely. May delete other open orders to do it.
reduceOnly Boolean Optional null Limits the size so the order can only reduce the position. Unlike closeOnly, never deletes other orders.
openClose OpenClose Optional AUTO Behaviour with netted positions. See Open / Close.
autoBorrow Boolean Optional null Borrows the required quantity automatically on margin. See Margin trading.
autoRepay Boolean Optional null Uses funds received from the order to repay outstanding loans and interest.
baseRiskRatio BigDecimal Optional null Factor between 0 and 1 reducing the order's available base risk. An available risk of 20 at 0.9 allows at most 18.
counterRiskRatio BigDecimal Optional null The same factor for counter risk. Spot instruments only.
useSpare boolean Optional false Draws on the reserved pool in the Trading Adapter's rate limiter. See Rate limit load.

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:

Defensive order (post-only by default)

@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:

Aggressive order that can immediately match

@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.

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.

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.


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_NEW to CONFIRMED).
  • 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:

State Description
READY Everything normal, no restrictions
DEGRADED Trading Adapter has lost the private connection to the exchange and cannot receive events like order updates or trades. Strategies can still try to cancel orders but success is not guaranteed. Entering new orders or amending existing orders is not allowed. If this state persists for too long, a strategy gets automatically stopped
DELAYED When trading derivatives, the system keeps track of all positions based on order updates, fill events, and actual position updates by the exchange. DELAYED means there is some discrepancy between these sources, usually because some event (like a fill) has not been received yet. Trading is not restricted in this case and current position is most likely still correct, but this is a first sign that there might be a bigger issue. Depending on how risk-averse a strategy is, this can be used to pause trading. If this state persists for too long, a strategy gets automatically stopped
NOT_AVAILABLE Trading Adapter was shut down externally, most likely by user input. Strategies will continue running, waiting for the Trading Adapter to be restarted, but placing, amending, or canceling orders is no longer possible
ERROR Severe error state. Not visible to a strategy because strategies are stopped automatically if this happens

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, OrderError is null. Each affected order also produces an onOrderUpdate reporting the cancellation.

  • If the request failed, OrderError carries the details.

This is the same shape as onOrderError, but scoped to the whole account rather than one order.

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.