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.
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.
There are various high-frequency strategies:
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)
}
}
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.
//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.
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.
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.
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.
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()]})
*/