| 空换,持仓量不变 | 双平,持仓量减少 |
当买入开多和卖出开空的数量一致的时候,持仓量增加,可以定义为双开;
当买入开多和卖出平多的数量一致,持仓量不变,可以定义为多换;
当买入平空和卖出开空数量一致,这个时候是空换,持仓量不变;
最后买入平空和卖出平多数量一样,持仓量减少,定义为双平。
在介绍完这八种“开平”类型后,让我们了解一下怎样使用tick数据反推交易历史的逻辑机制。
交易历史反推主要通过比较前后两次tick的数据来进行分析。其中,我们会计算以下两个指标:1. 成交量的变动,2. 持仓量的变动。
持仓量的变动计算:将当前tick的持仓量减去前一次tick的持仓量,得到持仓量的变动值。
根据持仓量的变动值进行分类:
成交量的变动计算:将当前tick的成交量减去前一次tick的成交量,得到成交量的变动值。
根据成交量的变动值进行分类:
根据持仓量和成交量的组合变化,可以得到如下情况:
可以看到,换手,单向增仓和单向减仓都有着不同的种类,所以我们需要价格的涨跌去进行进一步的区分。
在这以上三种情况中,如果价格上涨就意味着是多头的方向,包括多换,开多和空平;而如果价格下跌意味着空头的方向,包括空换,开空和多平;如果价格不变,意味着空闲,双向或者未知。这里价格涨跌的判断需要使用当前tick的最新价格和上一个tick的买价(判断下跌)或者卖价(判断上涨)。如果盘面盘口较大,最新成交价停留在盘口中间的某个位置,则需要和当前tick的盘口的买一价和卖一价比较。当然,特殊情况也一定要定义,未知Unkown,去描述无法判断的情况。了解完反推的逻辑之后,我们就可以编写代码去进行交易记录的反推。
首先我们定义一个构造函数,构造出用于计算逐笔成交的对象。接着定义逐笔成交信息的枚举类型的变量,包括8种正常“开平”信息和5种空闲或者未知的信息。这里定义涨为红色,跌为绿色,白色为价格不变。然后定义这些动作的枚举,首先利用持仓量和成交量定义delta_enum
键,其中delta_enum_NONE
,EXCHANGE
,OPENFWDOUBLE
,OPEN
,CLOSEFWDOUBLE
,CLOSE
分别表示空闲,换手,双开,增仓,双平,和减仓;然后使用价格的涨跌定义forward_enum
键,forward_enum_UP
表示价格上涨,forward_enum_DOWN
表示下跌,forward_enum_MIDDLE
表示持平。两个键的组合就可以定义对应的值,是一个包含两个元素的数组,分别是操作类型枚举值和颜色枚举值。
下面我们就是使用tick信息去定义delta_enum
键和forward_enum
键。首先利用当前tick(也就是info)和前一次tick(preinfo)去计算成交量变化值volume_delta
和持仓量变化值open_interest_delta
。然后利用两者我们刚才提到的关系去定义delta_enum
键(这里定义为变量delta_forward
),然后利用价格的涨跌去定义forward_enum
键(这里定义为变量order_forward
),最后根据两个键去选择相对应的值,包括操作类型枚举和颜色枚举。
开平信息 | 持仓量/成交量 | 价格 |
---|---|---|
type_enum.NOCHANGE(空闲) | delta_enum_NONE(空闲) | forward_enum_UP(上涨) |
type_enum.NOCHANGE(空闲) | delta_enum_NONE | forward_enum_DOWN(下跌) |
type_enum.NOCHANGE(空闲) | delta_enum_NONE | forward_enum_MIDDLE(下跌) |
type_enum.EXCHANGELONG(多换) | delta_enum_EXCHANGE(换手) | forward_enum_UP |
type_enum.EXCHANGESHORT(空换) | delta_enum_EXCHANGE | forward_enum_DOWN |
type_enum.EXCHANGEUNKOWN(未知) | delta_enum_EXCHANGE | forward_enum_MIDDLE |
type_enum.OPENDOUBLE(双开) | delta_enum_OPENFWDOUBLE(双向增仓) | forward_enum_UP |
type_enum.OPENDOUBLE(双开) | delta_enum_OPENFWDOUBLE | forward_enum_DOWN |
type_enum.OPENDOUBLE(双开) | delta_enum_OPENFWDOUBLE | forward_enum_MIDDLE |
type_enum.OPENLONG(多开) | delta_enum_OPEN(单向增仓) | forward_enum_UP |
type_enum.OPENSHORT(空开) | delta_enum_OPEN | forward_enum_DOWN |
type_enum.OPENUNKOWN(未知) | delta_enum_OPEN | forward_enum_MIDDLE |
type_enum.CLOSEDOUBLE(双平) | delta_enum_CLOSEFWDOUBLE(双向减仓) | forward_enum_UP |
type_enum.CLOSEDOUBLE(双平) | delta_enum_CLOSEFWDOUBLE | forward_enum_DOWN |
type_enum.CLOSEDOUBLE(双平) | delta_enum_CLOSEFWDOUBLE | forward_enum_MIDDLE |
type_enum.CLOSESHORT(空平) | delta_enum_CLOSE(单向减仓) | forward_enum_UP |
type_enum.CLOSELONG(多平) | delta_enum_CLOSE | forward_enum_DOWN |
type_enum.CLOSEUNKOWN(未知) | delta_enum_CLOSE | forward_enum_MIDDLE |
最后这里设置了一个return,用来将preInfo重置为null,用于清空之前的tick数据,以便重新开始计算。总体的思路可以回顾一下,根据tick的成交量,持仓量和价格的关系,去定义两个键,然后根据两个键的匹配去选择相对应的开平类型,进而推断逐笔交易的历史。
这里我们实盘运行一下,可以看到随着时间的更新,相对应的交易记录信息不断的打印出来。需要注意的是,我们推算出来到结果是根据tick数据反推出来的,实际上盘面上交易过程是非常复杂、快速的,可能一次tick切片行情变动中混合了以上多种成交组合。为了进行实践的验证,我们可以根据利用逐笔交易数据计算出来的成交金额和CTP协议得出的结果对比,可以发现我们的逐笔成交量数据是可靠的。大家有兴趣可以尝试下,利用反推出来的交易记录数据去做更多的策略探索。
K线是一种用于表示一段时间内市场价格变动情况的图表。我们获取的k线数据,是根据CTP协议(中国金融期货交易所的交易接口协议)提供的tick数据合成的。作为伴随时间流的k线数组,每根K线包含开盘价、最高价、最低价和收盘价,交易量和持仓量关键数据。虽然我们可以直接使用现成的指标函数比如TA.MA()
直接使用k线数据进行技术指标的计算,但这个过程实际上是一个黑盒,我们不能了解具体的底层K线处理机制。然而,了解底层K线的处理机制对于理解技术指标的计算过程和优化算法非常重要。
理解计算过程:了解底层K线的处理机制可以帮助我们理解技术指标的计算过程。通过了解每个步骤的具体实现逻辑和算法,我们可以更好地理解指标是如何被计算出来的,以及每个参数的含义和影响。
综上所述,了解底层K线的处理机制对于理解技术指标的计算过程、优化算法、策略定制和容错处理都非常重要。这样我们可以更好地理解和应用量化交易中的技术指标,提升交易策略的效果和稳定性。
今天,我们以移动平均值的计算为例,手写一个源码,探讨背后的k线处理机制。
function HANDMA(records, period) {
var MA = []; // 存储移动平均值的数组
// 遍历数据数组
for (var i = 0; i < records.length; i++) {
if (i < period-1) {
MA.push(NaN); // 添加NaN
} else {
var sum = 0;
// 计算每个数据点之前 period 个数据的和
for (var j = i - period + 1; j <= i; j++) {
sum += records[j].Close;
}
// 计算当前数据点的移动平均值
var ma = sum / period;
// 存储移动平均值到数组中
MA.push(ma);
}
}
return MA;
}
function main(){
var c = KLineChart({})
while (true){
exchange.SetContractType('rb888');
var records = exchange.GetRecords();
MA_20_hand = HANDMA(records, 20);
MA_20 = TA.MA(records, 20);
Log('手动移动平均数据',MA_20_hand);
Log('移动平均数据',MA_20);
for (var i = 0 ; i < records.length ; i++) {
var bar = records[i]
c.begin(bar)
c.plot(MA_20_hand[i], "手动移动平均", {overlay: true}) // 画在图表主图
c.plot(MA_20[i], "移动平均", {overlay: true}) // 画在图表主图
c.close()
}
}
}
根据代码显示,移动平均值函数接受两个参数:records(包含K线数据的数组)和period(移动平均的周期)。函数通过遍历数据数组来计算移动平均值,并将结果存储在一个数组MA中。 具体的计算步骤如下:
在函数定义完成后,我们在主函数中,获取k线数据,然后调用HANDMA(records, 20)
和TA.MA(records, 20)
函数分别计算了手动移动平均数据和移动平均数据,并将结果打印输出。使用KLineChart函数,我们进行了两条均线的呈现。
在回测结果中可以看到,在9点到9点30分之间,移动平均值设置20为周期,前20分钟内,移动平均值都是空值,当收集够足够的数量后,开始进行指标的计算。现成的指标和手动的指标两条线是重合的,证明我们的计算是没有问题的。
这里我们需要理解下,MA数组和K线数据的对应关系: records是一个数组,其中包含了伴随时间流的多个K线记录。每个K线记录都包含了开盘价、最高价、最低价和收盘价等信息。
MA是一个用于存储移动平均值的数组,虽然在图中没有呈现,但是通过打印MA数组,它的长度与records数组相同。在计算移动平均之前的周期(period)内,MA数组中的元素都是空值(null)。这些空值用来表示移动平均计算过程中前期数据不足的情况。当然,我们也可以选择其他的处理方式来处理数据不足的情况。例如,可以使用累计周期的均值作为移动平均的替代值,或者使用其他的填充值来表示未达到k线数量的移动平均值。
在给定的代码中,当索引i小于移动平均的周期period-1时,也就是前期数据不足以计算移动平均值时,将nan添加到MA数组中。MA数组的第一个有效值元素对应着records数组中的第period个元素的移动平均值,然后伴随k线的更新,新的ma值被对应计算出来。这样就能确保移动平均值和K线数据之间的一一对应关系。通过这种对应关系,我们可以方便地从MA数组中获取每个K线数据对应的移动平均值,进行进一步的分析和处理。
代码确实不复杂,但重要的是我们需要深入理解底层K线的处理机制。当我们使用K线进行技术指标计算时,无论是MACD还是RSI,我们需要了解底层K线是如何被使用的,以及指标结果与K线的对应关系。我们可以更好地理解技术指标的计算原理和应用场景。
在同时,了解底层K线的处理机制可以帮助我们理解K线数据的结构和含义,以及K线的时间周期和采样频率。这对于正确地解读技术指标的结果至关重要。我们需要知道每个指标结果对应的是哪个K线数据,以及指标结果如何随着时间的推移而更新。我们可以看到,当我们设定的周期为分钟时,MA计算返回的结果也是以分钟作为周期。但是需要温馨提醒下,在最后一根k线没有完成的时候,MA最后的一个值也是不固定的。
当然这只是使用k线计算移动平均值的小例子,希望从这份代码出发,大家可以理解k线处理的底层机制,更进一步的帮助我们更好地理解一些复杂技术指标的计算过程。如果有兴趣的话,指标计算源码在优宽平台是公开的(link),我们可以参考学习,并根据实际需求,进行更适合我们交易理念的定制化的指标计算和策略优化。
K线图是一种常见的股票或期货价格图形表达方式,它以矩形方块(称为K线实体)和细线(称为影线)来展示一段时间内的价格波动情况。K线图包含四个关键价格数据:开盘价(Open)、最高价(High)、最低价(Low)和收盘价(Close)。通过这些价格数据,可以进行以下的K线的处理:
K线处理的目标是辅助分析股票或期货市场的价格走势,并给出交易决策的参考。不同的处理方法和技术可以结合使用,根据实际情况和个人偏好进行选择和应用。
在上一节课程中,我们学习了使用k线处理的底层机制,本节课我们将要学习k线处理之平均K线图,也就是Heikin-Ashi算法。常用的指标在talib等指标库中可以找到。但是对于一些比较冷门且实用的算法、指标就很难找到现成的,本节课,我们将利用JavaScript语言“手搓”这个算法函数,实现一个自定义的模版类库。
平均K线图(Heikin-Ashi)是一种技术分析图表,常用于研究价格趋势和市场动态。它通过对每个价格点进行平均处理,以平滑价格波动,并提供更清晰的趋势信号。相比传统的K线图,平均K线图在绘制过程中考虑了前一根K线的信息,使得每根K线之间具有较强的连续性。这种平均处理有助于消除噪音和震荡,更准确地显示价格趋势。
平均K线图可以帮助交易者识别趋势的开始和结束,并捕捉价格的反转信号。常见的分析方法包括观察K线的颜色和形态,结合其他技术指标如移动平均线、趋势线、收敛/背离等,以提供交易决策的参考和确认。
我们来看下计算方法,在这里我们将作为参数的K线数据中的开盘价、最高价、最低价、收盘价命名为:Open、High、Low、Close。将所要计算的平均K线图的开盘价、最高价、最低价、收盘价命名为:avgOpen、avgHigh、avgLow、avgClose。平均K线图的计算主要分两个部分的处理:
因为这个算法是一个迭代算法,计算当前Bar的数据时需要引用到前一个Bar的数据(很多经典指标、算法都是这种迭代计算),所以第一根Bar的计算必然是与其后的迭代计算是不同的。根据这个指标的计算资料的描述,第一根平均K线Bar的计算方式是这样的:
avgOpen = (Open + Close) / 2
avgHigh = High
avgLow = Low
avgClose = (Open + High + Low + Close) / 4
平均开盘价 = (第一根K线开盘价和收盘价) 的平均值 平均最高价 = 第一根K线最高价 平均最低价 = 第一根K线最低价 平均收盘价 = (第一根开盘价 + 最高价 + 最低价 + 收盘价) / 4
除了第一根平均K线Bar之外,后续的平均K线Bar需要迭代计算。具体的计算方法是这样的,首先需要计算平均开盘价和平均收盘价。
平均开盘价 = (前一根K线平均开盘价 + 前一根K线平均收盘价) / 2 平均收盘价 = (开盘价 + 最高价 + 最低价 + 收盘价) / 4
接着利用平均开盘价和平均收盘价分别和最高价,最低价取最大值作为平均最高价,取最小值作为平均最低价。
平均最高价 = Math.max(最高价, 平均开盘价, 平均收盘价) 平均最低价 = Math.min(最低价, 平均开盘价, 平均收盘价)
avgOpen = (pre_avgOpen + pre_avgClose) / 2
avgHigh = Math.max(High, avgOpen, avgClose)
avgLow = Math.min(Low, avgOpen, avgClose)
avgClose = (Open + High + Low + Close) / 4
在策略库创建一个策略,选择策略语言为JavaScript,选择策略类型为「模板类库」,命名为:JavaScript扩展指标库。如果还希望“手搓”一些其它指标算法,也可以直接加入到这个模板代码中。
// 使用JavaScript语言实现的扩展指标
/**
* calcAvgRecords: 计算Heikin-Ashi,即平均K线图
* @param {Array<Object>} records - K线Bar数组
* @returns {Array<Object>} - 平均K线数组
*/
function calcAvgRecords(records) {
// 声明、初始化一个空数组,作为函数最终返回的变量
var ret = []
// 判断传入的K线数组参数records
if (!Array.isArray(records)) {
return null
}
// 遍历K线
for (var i = 0; i < records.length; i++) {
var bar = records[i]
var avgBar = {}
if (i == 0) {
// 处理第一根Bar,计算平均K线
avgBar.Open = (bar.Open + bar.Close) / 2
avgBar.High = bar.High
avgBar.Low = bar.Low
avgBar.Close = (bar.Open + bar.High + bar.Low + bar.Close) / 4
avgBar.Time = bar.Time
} else {
// 处理其它Bar,计算平均K线
avgBar.Open = (ret[i - 1].Open + ret[i - 1].Close) / 2
avgBar.Close = (bar.Open + bar.High + bar.Low + bar.Close) / 4
avgBar.High = Math.max(bar.High, avgBar.Open, avgBar.Close)
avgBar.Low = Math.min(bar.Low, avgBar.Open, avgBar.Close)
avgBar.Time = bar.Time
}
// 将计算出的平均K线Bar,依次放入数组ret中
ret.push(avgBar)
}
return ret
}
// 导出函数
$.CalcAvgRecords = calcAvgRecords
这段代码是一个计算平均K线的函数,接收一个K线数组作为参数,并返回一个包含平均K线的数组。函数首先声明并初始化一个空数组ret,用于存储最终结果。然后判断传入的K线数组records是否是一个数组,如果不是,则返回null。接下来,使用循环遍历每个K线对象。
在循环中,首先获取当前K线对象bar,然后声明一个空对象avgBar,用于存储计算得到的平均K线数据。 如果是第一根K线(即i == 0),则根据公式计算平均K线的各个属性值:开盘价(Open)为开盘价和收盘价的平均值,最高价(High)为原始K线的最高价,最低价(Low)为原始K线的最低价,收盘价(Close)为开盘价、最高价、最低价和收盘价的平均值,时间(Time)为原始K线的时间。
对于其他K线(即i > 0),则使用上一根计算得到的平均K线数据(ret[i - 1])来计算当前平均K线的各个属性值。开盘价(Open)为上一根平均K线的开盘价和收盘价的平均值,收盘价(Close)为当前原始K线的开盘价、最高价、最低价和收盘价的平均值,最高价(High)为当前原始K线的最高价、平均K线的开盘价和收盘价中的最大值,最低价(Low)为当前原始K线的最低价、平均K线的开盘价和收盘价中的最小值,时间(Time)为当前原始K线的时间。
最后,将计算得到的平均K线对象avgBar添加到数组ret中,并继续处理下一个K线对象。循环结束后,函数返回存储了所有平均K线数据的数组ret。
function main() {
var chart = KLineChart({})
while (true) {
if (exchange.IO("status")) {
// 设置螺纹钢主力合约
exchange.SetContractType("rb888")
// 获取K线数据
var r = exchange.GetRecords()
// 使用我们编写的算法函数,计算平均K线
var avgRecords = $.CalcAvgRecords(r)
if (avgRecords) {
// 使用KLineChart函数创建的对象画图,画出平均K线
avgRecords.forEach(function(bar, index) {
chart.begin(bar)
chart.close()
})
}
}
Sleep(500)
}
}
运行这个测试代码,通过设置合约,获取数据,使用我们编写的算法函数,计算平均K线,并利用KLineChart画图。我们可以在模拟回测测试页面看到平均K线图的图表。
最后温馨提醒下,平均K线图在不同市场和不同情况下的有效性可能存在差异。平均K线图的平滑作用可以帮助过滤掉一些噪音和短期波动,提供更清晰的趋势信号。它特别适合应对相对稳定、趋势明显的市场情况,如有较长持续时间的上升或下降趋势;而在市场波动较大、行情不明确的情况下,可能无法捕捉到价格的小幅波动和短期反转信号,这个时候可能需要使用其他技术指标或方法来辅助分析。
前面两节的课程,我们讲解了k线处理的底层机制和平均k线的算法,今天我们来学习合成k线。
合成K线是将原始K线数据进行加工处理,生成更高周期的K线数据。它的作用主要有以下几个方面:
数据压缩:合成K线可以将较低周期的原始K线数据进行压缩,生成较高周期的K线数据,从而减少数据量。这样可以降低数据存储和处理的成本,节省计算资源。
信号过滤:通过合成K线,可以平滑原始K线数据中的噪声和小幅波动,从而过滤掉一些不必要的细节,使得价格走势更加清晰、规律更明显。这有助于有效识别和捕捉市场趋势,避免对短期波动做出错误决策。
趋势分析:合成K线能够将较低周期的价格波动整合为较高周期的整体走势,更好地反映市场趋势的持续性和稳定性。通过分析合成K线的形态、趋势线和关键支撑阻力位,可以更精确地判断市场走势,并据此制定相应的交易策略。
时间分析:合成K线能够将较低周期的价格行情映射到较高周期的时间尺度上,更好地观察和分析价格走势的时间特征。这对于判断市场行情的节奏、周期和重要时间点非常有帮助,有利于把握市场节奏并做出及时的交易决策。
我们举一个例子说明,在期货市场中,庄家洗盘是一种市场操纵行为,通常指庄家在短时间内通过大笔交易来推动市场价格,吸引、迷惑或激发其他交易者进行买入或卖出,从而达到牟利的目的。通过合成K线,我们可以更好地识别和分析庄家洗盘等操纵行为的影响,警觉潜在的风险和市场异常,从而在交易中做出更明智的决策。
综上所述,合成K线可以提供更高层次的数据视角,使得交易者能够更全面、准确地分析市场行情,并制定相应的交易策略,有助于提高交易决策的准确性和盈利能力。
在编写程序化交易策略时,根据不同的交易理念,经常会有需求使用一些非标准周期K线数据的情况,例如需要使用12分钟周期K线数据、半个小时K线周期数据等等。通常这类非标准周期是无法直接获取的。那么我们如何应对此类需求呢?
在我们理解底层k线的处理机制后,这个问题不再变得困难。非标准周期可以通过更小周期的数据,合并合成获取,可以想象一下,多个周期中的最高价,可以算作合成后的最高价,最低价算作合成后的最低价,开盘价不会变,就用合成这根K线原料数据的第一个开盘价,收盘价对应的是用合成这根K线的原料数据的最后一个的收盘价,时间就是取的开盘价的时间,成交量用该时间段内的交易量求和计算得出。
我们举例示范一下:
时间 | 开盘价 | 最高价 | 最低价 | 收盘价 | 成交量 |
---|---|---|---|---|---|
1688000400000 | 3704 | 3708 | 3701 | 3708 | 10574 |
1688000460000 | 3708 | 3713 | 3706 | 3713 | 8572 |
1688000520000 | 3713 | 3717 | 3711 | 3716 | 11233 |
1688000580000 | 3716 | 3719 | 3715 | 3717 | 9655 |
1688000640000 | 3717 | 3724 | 3717 | 3721 | 13698 |
这五个1分钟周期的数据,合成一根5分钟周期的数据,开盘价就是就是第一根 k线时间的开盘价:3704 收盘价是最后一根的收盘价:3721 最高价就找这里面最高的价格:3724 最低价就找这里面最低的价格:3701 成交量就是每根成交量的求和就可以52432
5分钟周期的起始时间就是第一根1分钟K线的起始时间,最后合成出的 一根5分钟K线是这样的数据:
时间 | 开盘价 | 最高价 | 最低价 | 收盘价 | 成交量 |
---|---|---|---|---|---|
1688000400000 | 3704 | 3724 | 3701 | 3721 | 52432 |
理解了初步的思路以后,我们来使用代码进行实现。
/*backtest
start: 2023-06-26 09:00:00
end: 2023-06-27 09:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
mode: 1
*/
function GetNewCycleRecords(sourceRecords, targetCycle) {
// K线合成函数
var ret = []
// 检查周期数据
if (!sourceRecords || sourceRecords.length < 2) {
return null
}
// 获取源K线数据的周期和倍数
var sourceLen = sourceRecords.length
var sourceCycle = sourceRecords[sourceLen - 1].Time - sourceRecords[sourceLen - 2].Time
var multiple = targetCycle / sourceCycle
// 检查目标周期有效性
if (targetCycle % sourceCycle != 0) {
Log("targetCycle:", targetCycle)
Log("sourceCycle:", sourceCycle)
throw "targetCycle is not an integral multiple of sourceCycle."
}
var isBegin = true //可以设置期货的开盘时间,为了简化代码,这里我们直接设置为true
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++) {
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) {
exchange.SetContractType('rb888')
var r = exchange.GetRecords()
var r2 = GetNewCycleRecords(r, 1000 * 60 * 5)
Log('合成k线:', r2)
if(r2!= null){
$.PlotRecords(r2, "合成k线")
}
Sleep(1000) // 每次循环间隔 1000 毫秒,防止访问K线接口获取数据过于频繁,导致交易所限制。
}
}
这段代码是一个K线合成函数,用于将源K线数据按照指定的周期进行合成,生成新的K线数据。
函数接受两个参数:sourceRecords和targetCycle。sourceRecords是一个包含源K线数据的数组,每个元素都包含了K线的高、低、开、收、时间和成交量等信息。targetCycle是需要合成的目标周期,单位为时间。
该函数首先获取源K线数据的周期,即最后两个K线的时间差。然后通过计算目标周期与源周期的倍数关系,确定需要合成几个源K线形成一个新的K线。
其中if (!sourceRecords || sourceRecords.length < 2)
表示如果sourceRecords为空或长度小于2,则返回null。这段代码的作用是确保源K线数据的有效性,如果数据不满足要求,则无法进行后续的合成操作。
这段代码的作用是检查目标周期(targetCycle)是否是源周期(sourceCycle)的整数倍。如果不是整数倍关系,那么会输出日志信息(Log)显示目标周期和源周期的值,并抛出一个错误(throw),提示"targetCycle is not an integral multiple of sourceCycle."(目标周期不是源周期的整数倍)。这是为了确保合成的新周期能够准确对应源周期,以保证数据的准确性和一致性。
接下来,函数通过遍历源K线数据的方式,根据目标周期进行合成。在合成过程中,使用isBegin变量表示是否达到期货的开盘时间(在这里我们直接设置为始终为true)。count变量用于计数,记录已经合成了多少个源K线。high、low、open、close、time和vol变量则分别用于保存合成K线的高、低、开、收、时间和成交量。
在期货开盘的时间,根据合成的规则,使用count变量确定是否达到目标周期或者走到最后一根k线的时候,合成一个新的K线,并将其添加到ret数组中。之后,重置计数器count为0,继续合成下一个K线。
最后,函数返回合成后的新K线数据数组ret。
整体的逻辑确实不复杂,但有一些细节我们需要注意下。确定使用原始k线的数目是通过计算目标周期与源周期的倍数关系,因此目标周期应该是源周期的整数倍,所以不能使用小周期去合成更小的周期的数据。
在合成过程中,我们遍历源K线数据数组,通过count变量记录已经合成了多少个源K线。
count变量有三个阶段:
如果count为0,表示还没有开始合成一个周期的K线数据,所以将当前源K线数据的高、低、开、收、时间和成交量分别赋值给相应的变量(high、low、open、close、time和vol),并将计数器count加1。
当count小于目标周期与源周期的倍数时,说明还没有达到一个完整的目标周期,可以继续合成。
当count等于目标周期与源周期的倍数时,即达到一个完整的目标周期时,我们将当前合成的K线数据添加到ret数组中,并重置计数器count为0,以便继续合成下一个周期。
但是还有一种情况我们不能忽略,就是碰到不能达到一个完整的目标周期的时候,比如使用10分钟为周期进行合成数据,而期货市场在10点15分会中场停盘,10点十分以前是可以获取完整的目标周期的,在10点10分到10点15分,5分钟并不是一个完整的周期,这个时候我们选择使用10点10分到10点15的五分钟的数据进行合成。在代码中我们定义为如果已经遍历完所有的源K线数据(即i等于sourceLen - 1),需要将最后一段未合成的K线数据添加到ret数组中进行返回。
我们进行代码的测试,在主函数中,设置螺纹钢主力合约,通过 GetNewCycleRecords 函数 传入 原始K线数据 r , 和目标周期,1000(毫秒,就是一秒) * 60 * 5 即 目标合成的周期 是5 分钟线数据。然后使用画线类库进行画图。 在回测页面,可以看到在9点到10点之间,呈现12个5分钟合成k线。
我们的代码只是起到教学的作用,在实盘运行中,会碰到很多特殊的情况。例如不同的期货品种的开盘时间是不同的(日盘,夜盘,还有凌晨还在开盘的);第二种非标准周期,例如13分钟周期,就是不闭合的周期,这样的周期算出的数据不唯一,因为根据合成的数据起始点不同,合成出来的数据有差异。还有在夜盘开盘时,20点59分会有一根k线数据,表明集合竞价的k线,我们需要考虑下,因为第一根合成k线的时间会变为20点59分,所以造成夜盘周期的推移。这些实际情况,我们都需要考虑到,相信凭借大家的努力,都可以获得解决。
最后我们温馨提醒下,在期货市场中,合成K线有一些缺点需要我们注意:合成K线是通过对原始K线数据进行加工处理得到的,这个过程中可能会导致部分信息的损失。在同时,合成的过程中会有一定的延迟。这意味着合成K线上的信号和趋势可能比实际的市场行情稍有滞后,无法及时反映市场的变化和动态。甚至可能出现虚假信号的情况,由于合成K线对原始数据进行了整合和平滑处理,可能掩盖了一些短期的价格波动和噪音,使得市场走势看起来更加平稳。这可能会引发误判,导致交易者做出错误的决策。因此我们需要针对风险度不同的期货品种进行不同的合成k线处理。对于波动性较大的品种,比如纯碱和玻璃化工品种,合成K线的处理可能需要更加敏感和准确,以捕捉到更多的价格波动和噪音。而对于波动性较小的品种,比如玉米等农产品品种,则可以采用平滑处理,减少虚假信号的出现。
量化交易中的交互设计是指在量化交易系统或平台中,通过设计用户界面和用户体验,提供给交易者一种直观、高效的交互方式。它涉及到如何呈现交易数据和信息、如何操作交易功能和控件、如何反馈用户操作结果等方面。量化交易中的交互设计应该注重用户体验和效率,简洁明了、易于使用。通过合理的交互设计,可以提高交易者的满意度和系统的实用性。 优宽平台是一个提供量化交易策略开发和执行的在线平台,本平台交互设计是通过交互控件完成的。一个好的交互控件应具备良好的响应性能,快速响应用户的操作和指令;在同时提供及时的反馈,以确保用户对自己的操作有清晰的认知。在注重简洁、灵活和高效的同时,通过提供一定程度的用户定制能力,让用户根据自己的喜好和需求进行配置交互控件。优宽平台对以上的需求都进行了满足,下面我们就对优宽平台中的交互设计展开一个介绍。
首先我们来看下交互控制的设置。和策略参数一样,我们根据我们的需要进行不同类型参数的设置。策略交互控件同样具有五种类型,数字,布尔,字符,下拉框和按钮。我们举例示范下,我们可以设置一个按钮型,命名为action,描述为y一个操作;设置一个数值型,actionnumber,来定义指定操作的数量;布尔型ifaction,是或者否的选择;selected,下拉框,进行多项的选择;str,字符型,可以输入字符;在交互预览页面,我们就可以看到交互控件呈现的页面。
交互控件设置完成以后,在策略中我们通过全局函数GetCommand()
,获取交互命令字符串。GetCommand()
函数会获取策略交互界面发来的命令并清空缓存,没有命令就会返回空字符串。返回的命令格式为按钮名称加上参数,如果交互控件没有参数(例如不带输入框的按钮控件),那么命令就是按钮名称。
交互控件是在策略运行时才可以使用的,这就意味着在回测中,交互控件是不支持的。因此,首先我们创建实盘,定义策略标签,选择托管主机和运行策略,定义策略周期,添加交易平台,这样实时运行的实盘就创建成功了。
我们在实盘中运行一下这个控件测试代码,点开策略交互,显示出来我们刚才设置的五个按钮,我们分别点击一下。可以在日志信息中打印出来两个按钮的名称,如果有默认参数,就会附带上。按钮名称和按钮参数中间使用“:”进行分隔。如果我们只想打印参数,可以使用split函数。
这里我们先不设置控件需要进行的操作,只是使用log函数打印出来控件对应的操作,如果有数量的话,再加上数量。
function main() {
while (true) {
var cmd = GetCommand()
if (cmd) {
Log("cmd:", cmd)
var arr = cmd.split(":")
if (arr[0] == "action") {
Log("操作,该控件不带数量")
} else if (arr[0] == "actionnum") {
Log("操作,该控件带数量:", arr[1])
} else if (arr[0] == "ifaction") {
Log("是否进行操作", arr[1])
}
else if (arr[0] == "selected") {
Log("下拉框", arr[1])
}
else if (arr[0] == "str") {
Log("字符型", arr[1])
}
Sleep(1000)
}
}
}
我们在实盘中运行一下这个控件测试代码,点开策略交互,显示出来我们刚才设置的五个按钮,我们分别点击一下。可以在日志信息中打印出来两个按钮的名称,如果有默认参数,就会附带上。按钮名称和按钮参数中间使用“:”进行分隔。如果我们只想打印参数,可以使用split函数。
这里我们先不设置控件需要进行的操作,只是使用log函数打印出来控件对应的操作,如果有数量的话,再加上数量。可以看到,在实盘里,我们点击action按钮,会出现“买入,该控件不带数量”;点击actionnum,并设置数量2,会打印“操作,该控件带数量:2” 。请注意,这个数量2是字符型的,如果要确认为数值型,需要使用parseInt进行转换。其他的按钮大家可以自己修改探索一下。关于具体的使用场景,我们下面展开介绍。
接下来我们来看下交互控件的功能。
我们来看第一个应用场景:半自动化交易。半自动化交易是一种结合量化交易和人工决策的交易方式。在半自动化交易中,交易者使用计算机程序或交易平台上的工具和功能来执行交易决策,但最终的交易操作需要交易者自己做出。交互控件在半自动化交易活动中尤其友好。半自动化交易需要快速决策和执行,交互控件的快速选择交易品种功能非常有用。通过简洁直观的界面设计,交易者可以迅速浏览可交易的品种列表或使用搜索功能快速找到所需的品种。交互控件还允许交易者预设常用的交易参数,如数量、价格、止损止盈等。预设参数功能极大地简化了操作流程,交易者只需在初始设置时输入一次参数,之后就能够随时使用这些预设参数进行快速下单,提升了下单的速度和效率。
同时,交互控件还具备错误提示和确认功能,为半自动化交易提供了额外的安全保障。在输入有误或存在潜在风险时,交互控件可以及时提醒并帮助纠正错误。此外,在交易最终执行前,交互控件要求交易者再次确认交易指令,以确保下单操作的准确性和安全性。这有助于半自动化交易避免因输入错误或误操作而造成的交易错误,进一步减少了风险。总体而言,交互控件为半自动化交易提供了便利、效率和安全性。这里我们举一个例子来示范使用交互控件来选择不同的期货合约,并进行开多仓和平多仓操作。
function main() {
var contractlist = ['i2309', 'rb2309', 'hc2309'];
var p = $.NewPositionManager();
var contract = null; // 将contract的声明放到while循环之前
while (true) {
if (exchange.IO("status")) {
LogStatus(_D());
var cmd = GetCommand();
if (cmd) {
Log("cmd:", cmd);
var arr = cmd.split(":");
if (arr[0] === "selected") {
contract = contractlist[arr[1]]; // 不使用"var"关键字,直接赋值给外部声明的contract变量
Log("买入期货品种", contract);
} else if (arr[0] === "buy") {
p.OpenLong(contract, 1);
} else if (arr[0] === "covernum") {
var pos = p.GetPosition(contract, PD_LONG);
if (pos == null || pos.Amount < parseInt(arr[1])) {
Log('没有足够的多仓可平');
} else {
Log("平多仓数量", arr[1]);
p.Cover(contract, parseInt(arr[1]));
}
}
}
Sleep(1000);
} else {
LogStatus(_D(), "未连接CTP!");
Sleep(500);
}
}
}
这里我们举一个例子来示范使用交互控件来选择不同的期货合约,并进行开多仓和平多仓操作。
这段代码首先创建一个合约列表 contractlist,其中包含了三个期货合约代码:‘i2309’, ‘rb2309’, ‘hc2309’。
使用交易类库,创建一个新的单品种控制对象 p。 设置外部变量contract为空。 接着获取用户输入的命令,并将其存储在变量 cmd 中。 如果存在命令 (cmd),则执行以下逻辑: 记录命令信息到日志,输出命令内容。并将命令内容用冒号分割成一个数组 arr,这样为了获取不同命令的参数。 如果命令的第一个元素是 “selected”,表示选择了某个期货品种,根据命令的对应元素从合约列表中获取对应的合约代码,打印到日志中。 在选择品种之后,我们需要使用$.IsTrading判断是否在交易时间,如果不在,需要及时提醒。
如果命令的第一个元素是 “buy”,表示买入期货品种,使用单品种控制对象 p 执行开多仓的操作,买入数量为1。 如果命令的第一个元素是 “covernum”,表示平多仓,首先通过 p 获取当前持仓的信息,如果持仓量为空或者持有的多仓数量不足,则输出 “没有足够的多仓可平”;如果持有足够的多仓,先打印出来需要平多仓的数量,并使用cover执行平多仓的操作,平仓数量为命令的第二个元素。这里需要注意的是我们获取的arr[1]是一个字符型,需要转换为数值型。 关于每一个控件对应的操作我们就设置完成了,接下来我们要在实盘中查看一下控件的运行。
我们在实盘中,首先选择期货品种,日志信息显示选择成功,如果不在交易时间段,比如这个时间10点27,上午中间休息时间段,日志信息会显示不在交易时间。然后点击buy,点击一次开一手多仓,点击两次,开两手;然后我们使用covernum进行平仓,这里我们首先填写数字3,因为我们目前持有的多仓数目为2,所有会显示没有足够的多仓可平,我们改为数字1,日志信息中呈现我们平了一手多仓。
这就是使用交互控件进行交易的一个简单的例子。上面的代码示例演示了如何在策略中使用交互控件来实现期货交易的功能。通过设置不同类型的交互控件,我们可以提供给交易者一个直观、灵活的交互界面。用户可以通过点击按钮、选择下拉框等方式来输入指令,然后策略根据接收到的指令执行相应的交易操作。
请注意,这里只是为了展示控件的使用,还有很多不完善的地方。对于半自动化交易,使用交互控件可以使交易过程更加友好和灵活。通过在代码中集成交互控件,你可以与机器人进行交互,实现对交易的监控和操作,并根据需要进行自定义设置。例如,你可以添加一个交互控件来设置交易的执行时间,让用户选择在何时执行买入或平仓操作。这样,你可以根据市场的波动和行情状况,智能地调整交易时机,以提高交易的成功率和盈利能力。
如果你是一个“直觉型交易者”,厌烦于技术指标的死板或者频繁变动,但又经常感到懊恼没有参考某些市场的指标而冲动下单,交互控件可以帮助你增强交易决策。通过交互控件,你可以自定义并集成各种市场指标和数据源,以供参考和分析。例如,你可以设置下拉框控件来选择不同的指标,如移动平均线、相对强弱指标等。当你选择特定的指标后,交互控件可以在不停掉策略的情况下,实时选择不同的参数进行相应的计算和分析,并将结果反馈给你。并且交互控件还具有实时的调试功能,可以根据你的需要进行额外的策略代码的实时运行。通过交互控件的灵活组合和定制,你可以根据自己的交易风格和需求,获得更多个性化的交易参考和决策支持。
因此呢,交互控件不仅仅只有半自动化下单的功能,本节课我们来继续学习策略交互设计的实时修改参数和实时的调试功能,以及状态栏的自定义交互功能。
在前面的课程中,我们介绍了策略参数的使用,主要针对于策略运行中的不同类型的参数进行控制。策略参数在策略运行中是不能被改动的,如果需要改动,则必须停掉策略修改参数才可以;而有的同学如果希望在交易过程中对策略实时的进行一定程度的控制和修改,交互控件可以帮助你完成这一目标。 让我们举一个例子试试看。假如你的日内策略是使用20秒均线作为基准。然而由于美联储加息,你预感今天市场波动比较剧烈,所以你希望改变你的策略,使用10秒作为参考基准。如果你不设置交互控件,改变参数需要你停掉实盘,修改参数,然后重启实盘;在美联储加息平稳后,重新修改均线参数为20。而如果你使用交互控件,这一个问题变得简单了起来。
首先在策略交互中设置一个交互控件period为数值型,默认值是20。回到代码,默认的period为20,因此策