在编写程序化交易策略时,使用K线数据,经常会有需求使用一些非标准周期K线数据的情况,例如需要使用12分钟周期K线数据、4小时K线周期数据,通常这类非标准周期是无法直接获取的。那么我们如何应对此类需求呢? 答案肯定是有办法的。 非标准周期可以通过更小周期的数据,合并合成获取,可以想象一下,多个周期中的最高价,算作合成后的最高价,最低价算作合成后的最低价,开盘价不会变,就用合成这根K线原料数据的第一个开盘价,收盘价对应的是用合成这根K线的原料数据的最后一个的收盘价,时间就是取的开盘价的时间,成交量用原料数据的交易量求和计算得出。 就如图:
我们以区块链资产 市场 BTC_USDT 为例,用1小时合成为4小时。
时间 | 高 | 开 | 低 | 收 |
---|---|---|---|---|
2019.8.12 00:00 | 11447.07 | 11382.57 | 11367.2 | 11406.92 |
2019.8.12 01:00 | 11420 | 11405.65 | 11366.6 | 11373.83 |
2019.8.12 02:00 | 11419.24 | 11374.68 | 11365.51 | 11398.19 |
2019.8.12 03:00 | 11407.88 | 11398.59 | 11369.7 | 11384.71 |
这四个1小时周期的数据,合成一个根4小时周期的数据,开盘价即第一根 00:00 时间的开盘价:11382.57 收盘价是 最后一根 即 03:00 时的收盘价:11384.71 最高价就找这里面最高的价格:11447.07 最低价就找这里面最低的价格:11365.51 4小时周期 起始时间 就是 00:00 这根1小时K线的起始时间,即 2019.8.12 00:00 成交量每根1小时的求和即可(主要观察价格如何合成,成交量数据中没有显示),这里不做赘述。
合成出的 一根4小时K线即: 高:11447.07 开:11382.57 低:11365.51 收:11384.71 时间:2019.8.12 00:00
可以看到数据是一致的。
验证了初步的思路,就可以动手写一写代码初步实现一下这个需求了。
直接放出代码,代码仅供参考学习:
function GetNewCycleRecords (sourceRecords, targetCycle) { // K线合成函数
var ret = []
// 首先获取源K线数据的周期
if (!sourceRecords || sourceRecords.length < 2) {
return null
}
var sourceLen = sourceRecords.length
var sourceCycle = sourceRecords[sourceLen - 1].Time - sourceRecords[sourceLen - 2].Time
if (targetCycle % sourceCycle != 0) {
Log("targetCycle:", targetCycle)
Log("sourceCycle:", sourceCycle)
throw "targetCycle is not an integral multiple of sourceCycle."
}
if ((1000 * 60 * 60) % targetCycle != 0 && (1000 * 60 * 60 * 24) % targetCycle != 0) {
Log("targetCycle:", targetCycle)
Log("sourceCycle:", sourceCycle)
Log((1000 * 60 * 60) % targetCycle, (1000 * 60 * 60 * 24) % targetCycle)
throw "targetCycle cannot complete the cycle."
}
var multiple = targetCycle / sourceCycle
var isBegin = false
var count = 0
var high = 0
var low = 0
var open = 0
var close = 0
var time = 0
var vol = 0
for (var i = 0 ; i < sourceLen ; i++) {
// 获取 时区偏移数值
var d = new Date()
var n = d.getTimezoneOffset()
if (((1000 * 60 * 60 * 24) - sourceRecords[i].Time % (1000 * 60 * 60 * 24) + (n * 1000 * 60)) % targetCycle == 0) {
isBegin = true
}
if (isBegin) {
if (count == 0) {
high = sourceRecords[i].High
low = sourceRecords[i].Low
open = sourceRecords[i].Open
close = sourceRecords[i].Close
time = sourceRecords[i].Time
vol = sourceRecords[i].Volume
count++
} else if (count < multiple) {
high = Math.max(high, sourceRecords[i].High)
low = Math.min(low, sourceRecords[i].Low)
close = sourceRecords[i].Close
vol += sourceRecords[i].Volume
count++
}
if (count == multiple || i == sourceLen - 1) {
ret.push({
High : high,
Low : low,
Open : open,
Close : close,
Time : time,
Volume : vol,
})
count = 0
}
}
}
return ret
}
// 测试
function main () {
while (true) {
var r = exchange.GetRecords() // 原始数据,作为合成K线的基础K线数据,例如要合成4小时K线,可以用1小时K线作为原始数据。
var r2 = GetNewCycleRecords(r, 1000 * 60 * 60 * 4) // 通过 GetNewCycleRecords 函数 传入 原始K线数据 r , 和目标周期, 1000 * 60 * 60 * 4 即 目标合成的周期 是4小时K线数据。
$.PlotRecords(r2, "r2") // 策略类库栏 可以勾选画线类库,调用 $.PlotRecords 画线类库 导出函数 画图。
Sleep(1000) // 每次循环间隔 1000 毫秒,防止访问K线接口获取数据过于频繁,导致交易所限制。
}
}
其实要合成K线,就需要两个东西,第一是需要原料数据,即小周期的K线数据,例子中 var r = exchange.GetRecords()
获取的小周期K线数据。第二是需要明确合成为多大的周期,即 K线数据合成的目标周期。
然后通过 GetNewCycleRecords 函数的算法,就可以最后返回一个合成出来的K线数组结构的数据了。
需要注意的是:
1、目标周期不能小于你传入GetNewCycleRecords 函数作为数据原料的K线的周期。 因为无法用小周期去合成更小的周期的数据。
2、设置的目标周期必须是周期闭合的。
什么是周期闭合?
简单说就是在一小时内或者在一天之内,目标周期时间范围组合在一起,组成一个闭合的循环。
举例:
例如 12分钟周期的K线,从每个小时的0分0秒开始(以0时举例),第一个周期是00:00:00 ~ 00:12:00
,第二个周期是00:12:00 ~ 00:24:00
,第三个周期是00:24:00 ~ 00:36:00
,第四个周期是00:36:00 ~ 00:48:00
,第五个周期是00:48:00 ~ 01:00:00
,正好组成一个完整的1小时。
如果是 13分钟周期,就是不闭合的周期,这样的周期算出的数据不唯一,因为根据合成的数据起始点不同,合成出来的数据有差异。
实盘运行了一下:
对比交易所图表
经常有群友提问,我想计算每根K线的最高价的均线,怎么办?
通常,我们计算均线都是计算的收盘价的均值,组成均线,但是也有时候有需求计算最高价、最低价、开盘价等等。
这个时候就不能直接把exchange.GetRecords()
函数返回的K线数据直接传入 指标计算函数了。
例如: talib.MA 均线指标计算函数有两个参数,第一个参数是需要传入的数据,第二个参数是指标周期参数。 例如 我们要算如下图的指标
K线周期是4小时, 在交易所图表上,已经设置好了一条均线,均线周期参数为9。 并且设置计算的数据源是每根Bar的最高价。 即这条均线是9个4小时周期K线Bar的最高价平均计算出的均值,组成的指标均线。
我们自己动手构造一个数据算下,看是不是和交易所的图表计算得出的一样。
var highs = []
for (var i = 0 ; i < r2.length ; i++) {
highs.push(r2[i].High)
}
既然要计算每根Bar的最高价的均值得出均线指标。 那么就需要先构造一个数组,其中每个数据元素都是对应每根Bar的最高价。 可以看到 highs 变量初始为一个空数组,然后我们遍历 r2 这个K线数据变量(不记得r2了?看下上面合成4小时K线的main函数中的代码)。 读取r2每根Bar的最高价(即 r2[i].High , i取值范围 从 0 到 r2.length - 1 ),然后 push 进highs 。这样就构造了一个和K线数据Bar一一对应的数据结构。
此时 highs 就可以传入 talib.MA函数计算出均线了。
完整的例子:
function main () {
while (true) {
var r = exchange.GetRecords()
var r2 = GetNewCycleRecords(r, 1000 * 60 * 60 * 4)
if (!r2) {
continue
}
$.PlotRecords(r2, "r2") // 画出K线
var highs = []
for (var i = 0 ; i < r2.length ; i++) {
highs.push(r2[i].High)
}
var ma = talib.MA(highs, 9) // 用均线指标函数 talib.MA 计算 均线指标
$.PlotLine("high_MA9", ma[ma.length - 2], r2[r2.length - 2].Time) // 使用画线类库把均线指标画在图表上
Sleep(1000)
}
}
回测运行:
可以看到 图中鼠标停留位置的均线指标值均为 11466.9289
以上代码可以复制到策略中运行测试,记得勾选「画线类库」后保存!
发明者量化交易平台已经有封装好的接口,即 exchange.GetRecords 函数,即可获取K线数据。 下面着重讲解的是直接访问交易所K线数据接口获取数据,因为有时候需要指定参数获取更多的K线,封装的GetRecords 接口 一般是返回 100根。如果遇到策略初始需要超过100根的K线时,就需要收集等待。 为了让策略尽快进行运作,可以自己封装一个函数,直接访问交易所K线接口,指定参数获取更多的K线数据。
以火币币币交易 BTC_USDT 交易对为例,我们实现这个需求:
找到交易所的API文档,查看K线接口描述:
https://api.huobi.pro/market/history/kline?period=1day&size=200&symbol=btcusdt
参数:
参数名 | 类型 | 是否必要 | 描述 | 取值 |
---|---|---|---|---|
symbol | string | true | 交易对 | btcusdt, ethbtc… |
period | string | true | 返回数据时间粒度,也就是每根蜡烛的时间区间 | 1min, 5min, 15min, 30min, 60min, 1day, 1mon, 1week, 1year |
size | integer | false | 返回 K 线数据条数 | [1, 2000] |
测试代码:
function GetRecords_Huobi (period, size, symbol) {
var url = "https://api.huobi.pro/market/history/kline?" + "period=" + period + "&size=" + size + "&symbol=" + symbol
var ret = HttpQuery(url)
try {
var jsonData = JSON.parse(ret)
var records = []
for (var i = jsonData.data.length - 1; i >= 0 ; i--) {
records.push({
Time : jsonData.data[i].id * 1000,
High : jsonData.data[i].high,
Open : jsonData.data[i].open,
Low : jsonData.data[i].low,
Close : jsonData.data[i].close,
Volume : jsonData.data[i].vol,
})
}
return records
} catch (e) {
Log(e)
}
}
function main() {
var records = GetRecords_Huobi("1day", "300", "btcusdt")
Log(records.length)
$.PlotRecords(records, "K")
}
Python版本,访问火币交易所接口的范例:
#!python3
import json
import urllib2
def GetRecords_Huobi(period, size, symbol):
headers = {'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'}
url = "https://api.huobi.pro/market/history/kline?" + "period=" + period + "&size=" + size + "&symbol=" + symbol
request = urllib2.Request(url)
request.add_header('User-Agent','Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6')
opener = urllib2.build_opener()
f= opener.open(request)
ret = f.read().decode('utf-8')
try :
jsonData = json.loads(ret)
records = []
for i in range(len(jsonData["data"]) - 1, -1, -1):
records.append({
"Time" : jsonData["data"][i]["id"] * 1000,
"High" : jsonData["data"][i]["high"],
"Open" : jsonData["data"][i]["open"],
"Low" : jsonData["data"][i]["low"],
"Close" : jsonData["data"][i]["close"],
"Volume" : jsonData["data"][i]["vol"],
})
return records
except Exception as e:
Log(e)
def main():
r = GetRecords_Huobi("1day", "300", "btcusdt")
Log(len(r))
ext.PlotRecords(r, "K") # 需要引用Python画线类库
Python版本,访问币安交易所的K线接口的范例:
#!python3
import json
import urllib2
def GetRecords_Huobi(period, size, symbol):
headers = {'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'}
url = "https://api.binance.com/api/v3/klines?symbol=" + symbol + "&interval=" + period
request = urllib2.Request(url)
request.add_header('User-Agent','Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6')
opener = urllib2.build_opener()
f= opener.open(request)
ret = f.read().decode('utf-8')
try :
jsonData = json.loads(ret)
records = []
for i in range(len(jsonData)):
records.append({
"Time" : float(jsonData[i][0]),
"High" : float(jsonData[i][2]),
"Open" : float(jsonData[i][1]),
"Low" : float(jsonData[i][3]),
"Close" : float(jsonData[i][4]),
"Volume" : float(jsonData[i][5]),
})
return records
except Exception as e:
Log(e)
def main():
r = GetRecords_Huobi("1m", "300", "BTCUSDT")
Log(len(r))
ext.PlotRecords(r, "K") # 需要引用Python画线类库
可以看到日志上,打印 records.length 为 300, 即 records K线数据 bar 数量有300根。
bamsmen 贴主能修复一下问题么?无法用3小时或6小时k合成日k
bamsmen if (((1000 * 60 * 60 * 24) - sourceRecords[i].Time % (1000 * 60 * 60 * 24) + (n * 1000 * 60)) % targetCycle == 0) { isBegin = true } 这一句有问题,无法用3小时或6小时k合成日k,只能用1小时,2小时,4小时的k线合成日k
xis2004 如果要爬某品种全部的历史数据也能爬么?
willzhang 感谢回复
willzhang 请问,如果想要超过300根,怎么处理比较好呢?比如1000根K线数据。交易所单次好像支持每次300根。多谢
发明者量化-小小梦 好的,抽时间改改。
发明者量化-小小梦 这是访问交易所接口数据的,交易所给你多少数据就是多少。也就是最近的几百根吧一般来说。
发明者量化-小小梦 如果 超过交易所接口支持的最大返回数量,这样只能收集数据,等足够K线数据量。