1. Overview
A trading bot is a computer program that can automatically place orders to a market or exchange without the need for human intervention.
In this tutorial, we'll use Cassandre to create a simple crypto trading bot that will generate positions when we think it’s the best moment.
2. Bot Overview
Trading means “exchanging one item for another”.
In the financial markets, it’s buying shares, futures, options, swaps, bonds, or like in our case, an amount of cryptocurrency. The idea here is to buy cryptocurrencies at a specific price and sell it at a higher price to make profits (even if we can still profit if the price goes down with a short position).
We'll use a sandbox exchange; a sandbox is a virtual system where we have “fake” assets, where we can place orders and receive tickers.
First, let's see what we'll do:
- Add Cassandre spring boot starter to our project
- Add the required configuration to connect to the exchange
- Create a strategy:
- Receive tickers from the exchange
- Choose when to buy
- When it’s time to buy, check if we have enough assets and creates a position
- Display logs to see when positions are open/closed and how much gain we made
- Run tests against historical data to see if we can make profits
3. Maven Dependencies
Let's get started by adding the necessary dependencies to our pom.xml, first the Cassandre spring boot starter:
<dependency>
<groupId>tech.cassandre.trading.bot</groupId>
<artifactId>cassandre-trading-bot-spring-boot-starter</artifactId>
<version>4.2.1</version>
</dependency>
Cassandre relies on XChange to connect to crypto exchanges. For this tutorial, we're going to use the Kucoin XChange library:
<dependency>
<groupId>org.knowm.xchange</groupId>
<artifactId>xchange-kucoin</artifactId>
<version>5.0.8</version>
</dependency>
We're also using hsqld to store data:
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.5.2</version>
</dependency>
For testing our trading bot against historical data, we also add our Cassandre spring boot starter for tests:
<dependency>
<groupId>tech.cassandre.trading.bot</groupId>
<artifactId>cassandre-trading-bot-spring-boot-starter-test</artifactId>
<version>4.2.1</version>
<scope>test</scope>
</dependency>
4. Configuration
Let's edit create application.properties to set our configuration:
# Exchange configuration
cassandre.trading.bot.exchange.name=kucoin
cassandre.trading.bot.exchange.username=kucoin.cassandre.test@gmail.com
cassandre.trading.bot.exchange.passphrase=cassandre
cassandre.trading.bot.exchange.key=6054ad25365ac6000689a998
cassandre.trading.bot.exchange.secret=af080d55-afe3-47c9-8ec1-4b479fbcc5e7
# Modes
cassandre.trading.bot.exchange.modes.sandbox=true
cassandre.trading.bot.exchange.modes.dry=false
# Exchange API calls rates (ms or standard ISO 8601 duration like 'PT5S')
cassandre.trading.bot.exchange.rates.account=2000
cassandre.trading.bot.exchange.rates.ticker=2000
cassandre.trading.bot.exchange.rates.trade=2000
# Database configuration
cassandre.trading.bot.database.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
cassandre.trading.bot.database.datasource.url=jdbc:hsqldb:mem:cassandre
cassandre.trading.bot.database.datasource.username=sa
cassandre.trading.bot.database.datasource.password=
The configuration has four categories:
- Exchange configuration: The exchange credentials we set up for us a connection to an existing sandbox account on Kucoin
- Modes: The modes we want to use. In our case, we're asking Cassandre to use the sandbox data
- Exchange API calls rates: Indicates at which pace we want to retrieve data (accounts, orders, trades, and tickers) from the exchange. Be careful; all exchanges have maximum rates at which we can call them
- Database configuration: Cassandre uses a database to store positions, orders & trades. For this tutorial, we'll use a simple hsqld in-memory database. Of course, when in production, we should use a persistent database
Now let's create the same file in application.properties in our test directory, but we change cassandre.trading.bot.exchange.modes.dry to true because, during tests, we don't want to send real orders to the sandbox. We only want to simulate them.
5. The Strategy
A trading strategy is a fixed plan designed to achieve a profitable return; we can make ours by adding a Java class annotated with @CassandreStrategy and extending BasicCassandreStrategy.
Let’s create our strategy class in MyFirstStrategy.java:
@CassandreStrategy
public class MyFirstStrategy extends BasicCassandreStrategy {
@Override
public Set<CurrencyPairDTO> getRequestedCurrencyPairs() {
return Set.of(new CurrencyPairDTO(BTC, USDT));
}
@Override
public Optional<AccountDTO> getTradeAccount(Set<AccountDTO> accounts) {
return accounts.stream()
.filter(a -> "trade".equals(a.getName()))
.findFirst();
}
}
Implementing BasicCassandreStrategy forces us to implement two methods getRequestedCurrencyPairs() & getTradeAccount():
In getRequestedCurrencyPairs(), we have to return the list of currency pairs updates we want to receive from the exchange. A currency pair is the quotation of two different currencies, with the value of one currency being quoted against the other. In our example, we want to work with BTC/USDT.
To make it more clear, we can retrieve a ticker manually with the following curl command:
curl -s https://api.kucoin.com/api/v1/market/orderbook/level1?symbol=BTC-USDT
We'll get something like that:
{
"time": 1620227845003,
"sequence": "1615922903162",
"price": "57263.3",
"size": "0.00306338",
"bestBid": "57259.4",
"bestBidSize": "0.00250335",
"bestAsk": "57260.4",
"bestAskSize": "0.01"
}
The price value indicates that 1 BTC costs 57263.3 USDT.
The other method we have to implement is getTradeAccount(). On the exchange, we usually have several accounts, and Cassandre needs to know which one of the accounts is the trading one. To do so, e have to implement the getTradeAccount() method, which gives usw as a parameter the list of accounts we own, and from that list, we have to return the one we want to use for trading.
In our example, our trade account on the exchange is named “trade”, so we simply return it.
6. Creating Positions
To be notified of new data, we can override the following methods of BasicCassandreStrategy:
- onAccountUpdate() to receive updates about account
- onTickerUpdate() to receive new tickers
- onOrderUpdate() to receive updates about orders
- onTradeUpdate() )to receive updates about trades
- onPositionUpdate() to receive updates about positions
- onPositionStatusUpdate() to receive updates about position status change
For this tutorial, we'll implement a dumb algorithm: we check every new ticker received. If the price of 1 BTC goes under 56 000 USDT, we think it’s time to buy.
To make things easier about gain calculation, orders, trades, and closure, Cassandre provides a class to manage positions automatically.
To use it, the first step is to create the rules for the position thanks to the PositionRulesDTO class, for example:
PositionRulesDTO rules = PositionRulesDTO.builder()
.stopGainPercentage(4f)
.stopLossPercentage(25f)
.create();
Then, let's create the position with that rule:
createLongPosition(new CurrencyPairDTO(BTC, USDT), new BigDecimal("0.01"), rules);
At this moment, Cassandre will create a buy order of 0.01 BTC. The position status will be OPENING, and when all the corresponding trades have arrived, the status will move to OPENED. From now on, for every ticker received, Cassandre will automatically calculate, with the new price, if closing the position at that price would trigger one of our two rules (4% stop gain or 25% stop loss).
If one rule is triggered, Cassandre will automatically create a selling order of our 0.01 BTC. The position status will move to CLOSING, and when all the corresponding trades have arrived, the status will move to CLOSED.
This is the code we'll have:
@Override
public void onTickerUpdate(TickerDTO ticker) {
if (new BigDecimal("56000").compareTo(ticker.getLast()) == -1) {
if (canBuy(new CurrencyPairDTO(BTC, USDT), new BigDecimal("0.01"))) {
PositionRulesDTO rules = PositionRulesDTO.builder()
.stopGainPercentage(4f)
.stopLossPercentage(25f)
.build();
createLongPosition(new CurrencyPairDTO(BTC, USDT), new BigDecimal("0.01"), rules);
}
}
}
To sum up:
- For every new ticker, we check if the price is under 56000.
- If we have enough USDT on our trade account, we open a position for 0.01 BTC.
- From now on, for every ticker:
- If the calculated gain with the new price is over 4% gain or 25% loss, Cassandre will close the position we created by selling the 0.01 BTC bought.
7. Follow Positions Evolution in Logs
We'll finally implement the onPositionStatusUpdate() to see when positions are opened/closed:
@Override
public void onPositionStatusUpdate(PositionDTO position) {
if (position.getStatus() == OPENED) {
logger.info("> New position opened : {}", position.getPositionId());
}
if (position.getStatus() == CLOSED) {
logger.info("> Position closed : {}", position.getDescription());
}
}
8. Backtesting
In simple words, backtesting a strategy is the process of testing a trading strategy on prior periods. Cassandre trading bot allows us to simulate bots' reactions to historical data.
The first step is to put our historical data (CSV or TSV files) in our src/test/resources folder.
If we are under Linux, here is a simple script to generate them:
startDate=`date --date="3 months ago" +"%s"`
endDate=`date +"%s"`
curl -s "https://api.kucoin.com/api/v1/market/candles?type=1day&symbol=BTC-USDT&startAt=${startDate}&endAt=${endDate}" \
| jq -r -c ".data[] | @tsv" \
| tac $1 > tickers-btc-usdt.tsv
It'll create a file named tickers-btc-usdt.tsv that contains the historical rate of BTC-USDT from startDate (3 months ago) to endDate (now).
The second step is to create our(s) virtual account(s) balances to simulate the exact amount of assets we want to invest.
In those files, for each account, we set the balances of each cryptocurrency. For example, this is the content of user-trade.csv that simulate our trade account assets :
This file must also be in the src/test/resources folder.
BTC 1
USDT 10000
ETH 10
Now, we can add a test:
@SpringBootTest
@Import(TickerFluxMock.class)
@DisplayName("Simple strategy test")
public class MyFirstStrategyUnitTest {
@Autowired
private MyFirstStrategy strategy;
private final Logger logger = LoggerFactory.getLogger(MyFirstStrategyTest.class);
@Autowired
private TickerFluxMock tickerFluxMock;
@Test
@DisplayName("Check gains")
public void whenTickersArrives_thenCheckGains() {
await().forever().until(() -> tickerFluxMock.isFluxDone());
HashMap<CurrencyDTO, GainDTO> gains = strategy.getGains();
logger.info("Cumulated gains:");
gains.forEach((currency, gain) -> logger.info(currency + " : " + gain.getAmount()));
logger.info("Position still opened :");
strategy.getPositions()
.values()
.stream()
.filter(p -> p.getStatus().equals(OPENED))
.forEach(p -> logger.info(" - {} " + p.getDescription()));
assertTrue(gains.get(USDT).getPercentage() > 0);
}
}
The @Import from TickerFluxMock will load the historical data from our src/test/resources folder and send them to our strategy. Then we use the await() method to be sure all tickers loaded from files have been sent to our strategy. We finish by displaying the closed positions, the position still opened, and the global gain.
9. Conclusion
This tutorial illustrated how to create a strategy interacting with a crypto exchange and test it against historical data.
Of course, our algorithm was straightforward; in real life, the goal is to find a promising technology, a good algorithm, and good data to know when we can create a position. We can, for example, use technical analysis as Cassandre integrates ta4j.
All the code of this article is available over on GitHub.
The post Build a Trading Bot with Cassandre Spring Boot Starter first appeared on Baeldung.