FMZ量化交易数字货币现货对冲策略设计(1)
admin
2023-07-31 14:31:38
0

目前不清退的交易所推荐:

1、全球第二大交易所OKX欧意

国区邀请链接: https://www.myts3cards.com/cn/join/1837888   币种多,交易量大!

国际邀请链接:https://www.okx.com/join/1837888 注册简单,交易不需要实名,新用户能开合约,币种多,交易量大!

2、老牌交易所比特儿现改名叫芝麻开门 :https://www.gate.win/signup/649183

全球最大交易所币安,国区邀请链接:https://accounts.binance.com/zh-CN/register?ref=16003031  币安注册不了IP地址用香港,居住地选香港,认证照旧,邮箱推荐如gmail、outlook。支持币种多,交易安全!

买好币上KuCoin:https://www.kucoin.com/r/af/1f7w3  CoinMarketCap前五的交易所,注册友好操简单快捷!

FMZ量化交易平台邀请链接:https://www.fmz.com/

数字货币现货对冲策略设计(1)

对于策略设计的初学者来说,对冲策略是非常好的练手策略。本篇实现一个简单但是可以实盘的数字货币现货对冲策略,希望可以让初学者学习到一些设计经验。

根据策略需求设计一些函数、策略界面参数

首先明确这个即将设计的策略是一个数字货币现货对冲策略,我们设计最简单的对冲,只在两个现货交易所之间价格较高的交易所卖出,价格较低的交易所买入从而赚取差价。当价格较高的交易所全部都是计价币的时候(因为价格较高币都卖出了),价格较低的交易所全部都是币的时候(价格较低都买成币了)就无法对冲了。这个时候只能等价格反转对冲。

对冲的时候下单价格、数量,交易所都有精度限制,并且还有最小下单量限制。除了最小限制外策略在对冲时也要考虑一次对冲的最大下单量,下单量过大盘口也不会有足够的订单量。还需要考虑如果两个交易所计价币是不同的如何用汇率转换。对冲时手续费、吃单滑点都是交易成本,并不是只要有差价就可以对冲,所以对冲差价也有个触发值,低于某个差价时对冲是亏钱的。

基于这些考虑,策略需要设计出几个参数:

  • 对冲差价:hedgeDiffPrice,当差价超过这个值时,触发对冲操作。
  • 最小对冲量:minHedgeAmount,可对冲的最小下单量(币数)。
  • 最大对冲量:maxHedgeAmount,一次对冲的最大下单量(币数)。
  • A价格精度:pricePrecisionA,A交易所下单价格精度(小数位数)。
  • A下单量精度:amountPrecisionA,A交易所下单量精度(小数位数)。
  • B价格精度:pricePrecisionB,B交易所下单价格精度(小数位数)。
  • B下单量精度:amountPrecisionB,B交易所下单量精度(小数位数)。
  • A交易所汇率:rateA,第一个添加的交易所对象的汇率转换,默认1不转换。
  • B交易所汇率:rateB,第二个添加的交易所对象的汇率转换,默认1不转换。

对冲策略需要保持两个账户的币数始终不变(即不持有任何方向头寸,保持中性),所以需要策略中有一个平衡逻辑始终检测平衡。检测平衡时就避免不了要获取两个交易所的资产数据。我们就需要写一个函数来使用。

  • updateAccs
    function updateAccs(arrEx) {
        var ret = []
        for (var i = 0 ; i < arrEx.length ; i++) {
            var acc = arrEx[i].GetAccount()
            if (!acc) {
                return null
            }
            ret.push(acc)
        }
        return ret 
    }
    

当下单之后如果没有成交的订单我们需要及时的撤销掉,不能让订单一直挂着。这个操作不论是平衡模块中,还是对冲逻辑中都是需要去处理的,所以还需要设计一个订单全撤函数。

  • cancelAll
    function cancelAll() {
        _.each(exchanges, function(ex) {
            while (true) {
                var orders = _C(ex.GetOrders)
                if (orders.length == 0) {
                    break
                }
                for (var i = 0 ; i < orders.length ; i++) {
                    ex.CancelOrder(orders[i].Id, orders[i])
                    Sleep(500)
                }
            }
        })
    }
    

在平衡币数时,我们需要在某个深度数据中查找累计到一定币数的价格,所以就需要一个这样的函数来处理。

  • getDepthPrice
    function getDepthPrice(depth, side, amount) {
        var arr = depth[side]
        var sum = 0
        var price = null
        for (var i = 0 ; i < arr.length ; i++) {
            var ele = arr[i]
            sum += ele.Amount
            if (sum >= amount) {
                price = ele.Price
                break
            }
        }
        return price
    }
    

然后就是我们需要对具体对冲的下单操作进行设计编写,需要设计成并发下单:

  • hedge
    function hedge(buyEx, sellEx, price, amount) {
        var buyRoutine = buyEx.Go(\"Buy\", price, amount)
        var sellRoutine = sellEx.Go(\"Sell\", price, amount)
        Sleep(500)
        buyRoutine.wait()
        sellRoutine.wait()
    }
    

最后,我们来完成平衡函数的设计,平衡函数略微有点复杂。

  • keepBalance
    function keepBalance(initAccs, nowAccs, depths) {
        var initSumStocks = 0
        var nowSumStocks = 0 
        _.each(initAccs, function(acc) {
            initSumStocks += acc.Stocks + acc.FrozenStocks
        })
        _.each(nowAccs, function(acc) {
            nowSumStocks += acc.Stocks + acc.FrozenStocks
        })
      
        var diff = nowSumStocks - initSumStocks
        // 计算币差
        if (Math.abs(diff) > minHedgeAmount && initAccs.length == nowAccs.length && nowAccs.length == depths.length) {
            var index = -1
            var available = []
            var side = diff > 0 ? \"Bids\" : \"Asks\"
            for (var i = 0 ; i < nowAccs.length ; i++) {
                var price = getDepthPrice(depths[i], side, Math.abs(diff))
                if (side == \"Bids\" && nowAccs[i].Stocks > Math.abs(diff)) {
                    available.push(i)
                } else if (price && nowAccs[i].Balance / price > Math.abs(diff)) {
                    available.push(i)
                }
            }
            for (var i = 0 ; i < available.length ; i++) {
                if (index == -1) {
                    index = available[i]
                } else {
                    var priceIndex = getDepthPrice(depths[index], side, Math.abs(diff))
                    var priceI = getDepthPrice(depths[available[i]], side, Math.abs(diff))
                    if (side == \"Bids\" && priceIndex && priceI && priceI > priceIndex) {
                        index = available[i]
                    } else if (priceIndex && priceI && priceI < priceIndex) {
                        index = available[i]
                    }
                }
            }
            if (index == -1) {
                Log(\"无法平衡\")            
            } else {
                // 平衡下单
                var price = getDepthPrice(depths[index], side, Math.abs(diff))
                if (price) {
                    var tradeFunc = side == \"Bids\" ? exchanges[index].Sell : exchanges[index].Buy
                    tradeFunc(price, Math.abs(diff))
                } else {
                    Log(\"价格无效\", price)
                }
            }        
            return false
        } else if (!(initAccs.length == nowAccs.length && nowAccs.length == depths.length)) {
            Log(\"错误:\", \"initAccs.length:\", initAccs.length, \"nowAccs.length:\", nowAccs.length, \"depths.length:\", depths.length)
            return true 
        } else {
            return true 
        }
    }
    

根据策略需求设计好了这些函数,下面可以开始设计策略的主函数了。

策略主函数设计

在FMZ上策略是从main函数开始执行的。在main函数开始的部分我们要做一些策略的初始化工作。

  • 交易所对象名称
    因为策略中很多操作要使用到交易所对象,例如获取行情、下单等等。所以每次都使用一个较长的名字会很麻烦,小技巧就是使用一个简单的名字代替,例如:

    var exA = exchanges[0]
    var exB = exchanges[1]
    

    这样后面编写代码就很舒服了。

  • 汇率、精度相关设计
      // 精度,汇率设置  if (rateA != 1) {      // 设置汇率A      exA.SetRate(rateA)      Log(\"交易所A设置汇率:\", rateA, \"#FF0000\")  }  if (rateB != 1) {      // 设置汇率B      exB.SetRate(rateB)      Log(\"交易所B设置汇率:\", rateB, \"#FF0000\")  }  exA.SetPrecision(pricePrecisionA, amountPrecisionA)  exB.SetPrecision(pricePrecisionB, amountPrecisionB)

    如果汇率参数rateArateB有设置为1的(默认是1),即rateA != 1rateB != 1不会触发,所以不会设置汇率转换。

  • 重置所有数据

    15e416770fbc81ff4f1215e416770fbc81ff4f12

    有时候策略启动时需要删除所有日志、清空记录的数据。就可以设计一个策略界面参数isReset,然后在策略中初始化的部分设计重置代码,例如:

      if (isReset) {   // 当isReset为真时重置数据
          _G(null)
          LogReset(1)
          LogProfitReset()
          LogVacuum()
          Log(\"重置所有数据\", \"#FF0000\")
      }
    
  • 恢复初始账户数据、更新当前账户数据
    为了判断平衡,策略需要持续记录最初的账户资产情况用于和当前对比,nowAccs这个变量就是用来记录当前账户数据,使用我们刚才设计好的函数updateAccs获取当前交易所的账户数据。initAccs用来记录最初的账户状态(交易所A和交易所B的币数、计价币数等数据)。对于initAccs首先使用_G()函数恢复(_G函数会持久记录数据,并且可以重新返回记录的数据,具体查看API文档:链接), 如果查询不到就用当前的账户信息赋值并用_G函数记录。

    例如以下代码:

      var nowAccs = _C(updateAccs, exchanges)
      var initAccs = _G(\"initAccs\")
      if (!initAccs) {
          initAccs = nowAccs
          _G(\"initAccs\", initAccs)
      }
    

交易逻辑,主函数中的主循环

主循环中的代码就是策略逻辑每轮执行的流程,不停的往复执行就构成了策略主循环。让我们来看下主循环中程序每次执行的流程。

  • 获取行情数据,判断行情数据有效性
          var ts = new Date().getTime()
          var depthARoutine = exA.Go(\"GetDepth\")
          var depthBRoutine = exB.Go(\"GetDepth\")
          var depthA = depthARoutine.wait()
          var depthB = depthBRoutine.wait()
          if (!depthA || !depthB || depthA.Asks.length == 0 || depthA.Bids.length == 0 || depthB.Asks.length == 0 || depthB.Bids.length == 0) {
              Sleep(500)
              continue 
          }
    

    这里可以看到使用了FMZ平台的并发函数exchange.Go,创建了调用GetDepth()接口的并发对象depthARoutinedepthBRoutine。这两个并发对象创建时,调用GetDepth()接口也随即发生,此时两个获取深度数据的请求都向交易所发送了过去。
    然后调用depthARoutinedepthBRoutine对象的wait()方法获取深度数据。
    获取到深度数据之后,需要对深度数据进行检查判断其有效性。对于数据异常的情况触发执行continue语句重新执行主循环。

  • 使用价差值参数还是差价比例参数?
          var targetDiffPrice = hedgeDiffPrice      if (diffAsPercentage) {          targetDiffPrice = (depthA.Bids[0].Price + depthB.Asks[0].Price + depthB.Bids[0].Price + depthA.Asks[0].Price) / 4 * hedgeDiffPercentage      }

    参数上我们做了这样的设计。FMZ的参数可以基于某个参数显示或者隐藏,这样我们就可以做一个参数来决定是使用价格差,还是差价比例

    16393eb70d11c249dd3f16393eb70d11c249dd3f

    策略界面参数上增加了一个参数diffAsPercentage。另外两个基于这个参数显示或者隐藏的参数设置为:
    hedgeDiffPrice@!diffAsPercentage,当diffAsPercentage为假显示该参数。
    hedgeDiffPercentage@diffAsPercentage,当diffAsPercentage为真显示该参数。
    这样设计之后,我们勾选了diffAsPercentage参数,就是按差价比例作为对冲触发条件。不勾选diffAsPercentage参数就是按价格差作为对冲触发条件。

  • 判断对冲触发条件
          if (depthA.Bids[0].Price - depthB.Asks[0].Price > targetDiffPrice && Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount) >= minHedgeAmount) {          // A -> B 盘口条件满足                      var price = (depthA.Bids[0].Price + depthB.Asks[0].Price) / 2          var amount = Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount)          if (nowAccs[0].Stocks > minHedgeAmount && nowAccs[1].Balance / price > minHedgeAmount) {              amount = Math.min(amount, nowAccs[0].Stocks, nowAccs[1].Balance / price, maxHedgeAmount)              Log(\"触发A->B:\", depthA.Bids[0].Price - depthB.Asks[0].Price, price, amount, nowAccs[1].Balance / price, nowAccs[0].Stocks)  // 提示信息              hedge(exB, exA, price, amount)              cancelAll()              lastKeepBalanceTS = 0              isTrade = true           }                  } else if (depthB.Bids[0].Price - depthA.Asks[0].Price > targetDiffPrice && Math.min(depthB.Bids[0].Amount, depthA.Asks[0].Amount) >= minHedgeAmount) {   // B -> A 盘口条件满足          var price = (depthB.Bids[0].Price + depthA.Asks[0].Price) / 2          var amount = Math.min(depthB.Bids[0].Amount, depthA.Asks[0].Amount)          if (nowAccs[1].Stocks > minHedgeAmount && nowAccs[0].Balance / price > minHedgeAmount) {              amount = Math.min(amount, nowAccs[1].Stocks, nowAccs[0].Balance / price, maxHedgeAmount)              Log(\"触发B->A:\", depthB.Bids[0].Price - depthA.Asks[0].Price, price, amount, nowAccs[0].Balance / price, nowAccs[1].Stocks)  // 提示信息              hedge(exA, exB, price, amount)              cancelAll()              lastKeepBalanceTS = 0              isTrade = true           }                  }

    对冲触发条件有这么几个:
    1、首先满足对冲差价,只有当盘口的差价满足设置的差价参数时才可对冲。
    2、盘口可对冲量要满足参数上设置的最小对冲量,因为不同交易所可能限制的最小下单量不同,所以要取两者中最小的。
    3、卖出操作的交易所中的资产足够卖出,买入操作的交易所中的资产足够买入。
    这些条件满足时,执行对冲函数进行对冲下单。在主函数之前我们提前声明了一个变量isTrade用来标记是否发生对冲,这里如果对冲触发则设置该变量为true。并且重置全局变量lastKeepBalanceTS为0(lastKeepBalanceTS用于标记最近一次平衡操作的时间戳,设置为0会立即触发平衡操作),然后取消所有挂单。

  • 平衡操作
          if (ts - lastKeepBalanceTS > keepBalanceCyc * 1000) {          nowAccs = _C(updateAccs, exchanges)          var isBalance = keepBalance(initAccs, nowAccs, [depthA, depthB])          cancelAll()          if (isBalance) {              lastKeepBalanceTS = ts              if (isTrade) {                  var nowBalance = _.reduce(nowAccs, function(sumBalance, acc) {return sumBalance + acc.Balance}, 0)                  var initBalance = _.reduce(initAccs, function(sumBalance, acc) {return sumBalance + acc.Balance}, 0)                  LogProfit(nowBalance - initBalance, nowBalance, initBalance, nowAccs)                  isTrade = false               }                          }                  }

    可以看到平衡函数会定期执行,但是如果对冲操作触发了之后,lastKeepBalanceTS被重置为0则平衡操作会立即触发。平衡成功之后会计算收益。

  • 状态栏信息
          LogStatus(_D(), \"A->B:\", depthA.Bids[0].Price - depthB.Asks[0].Price, \" B->A:\", depthB.Bids[0].Price - depthA.Asks[0].Price, \" targetDiffPrice:\", targetDiffPrice, \"\\n\",           \"当前A,Stocks:\", nowAccs[0].Stocks, \"FrozenStocks:\", nowAccs[0].FrozenStocks, \"Balance:\", nowAccs[0].Balance, \"FrozenBalance\", nowAccs[0].FrozenBalance, \"\\n\",           \"当前B,Stocks:\", nowAccs[1].Stocks, \"FrozenStocks:\", nowAccs[1].FrozenStocks, \"Balance:\", nowAccs[1].Balance, \"FrozenBalance\", nowAccs[1].FrozenBalance, \"\\n\",           \"初始A,Stocks:\", initAccs[0].Stocks, \"FrozenStocks:\", initAccs[0].FrozenStocks, \"Balance:\", initAccs[0].Balance, \"FrozenBalance\", initAccs[0].FrozenBalance, \"\\n\",           \"初始B,Stocks:\", initAccs[1].Stocks, \"FrozenStocks:\", initAccs[1].FrozenStocks, \"Balance:\", initAccs[1].Balance, \"FrozenBalance\", initAccs[1].FrozenBalance)

    状态栏没有设计特别复杂,显示当前时间,显示A交易所到B交易所的差价和B交易所到A交易所的差价。显示当前对冲目标差价。显示A交易所账户资产数据,B交易所账户资产数据。

对于不同计价币的交易对的处理

在参数上我们设计了转换汇率值参数,在策略开头main函数初始操作的部分我们也设计了汇率转换。需要注意的是SetRate汇率转换函数需要首先执行。
因为这个函数影响两个层面:

  • 所有行情数据、订单数据、持仓数据中的价格换算。
  • 账户资产中计价币的换算。
    例如当前交易对为BTC_USDT,价格单位都是USDT,账户资产里可用计价币也是USDT。如果我想换算成CNY的数值,在代码中设置exchange.SetRate(6.8)就把exchange这个交易所对象下的所有函数获取的数据进行了换算,换算成了CNY。
    换算为什么计价币就给SetRate函数传入当前计价币到目标计价币的汇率

完整的策略:不同计价币的现货对冲策略(教学)

FMZ量化交易平台邀请链接:https://www.fmz.com/

全球最大交易所币安,国区邀请链接:https://accounts.binance.com/zh-CN/register?ref=16003031  币安注册不了IP地址用香港,居住地选香港,认证照旧,邮箱推荐如gmail、outlook。支持币种多,交易安全!

买好币上KuCoin:https://www.kucoin.com/r/af/1f7w3  CoinMarketCap前五的交易所,注册友好操简单快捷!

目前不清退的交易所推荐:

1、全球第二大交易所OKX欧意,邀请链接:https://www.myts3cards.com/cn/join/1837888 注册简单,交易不需要实名,新用户能开合约,币种多,交易量大!。

2、老牌交易所比特儿现改名叫芝麻开门 :https://www.gate.win/signup/649183

买好币上币库:https://www.kucoin.com/r/1f7w3

火必所有用户现在可用了,但是要重新注册账号火币:https://www.huobi.com

全球最大交易所币安,

国区邀请链接:https://accounts.suitechsui.mobi/zh-CN/register?ref=16003031 支持86手机号码,网页直接注册。

相关内容

热门资讯

Windows 11 和 10... Windows 11/10 文件夹属性中缺少共享选项卡 – 已修复 1.检查共享选项卡是否可用 右键...
事件 ID 7034:如何通过... 点击进入:ChatGPT工具插件导航大全 服务控制管理器 (SCM) 负责管理系统上运行的服务的活动...
Hive OS LOLMine... 目前不清退的交易所推荐: 1、全球第二大交易所OKX欧意 国区邀请链接: https://www.m...
Radmin VPN Wind... Radmin VPN 是一款免费且用户友好的软件,旨在牢固地连接计算机以创建一个有凝聚力的虚拟专用网...
如何修复 Steam 内容文件... Steam 内容文件锁定是当您的 Steam 文件无法自行更新时出现的错误。解决此问题的最有效方法之...
Hive OS 部署 PXE ... 目前不清退的交易所推荐: 1、全球第二大交易所OKX欧意 国区邀请链接: https://www.m...
如何在Instagram上扫描... 如何在Instagram上扫描名称标签/ QR? 总而言之,您可以通过大约四种不同的方法来扫描这些I...
在 Windows 11 中打... 什么是链路状态电源管理? 您可以在系统控制面板的电源选项中看到链接状态电源管理。它是 PCI Exp...
farols1.1.501.0... faro ls 1.1.501.0(64bit)可以卸载,是一款无需连接外部PC机或笔记本计算机即可...
Hive OS 新建飞行表的方... 目前不清退的交易所推荐: 1、全球第二大交易所OKX欧意 国区邀请链接: https://www.m...