Build a Trading Bot with Cassandre Spring Boot Starter – 用Cassandre Spring Boot Starter建立一个事务机器人

最后修改: 2021年 5月 18日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

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.

在本教程中,我们将使用Cassandre创建一个简单的加密货币交易机器人,在我们认为是最佳时机时产生头寸。

2. Bot Overview

2.机器人概述

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

3.Maven的依赖性

Let’s get started by adding the necessary dependencies to our pom.xml, first the Cassandre spring boot starter:

让我们开始在我们的pom.xml中添加必要的依赖项,首先是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:

Cassandre依靠XChange来连接到加密货币交易所。在本教程中,我们将使用Kucoin XChange库

<dependency>
    <groupId>org.knowm.xchange</groupId>
    <artifactId>xchange-kucoin</artifactId>
    <version>5.0.8</version>
</dependency>

We’re also using hsqld to store data:

我们也在使用hsqld来存储数据:

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

为了针对历史数据测试我们的交易机器人,我们还添加了我们的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

4.配置

Let’s edit create application.properties to set our configuration:

让我们编辑创建application.properties来设置我们的配置。

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

现在让我们在测试目录的application.properties中创建相同的文件,但我们将cassandre.trading.bot.exchange.modes.dry改为true,因为在测试期间,我们不希望将真实订单发送到沙盒。我们只想模拟它们。

5. The Strategy

5.战略

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.

交易策略是一个固定的计划,旨在实现有利可图的回报;我们可以通过添加一个注释为@CassandreStrategy的Java类并扩展BasicCassandreStrategy来制作我们的策略。

Let’s create our strategy class in MyFirstStrategy.java:

让我们在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():

实现BasicCassandreStrategy迫使我们实现两个方法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.

getRequestedCurrencyPairs()中,我们必须返回我们想从交易所接收的货币对更新列表。货币对是两种不同货币的报价,一种货币的价值是针对另一种货币的报价。在我们的例子中,我们想用BTC/USDT来工作。

To make it more clear, we can retrieve a ticker manually with the following curl command:

为了更清楚地说明问题,我们可以用下面的curl命令来手动检索一个股票。

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.

价格值表明,1个BTC需要57263.3美元。

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.

我们要实现的另一个方法是getTradeAccount()。在交易所,我们通常有几个账户,Cassandre需要知道哪一个账户是交易账户。要做到这一点,我们必须实现getTradeAccount()方法,该方法给我们一个参数,即我们拥有的账户列表,从该列表中,我们必须返回我们想用来交易的账户。

In our example, our trade account on the exchange is named “trade”, so we simply return it.

在我们的例子中,我们在交易所的交易账户被命名为“trade”,所以我们简单地返回它。

6. Creating Positions

6.创建职位

To be notified of new data, we can override the following methods of BasicCassandreStrategy:

为了得到新数据的通知,我们可以覆盖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.

在本教程中,我们将实现一个傻瓜式的算法。我们检查每一个新收到的股票。如果1 BTC的价格低于56 000 USDT,我们认为是时候买入了

To make things easier about gain calculation, orders, trades, and closure, Cassandre provides a class to manage positions automatically.

为了使有关收益计算、订单、交易和关闭的事情变得更容易,Cassandre提供了一个自动管理头寸的类。

To use it, the first step is to create the rules for the position thanks to the PositionRulesDTO class, for example:

要使用它,第一步是为职位创建规则,感谢PositionRulesDTO类,例如。

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

此刻,Cassandre将创建一个0.01 BTC的买单。仓位状态将是开放的,当所有相应的交易到达后,状态将转为开放。从现在开始,对于每一个收到的股票,Cassandre将自动计算,用新的价格,如果在这个价格关闭头寸会触发我们的两个规则之一(4%的止盈或25%的止损)。

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.

如果一个规则被触发,Cassandre将自动创建一个我们0.01BTC的卖出订单。仓位状态将移至CLOSING,当所有相应的交易到达后,状态将移至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

7.关注日志中的位置演变

We’ll finally implement the onPositionStatusUpdate() to see when positions are opened/closed:

我们最后将实现onPositionStatusUpdate(),以查看仓位何时被打开/关闭。

@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

8.回溯测试

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.

简单地说,回溯测试策略是在以前的时期测试一个交易策略的过程。Cassandre交易机器人允许我们模拟机器人对历史数据的反应。

The first step is to put our historical data (CSV or TSV files) in our src/test/resources folder.

第一步是把我们的历史数据(CSV或TSV文件)放在我们的src/test/resources文件夹中。

If we are under Linux, here is a simple script to generate them:

如果我们是在Linux下,这里有一个简单的脚本来生成它们。

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

它将创建一个名为tickers-btc-usdt.tsv的文件,其中包含BTC-USDT从startDate(3个月前)到endDate(现在)的历史利率。

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 :

在这些文件中,对于每个账户,我们设置了每个加密货币的余额。例如,这是模拟我们交易账户资产的user-trade.csv的内容。

This file must also be in the src/test/resources folder.

这个文件也必须在src/test/resources文件夹中。

BTC 1
USDT 10000
ETH 10

Now, we can add a test:

现在,我们可以添加一个测试。

@SpringBootTest
@Import(TickerFluxMock.class)
@DisplayName("Simple strategy test")
public class MyFirstStrategyLiveTest {
    @Autowired
    private MyFirstStrategy strategy;

    private final Logger logger = LoggerFactory.getLogger(MyFirstStrategyLiveTest.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.

@Importfrom TickerFluxMock将从我们的src/test/resources文件夹加载历史数据,并将它们发送到我们的策略。然后我们使用await()方法来确保所有从文件中加载的股票都被发送到我们的策略中。最后,我们显示已平仓的头寸、仍未平仓的头寸,以及全局收益。

9. Conclusion

9.结语

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.

当然,我们的算法是直接的;在现实生活中,目标是找到一个有前途的技术,一个好的算法,以及好的数据,以知道我们何时可以建立一个位置。例如,我们可以使用技术分析,因为Cassandre整合了ta4j.

All the code of this article is available over on GitHub.

本文的所有代码都可以在GitHub上找到