Introduction to Detailed High-Frequency Trading Strategies for Cryptocurrencies

Author: 发明者量化, Created: 2023-03-19 19:56:11, Updated: 2023-09-18 19:55:58

img

I wrote an article in 2020 introducing high-frequency trading strategies (https://www.fmz.com/digest-topic/6228). Although it received some attention, it was not very in-depth. Two and a half years have passed since then, and the market has changed. After I published that article, my high-frequency strategy was able to make stable profits for a long time, but profits gradually declined and even stopped at one point. In recent months, I have spent time revamping it, and it can still make some small profits. This article will provide a more detailed introduction to my high-frequency trading strategy and some simplified code, serving as a starting point for discussion and feedback.

Conditions for high-frequency trading

Commission rebate accounts

using Binance as an example, currently offer a maker rebate of 0.05% for every 100,000 units traded. If the daily trading volume is 100 million U, the rebate is 5,000 U. Of course, the taker fee still depends on the VIP rate, so if the strategy does not need to take orders, the VIP level has little effect on the high-frequency strategy. Different levels of commission rebates are available on different exchanges, requiring a high trading volume. In the early days, there was still profit to be made without rebates, but as competition intensified, rebates accounted for a larger proportion of profits, and high-frequency traders pursued the top rates.

Speed

High-frequency trading is so called because of its fast speed. Joining a trading exchange’s colocation server and obtaining the lowest latency and most stable connection has become one of the competition conditions. The strategy’s internal processing time should also be as low as possible. This article will introduce the WebSocket framework I used, which uses concurrent execution.

Suitable market

High-frequency trading is considered the jewel in the crown of quantitative trading, and I believe that many algorithmic traders have tried it, but most people should have stopped because they couldn’t make money and couldn’t find a way to improve. The main reason is probably because they chose the wrong trading market. In the initial stage of the strategy, relatively easy markets should be targeted for trading to earn profits and receive feedback for improvement, which is conducive to the strategy’s progress. If you start out in the most fiercely competitive market and compete with many potential opponents, you will lose money no matter how hard you try, and you will quickly give up. I recommend starting with newly launched perpetual contract trading pairs, where there are fewer competitors, especially those with relatively large trading volumes, making it easier to make money. BTC and ETH have the highest trading volumes and are the most active, but they are also the most difficult to survive in.

Face competition head-on

The market for any trading is constantly changing, and no trading strategy can be a one-time solution. This is even more obvious in high-frequency trading, where entering the market means competing directly with the smartest and most diligent traders. In a zero-sum game market, the more you earn, the less others earn. The later you enter, the harder it gets, and those who are already in the market must constantly improve and could be eliminated at any time. Three or four years ago was probably the best opportunity, but with the recent overall decline in activity in the digital currency market, it has become very difficult for beginners to start doing high-frequency trading.

High-Frequency Trading Principles

There are several high-frequency trading strategies, such as high-frequency arbitrage, which involves finding arbitrage opportunities through this or other exchanges, seizing the opportunity to eat up orders ahead of others and make profits with speed advantage; high-frequency trend trading, which involves profiting from short-term trends; and market making, which involves placing orders on both sides of the buy and sell trades, controlling positions well and earning profits through commission rebates. My strategy combines trend and market making, first identifying trends and then placing orders, selling immediately after execution and not holding inventory positions. Below is an introduction to the strategy code.

Strategy Architecture

The following code is based on the basic architecture of the Binance perpetual contract and mainly subscribes to the websocket depth order flow trades and position information. Since market data and account information are subscribed separately, read (-1) needs to be continuously used to determine whether the latest information has been obtained. Here, EventLoop (1000) is used to avoid direct dead loops and reduce system load. EventLoop (1000) blocks until there is a wss or concurrent task return, with a timeout of 1000ms.

var datastream = null
var tickerstream = null
var update_listenKey_time = 0

function ConncetWss(){
    if (Date.now() - update_listenKey_time < 50*60*1000) {
        return
    }
    if(datastream || tickerstream){
        datastream.close()
        tickerstream.close()
    }
    // need APIKEY
    let req = HttpQuery(Base+'/fapi/v1/listenKey', {method: 'POST',data: ''}, null, 'X-MBX-APIKEY:' + APIKEY) 
    let listenKey = JSON.parse(req).listenKey
    datastream = Dial("wss://fstream.binance.com/ws/" + listenKey + '|reconnect=true', 60)
    // Symbols is the pair of symbol
    let trade_symbols_string = Symbols.toLowerCase().split(',')
    let wss_url = "wss://fstream.binance.com/stream?streams="+trade_symbols_string.join(Quote.toLowerCase()+"@aggTrade/")+Quote.toLowerCase()+"@aggTrade/"+trade_symbols_string.join(Quote.toLowerCase()+"@depth20@100ms/")+Quote.toLowerCase()+"@depth20@100ms"
    tickerstream = Dial(wss_url+"|reconnect=true", 60)
    update_listenKey_time = Date.now()
}

function ReadWss(){
    let data = datastream.read(-1)
    let ticker = tickerstream.read(-1)
    while(data){
        data = JSON.parse(data)
        if (data.e == 'ACCOUNT_UPDATE') {
            updateWsPosition(data)
        }
        if (data.e == 'ORDER_TRADE_UPDATE'){
            updateWsOrder(data)
        }        
        data = datastream.read(-1)
    }
    while(ticker){
        ticker = JSON.parse(ticker).data
        if(ticker.e == 'aggTrade'){
            updateWsTrades(ticker)
        }
        if(ticker.e == 'depthUpdate'){
            updateWsDepth(ticker)
        }
        ticker = tickerstream.read(-1)
    }
    makerOrder()
}

function main() {
    while(true){
        ConncetWss()
        ReadWss()
        worker()
        updateStatus()
        EventLoop(1000)
    }
}

Strategy Indicators

As mentioned earlier, my high-frequency strategy requires first identifying trends before executing buy and sell trades. Judging short-term trends is mainly based on transaction data, that is, the aggTrade subscribed, which includes the direction, price, quantity, and transaction time. Buy and sell trades mainly refer to depth and transaction volume. The following are detailed indicators that need to be considered, most of which are divided into two groups for buy and sell and are dynamically counted within a certain time window. My strategy’s time window is within 10 seconds.

  • Average trading volume per transaction, which is the collection of transactions in the same direction, price, and different orders within 100ms. This reflects the size of buy and sell orders and has a high weight. If the trading volume of buy orders is greater than that of sell orders, it can be assumed that the market is dominated by buyers.
  • Order frequency or order interval, also based on transaction data, the previous average transaction volume did not consider the time concept and is not entirely accurate. If an order in one direction has a small average transaction volume but a high frequency, it also contributes to the strength of that direction. Average transaction volume * order frequency represents the total transaction volume at a fixed interval and can be used for direct comparison. The order arrival event follows a Poisson distribution, which can be used to estimate how much the total order value will be reached within a specific time interval, providing a reference for placing orders.
  • Average spread, which is the difference between the selling price and the buying price. The current spread is mostly one tick, and if the spread increases, it often indicates that there is a trend. Average buy and sell prices, which calculate the average price of buy and sell transactions respectively and compare them with the latest price. If the latest buy order price is greater than the average buy order price, a breakthrough can be preliminarily judged.

Strategy Logic

Judging Short-Term Trends

let bull =  last_sell_price > avg_sell_price && last_buy_price > avg_buy_price &&
            avg_buy_amount / avg_buy_time > avg_sell_amount / avg_sell_time;
let bear =  last_sell_price < avg_sell_price && last_buy_price < avg_buy_price && 
            avg_buy_amount / avg_buy_time < avg_sell_amount / avg_sell_time;

If the latest ask price is greater than the average ask price and the latest bid price is greater than the average bid price and the value of the buy order is greater than the value of the sell order at a fixed interval, then it is judged to be a short-term bullish market. The opposite is true for bearish markets.

Placing Orders

function updatePrice(depth, bid_amount, ask_amount) {

    let buy_price = 0
    let sell_price = 0
    let acc_bid_amount = 0
    let acc_ask_amount = 0

    for (let i = 0; i < Math.min(depth.asks.length, depth.bids.length); i++) {
        acc_bid_amount += parseFloat(depth.bids[i][1])
        acc_ask_amount += parseFloat(depth.asks[i][1])
        if (acc_bid_amount > bid_amount  && buy_price == 0) {
            buy_price = parseFloat(depth.bids[i][0]) + tick_size
        }
        if (acc_ask_amount > ask_amount  && sell_price == 0) {
            sell_price = parseFloat(depth.asks[i][0]) - tick_size
        }
        if (buy_price > 0 && sell_price > 0) {
            break
        }
    }
    return [buy_price, sell_price]
}

Here, the old method of iterating depth to the required quantity is still used. Assuming that a buy order that can be executed for 10 coins within 1 second and without considering the situation of new orders, the selling price is set to the position where the buy order

with a volume of 10 coins can hit. The specific time window size needs to be set by oneself.

Order Quantity

let buy_amount = Ratio * avg_sell_amount / avg_sell_time
let sell_amount = Ratio * avg_buy_amount / avg_buy_time

The ratio represents a fixed proportion of the latest sell order quantity, representing the buy order quantity as a fixed proportion of the latest sell order quantity. This allows the strategy to adjust the order size according to the current buying and selling activity.

Order Conditions

if(bull && (sell_price-buy_price) > N * avg_diff) {
    trade('buy', buy_price, buy_amount)
}else if(position.amount < 0){
    trade('buy', buy_price, -position.amount)
}
if(bear && (sell_price-buy_price) >  N * avg_diff) {
    trade('sell', sell_price, sell_amount)
}else if(position.amount > 0){
    trade('sell', sell_price, position.amount)
}

Among them, the avg_diff is the average difference in spread, and only when the buy and sell difference in placing orders is greater than a certain multiple of this value and the market is bullish will a buy order be placed. If holding a short position, the position will also be closed to avoid holding the position for a long time. Only-maker orders can be placed to ensure that orders are filled, and custom order IDs can be used to avoid waiting for order returns.

Concurrent Architecture

var tasks = []
var jobs = []

function worker(){
    let new_jobs = []
    for(let i=0; i<tasks.length; i++){
        let task = tasks[i]
        jobs.push(exchange.Go.apply(this, task.param))
    }
    _.each(jobs, function(t){
        let ret = t.wait(-1)
        if(ret === undefined){
            new_jobs.push(t)//未返回的任务下次继续等待
        }
    })
    jobs = new_jobs
    tasks = []
}

/*
tasks.push({'type':'order','param': ["IO", "api", "POST","/fapi/v1/order",
        "symbol="+symbol+Quote+"&side="+side+"&type=LIMIT&timeInForce=GTX&quantity="+
        amount+"&price="+price+"&newClientOrderId=" + UUID() +"&timestamp="+Date.now()]})
*/

Monitored Data

  • Latency: The importance of the speed of high-frequency trading strategies has been emphasized. The strategy needs to monitor and record various latencies, such as placing orders, canceling orders, returning positions, depth, order flow, overall cycle, etc. Any abnormal latency needs to be promptly investigated and ways to shorten the overall strategy latency should be found.
  • Volume proportion: Statistics show the proportion of trading volume to total trading volume. If the proportion is low, there is still room for improvement. At peak times, the strategy’s proportion can reach 10% or more of the total trading volume.
  • Closing profit rate: Statistics show the average closing profit rate, which is the most important reference for judging whether the strategy is effective.
  • Rebate ratio: Statistics show the proportion of rebates to total revenue, reflecting the degree to which the strategy depends on rebates. Different exchanges have different rebate levels, and a strategy that does not profit may be profitable with a higher rebate level. Order failure rate: Orders are only placed and filled, but due to delays in placing orders, they may not be filled. If this ratio is high, it indicates that the strategy’s speed is not optimal.
  • Order execution ratio: The platform often has requirements for order execution ratios. If it is too low, it indicates that the strategy is canceling orders too frequently and needs to be resolved. Average buy and sell order distance: This data reflects the distance between the strategy’s order placement and the market depth, and most of the orders are still occupying the positions of the buy and sell orders.

Other Suggestions

  • Trade multiple currencies: The high-frequency strategy in this article is limited to single exchanges, single currency pairs, and single market conditions, and has limited applicability. Most currencies cannot be profitable, but it is impossible to predict which currencies will be profitable in the future, so trading multiple or even all currencies is recommended to not miss opportunities. Even under the frequency restrictions of the exchange, a single robot can trade multiple trading pairs. Of course, for the best speed, one sub-account can trade one trading pair, one server corresponds to one robot, but this approach will have much higher costs.
  • Determine order size and conditions based on profitability. Trading multiple currencies can result in too high of a cost of trying, so if monitoring shows that it is not profitable, reduce the trading frequency and use the minimum trading volume until the strategy dynamically detects a positive profit rate and then gradually increase the trading volume to improve profitability.
  • Obtain more information: Another characteristic of high-frequency trading is that it handles more data and uses more information. All market data for a single exchange and currency pair should be considered, and perpetual data can also refer to spot data or data for the same currency pair on other exchanges or even other currencies. The more data, the greater the corresponding advantage. For example, Binance can subscribe to the

Related

More