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.
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.
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.
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)
}
}
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.
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.
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.
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.
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.
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() +"×tamp="+Date.now()]})
*/