I wrote an article in 2020 introducing high-frequency strategies, https://www.fmz.com/bbs-topic/9750. Although it received quite a bit of attention, it was not very in-depth. More than two years have passed since then, and the market has changed. After that article was published, my high-frequency strategy could make profits stably for a long time, but gradually, the profits declined and even stopped at one point. In recent months I have spent some effort to revamp it, and now it can still make some profits. In this article, I will provide a more detailed introduction to my high-frequency strategy ideas and some simplified code as a starting point for discussion; communications and feedbacks are welcome.
High-frequency trading conditions
Rebate accounts, taking Binance as an example, it has a maker rebate of 0.0005% currently. If the daily transaction amount is 100 million U, the rebate will be 5000 U. Of course, taker fees are still based on VIP rates, so if the strategy does not require takers, VIP level has little impact on high-frequency strategies. Different levels of exchanges generally have different rebate rates and require maintaining a high transaction amount. In the early times when some currency markets fluctuated greatly, there were profits even without rebates. As competition intensified, rebates accounted for a larger proportion of profits or even relied solely on them; high-frequency traders pursued top-level fees.
Speed. The reason why high-frequency strategies are called high-frequency is because they are very fast. Joining the exchange's colo server, obtaining the lowest latency and most stable connection has also become one of the conditions for internal competition. The internal consumption time of the strategy should be as little as possible, and this article will introduce the websocket framework I use, which adopts concurrent execution.
Suitable market. High-frequency trading is known as the pearl of quantitative trading, and many programmatic traders have tried it, but most people stopped because they can't make profit and can't find a direction for improvement. The main reason should be that they chose the wrong trading market. In the initial stage of strategy development, relatively easy markets should be chosen to make profits in trading so that there are profits and feedback for improvement, which is conducive to the progress of strategy. If you start competing in the most competitive market with many potential opponents, no matter how hard you try, you will lose money and soon give up. I recommend newly listed perpetual contract trading pairs when there are not so many competitors, especially those with relatively large transaction amount; this is when making profits is easiest. BTC and ETH have the largest transaction amount and are most active in transactions but also hardest to survive.
Facing competition. The market for any transaction is changing constantly, and no trading strategy can last forever, especially in high-frequency trading. Entering this market means competing with the smartest and most diligent traders directly. In a zero-sum game market, the more you earn, the less others will earn. The later you enter, the higher the difficulty; those already in the market must also improve continuously. 3-4 years ago was probably the best opportunity; recently, overall activity in digital currency markets has declined, making it very difficult for newcomers to start high-frequency trading now.
High-frequency principle
There are various high-frequency strategies:
High-frequency hedging, finding hedging opportunities through this exchange or other exchanges, relying on speed advantage to grab orders and make profits;
High-frequency trend, making profits by judging short-term trends;
Market maker, placing orders on both the buying and selling sides, controlling positions well and making profits through the rebates;
There are many others that I won't state one by one.
My strategy is a combination of trend and market maker. First, we judge the trend, then place an order. After the transaction is completed, place an order immediately to sell without holding inventory positions. Next, I will introduce it in conjunction with the strategy code.
Strategy framework
The following code is based on the basic framework of Binance perpetual contracts, mainly subscribing to websocket depth, depth order flow trades market data, and position information. Since the market data and account information are subscribed separately, it is necessary to use read(-1) continuously to determine whether the latest information has been obtained. Here EventLoop(1000) is used to avoid direct endless loops and reduce system load. EventLoop(1000) will block until there are wss or concurrent task returns 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 are the set trading pairs
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 determining the trend before executing buying and selling. The short-term trend is mainly judged based on tick-by-tick transaction data, that is, the aggTrade in the subscription, which includes transaction direction, price, quantity, transaction time, etc. Buying and selling mainly refer to depth and trading amount. The following are detailed introductions of the indicators to be concerned about; most of them are divided into buying and selling groups and are counted dynamically within a certain time window. The time window of my strategy is within 10 seconds.
Average transaction amount per trade, per trade transactions are the collection of different orders with the same direction and price within 100ms, reflecting the size of buy and sell orders. This data has a high weight, it can be assumed that if the volume of buy orders is greater than sell orders, this is a buyer-dominated market.
Order frequency or order interval, it is also based on transaction-by-transaction data, the average transaction amount mentioned earlier does not take into account the concept of time and is not entirely accurate. If an order in one direction has a small average transaction amount but a high frequency, it still contributes to the strength of that direction. The average transaction amount * order frequency represents the total transaction amount at fixed intervals and can be used for direct comparison. The arrival of orders follows a Poisson distribution, which can be used to simply estimate how much the total amount of orders arriving within a specific time interval is and provide reference for placing order positions.
Average spread, this is relatively easy to understand, that is, sell one minus buy one. The current market mostly has a 1-tick spread. If the spread becomes larger, it often means that there is a market trend.
Average buying and selling price, calculate the average price of each transaction separately, and compare it with the latest price. If the recent purchase order price is higher than the average purchase order price, it can be judged preliminarily that a breakthrough has occurred.
Strategy logic
Determine short-term trend
//bull represents short-term bullish, bear represents short-term bearish
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 selling price is higher than the average selling price, the latest buying price is higher than the average buying price, and the fixed interval buying order value is greater than the selling order value, then it is judged to be short-term bullish. Conversely, it's bearish.
Price for 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, we still adopt the old approach, iterating to the required depth. Assuming that 10 coins can be traded in 1 second, without considering new pending orders, the selling price is set at the position where 10 coins are bought. The specific size of the time window needs to be set by yourself.
Order amount
let buy_amount = Ratio * avg_sell_amount / avg_sell_time
let sell_amount = Ratio * avg_buy_amount / avg_buy_time
Ratio represents a fixed proportion, which means that the buy order quantity is a fixed proportion of the recent sell order quantity. In this way, the strategy can adjust the order size adaptively according to the current buying and selling activity.
Place 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)
}
Where avg_diff is the average market price difference, and a buy order will only be placed when the bid-ask spread is greater than a certain multiple of this value and it's bullish. If holding a short position, it will also close the position to avoid holding for an extended period. Orders can be placed as only-maker orders to ensure they are executed. Additionally, Binance's custom order ID can be used so that there is no need to wait for the order response.
Concurrent structure
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)//Unreturned tasks will continue to wait next time
}
})
jobs = new_jobs
tasks = []
}
/*
Write the required task parameters in param
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() +"×tamp="+Date.now()]})
*/
Monitored data
Delay, the importance of high-frequency strategy speed has been emphasized. In the strategy, various delays need to be monitored and recorded, such as placing orders, canceling orders, position returns, depth, order flow, positions, overall loops and so on. Any abnormal delays should be investigated in time and try to shorten the overall strategy delay.
Transaction amount ratio, calculate the transaction amount as a percentage of the total transaction amount. If the ratio is low, there is still room for growth. At peak times, it is possible for the strategy to account for more than 10% of the total trading amount.
Profit rate of closing positions, calculating the average closing position profit rate is the most important reference for judging whether a strategy is effective.
Rebate ratio, the proportion of rebates in total revenue, reflects the degree of reliance on rebates by the strategy. Exchange platforms have different rebate levels, and unprofitable strategies may be profitable with a higher level of rebate.
Placing order failure rate, orders are only traded by placing an order. Due to the delay in placing an order, it may not be placed. If this ratio is high, it means that the strategy's speed is not advantageous.
The proportion of completed orders, platforms often have requirements for the transaction rate. If it is too low, it means that the strategy cancels orders too frequently and needs to be resolved.
Average buying and selling order distance, which reflects the strategy of placing orders and the gap between the market, we can see that most of them still occupy the position of buying one and selling one.
Other suggestions
Trade multiple currencies, the high-frequency strategy in this article only refers to a single exchange, single currency and single market. It has great limitations, and most situations and currencies cannot make profits. However, it is impossible to predict which currency will be profitable in the future, so you can trade multiple or even all currencies without missing opportunities. Even under the frequency limit of exchanges, a robot can trade multiple trading pairs. Of course, for optimal speed, one sub-account can trade one trading pair with one server corresponding to one robot; however, this would result in much higher costs.
Determine the order quantity and order conditions based on the yield. Trading with multiple currencies will result in high cost of attempts, if monitoring is not profitable, use the minimum transaction amount and reduce the trading frequency until the strategy dynamically monitors a positive return rate, then increase the transaction amount to improve returns gradually.
Obtain more information, another feature of high-frequency trading is that it processes a larger amount of data and uses more information. All market information for a single trading pair within a single exchange should be referenced, and perpetual contracts can also refer to spot market data, as well as data from other exchanges for the same trading pair, or even data from other currencies. The more data there is, the greater the corresponding advantage. For example, Binance can subscribe to the best pending order information by Symbol, because the shortest push time for depth and order flow is 100ms; only this is real-time and very valuable for high-frequency strategies.
Binance's server is in aws Tokyo, other exchanges' servers vary, you can consult the technical staff of the exchange for details.
The strategy code in this article is just a simplified example code, with many cumbersome but necessary details removed. The indicators used are for reference only and should not be used directly. There are many details to pay attention to when running a high-frequency strategy, and it requires patience to modify and improve.