交易策略开发经验漫谈

Author: 小小梦, Created: 2019-08-06 17:15:13, Updated: 2023-10-20 20:06:49

img

交易策略开发经验漫谈

本文主旨是讲述一些在策略开发中的经验,以及小技巧,可以让读者快速领略交易策略开发中的心得。 在遇到一些策略设计中类似的细节问题时,马上可以构思出合理的解决方案。 以发明者量化交易平台为讲解、测试、练习平台。 策略编程语言:JavaScript 交易市场:区块链资产市场(BTC、ETH 等)

  • 数据的获取处理

    通常根据策略逻辑的不同,有可能使用以下几种不同的接口获取行情数据,因为通常策略的交易逻辑都是由行情数据驱动的(当然也有一些策略是不看行情的,例如定投策略)。

    • GetTicker : 获取实时tick行情。 一般用于快速获取当前最新价格,买一价格、卖一价格。

    • GetDepth :获取订单薄深度行情。 一般用于获取每档价格、订单量大小。 用于对冲策略、做市策略等

    • GetTrade :获取市场最近成交记录。 一般用于分析短时间内市场行为,分析市场微观变化。 通常用于高频策略、算法策略。

    • GetRecords :获取市场K线数据。 通常用于趋势跟踪策略。 用于计算指标。

    • 容错

      在设计策略时,通常新手会忽略各种错误的情况,直觉上认为策略中各个环节运行结果是既定的。但是实际上并非如此,在策略程序运行中,请求行情数据的时候,也是会遇到各种意想不到的情况。 例如一些行情接口返回了异常数据:

      var depth = exchange.GetDepth()
      
      // depth.Asks[0].Price < depth.Bids[0].Price      卖一价格低于了买一价格,这种情况不可能存在于盘面上,
      //                                                因为卖出的价格低于买入的价格,必定已经成交了。
      // depth.Bids[n].Amount = 0                       订单薄买入列表第n档,订单量为0
      // depth.Asks[m].Price = 0                        订单薄卖出列表第m档,订单价格为0
      

      或者直接 exchange.GetDepth() 返回了 null 值。

      此类奇怪的情况有很多。 所以必须对于这些可以预见到的问题作出对应的处理,此类处理方案就叫做容错处理。

      通常的容错处理方式就是丢弃数据,重新获取。

      例如:

      function main () {
          while (true) {
              onTick()
              Sleep(500)
          }
      }
      
      function GetTicker () {
          while (true) {
              var ticker = exchange.GetTicker()
              if (ticker.Sell > ticker.Buy) {       // 以 检测卖一价格是不是小于买一价这个错误的容错处理为例,
                                                    // 排除这个错误,当前函数返回 ticker 。
                  return ticker
              }
              Sleep(500)
          }
      }
      
      function onTick () {
          var ticker = GetTicker()                  // 确保获取到的 ticker 不会存在 卖一价格小于买一价格这种数据错误的情况。
          // ...  具体的策略逻辑
      }
      

      其它可以预见到的容错处理,都可以使用类似的方式。 设计原则就是,绝对不能给错误的数据去驱动策略逻辑。

    • K线数据的使用

      K线数据获取,调用:

      var r = exchange.GetRecords()
      

      获取到的K线数据是一个数组,例如这个样子:

      [
          {"Time":1562068800000,"Open":10000.7,"High":10208.9,"Low":9942.4,"Close":10058.8,"Volume":6281.887000000001},
          {"Time":1562072400000,"Open":10058.6,"High":10154.4,"Low":9914.5,"Close":9990.7,"Volume":4322.099},
          ...
          {"Time":1562079600000,"Open":10535.1,"High":10654.6,"Low":10383.6,"Close":10630.7,"Volume":5163.484000000004}
      ]
      

      可以看到每个花括号{}中间包含的都有 时间、开盘价(open)、最高价(high)、最低价(low)、收盘价(close)、成交量(volume) 。 这就是一根K线柱。一般K线数据用来计算指标,例如:MA均线、MACD 等。 把K线数据作为参数输入(原料数据),然后设置指标参数,计算出指标数据的函数,我们称之为 指标函数。 在发明者量化交易平台上有很多指标函数。

      举个例子,我们计算均线指标,根据我们传入的K线数据的周期不同,算出来的就是对应周期的均线。 例如传入 日K线数据(一根K线柱代表一天),计算出的就是日均线,同理如果传入均线指标函数的K线数据是1小时周期,那么计算出来的指标就是1小时均线。

      通常我们计算指标的时候往往会忽略一个问题,假如我要计算5日均线指标,那么我们首先准备好日K线数据:

      var r = exchange.GetRecords(PERIOD_D1)  // 给GetRecords 函数传入参数 PERIOD_D1就是指定获取日K线,
                                              // 具体函数使用可以参看:https://www.fmz.com/api#GetRecords
      

      有了日K线数据,我们就可以计算均线指标了,我们要计算5日均线,那么我们就要把指标函数的指标参数设置为5。

      var ma = TA.MA(r, 5)        // TA.MA() 就是指标函数,用来计算均线指标,第一个参数设置刚才获取的日K线数据r,
                                  // 第二个参数设置5,计算出来的就是5日均线,其它指标函数同理。
      

      我们忽略了一个潜在问题,如果r 日K线数据中,K线柱数量不足5根,怎么办,能否计算出有效的5日均线指标? 答案是肯定不行。 因为均线指标就是求一定数量K线柱的收盘价的均值。

      img

      所以在使用K线数据、指标函数计算指标数据之前,必须判断K线数据中K线柱数量是不是满足指标计算的条件(指标参数)

      所以在计算 5日均线之前,要加以判断,完整的代码如下:

      function CalcMA () {
          var r = _C(exchange.GetRecords, PERIOD_D1)     // _C() 是容错函数,目的就是避免 r 为 null , 具体可以查询文档:https://www.fmz.com/api#_C
          if (r.length > 5) {
              return TA.MA(r, 5)                         // 用均线指标函数 TA.MA 计算出均线数据,做为函数返回值,返回。
          }
      
          return false 
      }
      
      function main () {
          var ma = CalcMA()
          Log(ma)
      }
      

      img

      回测显示: [null,null,null,null,4228.7,4402.9400000000005, … ]

      可以看到计算出的5日均线指标,前4个是null ,就是因为K线柱数量不足5,无法计算出均值。到了第5根K线柱,就可以计算出了。

    • 判断K线更新的小技巧

      在我们写一些策略时经常有这样的场景,我们要在每根K线周期完成时处理一些操作,或者是打印一些日志。 我们怎么实现这样的功能呢?对于没有编程经验的初学者,可能想不到要用什么机制去处理,这里我们直接给出技巧。

      我们判断一根K线柱周期完成了,我们可以从K线数据中的 时间属性入手,我们每一次获取一次K线数据,我们就判断一次这个K线数据的最后一个K线柱的数据中Time这个属性值是不是发生了变化,如果是发生变化,即代表有新的K线柱产生(证明新产生的K线柱的前一根K线柱周期完成),如果没有发生变化,即代表没有新的K线柱产生(当前的最后一根K线柱周期还没有完成)。

      所以我们要有一个变量用来记录K线数据的最后一根K线柱的时间。

      var r = exchange.GetRecords()
      var lastTime = r[r.length - 1].Time       // lastTime 用来记录最后一根K线柱的时间。
      

      实际应用中通常是这样的结构:

      function main () {
          var lastTime = 0
          while (true) {
              var r = _C(exchange.GetRecords)
              if (r[r.length - 1].Time != lastTime) {
                  Log("新K线柱产生")
                  lastTime = r[r.length - 1].Time      // 一定要更新 lastTime ,这个至关重要。
      
                  // ... 其它处理逻辑
                  // ...
              }
      
              Sleep(500)
          }
      }
      

      img

      可以看到回测中,K线周期设置的是日(exchange.GetRecords 函数调用时不指定参数,就根据回测设置的K线周期为默认参数),每当新K线柱出现时,打印了一条日志。

  • 数值计算

    • 计算访问交易所接口耗时

      如果想对策略访问交易所的接口耗费时间有一定的显示或者控制,可以用如下代码:

      function main () {
          while (true) {
              var beginTime = new Date().getTime()
              var ticker = exchange.GetTicker()
              var endTime = new Date().getTime()
      
              LogStatus(_D(), "GetTicker() 函数耗时:", endTime - beginTime, "毫秒")
              Sleep(1000)
          } 
      }
      

      简单说就是用 调用GetTicker函数后记录的时间戳减去调用前的时间戳,算出经历的毫秒数,即GetTicker函数从执行到返回结果的耗时。

    • 使用 Math.min / Math.max 对数值的上下限限制

      如果希望数值有一个上限,通常使用 Math.min 限制

      例如在下卖单过程中,卖单量一定不能大于账户的币数。 因为如果大于了账户中可用币数,下单会报错。

      通常这样控制: 例如计划下卖单 0.2 的币。

      var planAmount = 0.2
      var account = _C(exchange.GetAccount)
      var amount = Math.min(account.Stocks, planAmount)
      

      这样就确保 amount 作为即将下单的数量,不会超过账户中可用币数的数量。

      同理, Math.max 用来确保一个数值的下限。 这通常适用于什么样的场景呢? 一般交易所对于某些交易对都有最小下单量限制,如果低于这个最小下单量,就会拒绝下单。这样下单也就失败了。 假设 BTC 通常最小下单量为 0.01 个。 交易策略通过计算有时有可能得出下单量小于0.01个,所以我们就可以使用 Math.max 来确保最小下单量。

    • 下单量、价格 精度控制

      可以使用 _N() 函数 或者 SetPrecision 函数对精度控制。

      SetPrecision() 函数设置一次即可,会在系统中自动截断下单量和价格数值多余的小数位数。

      _N() 函数是针对某一个数值进行小数位数截断(精度控制)

      例如:

      var pi = _N(3.141592653, 2)
      Log(pi)
      

      pi 的值经过小数位数截断,保留2位小数,即为: 3.14

      详情可以参看API 文档。

  • 一些逻辑设置

    • 定时,按一定时间周期执行一些操作

      可以使用这样的机制,用时间戳检测的方法,判断当前的时间戳减去上次定时任务执行完毕时刻的时间戳,实时计算已经经过的时间,当这个经过的时间超过某个设置的时间长度后,即执行新的操作。

      例如用于定投策略中。

      var lastActTime = 0
      var waitTime = 1000 * 60 * 60 * 12   // 一天的毫秒数
      function main () {
          while (true) {
              var nowTime = new Date().getTime()
              if (nowTime - lastActTime > waitTime) {
                  Log("执行定投")
                  // ... 具体的定投操作,买入操作。
      
      
                  lastActTime = nowTime
              }
      
              Sleep(500)
          }
      }
      

      这个是个简单的例子。

    • 给策略设计自动恢复机制

      使用发明者量化的_G()函数,和退出保存函数,很方便的就可以设计出策略退出保存进度,重启自动恢复状态的功能。

      var hold = {
          price : 0, 
          amount : 0,
      }
      
      function main () {
          if (_G("hold")) {
              var ret = _G("hold")
              hold.price = ret.price
              hold.amount = ret.amount
              Log("恢复 hold:", hold)
          }
      
          var count = 1
          while (true) {
              // ... 策略逻辑
              // ... 策略运行中,可能开仓,交易,把开仓的持仓价格赋值给 hold.price ,开仓的数量赋值给 hold.amount,用以记录持仓信息。
      
              hold.price = count++     // 模拟一些数值
              hold.amount = count/10   // 模拟一些数值
      
              Sleep(500)
          }
      }
      
      function onexit () {    // 点击机器人上的停止按钮,会触发执行这个函数,执行完毕机器人停止。
          _G("hold", hold)
          Log("保存 hold:", JSON.stringify(hold))
      }
      

      img

      可以看到,每次停止机器人的时候都保存了hold对象中的数据,每次重启,都读取数据,把hold的数值恢复成之前停止时的状态。 当然以上是一个简单的范例,如果用于实际策略中,要根据策略中需要恢复的关键数据进行设计(一般为 账户信息、持仓、盈利数值、交易方向等信息)。 当然也可以设置一些条件,是否进行恢复。

以上是一些开发策略时的小技巧,希望对于各位初学者和策略开发者有所帮助! 动手练练是进步最快的!祝各位收益长虹。


Related

More

weix1ao 感谢梦总分享,太适合最开始写不懂api的新手了,另外咨询一下我们平台是否支持es更高版本,比方说习惯使用了?.来过滤undefined

MAIKEO 谢谢梦神!梦神老师,真是文武双全啊,编程技术又高,文章文采又好,敬佩有佳!!!

小小梦 您好,目前支持的是ES8标准。

小小梦 嘿嘿 ~感谢支持FMZ量化!