New realtime endpoint: Ohio (AWS us-east-2, next to Kalshi) — Binance from Tokyo at ~65.0 ms one-way to your clientNew endpoint: Ohio · ~65.0 ms one-way

Configure
Low-Latency Trading Solutions
CART
Tutorials

The Strategy Development Kit

A quickstart guide to effortlessly design trading strategies. Four video lessons take you from an empty Java project to a running, backtested strategy. For the full written reference — strategy API, testing, message channels — see the Strategy SDK documentation.

01

Basic Setup

Introduction

Hi! Welcome to the first part of this tutorial series on creating your own strategy using the CryptoStruct Strategy SDK.

In the course of this series you will learn how to program your own strategies, upload and configure them – and finally how to run them on real crypto exchanges. In the fourth video, you’ll also learn how to locally test-run any strategies you developed.

In this first tutorial we will start a new Java project and create a strategy class that will hold our entire trading logic.

Classes that want to use the Strategy SDK simply need to implement Strategy. Almost all included methods (that you’ll need to override in your code) describe some form of event that originates directly from the crypto exchange.

That means, you can write event-based code that reacts instantly to any new information, such as incoming public trades, or changes to your positions.

Other events include start, stop and error events that communicate status updates of your strategy, or the methods onHeartbeat() or onTimer() that get called every 500 ms or once a timer (set by you) runs out respectively.

Prerequisites

  • The CryptoStruct Strategy SDK that you received from us
  • Any Java IDE (we recommend IntelliJ)
  • Apache Maven – a common build-management tool for Java
  • A pre-configured pom.xml file containing information for Maven on how to include the Strategy SDK
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.cryptostruct</groupId>
    <artifactId>example-strategy</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <cryptostruct.sdk.version>3.12.5</cryptostruct.sdk.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.cryptostruct</groupId>
            <artifactId>strategy-api</artifactId>
            <version>${cryptostruct.sdk.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>
        </plugins>
        <finalName>${artifactId}</finalName>
    </build>
</project>

Steps

  1. Unzip and install the Strategy SDK using the install script (available for Linux and Windows machines)
  2. Open your Java IDE with the provided pom.xml file
  3. Create a new Java file and call it e.g. ExampleStrategy.java
  4. Let your class implement Strategy
  5. Add the @Slf4j for easier logging
  6. Add the @ExportStrategy annotation and give your strategy a name (i.e. @ExportStrategy(name = "ExampleStrategy"))
  7. Each implemented method gets called upon receiving an event – let’s add simple log messages to each method (use e.g. log.info("onStarted()"))
  8. Use instrument.getSnapshot().printBook(5, log::info) to log the current state of an instrument’s order book (with a depth of 5)
  9. Use instrument.getLastTrades() in onInstrumentTrades() to get all newly recorded public trades
▶ Part 1: Setup
import com.cryptostruct.commons.annotations.ExportStrategy;
import com.cryptostruct.sdk.StopReason;
import com.cryptostruct.sdk.Strategy;
import com.cryptostruct.sdk.StrategyContext;
import com.cryptostruct.sdk.StrategyTimer;
import com.cryptostruct.sdk.instruments.DataInstrument;
import com.cryptostruct.sdk.instruments.TradingInstrument;
import com.cryptostruct.sdk.orders.*;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@ExportStrategy(name = "ExampleStrategy")
public class ExampleStrategy implements Strategy {
    private StrategyContext context;

    public ExampleStrategy(StrategyContext context) {
        this.context = context;
    }

    @Override
    public void onStarted() {
        log.info("onStarted()");
    }

    @Override
    public void onStop(StopReason stopReason, String message) {
        log.info("onStop(): {}", message);
    }

    @Override
    public void onMarketDataState(DataInstrument instrument) {
        log.info("onMarketDataState()");
    }

    @Override
    public void onInstrumentSnapshot(DataInstrument instrument) {
        log.info("onInstrumentSnapshot()");
        instrument.getSnapshot().printBook(5, log::info);
    }

    @Override
    public void onInstrumentTrades(DataInstrument instrument) {
        log.info("onInstrumentTrades()");
        instrument.getLastTrades().forEach(trade -> log.info(trade.toString()));
    }

    @Override
    public void onInstrumentPosition(TradingInstrument instrument) {
        log.info("onInstrumentPosition()");
    }

    @Override
    public void onOrderFill(Order order) {
        log.info("onOrderFill: {}", order.getDescriptiveString());
    }

    @Override
    public void onOrderUpdate(Order order) {
        log.info("onOrderUpdate(): {}", order.getDescriptiveString());
    }

    @Override
    public void onOrderError(Order order, OrderError orderError) {
        log.error("onOrderError(): {}", orderError.getMessage());
    }

    @Override
    public void onHeartbeat() {

    }

    @Override
    public void onTimer(StrategyTimer timer) {

    }
}

Let’s move on to the next video where we’ll add the actual strategy logic to our code!

02

Placing Orders

Introduction

In the second tutorial we will pick up where we left off in the last video and finish writing the code for our first trading strategy.

Later, when we upload the strategy to the trading dashboard, we will also create a configuration file that includes basic information about the strategy (e.g. its name), but also values for all variables that we want to keep open and easy modifiable – so we don’t have to change and recompile the code all the time!

We call these variables parameters and they require the @Parameter annotation to work.

You will see how easy it is to write a simple, reactive strategy with only a few lines of code, while the SDK takes care of all the boilerplate code, leaving you with more time to perfect your strategy!

Prerequisites

Steps

  1. Add an instance of TradingInstrument using the @Parameter annotation
  2. Add two instances of Order, with names bidOrder and askOrder
  3. In onInstrumentSnapshot() check whether an order is still null, and then place an order for that respective side (bid or ask) using tradingInstrument.placeOrder() and a fully instantiated OrderParams object that includes all needed configuration of our order
  4. Our orders should be limit orders, have a quantity of 100 and be placed exactly $15 away from the top of book
  5. You can get the current top of book using instrument.getSnapshot().getTopOfBook()
  6. Create a variable diffToToB of type Price to use instead of the hard-coded $15 and prepend it with @Parameter(optional = true)
  7. Add else-branches to the two checks to instead modify the two orders, if they are already placed and their prices have changed
  8. Simply use order.modifyPrice() to change an already placed order’s price
▶ Part 2: Place Orders

Note: Remember to add the @Parameter annotation to diffToToB (not shown in video).

import com.cryptostruct.commons.OrderSide;
import com.cryptostruct.commons.Price;
import com.cryptostruct.commons.Quantity;
import com.cryptostruct.commons.annotations.ExportStrategy;
import com.cryptostruct.commons.annotations.Parameter;
import com.cryptostruct.sdk.StopReason;
import com.cryptostruct.sdk.Strategy;
import com.cryptostruct.sdk.StrategyContext;
import com.cryptostruct.sdk.StrategyTimer;
import com.cryptostruct.sdk.instruments.DataInstrument;
import com.cryptostruct.sdk.instruments.TradingInstrument;
import com.cryptostruct.sdk.orders.*;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@ExportStrategy(name = "ExampleStrategy")
public class ExampleStrategy implements Strategy {
    @Parameter
    private TradingInstrument tradingInstrument;

    private Order bidOrder;
    private Order askOrder;

    @Parameter(optional = true)
    private Price diffToToB = Price.valueOf(15);

    private StrategyContext context;

    public ExampleStrategy(StrategyContext context) {
        this.context = context;
    }

    @Override
    public void onStarted() {
        log.info("onStarted()");
    }

    @Override
    public void onStop(StopReason stopReason, String message) {
        log.info("onStop(): {}", message);
    }

    @Override
    public void onMarketDataState(DataInstrument instrument) {
        log.info("onMarketDataState()");
    }

    @Override
    public void onInstrumentSnapshot(DataInstrument instrument) {
        log.info("onInstrumentSnapshot()");
        instrument.getSnapshot().printBook(5, log::info);

        if((bidOrder != null && bidOrder.getState() == OrderState.FILLED)
        && (askOrder != null && askOrder.getState() == OrderState.FILLED)) {
            // reset orders, to be able to place new
            bidOrder = null;
            askOrder = null;
        }

        if(bidOrder == null) {
            bidOrder = tradingInstrument.placeOrder(
                    OrderParams
                            .builder()
                            .price(instrument.getSnapshot().getTopOfBook().getBid().getPrice().subtract(diffToToB))
                            .quantity(Quantity.valueOf(100))
                            .side(OrderSide.BID)
                            .type(OrderType.LIMIT)
                    .build()
            );
        } else {
            Price tobBidPrice = instrument.getSnapshot().getTopOfBook().getBid().getPrice().subtract(diffToToB);

            if(!tobBidPrice.equals(bidOrder.getPrice())) {
                bidOrder.modifyPrice(tobBidPrice);
            }
        }

        if(askOrder == null) {
            askOrder = tradingInstrument.placeOrder(
                    OrderParams
                        .builder()
                        .price(instrument.getSnapshot().getTopOfBook().getAsk().getPrice().add(diffToToB))
                            .quantity(Quantity.valueOf(100))
                            .type(OrderType.LIMIT)
                            .side(OrderSide.ASK)
                    .build()
            );
        } else {
            Price tobAskPrice = instrument.getSnapshot().getTopOfBook().getAsk().getPrice().add(diffToToB);

            if(!tobAskPrice.equals(askOrder.getPrice())) {
                askOrder.modifyPrice(tobAskPrice);
            }
        }
    }

    @Override
    public void onInstrumentTrades(DataInstrument instrument) {
        log.info("onInstrumentTrades()");
        instrument.getLastTrades().forEach(trade -> log.info(trade.toString()));
    }

    @Override
    public void onInstrumentPosition(TradingInstrument instrument) {
        log.info("onInstrumentPosition()");
    }

    @Override
    public void onOrderFill(Order order) {
        log.info("onOrderFill: {}", order.getDescriptiveString());
    }

    @Override
    public void onOrderUpdate(Order order) {
        log.info("onOrderUpdate(): {}", order.getDescriptiveString());
    }

    @Override
    public void onOrderError(Order order, OrderError orderError) {
        log.error("onOrderError(): {}", orderError.getMessage());
    }

    @Override
    public void onHeartbeat() {

    }

    @Override
    public void onTimer(StrategyTimer timer) {

    }
}

Let’s move on to the next video, where we’ll upload and run our strategy!

03

Running the Strategy

Introduction

In the third tutorial we will take the finished Java code of the previous two tutorials, upload it to the trading dashboard and run it.

We will also upload a configuration file (in JSON format) that the server will use to execute your strategy.

The configuration file must contain at least the name of your strategy – the name you exported using @ExportStrategy(name = "…"), an instance name by which the strategy can be identified on the trading dashboard as well as values to all parameters that you have added to your code (using the @Parameter annotation).

By the way, any parameters with the annotation @Parameter(optional = true) can but need not be added to the configuration file.

Prerequisites

  • The final code created in the previous lesson (Java) or already packaged as .jar-file
  • An example configuration file (Note: don’t forget to replace the values between < and > with your own information!)
{
    "instanceName": "ExampleStrategyInstance",
    "strategyName": "ExampleStrategy",
    "parameters": {
        "tradingInstrument": {
            "id": <your desired instrument>,
            "accountId": <your account id>
            "minOrderSize": 100,
            "maxOrderCount": {
                "bid": 1,
                "ask": 1
            },
            "maxPosition": {
                "bid": "200",
                "ask": "200"
            },
            "maxNetPosition": {
                "bid": "200",
                "ask": "200"
            },
            "maxCounterPosition": {
                "bid": "0",
                "ask": "0"
            }
        },
        "diffToToB" : "10"
    }
}

Steps

  1. Place the final .jar file of your strategy into the (already existing) cryptostruct/strategies folder of your installation
  2. On your trading dashboard use the Settings menu Server Control to see the status of your servers and to restart the strategy server. This will register the freshly placed strategy file
  3. Go to BitMEX Testnet and create a free account
  4. Create an API key (menu Account & Preferences / API Keys), save the API key and API secret that you receive after creation
  5. On your dashboard, in the Exchanges menu choose "Testnet: BitMEX", click on Accounts and add a new account using the API key and secret that you received above
  6. Take the account_id automatically created here and paste it into the configuration file
  7. Go to menu Instrument Explorer, choose again "Testnet: BitMEX" and select a random instrument. Take the instrument’s id and also paste it into the configuration file
  8. In menu Strategies click on Strategy Instances and upload the final configuration file
  9. Click on the instance name of your strategy and press start
  10. You can now head over to the BitMEX Testnet website and observe your strategy in action!
▶ Part 3: Run Strategy
04

Strategy Backtesting

Introduction

In the fourth tutorial we will take our compiled code and run it locally against pre-compiled market data.

The backtesting tool quickly simulates days of trading data and lets you test the usefulness and risk involved with your strategy.

In this example we will use the data that you already received with your Strategy SDK package.

Prerequisites

  • The code created in the previous tutorials, packaged as a .jar-file

Steps

  1. Duplicate and open the simplespreader-backtest.json file included in your SDK archive
  2. Modify it to fit our strategy:
  3. Delete the second instrument
  4. Delete the unused parameters
  5. Add our own diffToToB parameter
  6. Change the first instrument’s name to our instrument variable name (tradingInstrument)
  7. Change instanceName and strategyName to fit our strategy
  8. Change the strategyFile path to point to our code’s .jar file
  9. In your SDK folder, run java -jar ./bin/backtest.jar -el output.json ./[path to your copied configuration file].json > output.log
  10. After a short time, this will have created both an output.json and an output.txt file in your current directory
  11. In the output.json file you can see all orders and modifications generated by your code
  12. In the output.log file you can see the actual logging output generated by your strategy’s log.info() lines
▶ Part 4: Strategy Backtesting
{
  "strategyFile" : "./example-strategy.jar",
  "strategyConfig" : {
		"instanceName": "ExampleStrategyInstance",
		"strategyName": "ExampleStrategy",
		"parameters": {
			"tradingInstrument": {
				"id": 22,
				"accountId": 15,
				"minOrderSize":"100",
				"maxOrderCount": {
					"bid": 1,
					"ask": 1
				},
				"maxPosition": {
					"bid": "200",
					"ask": "200"
				},
				"maxNetPosition": {
					"bid": "200",
					"ask": "200"
				},
				"maxCounterPosition": {
					"bid": "0",
					"ask": "0"
				}
			},
			"diffToToB": "10"
		}
   },
  "instrumentsFile" : "simplespreader-instruments.json",
  "underlyingsFile" : "simplespreader-underlyings.json",
  "instruments" : [{
    "instrumentId" : 1,
    "marketDataFile" : "simplespreader-marketdata.txt.gz",
    "latencyUs" : 10000,
    "timePrioHeuristic": "REMOVE_SUPERIOR"
  },{
    "instrumentId" : 8,
    "marketDataFile" : "simplespreader-marketdata.txt.gz",
    "latencyUs" : 10000,
    "timePrioHeuristic": "REMOVE_SUPERIOR"
  }]
}