数字货币高频策略详细入门

我在2020年写过一篇文章介绍高频策略,https://www.fmz.com/digest-topic/6228 。虽然得到不少关注,但写的并不深入。时间又过去了2年多,市场也发生了变化。那篇文章发出后,我的高频策略很长时间能很稳定的赚钱,但慢慢的利润逐渐下降,甚至一度也中止过。最近几个月又花了精力进行了改造,目前还能能赚些小钱。这篇文章将更详细的介绍我高频策略的思路和一部分简化的代码,起到抛砖引玉的作用,欢迎大家交流反馈。

高频交易的条件

  • 返佣的账户,以币安为例,目前maker返佣十万分之5,如果每天的成交额是1亿U,返佣就有5000U。当然taker还是依据vip费率,所以如果策略不需要吃单,vip等级对高频策略影响不大。一般交易所不同的等级还有不同的返佣费率,需要维护较高的成交额。在很早之前一些币种行情波动大的时候,没返佣也是有利润的,随着内卷的加剧,返佣占了利润较大的比例,甚至全靠返佣,高频交易者都追求顶级的费率。
  • 速度。高频策略之所以称之为高频,就是因为速度很快。加入交易所colo服务器,获得最低的延时和最稳定的连接也成了内卷的条件之一。策略的内部耗时也要尽可能低,本文将介绍下我使用的websocket的框架,用了并发执行。
  • 合适的市场。高频交易被称为量化交易的明珠,相信很多程序化交易者都进行过尝试,但大部分人都应该会因为不赚钱也找不到改进的方向而停下来,主要的原因应该是找错了交易市场。策略的起步阶段应该找相对容易的市场赚钱进行交易,这样有利润也有改进的反馈,有利于策略的进步。如果一开始就在竞争最激烈的市场中,和很多潜在的对手竞争,怎么尝试都是亏钱,很快就坚持不下去。我推荐新上永续合约交易对,这时候竞争者没有那么多,特别是交易量相对大的,这时候赚钱最容易。BTC和ETH的交易量最大,成交最为活跃,但也是最难生存的。
  • 直面竞争。任何交易的市场都是动态变化的,没有交易策略能一劳永逸,高频交易更加明显,进入这个市场就直接和一批最聪明、最勤奋的交易者做对手。在零和博弈的市场,你赚的多等于别人赚的就少。进入的越晚,难度就越高,已经在市场中的也要不断改进,随时可能被淘汰。三四年前应该是最好的机会,最近数字货币市场整体活跃性下降,现在从新手开始做高频交易的,已经非常困难。

高频原理

高频策略分为多种

  • 高频对冲,通过本交易所或者其他交易所找到对冲的机会,靠速度优势抢先吃下订单获得利润
  • 高频趋势,通过对短期趋势的判断来获利
  • 做市商,在买卖两侧都挂单成交,控制好持仓,通过赚取返佣获利。
  • 其它很多,不一一描述

我的策略是趋势和市商的结合,先判断趋势,然后挂单,成交后立刻挂单卖出,不持有库存仓位, 下面结合策略代码介绍。

策略架构

下面的代码是基于币安永续合约的基础的架构,主要订阅了websocket深度depth订单流trades行情,以及仓位position信息。由于行情和账户信息的是分别的订阅的,需要不断用read(-1)来判断是否获取到最新信息,这里用到了EventLoop(1000),避免了直接的死循环,降低了系统负担。EventLoop(1000)会阻塞到有wss或者并发任务返回,超时时间1000ms。

javascriptCopy codevar 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()
    }
    //需要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是设定的交易对
    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)
    }
}

策略指标

前面说过我的高频策略需要先判断趋势再执行买卖。判断短期趋势主要依据逐笔成交数据,即订阅里的aggTrade,包含成交方向,价格,数量,成交时间等。买卖主要参考深度和成交量。下面详细介绍下需要关注的指标,大部分指标都分为买卖两组,并且都是在一定时间窗口动态统计,我的策略的时间窗口在10s以内。

  • 逐笔成交平均成交量,逐笔成交是100ms内的同方向同价格不同订单的归集,反映了买卖订单的大小,这个数据权重较高,可以假设如果买单的成交量大于卖单,这个时候是买方主导的市场。
  • 订单频率或者订单间隔,同样基于逐笔成交数据,前面的平均成交量是没看时间概念的,并不完全准确,如果一个方向的订单虽然成交量平均很小,但频率很高,同样贡献了这个方向的强度。平均成交量*订单频率代表固定间隔的总成交量,可以用于直接比较。订单到达的事件符合泊松分布,可以用来简单估计特定时间间隔到达订单总额是多少,为挂单位置提供参考。
  • 平均盘口差价,这个比较容易理解,即卖一减买一。当前盘口大部分都是1个tick的差价,如果盘口差价变大,往往代表有行情出现。
  • 平均买卖价格,把逐笔成交的买卖分别计算均价,和最新的价格相比。如最近一笔买单价格大于平均买单价格,可以初步判断产生了突破。

策略逻辑

判断短期趋势

javascriptCopy code//bull代表短期看涨,bear短期看跌
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;

如果最新卖价大于卖单平均价,最新买价大于买单平均价,固定间隔买单价值大于卖单价值,此时判断短期看涨。反过来看跌。

下单价格

javascriptCopy codefunction 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]
}

这里还是采取老的思路,迭代深度到所需要的量,这里假设1s内能成交10个币的买单,不考虑新挂单的情况下,卖单价格设置为10个币的买单冲击到的位置。具体多大的时间窗口需要自己设定。

下单数量

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

Ratio代表固定比例, 代表买单量为最近卖单数量的固定比例。这样策略能自适应的根据当前买卖活跃度调整下单大小。

下单条件

javascriptCopy codeif(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)
}

其中avg_diff是平均盘口的差价,只有当下单买卖差价大于一定倍数这个值并且看涨的的时候才会下买单,如果持有空单,此时也会平仓,避免长期持单。下单可以下only-maker的订单,确保挂单成交。并且可以用币安的自定义订单id,这样可以不用等待订单返回。

并发架构

javascriptCopy codevar 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 = []
}

/*
需要的任务参数写在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() +"&timestamp="+Date.now()]})
*/

监控的数据

  • 延时,高频策略速度的重要性已经强调过,策略里要监视和记录各种延时,如下单、撤单、仓位返回、深度、订单流、仓位、整体循环等。有不正常的延时要及时排查,并想办法缩短整体的策略延时。
  • 成交量占比,统计下成交量占总成交量的占比,如果占比低,还有上升的空间。高峰时刻,策略占比达到总交易量的10%以上也是可能的。
  • 平仓收益率,统计下平均的平仓收益率,是判断策略是否有效的最重要的参考。
  • 返佣占比,统计返佣占总收益的占比,反映了策略对返佣的依赖程度。交易所有不同的返佣等级,不赢利的策略也许返佣高一个级别就能盈利。
  • 下单失败比例,订单都是只挂单成交,由于下单的延时,可能无法挂上去,如果这个比例高,说明策略的速度不占优。
  • 成交的订单比例,平台往往对成交率有要求,如果太低,说明策略撤单过于频繁,需要解决。
  • 平均买卖订单距离,这个数据反映了策略下单和盘口的间距,可以看到大部分还是是占据买一卖一的位置。

其他建议

  • 交易多币种,本文的高频策略属于只参考单交易所单币种单行情,局限性较大,大部分情况和大部分币种是无法盈利的,但又无法预测哪个币种未来会盈利,所以可以交易多个甚至所有币种,不错过机会。即使在交易所频率限制下,一个机器人也可交易多个交易对,当然为了最优的速度,可以一个子账号交易一个交易对,一个服务器对应一个机器人,只是这样成本会高很多。
  • 根据收益率决定下单量和下单条件。交易多币种会导致尝试的成本太高,如果监测的不盈利,就用最小交易量并减少交易频率,直到策略动态监测到收益率为正,再逐步提高交易量提升收益。
  • 获得更多的信息,高频交易另一个特点是处理的数据量更大,使用的信息更多。单交易所内单交易对的的所有行情信息都应该参考,并且永续也可以参考现货的数据,也可以参考其他交易所该交易对的数据,甚至是其他币种的数据,数据越多,相应的优势也越大。如币安可以订阅按Symbol的最优挂单信息,因为深度和订单流的最短推送都是100ms,只有这个是实时的,对高频策略很有价值。
  • 币安的服务器在aws东京,其他交易所服务器各有不同,具体可以咨询交易所技术人员。
  • 本文的策略代码仅仅是精简的示例代码,删除了很多繁琐但不得不考虑的细节,所用的指标也仅用于参考,不要直接使用。真正的运行一个高频策略需要关注的细节非常多,需要耐心修改完善。

发表评论