和老白一起玩转JavaScript -- 创造一个会做买卖的小伙伴(5) 机器人也生虫(BUG)
使用JS 编写量化机器人的一个教程: https://www.botvs.com/bbs-topic/705 (先 Mark 下)。
-
1、 K线的 数据基础知识 、 量化交易中使用这些数据出现的问题。
-
什么是K线数据:
K线图这种图表源处于日本德川幕府时代,被当时日本米市的商人用来记录米市的行情与价格波动,后因其细腻独到的标画方式而被引入到股市及期货市场。目前,这种图表分析法在我国以至整个东南亚地区均尤为流行。由于用这种方法绘制出来的图表形状颇似一根根蜡烛,加上这些蜡烛有黑白之分,因而也叫阴阳线图表。通过K线图,我们能够把每日或某一周期的市况表现完全记录下来,股价经过一段时间的盘档后,在图上即形成一种特殊区域或形态,不同的形态显示出不同意义。我们可以从这些形态的变化中摸索出一些有规律的东西出来。K线图形态可分为反转形态、整理形态及缺口和趋向线等。
(查询自百度)具体图形不解释了,我们看下 使用JS 语言定义的K线的数据结构:
{ Time : 1487034000000, // 一个时间戳, 精确到毫秒,与Javascript的 new Date().getTime() 得到的结果格式一样 Open : 3425, // 开盘价 High : 3446, // 最高价 Low : 3423, // 最低价 Close : 3438, // 收盘价 Volume : 177657.99, // 交易量 }
我们再看下调用 GetRecords 函数获取的数据: (记得先调用 exchange.SetContractType("rb1705"); 明确要操作的合约类型)
[ {"Time":1487034000000,"Open":3425,"High":3446,"Low":3423,"Close":3438,"Volume":177657.9999999999}, {"Time":1487035800000,"Open":3438,"High":3448,"Low":3382,"Close":3385,"Volume":494882}, {"Time":1487037600000,"Open":3385,"High":3398,"Low":3383,"Close":3394,"Volume":83656.00000000015} ]
可见K线数据都是一个对象数组,每个对象就是一个 K线 Bar ,包含 这个Bar 周期内的 最高价格、 最低价格、 开盘价格(即这个K线时间戳开始时的价格)、收盘价格(即这个K线周期时间结束时的价格)、成交量(周期内的成交量)。而这个周期 就是 K线周期。
比如上面的数组中的数据怎么判断是多大周期的K线呢? 可以用2根Bar 的时间戳只差计算: 1487035800000 - 1487034000000
得出: 1800000 , 这个数值的单位是 毫秒,所以换算下: 1800000 / 1000 / 60 = 30 (分钟) , 这个K线周期就是30分钟。第一个容易出现的问题:
使用K线数据时,忽略了 数组长度。导致数组访问溢出(这类BUG在以前写C程序的时候很常见)。所以我们在使用K线前需要对其判断
比如获取K线:
exchange.SetContractType("rb1705"); // 切换设置 为 螺纹钢 1705 合约。
var records = exchange.GetRecords(); // 获取 rb1705 螺纹钢 合约的 默认K线周期数据。
具体可以获取到多少根Bar 的K线数据 是具体看 交易所的 API 推送的。所以在开始的时候如果需要比较多的K线Bar,必须让程序收集一段时间。并且在程序里面要增加判断,是否收集足够,可以这样写:if(records.length < n){ // n 就是我们限定的 n线数量。 return; // 当前函数返回。 }
第二个容易出现的问题:
K线最后一个Bar 的数据,除了 Time 属性、 Open属性 以外,其他的属性都是有可能变化, Close 属性更是实时在变动。
初学者在 处理K线的时候由于不明白这点会有很多困惑。比如上一章节,讲到均线交叉。使用倒数第一Bar 还是倒数第二Bar ?第三个问题:
K线的周期,时间戳就是这个周期的起始时刻,时间戳是一个 毫秒数, 值为0 的时间戳 代表的时刻是 1970年1月1日 (具体写程序的时候判断还需要考虑所在时区)。
可以用以下这一句在 w3school 或者 BotVS沙盒系统中 测试下:var arr = new Date(0);
显示为:
Thu Jan 01 1970 08:00:00 GMT+0800 (CST) // 显示的为东八时区
这个值是从1970年自己累加到现在了(每过1秒增加1000, 因为1秒是1000毫秒),所以这个数值已经是比较大了。
这里有个小技巧:时间戳由于是确定K线周期的K线中每个Bar唯一的,所以一旦时间戳发生变化,那么就可以确定接收到最新的K线数据了。这个在实际处理K线数据中也是很有用的。
-
-
2、 指标调用详解,经常遇到的问题 满足周期 、 返回值 、参数
在程序化或者量化策略编写时,也会用到不少指标函数, 比较好用的指标库有 talib 库,这个有各种版本,我们这里用到JS版本。
在老白第一次使用指标库的函数时也出过不少错误:
- 第一,参数周期(指标参数, 区别于K线周期,K线周期为多少,计算出来的指标K线周期也就是多少,比如30分钟K线计算出来的就是30分钟周期的 MACD 指标,参数为参数周期)设置过大,K线数据长度不足:
比如MACD指标,描述:
在使用时如果参数周期设置 12,26,9, 我们传入用于计算指标的K线数据records, 代码这样写:MACD(Records[Close],Fast Period = 12,Slow Period = 26,Signal Period = 9) = [Array(outMACD),Array(outMACDSignal),Array(outMACDHist)]
如果此时传入的 records 数据的长度过小。计算出来的就是这样的:var macd = talib.MACD(records, 12, 26, 9);
这个就是由于K线数据不足引起的,计算出的指标如果使用就会引起BUG,所以我们在程序前加个限定条件:[ [null,null,null,null,null,null,null,null,null,null,null,null,null], [null,null,null,null,null,null,null,null,null,null,null,null,null], [null,null,null,null,null,null,null,null,null,null,null,null,null] ]
直到获取足够50根K线,才跳出循环。执行如下:while(!records || records.length < 50){ records = exchange.GetRecords(); Sleep(1000); }
- 第一,参数周期(指标参数, 区别于K线周期,K线周期为多少,计算出来的指标K线周期也就是多少,比如30分钟K线计算出来的就是30分钟周期的 MACD 指标,参数为参数周期)设置过大,K线数据长度不足:
有细心的读者可以看到,为什么这个指标计算出的数据是一个二维数组(即 一个数组的每个元素又是一个数组),因为MACD指标计算出来的并不是一条线,而是三条线分别为: dif 、dea 、macd量柱。所以每个指标的返回值可能都不一样,具体还是需要看下指标描述。
```
[
[null,null,null,null,null,null,null,null,数据...],
[null,null,null,null,null,null,null,null,数据...],
[null,null,null,null,null,null,null,null,数据...]
]
```
有几次也是因为没注意指标的返回结构,出现BUG。
-
第二,指标函数计算使用的均线不一样或者指标算法不一样导致结果不同。
比较明显的是 STOCH RSI 这个指标,描述如下:
STOCHRSI(Records[Close],Time Period = 14,Fast-K Period = 5,Fast-D Period = 3,Fast-D MA = 0) = [Array(outFastK),Array(outFastD)]
这个计算出来的值明显不同于其他的算法,这个指标在本系列的第一章中给出了我自己的算法代码。有兴趣的读者可以对比下。
原因有可能是使用的均线系统不一致导致的,有些库的算法习惯使用MA、有些习惯用EMA。 部分指标都是每天迭代计算出来的,如果K线数据给定的数量不同可能算出的值有差别。 -
3、 API 容错处理
-
Cannot read property 'length' of null 这个BUG 是 出现频率最高的,没有之一。
原因就是API有时由于各种原因会发生获取数据错误、或者没有获取到数据。这时一些获取数据的API取到的就是null值这些数据一般都是数组结构,经常需要访问数组的长度。
对于所有的API 调用都需要 进行容错处理,甚至有些时候需要检测一下数据是否正常(有时会出现异常数据)。我们的程序只能保证在自身代码内的准确性,但是对于网络上奔跑穿梭的数据信息是无法保证其100%准确的(丢包什么的不可避免),于是就必须对获取的数据容错处理,过滤掉所有的异常数据。
由于没有单步调试、没有断点调试、没有变量值监控 等等~
我平时DEBUG的方法就是最简单的 Log 大法!
对于程序流程中合理使用Log 输出文本信息,分析程序输出的日志。可以大概了解程序的运行过程,也可以结合 try , catch , throw JS的异常捕获来处理BUG,但是我的建议是不到必须使用异常捕获的时候,尽量不要用。(程序化要求我们必须处理交易、分析数据的各个方面,万万不能勉强运行,可能灾难性的BUG就在某个角落等待触发)。
对于DEBUG来说,用最原始的Log大法确实是个经验活,从培养DEBUG能力的角度来说这样是很有效的。积累到一定程度,无惧任何BUG。
-