本文主旨是讲述一些在策略开发中的经验,以及小技巧,可以让读者快速领略交易策略开发中的心得。 在遇到一些策略设计中类似的细节问题时,马上可以构思出合理的解决方案。 以发明者量化交易平台为讲解、测试、练习平台。 策略编程语言: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线数据获取,调用:
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线柱的收盘价的均值。
所以在使用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)
}
回测显示: [null,null,null,null,4228.7,4402.9400000000005, … ]
可以看到计算出的5日均线指标,前4个是null ,就是因为K线柱数量不足5,无法计算出均值。到了第5根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)
}
}
可以看到回测中,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 限制
例如在下卖单过程中,卖单量一定不能大于账户的币数。 因为如果大于了账户中可用币数,下单会报错。
通常这样控制: 例如计划下卖单 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))
}
可以看到,每次停止机器人的时候都保存了hold对象中的数据,每次重启,都读取数据,把hold的数值恢复成之前停止时的状态。 当然以上是一个简单的范例,如果用于实际策略中,要根据策略中需要恢复的关键数据进行设计(一般为 账户信息、持仓、盈利数值、交易方向等信息)。 当然也可以设置一些条件,是否进行恢复。
以上是一些开发策略时的小技巧,希望对于各位初学者和策略开发者有所帮助! 动手练练是进步最快的!祝各位收益长虹。
weix1ao 感谢梦总分享,太适合最开始写不懂api的新手了,另外咨询一下我们平台是否支持es更高版本,比方说习惯使用了?.来过滤undefined
MAIKEO 谢谢梦神!梦神老师,真是文武双全啊,编程技术又高,文章文采又好,敬佩有佳!!!
发明者量化-小小梦 您好,目前支持的是ES8标准。
发明者量化-小小梦 嘿嘿 ~感谢支持FMZ量化!