教程指南:该教程为优宽平台《商品期货量化交易实践系列课程》配套教程文本。
请配合视频一起使用,如果有错误,请及时提醒,我们后续将会不断完善该教程。
1.设计实践超级趋势策略
Hello,各位喜欢量化的小伙伴们,前期我们系统讲解了使用pine语言和javascript语言的进行期货量化交易的学习,课程部分呢可能过于理论性,因此讲述的风格比较严谨。有些同学们反应,虽然了解了基本的概念和理论,但是应用于实战还是感觉无从下手,因此,针对于这一痛点,从本节课开始,我们将开始实践性的讲解一些课程,从一行行的代码轻轻敲起,打开量化交易的大门。
虽然我们试图从0开始教学,但是有一些在前面课程中基础性的知识,我们不会过多于阐述。有些地方,我会标注来源于哪节课程,大家可以翻看复习下(欢迎翻看复习,前期视频播放量太惨了~~)。如果有遗漏的话,大家也可以留言弹幕或者评论区,我们会细心一条条进行解释。
万事开头难,有许多想学习量化学习的同学,经常苦恼于一个好的量化交易学习平台的选择。交易的数据作为实时的时间流数据,不像我们做一些基本的统计分析,只需要下载数据,导入编程语言进行分析就可以。如果想进行交易数据的量化分析,我们需要海量的历史数据,下载到本地的电脑肯定是不划算的。另外,如果想进行实时的模拟或者真实交易的话,我们还需要对接API接口、搭建回测平台等。从底层开始搭建我们的量化交易系统。而在优宽平台,这些问题通通帮你搞定。优宽平台有着海量的数据可以让你任意调取分析,完善的回测系统帮你查看策略的运行效果,同时拥有模拟回测和实盘交易系统,帮助我们学习和研究。学习一段时间后,可以使用操作仿真账户的实盘进行真正的跟盘测试。等到策略真正的成熟,可以应用于真实的实盘交易。在优宽平台,可以让我们下定决心的开始,简单、轻松一点入门量化交易领域。
优宽平台托管者和交易所配置
前面啰嗦的够多了,现在开始,我将手把手教大家搭建一个操作仿真账户的实盘策略。操作仿真账户的实盘就和期货软件上的模拟账户一样,不是真金白银,但是对接的是真实的市场。我们可以申请一个仿真账户,「上期模拟」 和 「N视界」 都可以,这里我们选择N视界。申请好账号以后,点开个人主页,基本信息中有我们的交易账户和密码,我们需要记录下来。
前面的课程,我们大多时间使用的都是模拟回测系统。在策略库中写好策略,就可以使用历史的数据查看策略的有效性。本次,我们要搭建一个操作仿真账户的实盘策略。除了一个可运行的策略之外,我们需要进行托管者和交易所的配置。
首先来进行托管者的配置,托管者可以理解为我们的交易策略的执行者。托管者运行在我们配置的服务器上,即使优宽量化交易平台网站出现网络故障也不影响您的托管者运行。托管者可运行在本地的电脑,或者云服务器上,系统支持Linux,Windows,Mac OS等主流操作系统。托管者的配置很简便,我们点击部署托管者,然后根据不同的系统进行安装,具体的配置教程请看评论链接第一条。大家也可以选择一键租用托管者,这样会更加方便一点。这里我配置的托管者使用的是云服务器,一个入门级的服务器就可以。
接着我们来看交易所的配置,交易所可以添加模拟的仿真账户或者真实的期货账户,但是对于真实的期货账户,我们还需要进行看穿式认证。教程也会放在评论区。这里我们添加N视界模拟账号,在添加交易所页面我们首先选择:N视界模拟(NSight)。然后填写N视界仿真账户的交易账号和密码。就是我们刚才记录下来的N视界的模拟账号和密码,这里的密码是交易所的(交易所是一个泛指概念,这里指的是:N视界仿真账户),请大家不要填写优宽平台的密码。点击添加交易所,再填写优宽平台的密码验证,交易所就配置完成了。
编写超级趋势策略
接下来到了我们的重点,编写超级趋势策略。首先来认识一下这个指标。超级趋势指标(SuperTrend Indicator)是一种技术分析工具,用于识别市场趋势和价格动量。超级趋势指标基于波动性原理,通过计算当前价格与最高价/最低价的偏离程度来确定趋势方向,并提供了可能的买入和卖出信号。该指标兼具趋势跟踪和止损保护的功能,适用于各种交易策略。
超级趋势指标的交易策略是这样的:
- 当价格上穿超级趋势线时,产生买入信号;
- 当价格下穿超级趋势线时,产生卖出信号。
超级趋势指标的特点是灵活性较高,可以根据不同的市场和交易策略进行参数调整。它可以帮助交易者捕捉到趋势行情并避免明显的市场调整。
了解完基本的概念以后,我们来手敲代码进行实现。打开策略编写页面,选择语言为javascript。首先设置策略参数,这里我们设置三个参数,分别为:
| 变量名 | 描述 | 类型 |
|---|---|---|
| Symbol | 合约 | 字符串 |
| pd | 长度 | 数字型 |
| factor | 因子 | 也是数字型 |
javascript
/*backtest
start: 2023-06-05 09:00:00
end: 2023-08-27 15:00:00
period: 15m
basePeriod: 5m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["Symbol","SA401"],["pd",35]]
*/
// 全局变量
var mp = 0;
var _q = $.NewTaskQueue();
function SuperTrend(r, period, multiplier) {
// atr
var atr = talib.ATR(r, period)
// baseUp , baseDown
var baseUp = []
var baseDown = []
for (var i = 0; i < r.length; i++) {
if (isNaN(atr[i])) {
baseUp.push(NaN)
baseDown.push(NaN)
continue
}
baseUp.push((r[i].High + r[i].Low) / 2 + multiplier * atr[i])
baseDown.push((r[i].High + r[i].Low) / 2 - multiplier * atr[i])
}
// fiUp , fiDown
var fiUp = []
var fiDown = []
var prevFiUp = 0
var prevFiDown = 0
for (var i = 0; i < r.length; i++) {
if (isNaN(baseUp[i])) {
fiUp.push(NaN)
} else {
fiUp.push(baseUp[i] < prevFiUp || r[i - 1].Close > prevFiUp ? baseUp[i] : prevFiUp)
prevFiUp = fiUp[i]
}
if (isNaN(baseDown[i])) {
fiDown.push(NaN)
} else {
fiDown.push(baseDown[i] > prevFiDown || r[i - 1].Close < prevFiDown ? baseDown[i] : prevFiDown)
prevFiDown = fiDown[i]
}
}
var st = []
var prevSt = NaN
for (var i = 0; i < r.length; i++) {
if (i < period) {
st.push(NaN)
continue
}
var nowSt = 0
if (((isNaN(prevSt) && isNaN(fiUp[i - 1])) || prevSt == fiUp[i - 1]) && r[i].Close <= fiUp[i]) {
nowSt = fiUp[i]
} else if (((isNaN(prevSt) && isNaN(fiUp[i - 1])) || prevSt == fiUp[i - 1]) && r[i].Close > fiUp[i]) {
nowSt = fiDown[i]
} else if (((isNaN(prevSt) && isNaN(fiDown[i - 1])) || prevSt == fiDown[i - 1]) && r[i].Close >= fiDown[i]) {
nowSt = fiDown[i]
} else if (((isNaN(prevSt) && isNaN(fiDown[i - 1])) || prevSt == fiDown[i - 1]) && r[i].Close < fiDown[i]) {
nowSt = fiUp[i]
}
st.push(nowSt)
prevSt = st[i]
}
var up = []
var down = []
for (var i = 0; i < r.length; i++) {
if (isNaN(st[i])) {
up.push(st[i])
down.push(st[i])
}
if (r[i].Close < st[i]) {
down.push(st[i])
up.push(NaN)
} else {
down.push(NaN)
up.push(st[i])
}
}
return [up, down]
}
function main() {
SetErrorFilter("login|ready|流控|连接失败|初始|Timeout");
while (true) {
while (!exchange.IO("status")) {
Sleep(3000);
LogStatus("正在等待与交易服务器连接, " + _D());
}
exchange.SetContractType(Symbol)
var r = _C(exchange.GetRecords)
if (r.length < pd) {
Sleep(1000)
continue
}
var st = SuperTrend(r, pd, factor)
$.PlotRecords(r, "K")
$.PlotLine("up", st[0][st[0].length - 2], r[r.length - 2].Time)
$.PlotLine("down", st[1][st[1].length - 2], r[r.length - 2].Time)
if (isNaN(st[0][st[0].length - 3]) && !isNaN(st[0][st[0].length - 2])) {
if (mp == 0) {
_q.pushTask(exchange, Symbol, "buy", 1, function (task, ret) {
mp = 1;
if (ret) {
Log(Symbol, "开多 #ff0000");
}
});
}else if (mp == -1) {
_q.pushTask(exchange, Symbol, "closesell", 1, function (task, ret) {
mp = 0;
if (ret) {
Log(Symbol, "平空 #ff0000");
}
});
}
}
if (isNaN(st[1][st[1].length - 3]) && !isNaN(st[1][st[1].length - 2])) {
if (mp == 0) {
_q.pushTask(exchange, Symbol, "sell", 1, function (task, ret) {
mp = -1;
if (ret) {
Log(Symbol, "开空 #ff0000");
}
});
}else if (mp == 1) {
_q.pushTask(exchange, Symbol, "closebuy", 1, function (task, ret) {
mp = 0;
if (ret) {
Log(Symbol, "平多 #ff0000");
}
});
}
}
_q.poll();
LogStatus('策略正在运行中', _D());
Sleep(1000)
}
}
我们回到代码部分,设置全局变量,虚拟持仓mp为0,_q代表多品种控制对象。然后编写我们的主函数,在主函数中,设置while循环,首先连接交易所,如果不成功,就输出状态"正在等待与交易服务器连接,",并使用_D()打印时间。exchange.SetContractType设置我们的合约,合约的名称是策略参数Symbol。然后获取我们的k线,接着设置如果k线长度不满足要求的话,设置休息1秒钟,然后continue;这样交易逻辑前面的部分我们就设置完成了。
接下来,交易逻辑部分,首先获取超级趋势指标。超级趋势指标在成熟的内置函数中是没有的,这里我们直接复制小小梦大神的计算超级趋势的指标函数。这段指标计算的函数还是比较复杂的,如果大家不理解的话,可以直接复制这段代码到chatgpt中,让它帮你进行梳理和解释。chatgpt作为提高生产力的好伙伴,大家有什么问题都可以向它请教,它的专业性和耐心都是相当可以的。这个超级趋势指标会返回一个数组,分别是up和down值。
接下来,我们进行画图的展示,原生的chart函数是比较复杂的,这里我们直接使用画线类库。使用$.PlotRecords画出k线,为了防止最新的k线没有走完,这里使用$.PlotLine画出倒数第二根k线的up和down值,然后对应的时间戳也是r[r.length - 2].Time。
这里我们直接画图展示一下。模版应用勾选画线类库和商品期货交易类库,时间设置为今年6月到8月,合约名称为最近比较火的纯碱主力SA401合约。周期为15分钟。
我们点击回测,研究一下交易信号的获取和具体交易操作的执行逻辑。在图像中可以看到,上升的趋势是由up线主导的,而下降的趋势是由down线主导的,因此在up线和down线交汇的地方,可以认为是趋势转换的地方,我们可以进行相应的交易操作。
所以我们初步构思:
- (1)由down线转换为up线,意味着从下降的趋势转变为了上升的趋势,这个时候可以进行平空开多的操作。
- (2)由up线转换为down线,上升趋势改为下降趋势,这个时候进行平多开空。
使用程序化的语言怎么表达呢,由down线转换为up线,就是前一时刻的up值为空,这一时刻up值不为空,这里我们使用的是倒数第三根和倒数第二根。使用虚拟持仓,如果当前没有持仓,mp为0,使用pushTask进行下多单的操作,这里填写为buy,这里我们设置回调函数,为了防止重复的开仓,mp设置为1,然后设置开仓成功,打印出开多的信息;如果当前持仓为空头方向,mp为-1,使用pushTask,进行closesell平空的操作,设置mp为0,打印出平空的消息。
同样的思路,由up线转换为down线,上一时刻的down值为空,这一时刻的down值不为空,我们进行相应的开空和平多的操作。
最后,设置程序轮询间隔时间1000毫秒。这样基本的程序编写已经完成。这时刻要测试我们的代码运行效果了,根据回测结果,可以看到设置了交易任务,但是并没有设置具体的操作。具体的原因在哪里呢?回到代码,这里我们使用pushtask函数,忘了加上poll()函数执行具体的交易操作。
注意:这里策略参数是经过调参确定下来的,根据不同的合约,可以使用历史的数据确定策略的最终使用参数。
我们再次点击运行。根据回测结果,可以看到,策略代码根据我们的设想进行了相应的交易操作。根据收益曲线,在行情平稳的时候,策略的收益也是比较平稳,而在8月15号到8月21号行情出现剧烈上涨,策略这一波的上涨趋势抓的还是不错的。因此,这个指标对于趋势的把握还是有一定参考依据的。
在回测结果中应用的结果不错,我们可以应用于模拟实盘看下。设置实盘名称为模拟超级趋势实盘,k线周期为15分钟,运行策略为刚才我们编写的超级趋势策略。托管主机选择服务器,交易平台选择N视界。点击创建实盘。这个实盘就创建成功了。目前这个策略已经在优宽平台公开了,大家可以打开围观板块检查这个策略的效果。
本节课我们从托管者和交易所的配置的开始,交易理念的构思和交易策略的编写,回测和确定,以及最后实盘的创建。希望大家从这个流程中了解到一个完整的实盘创建流程。当然这个策略属于比较简单的策略,后续我们将会学习更多复杂策略的设置。如果大家有哪些编写策略的好的想法和电子,可以留在评论区,我们也将试图将他们编写成为成熟的策略进行量化展示,欢迎大家来投稿。
视频参考链接:
2.Pine脚本移植为多品种JavaScript策略
Trading View的Pine脚本语言相信大家都听说过,它是专门为Trading View平台设计的一种脚本语言,用来开发自定义的技术分析指标和策略。它结合了易读易写的特点和广泛的技术分析工具,使得用户呢,可以快速实现各种个性化的交易观点。TradingView上有很多受欢迎的策略都是由 Pine 脚本语言编写的。但是Pine 脚本确实在某些方面存在一定的局限性,其中之一就是它的单品种限制。目前的版本中,每个Pine脚本只能针对单个品种进行策略编写和回测。如果要在多个品种上使用相同的策略,通常需要为每个品种创建一个独立的实盘运行,确定不太方便。因此,有的同学就想使用pine语言的热门的交易指标同时管理多个品种,可以做到吗?可以,今天我们使用javascript语言来尝试移植一个Pine语言的单品种策略,成为多品种策略尝试一下。
指标讲解
今天我们介绍的热门的pine语言策略,是“Bottom and top hunter”,"顶部和底部猎人"指标。它结合了两个流行的技术分析工具,斐波那契回撤水平和相对强度指数(RSI),用来识别市场的潜在交易机会。
我们简单的介绍一下。斐波那契回撤水平是基于斐波那契数列。在交易中,斐波那契回撤水平根据最近的价格行为,确定潜在的支撑和阻力水平。该指标使用两个斐波那契水平值,通常设置为0.382和0.618。0.618黄金分割数,相信大家都听说过,自然界的很多比率都是黄金分割数。这里我们使用到了价格的涨跌规律中,使用这两个水平值0.382和0.618,用来确定常见的回撤比率。
为了计算斐波那契水平,该指标考虑一个指定范围内的最高和最低价格,通常是最后一段时间周期内的的最高和最低价格。它计算出最高价和最低价之间的间隔。然后,通过从最高价中减去斐波那契比率来确定顶部阻力和底部支撑。
另外一个指标是RSI,他也是我们的老朋友,RSI值基于是收盘价计算出来的,用于衡量价格运动的速度和变化。它有助于识别市场中的超买和超卖状态。该指标中使用的RSI参数是RSI计算的长度,超买状态的上限阈值)和r超卖状态的下限阈值。
我们来看下该指标根据特定条件生成的买入和卖出信号:
-
买入条件:当RSI穿过超卖水平并且收盘价高于底部支撑线。
-
卖出条件:当RSI穿过超买水平并且收盘价低于顶部阻力线撤。
-
买入条件:当RSI穿过超卖水平并且收盘价高于底部支撑线时,触发买入信号。这表明潜在的反转,代表斐波那契支撑水平反弹。
-
卖出条件:当RSI穿过超买水平并且收盘价低于顶部阻力线时,触发卖出信号。这表明潜在的反转,代表斐波那契阻力水平回撤。
作为一个震荡指标,该指标结合了斐波那契回撤水平和RSI的力量,用来识别潜在的交易机会。它帮助我们,找到斐波那契支撑或阻力水平与RSI读数之间的共振,表明潜在的趋势反转或反弹。我们可以利用这些信息来做出关于进出市场位置的明智决策。
Pine语言实现
首先,我们来试着编写一下这个Pine语言策略。不熟悉Pine语言的朋友可以翻看,我们前面的视频,详细讲解了在youquant平台使用Pine语言进行商品期货的量化交易。
打开策略编辑页面,选择这个小松树,就是pine语言。命名为斐波那契教学pine。
pine
/*backtest
start: 2023-06-01 09:00:00
end: 2023-08-29 15:00:00
period: 15m
basePeriod: 5m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["RunMode",1,360008],["ContractType","rb888",360008]]
*/
//@version=5
indicator("Top and bottom Hunter")
// Fibonacci Levels
fib_0 = input.float(0.382, "Fib Level 0")
fib_1 = input.float(0.618, "Fib Level 1")
range = input(4, "range")
// Calculate Fibonacci levels
fib_range = ta.highest(high, range) - ta.lowest(low, range)
fib_level_0 = ta.highest(high, range) - (fib_range * fib_0)
fib_level_1 = ta.highest(high, range) - (fib_range * fib_1)
// RSI
rsi_length = input(14, "RSI Length")
rsi_overbought = input(70, "RSI Overbought Level")
rsi_oversold = input(30, "RSI Oversold Level")
rsi_value = ta.rsi(close, rsi_length)
// Determine buy and sell conditions
buy_condition = ta.crossover(rsi_value, rsi_oversold) and close > fib_level_1
sell_condition = ta.crossunder(rsi_value, rsi_overbought) and close < fib_level_0
// Plot Fibonacci levels
plot(fib_level_0, "fib_level_0", color=color.red, linewidth=2, overlay=true)
plot(fib_level_1, "fib_level_1", color=color.green, linewidth=2, overlay=true)
// Trade
if buy_condition
strategy.entry("Enter Long", strategy.long)
else if sell_condition
strategy.entry("Enter Short", strategy.short)
第一部分,编写斐波那契顶部和底部价格线。首先需要三个策略参数,使用input进行设置,分别是fib_0,初始值为0.382,fib_1,初始值为0.618,计算周期range初始值定义为4。接下来,我们来计算,第一个fib_range,它是range周期内的最高价减去range内的最低价,然后使用周期内最高价减去斐波区间乘以fib_0的值为0.382,确定顶部阻力价格;最高价减去fib_1,0.618确定底部支撑线。
接下来我们来计算rsi,也需要使用input进行三个参数的设置,第一个rsi计算周期,第二个rsi超买阈值,定义为70,第三个rsi超卖阈值,定义为30。计算rsi,使用内置函数ta.rsi,参数填写close 和 rsi_length。
两个指标计算过后,我们来确定交易的信号,第一个buy_condition,如果rsi上穿超卖阈值,使用ta.crossover,并且最新的收盘价大于底部支撑线,我们定义为可以平空开多。第二个sell_condition,刚好相反,如果rsi下穿超买阈值,使用ta.crossunder,并且最新的收盘价小于顶部阻力线,我们定义为可以平多开空。在一定意义上,这是一个震荡策略,而不是突破策略。
我们画图展示一下,使用plot,将指标,名称,颜色,宽度,overlay,是否设置在主图呈现,分别画出顶部线和底部线。
最后,我们进行交易的操作,如果满足buy_condition,使用strategy.entry进行long的操作;满足sell_condition,使用strategy.short进行short的操作。有些同学可能会好奇,不需要进行平多和平空么?在pine语言上,单向持仓,所以程序在开多和开空的时候,会自动的进行相反仓位的平仓操作。
我们回测测试一下,选择日期为最近的两个月,周期为15分钟,选择品种为最近走势比较平稳的螺纹钢。
在日志信息里可以看到,pine语言代码顺利的执行了我们的交易策略。有没有感觉pine语言的语法确定很简洁,这是因为pine作为专门的交易语言,对很多地方都进行了封装。大家感兴趣的话,可以观看我们前面的教程。有些同学好奇,把单品种策略改为多品种策略会不会很麻烦,尤其是刚刚看完我们用三节课讲完的多品种海龟策略以后。不用担心,今天呢,我们使用交易类库的CTA函数,同样实现简洁优雅。
CTA函数移植
我们前面讲过的cta函数,更多的是使用它作为单品种的趋势策略。其实它也可以实现多品种的策略设计。我们来尝试一下。在交易类库的源码中,有一段单品种的cta示例代码。品种这里我们只是使用了一个品种(这里的斜杠是映射,指K线信息看MA000, 下单映射到MA888主力连续上),其实这里可以填写一个合约的列表(使用逗号进行分割),cta函数会使用轮询进行处理。这里的处理逻辑在交易类库源码中,有具体的解释,大家可以翻阅下。将这里的单个合约,改为合约列表以后,会使用轮询的方式,对每个品种进行相应的交易处理。
javascript
/*backtest
start: 2023-06-01 09:00:00
end: 2023-08-31 14:20:00
period: 15m
basePeriod: 5m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["symbol","rb2310,hc2310,i2401"]]
*/
function main() {
$.CTA(symbol, function(st) {
var r = st.records
if (r.length < rsi_length) {
return
}
var fib_range = TA.Highest(r, range, 'High') - TA.Lowest(r, range, 'Low')
var fib_level_0 = TA.Highest(r, range, 'High') - (fib_range * fib_0)
var fib_level_1 = TA.Highest(r, range, 'High') - (fib_range * fib_1)
var rsi_value = TA.RSI(r, rsi_length)
$.PlotMultRecords("期货合约" + st.symbol, "k线", r, {layout: 'single', col: 12, height: '600px'})
$.PlotMultLine("期货合约" + st.symbol, "斐波那契顶部", fib_level_0, r[r.length - 2].Time)
$.PlotMultLine("期货合约" + st.symbol, "斐波那契底部", fib_level_1, r[r.length - 2].Time)
var buy_condition = r[r.length-2].Close > fib_level_1 && rsi_value[rsi_value.length - 3] < rsi_oversold && rsi_value[rsi_value.length - 2] > rsi_oversold;
var sell_condition = r[r.length-2].Close < fib_level_0 && rsi_value[rsi_value.length - 3] > rsi_overbought && rsi_value[rsi_value.length - 2] < rsi_overbought;
if (st.position.amount <= 0 && buy_condition) {
Log("当前持仓", st.position);
return st.position.amount < 0 ? 2 : 1
} else if (st.position.amount >= 0 && sell_condition) {
Log("当前持仓", st.position);
return st.position.amount > 0 ? -2 : -1
}
});
}
我们可以复制这段代码到策略编辑界面进行改写,相对于pine语言将参数设置在代码内部,javascript语言需要设置外部参数,基本和pine语言一样,只不过这里我们设置的symbol是合约列表。回到代码部分,我们删掉策略主体部分,只保留框架,这个函数中,会用到cta中一些简便的用法,对cta不熟悉的同学可以观看前面的视频,这里的合约改为合约列表,symbol。第一步获取k线,使用st.records,st是回调函数,包含轮询合约的k线,仓位,账户等信息,如果不满足长度要求,进行返回。接下来也是首先计算斐波那契顶部和底部阻力支撑线,使用的语法和pine语言几乎一致,只不过函数的大小写有些改变,第一个指标range,周期内最高价减去周期内最低价,然后计算顶部阻力fib_level_0,最高价减去fib_0乘以range,底部支撑线,最高价减去fib_1乘以range。
然后是rsi指标的获取,使用TA.RSI。
画图展示,这次我们不使用原生的chart函数了,这次使用多图表画线类库,定义好图表的名称,"期货合约" + st.symbol,然后使用PlotMultRecords和PlotMultLine往图表上加上"k线", r,"斐波那契顶部", fib_level_0,这里设置时间r[r.length - 2].Time,"斐波那契底部", fib_level_1。因此,我们想每行只展示一幅画,所以layout设置为'single', col宽度为12,也就是全部, height为 '600px'。
接下来,根据两个指标决定买入信号和卖出信号。当判断最新的价格大于底部支撑的时候,并且rsi上穿超买rsi阈值,就是倒数第三根k线rsi小于30,倒数第二根k线大于30,我们定义为buy_condition。类似的,sell_condition,是当最新的价格小于顶部压力线,rsi下穿超买阈值,倒数第三根大于70,倒数第二根小于70。
定义好交易信号以后,我们进行交易的操作。需要注意的是,在cta框架中,下单操作是根据return返回值决定的,这部分的逻辑我们前面课程中着重将结果。如果当前品种的position.Amount仓位数量小于等于0,代表目前没有持仓或者持有空仓,当买入信号出现的时候,使用return进行交易的操作。如果判断当前的仓位如果小于0,证明是持有空仓,想要持有多仓,就要先平掉空仓,再开一手多仓,所以return返回正数 2;而如果当前仓位是0,直接开一手多仓就好,return返回正数1。
卖出信号出现,操作的思路也是一致的,根据当前的仓位如果是多仓,大于0,就要先平多仓,再开空仓,return 2;如果没有持仓,直接开一手空仓。这里的逻辑如果大家不清晰的话,大家可以翻看我们以前的视频。
下面我们进行一下回测,不要忘记勾选模版引用,这里我们使用了多图表画线类库和交易类库,勾选上。
在回测系统里,配置参数设置好时间,k线周期,策略的参数,可以使用默认值,最后symbol填写合约列表,螺纹钢,热卷,和铁矿石,黑色三兄弟,注意这里合约之间不要有空格。在回测结果里,看到cta函数很贴心的为我们进行了持仓状态和资金信息的展示,可以时刻关注策略的运行状态。这里我们看到铁矿石亏损是比较多的,所以我们可以更换一个合约尝试下。在图表显示里,可以看到使用多图表画线类库,也实现了多图表的呈现,这里可以切换下按钮,查看最近8小时的铁矿石走势。
这样我们就实现了将一个单品种pine语言策略,移植成为多品种javascript语言的策略,当然这只是一个简单的策略,所以改编的难度并不高,如果遇到复杂的pine语言策略,我们可以借助chatgpt首先理解策略的原理,然后再进行改写。可以发现,使用cta函数,同样实现了简洁优雅的特点,并且实现了多品种的交易。
可以发现两个语言的语法结构具有相当多的一致性,其实pine语言就是基于JavaScript语法,并添加了一些专门用于金融市场的扩展和函数。所以,如果我们熟悉JavaScript,学习Pine语言将更加容易。当然如果你是个pine语言高手,也可以尝试在youquant平台,使用javascript语言尝试实现更多的pine语言策略。
视频参考链接:
3.伪高频策略初探(一)
高频交易(High-Frequency Trading,HFT)是一种利用计算机算法进行快速、大量的交易的策略。高频策略依赖于快速执行交易订单,通常通过使用低延迟的交易系统和高速网络连接来实现。目标是以极快的速度抓取市场机会,并在瞬息万变的市场环境中进行交易。高频策略分为以下几种类型:
-
套利交易:高频策略的一个主要目标是通过利用市场的微小价格差异来进行套利交易。这些差异可以是不同交易所、期货合约或其他相关资产之间的价格差异。高频交易员会迅速识别出这些差异并执行相应的交易,从而获得利润。
-
做市策略:高频策略的另一种方式是充当市场交易商(Market Maker)。Maker通过同时提供买入和卖出报价,并从买方和卖方之间的价格差(即买卖价差)中获取利润。这种策略通常需要快速的交易决策和高度的流动性。
-
统计套利:高频策略还可以基于统计模型和算法来寻找价格变动中的统计套利机会。这些算法会分析历史数据和市场信息,以发现可能的价格趋势、反转和其他模式,并根据这些模式执行相应的交易。
所以,可以总结呢,高频策略依赖于快速的执行和低延迟的交易系统,对硬件基础设施的要求非常高。对于我们个体散户来说,参与高频量化交易可能相对困难,因为高频交易通常需要专业的硬件系统、大量的市场数据和高速的交易执行能力。但是呢,个体投资者也可以通过获取可靠、实时和低延迟的市场数据,建立起快速且稳定的交易系统,以便在短时间内执行交易,这一点呢,在优宽平台可以满足;同时针对于策略要求,可以选择开发自己的高频交易策略,基于特定的市场洞察、价格模式或其他统计模型。策略开发涉及到编写和测试算法代码,并进行回测和优化来验证其有效性。今天呢,我们就在youquant平台实现一个伪高频的做市策略,来尝试一下高频交易的魅力。
注:为什么叫做伪高频呢?因为真正的高频是毫秒级别的,本课程的高频策略是以百毫秒为时间间隔。
框架介绍
在优宽上回测系统分为「模拟级别回测」、「实盘级别回测」。一般来说对于趋势策略,使用模拟级别回测比较合适,因为数据量小,回测速度也快。而对于伪高频策略,使用实盘级别回测则比较合适,接下来我们就一起来探讨,使用实盘级别回测设计伪高频策略。
我们挑选一种最简单的高频做市策略思路来设计。注意,本节课目的不是设计一个行之有效的策略。针对于个体投资者,有效的高频做市策略确实,难以发掘。本节课呢,我们使用高频做市的策略思路来设计策略,来了解高频策略的设计原理和程序的实现。策略原理是比较简单的。在盘口买单、卖单列表中挂单提供流动性做市,不对价格做任何预测。这样在市场流动性较好的时候,我们快速地获利了解,这样的风险在于市场单边运行时,手上会有亏损的单边头寸。
下面,我们来具体说明一下策略的设计思路。交易时间开启,我们根据当前的价格分别挂一个低于当前买一价偏移点位的多单,和高于当前卖一价偏移单位的空单,等待哪边的挂单先成交,然后取消相反方向的挂单。下单成功以后,这时候我们第二次挂单,多单情况下,以当前卖一价加上盈利点位的价格,进行多单平仓的挂单,或者空单情况下,以当前的买一价减去盈利点位的价格,进行空单平仓的挂单,当挂单成交以后,我们就获利了解。当然,也要考虑止损的情况,如果持仓以后,出现相反的单边行情,当亏损的价格达到亏损点位的时候,我们需要及时的进行止损。这里的挂单偏移点位,盈利点位,和亏损点位的设置都可以作为外部的参数进行调参。一笔交易完成以后,我们就再次开始以当前价格上下点位进行双向挂单的操作,继续循环上面的逻辑。
下面,我们来编写代码来尝试实现这样一个maker高频策略。首先设置策略参数,interval间隔,symbol合约,priceTick价格一跳点数,deviation跳动区间,maxCoverDeviation最大止损间隔,profit盈利区间。
我们可以先设想一下,如果想实现刚才讲述的交易逻辑。除了主函数外,需要几个辅助的功能函数。首先判断当前有没有挂单,没有挂单并且没有持仓才能进行挂单的操作。所以第一个我们需要一个查找目标品种order的函数,getOrderBySymbol,然后需要一个判断order类型的函数hasOrder,当成功判断当前order后,我们需要根据条件进行交易的操作,所以需要设置交易的函数,由于具有四个方向(开多,开空,平多和平空)所以也可以封装起来,作为第三个函数trade,当然我们还需要一个可以删除挂单的函数cancelOrders。这样伴随策略的主逻辑,我们的高频策略就可以进行了。这些功能函数的编写,我们将伴随主函数策略的运行进行完善。
代码解释
javascript
/*backtest
start: 2023-08-02 09:00:00
end: 2023-08-02 15:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
mode: 1
*/
//判断是否具有挂单
function getOrderBySymbol(symbol, orders) {
var ret = [];
_.each(orders, function(order) {
if (order.ContractType == symbol) {
ret.push(order);
}
});
return ret;
}
//获取挂单类型
function hasOrder(orders, type, offset) {
var ret = false;
_.each(orders, function(order) {
if (order.Offset == offset && order.Type == type) {
ret = order;
}
});
return ret;
}
//交易的函数
function trade(distance, price, amount) {
var tradeFunc = null;
if (distance == "buy") {
tradeFunc = exchange.Buy;
} else if (distance == "sell") {
tradeFunc = exchange.Sell;
} else if (distance == "closebuy" || distance == "closebuy_today") {
tradeFunc = exchange.Sell;
} else if (distance == "closesell" || distance == "closesell_today") {
tradeFunc = exchange.Buy;
}
exchange.SetDirection(distance);
return tradeFunc(price, amount);
}
//删除挂单
function cancelOrders(symbol, offset) {
var orders = null;
while (1) {
orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
for (var i = 0; i < orders.length; i++) {
if ((orders[i].ContractType == symbol && orders[i].Offset == offset) || typeof(offset) == "undefined") {
exchange.CancelOrder(orders[i].Id, orders[i]);
Sleep(interval);
}
}
Sleep(interval);
}
return orders;
}
var p = $.NewPositionManager(); //交易类库函数
var profitprice = null; //止盈挂单价格
var lossprice = null; //止损挂单价格
var tblStatus = {
type: "table",
title: "策略运行状态信息",
cols: ["合约名称", "当前价格", "多头挂单", "空头挂单", "止盈价格","止损价格", "持仓方向", "持仓价格", "持仓数量", "持仓盈亏", "止损次数", "成功次数"],
rows: []
};
//主函数
function main() {
while (true) {
if (exchange.IO("status")) {
exchange.SetContractType(symbol);
var t = exchange.GetTicker();
var r = exchange.GetRecords();
var positions = _C(exchange.GetPosition);
var pos = [p.GetPosition(symbol, PD_LONG, positions), p.GetPosition(symbol, PD_SHORT, positions)];
var orders = getOrderBySymbol(symbol, _C(exchange.GetOrders));
if (orders.length == 0 && (!pos[0] && !pos[1])) {
trade("buy", t.Buy - priceTick * deviation, 1);
trade("sell", t.Sell + priceTick * deviation, 1);
}else if (pos[0] || pos[1]) {
if ((pos[1] && hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_OPEN)) || (pos[0] && hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_OPEN))) {
cancelOrders(symbol, ORDER_OFFSET_OPEN);
Log('删除挂单:', pos[0] ? '空单' : '多单');
}
var longCoverOrder = hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_CLOSE);
var shortCoverOrder = hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_CLOSE);
if (pos[0] && !longCoverOrder ) {
profitprice = t.Sell + priceTick * profit
lossprice = t.Sell - priceTick * maxCoverDeviation
trade("closebuy", profitprice, pos[0].Amount);
}
if (pos[1] && !shortCoverOrder) {
profitprice = t.Buy - priceTick * profit
lossprice = t.Buy + priceTick * maxCoverDeviation
trade("closesell", profitprice, pos[1].Amount);
}
if (pos[0] && longCoverOrder) {
Log('多头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
Log('空头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
}
} else {
LogStatus("未连接", _D());
}
Sleep(interval);
}
}
回到代码部分,首先设置全局变量,p是交易类库函数,方便我们进行查询持仓和止损的交易操作,profitprice,和lossprice,代表下单过后设定的止盈价格和止损价格。
进入主函数, while (true) ,使用判断连接交易所的状态下,设置合约,由于是高频策略,需要我们获取ticker数据,和k线数据。起始阶段的挂单需要在没有持仓和挂单情况下,持仓我们可以通过GetPosition获取,具体的两个类型的仓位可以通过Position结构中的Type属性是PD_LONG还是PD_SHORT获取。然后我们需要获取对应品种的挂单信息,这里我们需要设置第一个函数getOrderBySymbol,这里的参数是合约,和orders列表,可以使用exchange.GetOrders获取到,首先设置ret作为返回结果,因为当前挂单的品种可能有很多,所以我们需要在所有order信息中获取到目标品种的挂单信息,使用_.each轮询,当查找到对应品种时,添加到ret,最后进行返回。这样就设置好了第一个模块函数getOrderBySymbol。
获取到两个信息,我们开始交易逻辑的进行,如果当前没有挂单orders.length == 0,没有持仓,pos[0]和pos[1]都为空,会基于盘口挂单。这里我们使用trade功能函数进行挂单,首先我们来完善下这个函数,trade函数需要三个参数,设置参数为distance下单方向, price下单价格, amount下单价格,在函数体内,设置返回结果tradeFunc。接着根据distance,设置对应下单的函数。最后再根据distance设置exchange.SetDirection,在返回tradeFunc结果,里面填入价格和数量,这样我们的下单函数就封装好了。
回到主函数,继续刚才判断为没有持仓,没有挂单,使用trade进行下单的操作,分别设置为buy和sell。对于开多单,价格为当前的买价减去跳动区间;空单是卖价加上跳动区间。这样我们的开仓挂单就设置好了。
等待一个方向下单成功以后,就是pos[0] 或者pos[1]其中一个为真,这个时候就要取消另一个方向的挂单。这个时候需要先判断持仓和对应方向挂单的存在,使用hasOrder函数检查对应方向挂单。在hasOrder函数里,参数为orders挂单列表,type是类型,offset是开平仓方向。遍历挂单列表,找到对应方向挂单的话,进行返回。回到我们的主逻辑,检查持有空仓和多单挂单或者持有多仓和空单列表,我们就要取消对应方向的挂单,使用最后的一个功能函数。cancelOrders,参数为symbol合约和offset,开平仓方向,设置函数的主体部分,通过检查目前的挂单列表,如果长度为0,直接返回;否则进入轮询,找到对应合约的挂单,使用exchange.CancelOrder进行取消。
回到主函数,参数填写合约,和开仓订单的类型ORDER_OFFSET_OPEN,就可以取消对应方向的开仓挂单列表。然后我们打印出删除对应方向挂的信息。
接下来就要进入我们的平仓逻辑的设置了。在持有仓位以后,我们就要挂一个止盈的单子进行平仓。所以在检查到持有仓位并且没有挂止盈单的情况下,进行挂单。这个时候首先需要检查是否已经挂单,使用刚才讲过的hasOrder函数,
分别检查多单平仓挂单longCoverOrder,和空单平仓挂单shortCoverOrder的存在。
接下来就进入止盈挂单的设置,如果检查到有多头仓位并且没有挂单,首先设置止盈价格,为当前的卖价加上盈利的点数,止损价格为当前卖价减去止损区间的点数,然后使用trade函数设置参数closebuy, 价格止盈价格
,数量是多头仓位的数量。
对于空头仓位处理的逻辑也是一致的,检测到没有挂单,计算相应的止盈价格和止损价格,然后使用trade进行挂单。
止盈的挂单并不是一定能成交的,当遇到单边损失的行情,我们就要及时的进行止损。同样分为多头止损和空头止损两种情况。
当判断条件拥有多头仓位,和止盈挂单,但是最新的价格小于止损点数,就要进行多头止损的操作,打印出需要进行止损,然后cancel止盈的挂单,使用cover交易类库函数进行迅速的平仓。
空头仓位的逻辑也是一样的,判断是否达到止损的位置,如果是删除挂单,使用cover进行平仓。
最后我们设置好未连接状态的信息显示,和策略的休息间隔。到这里就是我们做市进行伪高频交易的逻辑了。我们可以先运行一下。
我们回测运行一下,设置时间是8月1日,上午9点到下午3点,k线周期为1分钟,这里选择实盘级tick。策略参数合约选择rb2310,其它参数设置为初始值就好。
从回测结果数据中可以看到平仓盈亏是正数,但是亏损都在手续费上,这确实符合高频交易的特点,频繁地交易确实会花费更多的手续费。在日志信息里,可以看到准时9点开盘,我们进行双向的挂单,当其中一个方向挂单成交以后,撤销另一个方向的挂单,然后挂止盈的单子,当止盈单子成交以后。就要重新挂单,当然并不全是止盈的交易,也会遇到止损的情况,当最新的价格达到止损点数的时候,会撤掉止盈的单子,进行止损平仓的操作。
附加plus版本:
javascript
/*backtest
start: 2023-08-02 09:00:00
end: 2023-08-02 15:00:17
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
mode: 1
*/
function cancelOrders(symbol, offset) {
var orders = null;
while (1) {
orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
for (var i = 0; i < orders.length; i++) {
if ((orders[i].ContractType == symbol && orders[i].Offset == offset) || typeof(offset) == "undefined") {
exchange.CancelOrder(orders[i].Id, orders[i]);
Sleep(interval);
}
}
Sleep(interval);
}
return orders;
}
function trade(distance, price, amount) {
var tradeFunc = null;
if (distance == "buy") {
tradeFunc = exchange.Buy;
} else if (distance == "sell") {
tradeFunc = exchange.Sell;
} else if (distance == "closebuy" || distance == "closebuy_today") {
tradeFunc = exchange.Sell;
} else if (distance == "closesell" || distance == "closesell_today") {
tradeFunc = exchange.Buy;
}
exchange.SetDirection(distance);
return tradeFunc(price, amount);
}
function getOrderBySymbol(symbol, orders) {
var ret = [];
_.each(orders, function(order) {
if (order.ContractType == symbol) {
ret.push(order);
}
});
return ret;
}
function hasOrder(orders, type, offset) {
var ret = false;
_.each(orders, function(order) {
if (order.Offset == offset && order.Type == type) {
ret = order;
}
});
return ret;
}
var p = $.NewPositionManager(); //交易类库函数
var success_count = 0; //成功次数统计
var fail_count = 0; //止损次数统计
var orderlong = null; //多头挂单价格
var ordershort = null; //空头挂单价格
var profitprice = null; //止盈挂单价格
var lossprice = null; //止损挂单价格
var tblStatus = {
type: "table",
title: "策略运行状态信息",
cols: ["合约名称", "当前价格", "多头挂单", "空头挂单", "止盈价格","止损价格", "持仓方向", "持仓价格", "持仓数量", "持仓盈亏", "止损次数", "成功次数"],
rows: []
};
function main() {
var initAccount = _C(exchange.GetAccount);
var preprofit = 0 //先前权益
var curprofit = 0 //当前权益
var holdPrice = null //持仓价格
var holdType = null //持仓类型
var holdAmount = null //持仓数量
var holdProfit = null //持仓盈亏
var isLock = false //止盈挂单锁
while (true) {
if (exchange.IO("status")) {
exchange.SetContractType(symbol);
var t = exchange.GetTicker();
var r = exchange.GetRecords();
var ContractType = symbol
var curprice = r[r.length-1].Close
var positions = _C(exchange.GetPosition);
var pos = [p.GetPosition(symbol, PD_LONG, positions), p.GetPosition(symbol, PD_SHORT, positions)];
var orders = getOrderBySymbol(symbol, _C(exchange.GetOrders));
if (orders.length == 0 && (!pos[0] && !pos[1])) {
profitprice = null
lossprice = null
isLock = false //解锁
holdPrice = ''
holdType = ''
holdAmount = ''
holdProfit = ''
preprofit = curprofit
curprofit = exchange.GetAccount().Balance - initAccount.Balance
LogProfit(curprofit, "权益", '&');
if(preprofit < curprofit){
success_count += 1
$.PlotFlag(r[r.length-2].Time, '止盈离场', '止盈离场')
}
if(preprofit > curprofit){
fail_count += 1
$.PlotFlag(r[r.length-2].Time, '止损离场', '止损离场')
}
orderlong = t.Buy - priceTick * deviation
ordershort = t.Sell + priceTick * deviation
// 当前没有挂单,没有持仓,基于盘口挂单
trade("buy", orderlong, 1);
trade("sell", ordershort, 1);
} else if (pos[0] || pos[1]) {
// 只要有持仓
orderlong = null
ordershort = null
var cur_pos = pos[0] ? pos[0] : pos[1]
holdPrice = cur_pos.Price
holdType = pos[0] ? '多头方向' : '空头方向'
holdAmount = cur_pos.Amount
holdProfit = cur_pos.Profit
if ((pos[1] && hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_OPEN)) || (pos[0] && hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_OPEN))) {
$.PlotFlag(r[r.length-2].Time, pos[0] ? '多单进场' : '空单进场', pos[0] ? '多单进场' : '空单进场')
cancelOrders(symbol, ORDER_OFFSET_OPEN);
Log('删除挂单:', pos[0] ? '空单' : '多单');
}
var longCoverOrder = hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_CLOSE);
var shortCoverOrder = hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_CLOSE);
if (pos[0] && !longCoverOrder && isLock == false) {
profitprice = t.Sell + priceTick * profit
lossprice = t.Sell - priceTick * maxCoverDeviation
trade("closebuy", profitprice, pos[0].Amount);
isLock = true //加锁
}
if (pos[1] && !shortCoverOrder && isLock == false) {
// 有空头持仓
profitprice = t.Buy - priceTick * profit
lossprice = t.Buy + priceTick * maxCoverDeviation
trade("closesell", profitprice, pos[1].Amount);
isLock = true //加锁
}
// 重新平仓条件
if (pos[0] && longCoverOrder && t.Sell < lossprice) {
Log('多头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
Log('空头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
}
$.PlotRecords(r, "K线数据")
$.PlotLine("多头挂单", orderlong, r[r.length - 1].Time)
$.PlotLine("空头挂单", ordershort, r[r.length - 1].Time)
$.PlotLine("挂单盈利线", profitprice, r[r.length - 1].Time)
$.PlotLine("挂单止损线", lossprice, r[r.length - 1].Time)
tblStatus.rows = [];
tblStatus.rows.push([ContractType, curprice, orderlong, ordershort, profitprice, lossprice, holdType, holdPrice, holdAmount, holdProfit, fail_count, success_count]);
lastStatus = '`' + JSON.stringify([tblStatus]) + '`';
LogStatus(lastStatus);
} else {
LogStatus("未连接", _D());
}
Sleep(interval);
}
}
视频参考链接:
《设计伪高频策略以及使用实盘级别回测来研究伪高频策略》
《伪高频策略实现(PLUS)》
4.伪高频策略初探(二)
本节课我们继续针对于个体交易者伪高频策略的研究,首先我们先讲解在高频策略实操过程中,对于遇到的问题进行解决。
上节课,有些同学利用我们的代码进行尝试的时候,会出现“ERR_INVALID_POSITION”的错误,我们可以试着来解决一下,可以看到这个错误,只会在进行止盈平仓的时候出现,所以我们在止盈挂单的时候,打印当前的两个条件,exchange.GetPosition()当前的仓位和挂单。我们再次运行一下,
可以看到第一次挂单止盈的时候,仓位是存在的,而当错误出现的时候,这里会显示持有的仓位是0。
既然能进入这个条件,证明在if条件判断当时的仓位是存在的,但是执行到函数里,完成了交易的操作,重新设置平仓会产生错误。这个问题怎样解决呢,在止盈挂单期间和止盈完成期间,我们不希望进行额外的止盈挂单操作。其实我们可以一个类似锁的设置,在止盈挂单后将锁锁上,止盈平仓以后设置将锁打开,这样就可以避免再次进行止盈挂单。
javascript
/*backtest
start: 2023-08-02 09:00:00
end: 2023-08-02 15:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
mode: 1
*/
//判断是否具有挂单
function getOrderBySymbol(symbol, orders) {
var ret = [];
_.each(orders, function(order) {
if (order.ContractType == symbol) {
ret.push(order);
}
});
return ret;
}
//获取挂单类型
function hasOrder(orders, type, offset) {
var ret = false;
_.each(orders, function(order) {
if (order.Offset == offset && order.Type == type) {
ret = order;
}
});
return ret;
}
//交易的函数
function trade(distance, price, amount) {
var tradeFunc = null;
if (distance == "buy") {
tradeFunc = exchange.Buy;
} else if (distance == "sell") {
tradeFunc = exchange.Sell;
} else if (distance == "closebuy" || distance == "closebuy_today") {
tradeFunc = exchange.Sell;
} else if (distance == "closesell" || distance == "closesell_today") {
tradeFunc = exchange.Buy;
}
exchange.SetDirection(distance);
return tradeFunc(price, amount);
}
//删除挂单
function cancelOrders(symbol, offset) {
var orders = null;
while (1) {
orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
for (var i = 0; i < orders.length; i++) {
if ((orders[i].ContractType == symbol && orders[i].Offset == offset) || typeof(offset) == "undefined") {
exchange.CancelOrder(orders[i].Id, orders[i]);
Sleep(interval);
}
}
Sleep(interval);
}
return orders;
}
var p = $.NewPositionManager(); //交易类库函数
var profitprice = null; //止盈挂单价格
var lossprice = null; //止损挂单价格
var tblStatus = {
type: "table",
title: "策略运行状态信息",
cols: ["合约名称", "当前价格", "多头挂单", "空头挂单", "止盈价格","止损价格", "持仓方向", "持仓价格", "持仓数量", "持仓盈亏", "止损次数", "成功次数"],
rows: []
};
//主函数
function main() {
var isLock = false //止盈挂单锁
while (true) {
if (exchange.IO("status")) {
exchange.SetContractType(symbol);
var t = exchange.GetTicker();
var r = exchange.GetRecords();
var positions = _C(exchange.GetPosition);
var pos = [p.GetPosition(symbol, PD_LONG, positions), p.GetPosition(symbol, PD_SHORT, positions)];
var orders = getOrderBySymbol(symbol, _C(exchange.GetOrders));
if (orders.length == 0 && (!pos[0] && !pos[1])) {
isLock = false //解锁
trade("buy", t.Buy - priceTick * deviation, 1);
trade("sell", t.Sell + priceTick * deviation, 1);
}else if (pos[0] || pos[1]) {
if ((pos[1] && hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_OPEN)) || (pos[0] && hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_OPEN))) {
cancelOrders(symbol, ORDER_OFFSET_OPEN);
Log('删除挂单:', pos[0] ? '空单' : '多单');
}
var longCoverOrder = hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_CLOSE);
var shortCoverOrder = hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_CLOSE);
if (pos[0] && !longCoverOrder && isLock == false) {
profitprice = t.Sell + priceTick * profit
lossprice = t.Sell - priceTick * maxCoverDeviation
trade("closebuy", profitprice, pos[0].Amount);
Log("止盈单挂单结束", "closebuy", exchange.GetPosition())
isLock = true //加锁
}
if (pos[1] && !shortCoverOrder && isLock == false) {
profitprice = t.Buy - priceTick * profit
lossprice = t.Buy + priceTick * maxCoverDeviation
trade("closesell", profitprice, pos[1].Amount);
Log("止盈单挂单结束", "closesell", exchange.GetPosition())
isLock = true //加锁
}
if (pos[0] && longCoverOrder && t.Sell < lossprice) {
Log('多头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
Log('空头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
}
} else {
LogStatus("未连接", _D());
}
Sleep(interval);
}
}
所以在主函数中,设置islock,初始值为false,在进行止盈挂单的时候,这里增加一个条件islock为false,进行止盈挂单,然后设置islock为true进行加锁。什么时候解锁呢,在该笔交易完成以后,就是完成平仓,进行islock的解锁。这点确实让人比较困惑,但是在程序化交易中,我们需要考虑到这些问题的存在,然后试着去解决它,这样我们才能真正的获得成长。
盘口偏移策略
下面我们继续来高频策略的优化的尝试。上节课,我们了解了根据盘口价格做市maker策略。整体来看,这个策略确实比较粗糙,根据盘口的价格进行上下等间距的挂单,然后依据条件进行止盈和止损的操作。止盈的利润点数是固定的,而当止损的点数大于止盈的时候,亏损就产生了。因此,即使等概率成功和失败的次数,加上手续费的消耗,这个策略是亏损的。因此,我们策略优化的重点在哪里呢?我们要做的是,移动这个盘口的天平,朝着盈利的方向偏斜一点点,在大样本高频交易的基础上,努力实现一个正的期望(正收益)。
本节课我们就基于上节课讲述的策略框架,提出一些优化应用的实际例子,当然也是属于抛砖引玉,更多的是为大家提供优化的思路。让我们现在开始。
今天我们提出的第一个优化思路是利用盘口深度数据,进行开仓挂单价格的偏移。基于盘口深度数据进行挂单偏移,就是意味着使用当前市场价格为基准,通过观察挂单价格和数量的变化,来尝试判断市场走势,在开仓入市的时候调整挂单价格和数量,用来增加成交的概率,并获取更多的利润。
具体而言,可以对市场上的最优买卖挂单进行监控,买单和卖单会包含不同的数量,如果哪一方的数量较多,证明市场可能会朝着这个方向进行倾斜,因此我们对应这个方向的挂单价格可以设置的离盘口价格更近一点,而与此对应的,另一个方向的挂单设置的离盘口价格远一点,这样呢,可以增加有利方向的成交概率和获利的机会,从而在大样本下努力的提高正收益的期望。

我们来使用代码演示下,基本的框架是不需要改变的。在策略编写页面,我们可以选择复制策略,这样就可以复制一份原有的策略,包括具体的参数设置和策略的源码。
javascript
/*backtest
start: 2023-08-02 09:00:00
end: 2023-08-02 15:00:17
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
mode: 1
*/
function cancelOrders(symbol, offset) {
var orders = null;
while (1) {
orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
for (var i = 0; i < orders.length; i++) {
if ((orders[i].ContractType == symbol && orders[i].Offset == offset) || typeof(offset) == "undefined") {
exchange.CancelOrder(orders[i].Id, orders[i]);
Sleep(interval);
}
}
Sleep(interval);
}
return orders;
}
function trade(distance, price, amount) {
var tradeFunc = null;
if (distance == "buy") {
tradeFunc = exchange.Buy;
} else if (distance == "sell") {
tradeFunc = exchange.Sell;
} else if (distance == "closebuy" || distance == "closebuy_today") {
tradeFunc = exchange.Sell;
} else if (distance == "closesell" || distance == "closesell_today") {
tradeFunc = exchange.Buy;
}
exchange.SetDirection(distance);
return tradeFunc(price, amount);
}
function getOrderBySymbol(symbol, orders) {
var ret = [];
_.each(orders, function(order) {
if (order.ContractType == symbol) {
ret.push(order);
}
});
return ret;
}
function hasOrder(orders, type, offset) {
var ret = false;
_.each(orders, function(order) {
if (order.Offset == offset && order.Type == type) {
ret = order;
}
});
return ret;
}
var p = $.NewPositionManager(); //交易类库函数
var success_count = 0; //成功次数统计
var fail_count = 0; //止损次数统计
var orderlong = null; //多头挂单价格
var ordershort = null; //空头挂单价格
var profitprice = null; //止盈挂单价格
var lossprice = null; //止损挂单价格
var tblStatus = {
type: "table",
title: "策略运行状态信息",
cols: ["合约名称", "当前价格", "多头挂单", "空头挂单", "止盈价格","止损价格", "持仓方向", "持仓价格", "持仓数量", "持仓盈亏", "止损次数", "成功次数"],
rows: []
};
function main() {
var initAccount = _C(exchange.GetAccount);
var preprofit = 0 //先前权益
var curprofit = 0 //当前权益
var holdPrice = null //持仓价格
var holdType = null //持仓类型
var holdAmount = null //持仓数量
var holdProfit = null //持仓盈亏
var isLock = false //止盈挂单锁
while (true) {
if (exchange.IO("status")) {
exchange.SetContractType(symbol);
var t = exchange.GetTicker();
var r = exchange.GetRecords();
var ContractType = symbol
var curprice = r[r.length-1].Close
var positions = _C(exchange.GetPosition);
var pos = [p.GetPosition(symbol, PD_LONG, positions), p.GetPosition(symbol, PD_SHORT, positions)];
var orders = getOrderBySymbol(symbol, _C(exchange.GetOrders));
// 获取深度数据
var dep = exchange.GetDepth()
// 计算买方数量和卖方数量
if (dep.Asks.length >= 5) {
var AskAmount = dep.Asks.reduce(function(sum, ask) {
return sum + ask.Amount;
}, 0);
var BidAmount = dep.Bids.reduce(function(sum, bid) {
return sum + bid.Amount;
}, 0);
}
if (orders.length == 0 && (!pos[0] && !pos[1])) {
profitprice = null
lossprice = null
isLock = false //解锁
holdPrice = ''
holdType = ''
holdAmount = ''
holdProfit = ''
preprofit = curprofit
curprofit = exchange.GetAccount().Balance - initAccount.Balance
LogProfit(curprofit, "权益", '&');
if(preprofit < curprofit){
success_count += 1
$.PlotFlag(r[r.length-2].Time, '止盈离场', '止盈离场')
}
if(preprofit > curprofit){
fail_count += 1
$.PlotFlag(r[r.length-2].Time, '止损离场', '止损离场')
}
// 按照买方/卖方数量确定盘口偏移
var longDev = Math.round(priceTick * deviation * AskAmount / BidAmount)
var shortDev = Math.round(priceTick * deviation * BidAmount / AskAmount)
orderlong = t.Buy - longDev
ordershort = t.Sell + shortDev
Log('买方偏移:', longDev)
Log('卖方偏移:', shortDev)
// 当前没有挂单,没有持仓,基于盘口挂单
trade("buy", orderlong, 1);
trade("sell", ordershort, 1);
} else if (pos[0] || pos[1]) {
// 只要有持仓
orderlong = null
ordershort = null
var cur_pos = pos[0] ? pos[0] : pos[1]
holdPrice = cur_pos.Price
holdType = pos[0] ? '多头方向' : '空头方向'
holdAmount = cur_pos.Amount
holdProfit = cur_pos.Profit
if ((pos[1] && hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_OPEN)) || (pos[0] && hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_OPEN))) {
$.PlotFlag(r[r.length-2].Time, pos[0] ? '多单进场' : '空单进场', pos[0] ? '多单进场' : '空单进场')
cancelOrders(symbol, ORDER_OFFSET_OPEN);
Log('删除挂单:', pos[0] ? '空单' : '多单');
}
var longCoverOrder = hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_CLOSE);
var shortCoverOrder = hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_CLOSE);
if (pos[0] && !longCoverOrder && isLock == false) {
profitprice = t.Sell + priceTick * profit
lossprice = t.Sell - priceTick * maxCoverDeviation
trade("closebuy", profitprice, pos[0].Amount);
isLock = true //加锁
}
if (pos[1] && !shortCoverOrder && isLock == false) {
// 有空头持仓
profitprice = t.Buy - priceTick * profit
lossprice = t.Buy + priceTick * maxCoverDeviation
trade("closesell", profitprice, pos[1].Amount);
isLock = true //加锁
}
// 重新平仓条件
if (pos[0] && longCoverOrder && t.Sell < lossprice) {
Log('多头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
Log('空头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
}
$.PlotRecords(r, "K线数据")
$.PlotLine("多头挂单", orderlong, r[r.length - 1].Time)
$.PlotLine("空头挂单", ordershort, r[r.length - 1].Time)
$.PlotLine("挂单盈利线", profitprice, r[r.length - 1].Time)
$.PlotLine("挂单止损线", lossprice, r[r.length - 1].Time)
tblStatus.rows = [];
tblStatus.rows.push([ContractType, curprice, orderlong, ordershort, profitprice, lossprice, holdType, holdPrice, holdAmount, holdProfit, fail_count, success_count]);
lastStatus = '`' + JSON.stringify([tblStatus]) + '`';
LogStatus(lastStatus);
} else {
LogStatus("未连接", _D());
}
Sleep(interval);
}
}
策略的整体交易逻辑是不变的,这里我们需要的仅仅是利用盘口的深度数据,进行盘口价格的偏移设置。首先,我们挑选合适的品种,上交所的品种有五档的深度数据,可以帮助我们更好的了解市场的趋势。所以在策略参数里,我们设置的合约是螺纹钢2310合约。
接着在代码中,我们首先使用GetDepth获取深度数据。在确保获取的深度数据是五档的时候,分别计算卖单数量AskAmount,和买单数量BidAmount。接下来,我们就要设置价格偏移了,
因为我们希望哪一方的数量越多,该方向的偏移就越小,所以定义longDev买单的偏移为,Math.round(priceTick * deviation * AskAmount / BidAmount),使用卖方的深度数量除以买方的深度数量乘以设置的偏移区间;对应的卖单偏移是BidAmount / AskAmount。
接下来,就是用计算出来的偏移值代替原始的偏移值,并打印出来。这样基本的设置就完成了,我们运行一下看看回测结果。
设定日期为8月1号上午9点到下午三点,日志信息里显示首先计算挂单的偏移,然后进行挂单,根据回测的结果,这一段时间,止盈了15次,止损了5次。但是因为止损的损失比止盈多,所以加上手续费,最后的收益为负的。其实这也是正常的,因为对于盘口的多单和空单的数量是实时变化的,我们挂单时候的多空数量比,和下单成功时候的多空数量比,可能发生了显著的变化,所以这个策略还有优化的地方。
Penny Jump策略
接下来,我们提出第二种优化的思路。我们在上面的高频策略中,挂单价格的偏移是人为设置的。但是,还有一种情况,就是盘口的买一价和卖一价本身就有一定距离的偏移,我们可以使用这个偏移进行挂单。正常情况下商品期货主力合约交易比较频繁,盘口买一与卖一只有一跳的价差,几乎没有下手机会。所以我们把精力放到交易不是太活跃的次主力合约上面,次主力合约偶尔会有两跳甚至三跳的机会出现。比如在MA次主力909合约的盘口就出现下面这种情况:卖一为2225量551,买一2223量565,在几个tick推送后,这个价差消失,变成2225,2224,这种情况,我们视为市场的自我纠正。我们要做的就是去填补盘口差价空隙,如果速度够快,就可以排在委托单的最前位置,做为Maker以最快的速度成交后反手卖出,这个持仓时间很短。这种逻辑使用人工盯盘确实比较困难,因为商品期货盘口差价较大的情况很少出现,当出现的时候,市场也很快会去弥补。

这种策略有一个很可爱的名字,Penny Jump 策略,翻译成中文就是微量加价的意思,其原理是跟踪盘口买价和卖价,然后不停的根据盘口价格加上或减去微量价格挂单,很明显这是一个被动成交的挂单策略,属于卖方做市策略的一种。
了解完这个策略逻辑以后,我们来使用代码进行实现。同样可以使用我们原有的代码框架。在这里我们需要修改下参数的名称,原有的跳动区间改为盘口间隔。
这里需要获取实时的买价和卖价,然后计算两者的差额。当差额大于3的时候,我们立刻挂单,以当时买一价加一进行多头的挂单,以当时卖一价减一进行空头的挂单,这里就相当于填补盘口的差价。相对于前两种策略,这种下单的方式速度会更快一些。
如果成功下单,这时候就要设置对应的止盈价格和止损价格,对于多单来说,止盈价格是入场的卖价curask加上盈利的点数,止损点数是curask减去止损的点数;空单的止盈价格是入场的买价curbid减去盈利的点数,止损价格是curbid加上盈利的点数。
我们来试着运行一下,在回测页面,我们挑选非主力合约,玻璃403,盘口间隔设置为2,最大止损间隔为5,盈利区间可以设置为1。根据回测结果,可以发现策略似乎运行到一半停止了。这是什么原因呢,我们检查一下,我们打印下持有的仓位,结果发现,这里同时拥有了两个仓位,证明我们的开仓挂单同时交易成功,因此策略的逻辑被打乱了;这是因为Penny Jump策略下单的价格确实很接近,而前两种策略下单的距离我们设置为20,所以没有发生这种情况?具体应该怎样解决呢,其实也很简单,当检查到同时拥有仓位的时候,我们可以简单粗暴的同时平仓,当然也更好的办法也欢迎大家提出来。
javascript
/*backtest
start: 2023-08-01 09:00:00
end: 2023-08-02 15:00:11
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
mode: 1
args: [["symbol","FG403"],["deviation",2],["maxCoverDeviation",4]]
*/
function cancelOrders(symbol, offset) {
var orders = null;
while (1) {
orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
for (var i = 0; i < orders.length; i++) {
if ((orders[i].ContractType == symbol && orders[i].Offset == offset) || typeof(offset) == "undefined") {
exchange.CancelOrder(orders[i].Id, orders[i]);
Sleep(interval);
}
}
Sleep(interval);
}
return orders;
}
function trade(distance, price, amount) {
var tradeFunc = null;
if (distance == "buy") {
tradeFunc = exchange.Buy;
} else if (distance == "sell") {
tradeFunc = exchange.Sell;
} else if (distance == "closebuy" || distance == "closebuy_today") {
tradeFunc = exchange.Sell;
} else if (distance == "closesell" || distance == "closesell_today") {
tradeFunc = exchange.Buy;
}
exchange.SetDirection(distance);
return tradeFunc(price, amount);
}
function getOrderBySymbol(symbol, orders) {
var ret = [];
_.each(orders, function(order) {
if (order.ContractType == symbol) {
ret.push(order);
}
});
return ret;
}
function hasOrder(orders, type, offset) {
var ret = false;
_.each(orders, function(order) {
if (order.Offset == offset && order.Type == type) {
ret = order;
}
});
return ret;
}
var p = $.NewPositionManager(); //交易类库函数
var success_count = 0; //成功次数统计
var fail_count = 0; //止损次数统计
var orderlong = null; //多头挂单价格
var ordershort = null; //空头挂单价格
var profitprice = null; //止盈挂单价格
var lossprice = null; //止损挂单价格
var tblStatus = {
type: "table",
title: "策略运行状态信息",
cols: ["合约名称", "当前价格", "多头挂单", "空头挂单", "止盈价格","止损价格", "持仓方向", "持仓价格", "持仓数量", "持仓盈亏", "止损次数", "成功次数", '时间'],
rows: []
};
var curbid = null //开仓挂单买价
var curask = null //开仓挂单卖价
function main() {
var initAccount = _C(exchange.GetAccount);
var preprofit = 0 //先前权益
var curprofit = 0 //当前权益
var holdPrice = null //持仓价格
var holdType = null //持仓类型
var holdAmount = null //持仓数量
var holdProfit = null //持仓盈亏
var isLock = false //止盈挂单锁
var cur_pos = null
while (true) {
if (exchange.IO("status")) {
exchange.SetContractType(symbol);
var t = exchange.GetTicker();
var r = exchange.GetRecords();
var ContractType = symbol
var curprice = r[r.length-1].Close
var positions = _C(exchange.GetPosition);
var pos = [p.GetPosition(symbol, PD_LONG , positions), p.GetPosition(symbol, PD_SHORT, positions)];
var orders = getOrderBySymbol(symbol, _C(exchange.GetOrders))
var diff = t.Sell - t.Buy
if (orders.length == 0 && (!pos[0] && !pos[1]) && diff >= deviation * priceTick) {
profitprice = null
lossprice = null
isLock = false //解锁
holdPrice = ''
holdType = ''
holdAmount = ''
holdProfit = ''
cur_pos = null
preprofit = curprofit
curprofit = exchange.GetAccount().Balance - initAccount.Balance
LogProfit(curprofit, "权益", '&');
if(preprofit < curprofit){
success_count += 1
$.PlotFlag(r[r.length-2].Time, '止盈离场', '止盈离场')
}
if(preprofit > curprofit){
fail_count += 1
$.PlotFlag(r[r.length-1].Time, '止损离场', '止损离场')
}
curbid = t.Buy
curask = t.Sell
// 当前没有挂单,没有持仓,基于盘口挂单
trade("buy", curbid + 1, 1);
trade("sell", curask -1, 1);
orderlong = curbid + 1
ordershort = curask -1
} else if (pos[0] || pos[1]) {
// 只要有持仓
orderlong = null
ordershort = null
cur_pos = pos[0] ? pos[0] : pos[1]
holdPrice = cur_pos.Price
holdType = pos[0] ? '多头方向' : '空头方向'
holdAmount = cur_pos.Amount
holdProfit = cur_pos.Profit
if ((pos[1] && hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_OPEN)) || (pos[0] && hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_OPEN))) {
$.PlotFlag(r[r.length-1].Time, pos[0] ? '多单进场' : '空单进场', pos[0] ? '多单进场' : '空单进场')
cancelOrders(symbol, ORDER_OFFSET_OPEN);
Log('删除挂单:', pos[0] ? '空单' : '多单');
}
var longCoverOrder = hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_CLOSE);
var shortCoverOrder = hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_CLOSE);
if (pos[0] && !longCoverOrder && isLock == false) {
profitprice = curask + priceTick * profit
lossprice = curask - priceTick * maxCoverDeviation
trade("closebuy", profitprice, pos[0].Amount);
isLock = true //加锁
}
if (pos[1] && !shortCoverOrder && isLock == false) {
// 有空头持仓
profitprice = curbid - priceTick * profit
lossprice = curbid + priceTick * maxCoverDeviation
trade("closesell", profitprice, pos[1].Amount);
isLock = true //加锁
}
// 重新平仓条件
if (pos[0] && longCoverOrder && t.Sell < lossprice) {
Log('多头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
Log('空头止损');
cancelOrders(symbol);
p.Cover(symbol);
}
// 双向持仓
if (pos[0] && pos[1]) {
profitprice = null
lossprice = null
profitprice = null
lossprice = null
Log('双向持仓')
cancelOrders(symbol);
p.CoverAll(symbol);
}
}
$.PlotRecords(r, "K线数据")
$.PlotLine("多头挂单", orderlong, r[r.length - 1].Time)
$.PlotLine("空头挂单", ordershort, r[r.length - 1].Time)
$.PlotLine("挂单盈利线", profitprice, r[r.length - 1].Time)
$.PlotLine("挂单止损线", lossprice, r[r.length - 1].Time)
tblStatus.rows = [];
tblStatus.rows.push([ContractType, curprice, orderlong, ordershort, profitprice, lossprice, holdType, holdPrice, holdAmount, holdProfit, fail_count, success_count, _D()]);
lastStatus = '`' + JSON.stringify([tblStatus]) + '`';
LogStatus(lastStatus);
} else {
LogStatus("未连接", _D());
}
Sleep(interval);
}
}
我们再次运行一下,设置盘口距离为3,止损点数是5,止盈点数为0,就是填补盘口间隔的盈利。根据最后统计结果,虽然成功了72次,失败24次,但是加上手续费的消耗,结果预估收益更为惨烈。
总结一下,我们讲述的这些高频策略并不是十分的完善,课程的目的呢主要是为了让大家了解高频做市策略的基础,其中可以优化的地方还有很多。比如具体挂单的时间,挂单的价格间隔,以及止盈和止损的设置,欢迎大家集思广益,提出更好的解决办法,大家可以发在评论区,我们也会争取为大家进行实现。
视频参考链接:
5.交易插件:定制半自动交易工具
优宽身为量化交易平台,主要是为了服务程序化交易者。但也提供了基础的交易终端,虽然功能简单,但我们也可以通过终端进行下单,撤单,和查看行情账户等基本的交易操作。
我们经常看到类似文华财经,MT4等具有一键下单,多种功能的止盈止损功能等辅助交易模块,然而这种类似方便的功能需要我们额外的付费(年费通常在7000元左右)。优宽平台为了完善交易终端的体验,现在增加了插件功能。有时候,我们需要一个小功能来辅助交易,比如一键平仓、一键对冲、阶梯挂单、冰山委托等操作,这种类似的功能并不太需要经常查看执行日志,所以新建一个实盘有些繁琐,我们直接在终端点击一下插件,就能够立即实现相应的功能,这样可以大大方便手动交易。
这些自定义的交易插件,我们自身可以编码实现。并且这个交易终端是定制化的,这就意味着我们可以根据我们的交易习惯,自定义交易功能辅助面板。
本节课,我们来学习交易插件的使用和编写。这个界面是为了快速的进行跨期对冲交易布置好的页面,原始的页面可以通过点击重置页面布局,进行恢复。这个就是初始的界面布局。如果我们想添加交易插件,通过右上角的拼图按钮,我们就可以点击交易插件进行使用。比如,我们点击近远月差价显示的插件,它就会展示在交易终端页面,输入目标合约,点击执行就会出现目标合约的跨期差价图像。需要注意的是,插件不会显示出日志,但是可以返回显示表格。交易终端插件运行时长最长为3分钟,超过3分钟自动停止运行,但是当我们需要进行一些时间较短的交易操作,或者当前的状态查看,3分钟还是足够的;并且也可以用来测试我们的实盘策略,新鲜的策略通过交易插件测试完成后,可以应用于实盘进行成熟的仿真或者实盘交易。
在插件里面是可以设置参数,没有参数的插件可以直接运行。插件的摆放位置,大小都是可以调整的,关闭插件点击右上角x号就可以,我们可以根据我们的交易习惯,进行不同功能模块的编写和摆放。
我们来稍微了解下插件的原理,插件其实相当于立即运行的实盘,功能和调试工具相同,所以插件运行直接对接真实市场的,在交易终端所选的托管者、交易对、K线周期就是默认的相应参数。交易插件会发送一段代码到交易终端页面的托管者进行执行,并且支持返回图表和表格,交易插件和调试工具都是免费使用的。
我们来看下插件是怎样编写的,在新建策略页面,设置策略类型为:交易插件。语言编写支持JavaScript、Python、C++、My语言。 插件的main函数return的结果会在运行结束后,在终端弹出,支持字符串、画图和表格。因为插件执行看不到日志,可以将插件的执行结果return返回。
插件的用途很广泛的,在很多时候手动交易需要很多重复执行的操作,其实这些操作都可以用插件实现。今天,我们讲解的插件使用,是辅助手动期货跨期对冲的插件套装。
期货跨期对冲是很常见的策略,由于频率不是很高,我们经常会手动操作,需要在分析差价走势的基础上,一个合约做多,一个合约做空。如果在期货软件上,我们可能需要复杂的分析和操作,而在交易终端使用插件,将大大节省我们的精力。当我们想进行手动的对冲交易的时候,我们可以调整下布局,将我们需要的插件摆放到合适的位置。
为方便进行跨期对冲,我们编写了三个插件,第一个是刚才介绍的可以查看最新的跨期合约差价,这里我们填写目标合约,点击执行就会呈现最新以秒为单位的差价,并且这里还有有均线的显示,我们可以进行正套或者反套的交易;当我们认为入场的时机到,我们就要迅速的进场进行交易,在没有辅助功能的期货软件中,我们需要手动的找到两个目标品种,然后填写价格进行相反方向的开仓,然而有时候,入场的最佳时间是很短暂的,当我们手忙脚乱开仓后,可能最佳的入场点已经过去;而使用我们的交易插件,当事先设置好交易品种,数量和方向(这里的reverse,代表是正套还是反套),一键点击执行就可以完成双向开仓的操作,这里点击一下仓位;可以显示目前的仓位状态;最后,当价差回归正常,我们就要进行双向的平仓,我们事先设置好滑价,点击就可以平仓。是不是很方便,今天呢,我们就要学习这些插件是如何编写出来的,它和平常的策略编写是有一些不同的,我们在讲解编码的过程中会为大家及时提醒。
首先介绍的是画跨期差价插件,这里我们首先设置好外部的参数,期货合约A和B。然后回到代码部分,这里使用原生的chart画图函数,设置图表对象chart,设置好title,x轴,y轴和数据列表,这里想呈现两根线,所以分别设置diff和meandiff。
回到主函数,首先分别获取两个目标合约的最新k线,这里的exchange.GetRecords中填写参数为1,表示要获取的k线周期为1s。接下来,我们要利用K线数据进行差价的计算,两个k线是轮询获取的,因此数组的长度可能会有不一致,为了保存两个差值计算时间的一致对照性,所以我们取两者的最小值,定义为变量rlength;然后定义差价储存列表difflist。
使用for循环,根据获取的k线长度,按索引计算两个合约的diff值,这里的时间索引的设置很巧妙的,大家可以停下来思考下;然后向chart的第一个数据系列series[0]添加数据,时间戳也是这样的设置;接下来计算diff的均值,使用difflist收集,使用TA.MA进行计算,这里设置的周期为20,为了更及时的展现变化,最后向chart.series[1]添加最新的diff均值。这样图表的设置就完成了,最后return一下chart结果就可以。有些同学们可能会好奇,这里我们不设置chart更新,和while循环吗?这里需要解释的是,我们这里进行的图像展示,是一个瞬时的差价快照,可以帮助我们迅速的判断当前的差价状态,所以没有设置chart更新和while循环。
javascript
var chart = {
__isStock: true,
title : { text : '差价分析图'},
xAxis: { type: 'datetime'},
yAxis : {
title: {text: '差价'},
opposite: false,
},
series : [
{name : "diff", data : []},
{name : "meandiff", data : []},
]
}
function main() {
exchange.SetContractType(Contract_A)
var recordsA = exchange.GetRecords(1)
exchange.SetContractType(Contract_B)
var recordsB = exchange.GetRecords(1)
var rlength = Math.min(recordsA.length,recordsB.length)
var difflist = []
for(var i = 0; i < rlength; i++){
var diff = recordsA[recordsA.length - rlength + i].Close - recordsB[recordsB.length - rlength + i].Close
chart.series[0].data.push([recordsA[recordsA.length - rlength + i].Time, diff])
difflist.push(diff)
var meandiff = TA.MA(difflist, 20)
chart.series[1].data.push([recordsA[recordsA.length - rlength + i].Time, meandiff[meandiff.length - 1]])
}
return chart
}
这样我们的第一个插件就设置好了,接下来我们来看第二个插件的设置:一键对冲开仓插件。同样的,首先设置策略的参数,在策略参数里,我们设置好交易合约A和B,开仓数量,滑价和进行正套还是反套的交易。这里给大家稍微解释下,当差价大的时候,我们进行正套的交易,reverse设置为true,进行空A多B的操作;当差价小的时候,我们进行反套的交易,reverse设置为false,进行多A空B的操作;
所以在主函数里,这里我们首先设置开多的交易操作,使用reverse判断进行正套还是反套的交易,正套需要多合约B,反套需要多合约A,所以使用三元表达式这样设置获取合约,然后获取ticker数据,这里还进行了容错的处理,如果没有获取到ticker数据,直接报错;SetDirection是buy,因为我们想快速的成交,所以设置的价格为当前的卖价加上滑价,这样一个较高的价格可以提升快速成交的成功率;于此相反的,对于空头的操作,和多头的操作刚好相反就可以,这里对于正套我们需要空合约A,然后设置SetDirection是sell,价格设置是当前较低的买价再减去滑价。这样就完成了开仓的操作。
开仓完成以后,我们想在插件中呈现两个开仓的品种,方向,价格和差价,我们这里设置一个表达tbl,包含需要呈现的开仓信息。接着,在下面的代码中,设置开仓完成以后,休息1s,接下来我们获取仓位的数据。如果判断有仓位,就是开仓成功,使用轮询,然后使用if找到我们需要的品种,获取对应的品种名称,开仓方向和价格,然后计算开仓是的差价,push到tbl中,最后使用return进行呈现。
在插件中,我们不想呈现多余的信息,所以SetErrorFilter设置一下,这在前面的海龟交易策略里面我们有提到过。这样一键对冲开仓插件就设置好了。
javascript
function main(){
var tbl = {
type: 'table',
title: '开仓信息 ' + _D(),
cols: ['合约A', '合约A价格', '合约A方向', '合约B', '合约B价格', '合约B方向', '价差'],
rows: []
}
SetErrorFilter("login|ready|流控|连接失败|初始|Timeout|CancelOrder");
exchange.SetContractType(Reverse ? Contract_B : Contract_A)
var ticker_A = exchange.GetTicker()
if(!ticker_A){return '无法获取数据'}
exchange.SetDirection('buy')
exchange.Buy(ticker_A.Sell+Slip, Amount)
exchange.SetContractType(Reverse ? Contract_A : Contract_B)
var ticker_B = exchange.GetTicker()
if(!ticker_B){return '无法获取数据'}
exchange.SetDirection('sell')
exchange.Sell(ticker_B.Buy-Slip, Amount)
Sleep(1000)
var pos = _C(exchange.GetPosition)
if(pos){
for(var i=0;i < pos.length; i++){
if(pos[i].ContractType == Contract_A){
var contractA = pos[i].ContractType
var typeA = pos[i].Type == 0 ? '多头' : '空头'
var priceA = pos[i].Price
}
if(pos[i].ContractType == Contract_B){
var contractB = pos[i].ContractType
var typeB = pos[i].Type == 0 ? '多头' : '空头'
var priceB = pos[i].Price
}
}
var diff = priceA - priceB
tbl.rows.push([contractA, priceA, typeA, contractB, priceB, typeB, diff])
return tbl
}
}
接下来是最后一个插件的设置,一键平仓。这里我们只需要设置一个参数,滑价Slip。在主函数里,首先获取起始状态的仓位信息,如果当前没有仓位,直接返回'已无持仓'。在判断具有仓位信息的情况下,使用轮询的方式,找到对应的多头仓位和空头仓位,然后设置对应的合约,获取最新的ticker数据,然后设置对应的交易方向和函数,同样为了保证交易,这里使用加减滑价的设置。这里稍微提醒一下,这里的SetDirection需要设置为closebuy_today和closesell_today,如果我们没有加today,会有报错,没有平昨的仓位。注意:在回测系统,平今和平昨没有加以区分,都可以使用。
接下来,休息1s。然后获取结束状态的仓位pos_end,如果判断仓位已经全部平掉,打印'平仓完成'的信息。
javascript
function main(){
SetErrorFilter("login|ready|流控|连接失败|初始|Timeout|CancelOrder");
var pos_start = exchange.GetPosition()
if(!pos_start || pos_start.length == 0 ){return '已无持仓'}
for(var i=0;i < pos_start.length; i++){
if(pos_start[i].Type == PD_LONG){
exchange.SetContractType(pos_start[i].ContractType)
var tickerA = exchange.GetTicker()
if(!tickerA){return '无法获取tickerA'}
exchange.SetDirection('closebuy_today')
exchange.Sell(tickerA.Buy - Slip, pos_start[i].Amount)
}
if(pos_start[i].Type == PD_SHORT){
exchange.SetContractType(pos_start[i].ContractType)
var tickerB = exchange.GetTicker()
if(!tickerB){return '无法获取tickerB'}
exchange.SetDirection('closesell_today')
exchange.Buy(tickerB.Sell + Slip, pos_start[i].Amount)
}
}
Sleep(1000)
var pos_end = exchange.GetPosition()
if(!pos_end || pos_end.length == 0 ){
return '平仓完成'
}
}
这样我们三个小插件就编写完成了,当然这个插件还是比较简陋的,容错性还需要提高,大家在使用的过程中,如果遇到错误,可以留言评论区,我们可以及时改正。不过对于基本的使用还是没有问题的,在交易终端里,我们使用N视界仿真账户,设置好目标合约,首先查看差价,等到入场时机设置reverse对应开仓,到达心理盈利点位或者止损点位,进行平仓,使用起来还是比较丝滑的,比手动进行开仓确实方便多了。
看了这个插件的使用,我们应该也有了自己的想法,大家可以根据自己的交易习惯,不妨写成插件方便自己的手动交易。大家如果有好的想法和需求,也可以提出来,我们尽力也会为你实现!
视频参考链接:
《商品期货预定开仓后平仓机器人》
6.托管者部署、Docker部署
大家好,优宽平台最近更新了,可以看到,网页画面更加的清新,各个板块的布局和功能也在各位用户的建议下,设计的也更加的完善。但是有很多老朋友反应对新版UI的托管者设置不太熟悉,当然还有很多想进行仿真或者实盘交易的新朋友,对托管者的设置也存在一定的疑问。因此,本视频就来展示一下在各个系统进行托管者设置的详细步骤。
首先来讲解下托管者的功能,托管者是交易实盘的载体,是管理实盘的程序,主要负责处理底层数据、访问接口、通信等任务,所以可以理解为是交易策略的执行者。托管者运行在我们配置的服务器上,即使优宽量化交易平台网站出现网络故障也不影响您的托管者运行。托管者可运行在本地的电脑,云服务器或者docker部署,系统支持Linux,Windows,Mac OS,android等等。需要注意的是,在策略运行期间,托管者也是需要一直运行的,当托管者关闭,实盘运行的策略也会停止运行。
下面我们就来讲解下在各个平台的部署步骤。打开托管者,点击部署托管者,这里有一键租用托管者和手动部署托管者。如果大家嫌手动部署比较麻烦的话,可以选择一键部署托管者。这里有入门级和内存性两种不同种类的托管者,适合于不同层次的量化选手。点击选择购买后,云服务器会自动的为我们进行托管者程序的安装和部署,并且对于python语言,也会安装一些量化交易常用的包和工具。稍微等待片刻,可以看到当托管者展示部署完成的时候,我们就可以直接使用。当我们不想使用的话,可以直接删除,扣费也会自动停止。
接下来进入手动部署的页面,手动部署托管者可以在我们自己购买的云服务器或者本地的电脑。本地电脑部署托管者可以根据策略运行的需要,随用随停,不需要额外的花费;但是当我们需要长时间运行一个策略的时候,相对来说云服务器是更加适合的。云服务器更新稳定,不容易受外部条件,比如硬件故障,断网或者停电的影响。关于云服务器的选择,一个初级入门,价格低廉的服务器就可以满足基本的策略运行需要。
可以看到这里有三大栏: Linux&Mac, Windows,和Docker部署。每一栏下面都有相应的安装包和具体安装步骤的讲解,并且也很贴心的在需要代码的地方,为我们提供了复制的按钮。
Windows系统部署
首先我们讲解下Windows的部署,Windows同时具有命令版和界面版部署。不熟悉命令行的朋友可以选择界面版,根据自身电脑系统点击下载程序,然后解压安装,打开输入地址node.youquant.com/数字串,数字串每位用户均不同,需要根据你的界面显示进行填写。这里很贴心的可以直接点击复制,然后输入本平台的密码,当界面出现“Login OK”的时候,Windows界面版托管者就部署完成了。点击进入托管者界面,可以看到我们刚刚部署好的托管者。
对于命令行版,我们也要下载对应的安装包,然后进行解压,接着打开终端,打开安装包的地址,输入这段命令robot.exe -s node.youquant.com/数字串-p 本平台登录密码。当命令行出现“Login OK”,代表部署成功。
Mac OS系统部署(苹果电脑)
然后,我们看下Mac系统布置,这里根据苹果系统下载对应的安装包,进行解压。解压完成打开终端,首先进入安装包所在的文件,这里我们进入桌面,然后输入第一行代码chmod +x robot,接着第二行./robot -s node.youquant.com/数字串 -p 本平台登录密码 ,这里会显示无法验证开发者,我们这时候需要进入系统偏好设置,安全性和隐私,点击解锁,然后点击仍然运行,再次输入这段代码,当出现“Login OK”部署成功。
Linux服务器
以上都是部署在本地电脑上的,下面我们使用云服务器进行一下Linux系统的部署。这里有一个托管中部署的帮助文档大家可以参考一下,我给大家展示一下。首先登陆云服务器,具体登录云服务器的方法,大家可以根据自己的习惯,这里我们选择在腾讯云直接登录:
- 第一步,在服务器输入wget下载托管者程序
wget https://www.youquant.com/dist/robot_linux_amd64.tar.gz
- 第二步,输入这行代码进行robot解压;
tar -xzvf robot_linux_amd64.tar.gz
- 第三步,测试托管者运行,输入这两行代码,然后添加优宽登录密码;
chmod +x robot
./robot -s node.youquant.com/数字串
- 第四步,当出现Login OK就代表云端托管者设置成功。
当然Linux的托管者也是可以部署在本地的,大家也可以尝试一下。
Docker镜像部署
最后,我们来看下使用docker部署托管者,这是我们使用云服务器进行部署,首先在云服务器下载安装docker,不熟悉的朋友可以询问chatgpt,按照步骤安装docker。
-
在确保docker安装完成后,按照教程首先下载镜像,运行这段代码:
docker pull youquantcom/docker:latest -
运行镜像,输入这段代码:
docker run -d --name 优宽Docker -e UID=数字串 -e ZONE=CN -e PASSWORD=密码 youquantcom/docker -
输入这段代码查看docker日志:
优宽Docker logs
当日志出现Login OK,docker版的托管者也部署成功。
以上呢,就是在不同平台部署托管者的详细步骤,大家可以参考一下,如果有问题,也可以留言评论区,我们会为大家热心解答。
视频参考链接:
《优宽量化的托管者概念》
《优宽量化交易平台托管者部署简介》
7. 热门Pine语言移植:抓住那个涨停板!
最近有小伙伴联系我们,说在Trading View上有一个策略很神奇,可以抓住一个涨停的品种,并且可以逐步加仓,在数字货币取得了很好的收益。刚好,最近八月份纯碱的期货市场很是热闹,出现了移仓换月时的“多头逼仓现象”,问我们能不能改造成为商品期货的版本,应用于国内的期货市场。我们的答案当然是可以!今天我们就来看一下,怎样将TradingView上的热门Pine语言策略,应用于商品期货市场。
我们首先来看一下这个策略,可以看到这个策略名称叫做“BTC金字塔策略”,小火箭有500多个,确实很受欢迎。通过策略回测图像可以看到,这个策略对于单边上涨的趋势确实抓的很好,并且也可以实现较好的入场和出场。
我们来看下策略的源码。代码长度一共有94行。优宽平台也是可以运行Pine语言的,我们复制这段代码到优宽,看这段代码能否应用于国内的期货市场。
pine
//@version=4
strategy(title='Pyramiding BTC 5 min no security', overlay=true, pyramiding=7, initial_capital=10000, default_qty_type=strategy.percent_of_equity, default_qty_value=20, commission_type=strategy.commission.percent, commission_value=0.075)
//
fastLength = input(250, title="Fast filter length ", minval=1)
slowLength = input(500,title="Slow filter length", minval=1)
source=close
v1=ema(source,fastLength)
v2=ema(source,slowLength)
//
//Backtest dates
fromMonth = input(defval=1, title="From Month")
fromDay = input(defval=10, title="From Day")
fromYear = input(defval=2020, title="From Year")
thruMonth = input(defval=1, title="Thru Month")
thruDay = input(defval=1, title="Thru Day")
thruYear = input(defval=2112, title="Thru Year")
showDate = input(defval=true, title="Show Date Range")
start = timestamp(fromYear, fromMonth, fromDay, 00, 00) // backtest start window
finish = timestamp(thruYear, thruMonth, thruDay, 23, 59) // backtest finish window
window() => // create function "within window of time"
time >= start and time <= finish ? true : false
//
length = input(50, title = "Period", type = input.integer)
hma(_src, _length)=>
wma((2 * wma(_src, _length / 2)) - wma(_src, _length), round(sqrt(_length)))
hma3(_src, _length)=>
p = length/2
wma(wma(close,p/3)*3 - wma(close,p/2) - wma(close,p),p)
b =hma3(close[1], length)
//plot(a,color=color.gray)
//plot(b,color=color.yellow)
close_price = close[0]
len = input(25)
linear_reg = linreg(close_price, len, 0)
filter=input(true)
buy=crossover(linear_reg, b)
longsignal = (v1 > v2 or filter == false ) and buy and window()
//set take profit
ProfitTarget_Percent = input(3)
Profit_Ticks = close * (ProfitTarget_Percent / 100) / syminfo.mintick
//set take profit
LossTarget_Percent = input(10)
Loss_Ticks = close * (LossTarget_Percent / 100) / syminfo.mintick
//Order Placing
strategy.entry("Entry 1", strategy.long, when=strategy.opentrades == 0 and longsignal)
strategy.entry("Entry 2", strategy.long, when=strategy.opentrades == 1 and longsignal)
strategy.entry("Entry 3", strategy.long, when=strategy.opentrades == 2 and longsignal)
strategy.entry("Entry 4", strategy.long, when=strategy.opentrades == 3 and longsignal)
strategy.entry("Entry 5", strategy.long, when=strategy.opentrades == 4 and longsignal)
strategy.entry("Entry 6", strategy.long, when=strategy.opentrades == 5 and longsignal)
strategy.entry("Entry 7", strategy.long, when=strategy.opentrades == 6 and longsignal)
if strategy.position_size > 0
strategy.exit(id="Exit 1", from_entry="Entry 1", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 2", from_entry="Entry 2", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 3", from_entry="Entry 3", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 4", from_entry="Entry 4", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 5", from_entry="Entry 5", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 6", from_entry="Entry 6", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 7", from_entry="Entry 7", profit=Profit_Ticks, loss=Loss_Ticks)
我们复制到优宽策略编辑页面,策略开头说明使用的pine语言版本是version4,因此它的语法和version5有一些小的区别,大家需要注意一下,但是在优宽平台这两个版本的pine语言都是支持的。
原始的strategy里面定义了很多参数,但是这些参数有的不适合期货市场,有的在回测系统有设置,所以我们只需要改变保留pyramiding最大加仓次数参数就可以。
我们继续向下看,这里代码前面的斜杠删除,首先定义快速滤波器长度fastLength,和慢速滤波器长度slowLength参数,初始值为250和500,然后设置source为收盘价。使用ema指数移动平均值分别计算快速滤波器v1和慢速滤波器v2。
第二部分,定义了回测周期,这在优宽的模拟回测页面可以定义,所以这部分可以删除。
继续往下看,这里首先定义周期参数length,然后定义了Hull移动平均线函数hma,和改进的Hull移动平均线函数hma3,经过检查发现,hma函数是没有使用到的,我们可以直接删除;并且hma3中的_src参数没有使用到,我们也要改下,将这里的close改为_src;然后使用hma3函数计算移动平均线,定义为变量b。
第四部分,获取当前收盘价定义为close_price,定义线性回归的长度参数,默认值是25,接着利用linreg内置函数计算线性回归值linea_reg。
这里定义一个过滤器参数,默认为true。
指标计算完成以后,接着就要定义交易信号了。当linear_reg上穿Hull移动平均值b的时候,定义为buy。然后买入信号的发出还可以添加过滤器的确定,当设置过滤器filter为真,我们需要检查快速滤波器的指数移动平均值v1是否大于慢速值v2;当过滤器信号和buy双向确定,这时候确定发出交易信号longsignal,这里的回测周期window删除。如果设置filter为假,只需要buy信号确定就可以了。
然后再止盈止损的设置了,设置止盈百分比参数ProfitTarget_Percent,计算止盈目标点数,使用当前收盘价乘以百分比,这里的syminfo.mintick是每跳的点数;接着止损参数的设置也是一样的步骤,初始值为10,然后计算止损目标点数。
当买入信号设置完成以后,接着就是交易的操作了,第一笔是没有持仓strategy.opentrades等于0,并且买入信号成立,产生第一笔买入订单,随后当已有一笔买入订单且满足买入条件时,买入第二笔,接着也就是第三到第七笔交易的完成。当强劲的上涨信号连续出现的时候,我们就要及时的加仓,直到完成加仓的上限。
接下来就是止盈和止损的操作,分别设置好七笔的止盈目标和止损目标,这样原始的pine语言代码我们就拆解完成了。
pine
//@version=4
strategy(title='鉴别涨停品种策略', overlay=true, pyramiding=5)
fastLength = input(250, title="Fast filter length ", minval=1)
slowLength = input(500,title="Slow filter length", minval=1)
source=close
v1=ema(source,fastLength)
v2=ema(source,slowLength)
length = input(50, title = "Period", type = input.integer)
hma3(_src, _length)=>
p = length/2
wma(wma(_src,p/3)*3 - wma(_src,p/2) - wma(_src,p),p)
b =hma3(close[1], length)
plot(b,color=color.yellow)
close_price = close[0]
len = input(25)
linear_reg = linreg(close_price, len, 0)
filter=input(true)
buy=crossover(linear_reg, b)
longsignal = (v1 > v2 or filter == false ) and buy
//set take profit
ProfitTarget_Percent = input(3)
Profit_Ticks = close * (ProfitTarget_Percent / 100) / syminfo.mintick
//set take loss
LossTarget_Percent = input(10)
Loss_Ticks = close * (LossTarget_Percent / 100) / syminfo.mintick
//Order Placing
strategy.entry("Entry 1", strategy.long, when=strategy.opentrades == 0 and longsignal)
strategy.entry("Entry 2", strategy.long, when=strategy.opentrades == 1 and longsignal)
strategy.entry("Entry 3", strategy.long, when=strategy.opentrades == 2 and longsignal)
strategy.entry("Entry 4", strategy.long, when=strategy.opentrades == 3 and longsignal)
strategy.entry("Entry 5", strategy.long, when=strategy.opentrades == 4 and longsignal)
strategy.entry("Entry 6", strategy.long, when=strategy.opentrades == 5 and longsignal)
strategy.entry("Entry 7", strategy.long, when=strategy.opentrades == 6 and longsignal)
if strategy.position_size > 0
strategy.exit(id="Exit 1", from_entry="Entry 1", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 2", from_entry="Entry 2", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 3", from_entry="Entry 3", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 4", from_entry="Entry 4", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 5", from_entry="Entry 5", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 6", from_entry="Entry 6", profit=Profit_Ticks, loss=Loss_Ticks)
strategy.exit(id="Exit 7", from_entry="Entry 7", profit=Profit_Ticks, loss=Loss_Ticks)
可以发现,我们的改动确实很少,接着就要使用改写完成的策略,使用到国内的期货市场,看看运行效果怎么样?我们在回测页面定义一下时间,8月1号到8月31号,k线周期使用建议的5分钟。策略的原始参数我们不加以改变,执行方式是实时价模型,品种代码是SA888,我们来运行一下。可以看到在这一个月的时间,在行情比较平稳的时候8月1号到8月20号,确定没有进行任何的交易,当行情出现剧烈上涨,也就是8月21日,我们开始了交易,并且在8月26日达到了收益顶峰,虽然之后产生了比较大的亏损,但是最后的盈利仍然是27000多元,证明比较好的抓住了一个品种的上涨单边行情。但是这个策略是完美的吗,我可能要给大家泼一盆冷水了,当我们延长回测时间,到9月15的时候,可以发现策略收益回撤到了4000多元,证明该策略并不能保证稳定的获得盈利,根据回测图像发现,当行情出现剧烈下跌的时候,该策略没有实现及时的止损。因此,该策略作为单边上涨的入场信号判断,还是比较准确的,但是对于出场信号,设计的可能比较草率,大家可以思考一下,怎么更好的设置出场?如果有好的想法,大家也可以提出来,我们一起来实现。
Pine语言语法确实很优雅,作为专门的交易语言,它对于各个信号的计算和交易操作的设置,都是简洁明朗。当我们使用Javascript语言,有时候会嫌弃过于它过于繁琐。我相信把这个Pine语言策略改为Javascript语言,代码的长度只会只多不少,大家也不一定感兴趣。但是如果我们发挥Javascript语言的优势,大家有没有什么好的想法。
我们看到上面这个策略对于单边上涨的趋势判断的还是比较准确的,但是Pine语言一次只能针对于一个品种,而如今期货市场高达上百个品种,每个品种在一个月的时间内总会有几个出现剧烈上涨或者下跌的行情,我们使用这个策略一个个去判断,确实不太方便。如果我们建立一个实盘,使用轮询的方式,对期货市场的所有品种进行检测,然后给出重点关注的品种,也许可以帮助大家抓住更多的盈利机会。
下节课呢,我们就将这种想法使用Javascript进行实现,大家稍微等待一下,等我们设计完善,马上发布!
视频参考链接:
《BTC五分钟金字塔策略》
8. 商品期货多品种涨停预测推送策略
在各位小伙伴们的催促下,Javascript版的多品种涨停鉴定策略终于出炉了。相对于Pine语言只能实现单品种涨停信号的识别和交易,Javascript语言发挥自身的优势,可以实现多品种合约涨停信号的判断和信息的及时推送。大家在进行期货交易的过程中,经常会收到类似大师每日涨停品种推荐的信息,有不少的朋友都付费尝试过,今天呢,我们就使用量化策略打造一个大师,帮助我们进行每日交易的参考。需要注意的是,本策略只是进行涨停信号的判断,至于具体的交易操作,大家可以自行添加,有不明白的地方可以留言。
javascript
/*backtest
start: 2023-08-01 09:00:00
end: 2023-08-31 23:00:00
period: 5m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["Instruments","SA888"]]
*/
var DetectManager = {
New: function(symbol) {
var obj = {
symbol: symbol,
isFirst: true,
PreBarTime: 0,
buySignal: false,
rData: [],
};
obj.Show = function() {
return [obj.symbol, obj.buySignal]
};
// 信号鉴定逻辑
obj.Detect = function() {
_C(exchange.SetContractType, obj.symbol);
var r = exchange.GetRecords()
if(filter){
if(r.length < slowLength) return
}else{
if(r.length < length) return
}
// hma均线计算
function hma3(_src, _length) {
var p = _length/2
var p_3 = talib.WMA(_src,p/3)
var p_2 = talib.WMA(_src,p/2)
var p_1 = talib.WMA(_src,p)
var ret = []
for (var i = 0; i < p_1.length; i++) {
if(p_1[i] > 0){
ret.push(p_3[i] * 3 - p_2[i] - p_1[i])
}
}
var hmares = talib.WMA(ret, p)
return hmares
}
if(obj.isFirst){
for(var i = 0; i < r.length - 1; i++){
obj.rData.push(r[i])
}
obj.PreBarTime = r[r.length - 2].Time
obj.isFirst = false
} else {
if(obj.PreBarTime != r[r.length - 2].Time){
Log('K线更新')
obj.rData.push(r[r.length - 2])
var v1 = TA.EMA(obj.rData,fastLength)
var v2 = TA.EMA(obj.rData,slowLength)
var hmavalue = hma3(obj.rData, length)
// 线性回归值
var linear_reg = talib.LINEARREG(obj.rData, len, 0)
var buy = linear_reg[linear_reg.length - 2] < hmavalue[hmavalue.length - 2] && linear_reg[linear_reg.length - 1] > hmavalue[hmavalue.length - 1]
obj.buySignal = (v1[v1.length - 1] > v2[v2.length - 1] || filter == false ) && buy
Log('检查品种:', obj.symbol, '检查结果:', obj.buySignal)
obj.PreBarTime = r[r.length - 2].Time
}else{
obj.buySignal = false
}
}
}
return obj
}
}
function main() {
var arr = Instruments.split(',')
var detectlist = []
for (var i = 0; i < arr.length; i++) {
var symbol = arr[i].replace(/^\s+/g, "").replace(/\s+$/g, "")
var obj = DetectManager.New(symbol)
detectlist.push(obj)
}
while(true){
if(exchange.IO('status')){
LogStatus("已经连接CTP, " + new Date())
var tblRecommand = {
type: "table",
title: "推荐品种",
cols: ["时间", "合约名称"],
rows: []
}
var recommandlist = []
for (var i = 0; i < detectlist.length; i++) {
detectlist[i].Detect();
var res = detectlist[i].Show();
if(res[1] == true){
tblRecommand.rows.push([_D(), res[0]])
LogStatus(tblRecommand)
recommandlist.push(res[0])
}
}
if(recommandlist.length > 0) Log(_D(), '推荐品种:', recommandlist, "@")
}else{
LogStatus('未连接CTP', _D())
}
Sleep(1000);
}
}
话不多说,我们直接开始。首先我们进行的还是多品种策略框架的设计,这里我们参考了多品种海龟策略的框架,将信号计算和品种轮询进行分割,这样代码各部分更加清晰,完善起来也更加方便。第一部分,我们创建DetectManager变量,这个对象主要就是用来鉴别各品种的涨停信号;第二部分是main主函数,这里的主函数首先创建各品种涨停信号计算的逻辑对象,然后是while循环,该循环为策略的主要循环,这一部分主要是遍历所有合约信号计算的逻辑对象,进行涨停信号的判断,然后实现及时的推送。还记得我们前面讲过的CTA函数的多品种策略吗,大家也可以选择使用,将最后的return结果返回0,不进行任何操作就可以。
多品种策略框架搭建好以后,第一部分我们来编写DetectManager变量。首先创造New函数,作为构造函数。这里我们设置参数是合约的名称symbol。然后创建不同合约对象的属性信息,包括合约名称symbol,isFirst代表是否为第一次获取k线,PreBarTime先前的k线时间,buySignal涨停信号的初始值,rData是获取的k线值。接着我们定义Show方法,用来展示当前品种的名称和涨停信号的真假。
最后来定义Detect方法,用来处理信号鉴定逻辑。这里我们首先合约的设置和k线的获取。这里设置好合约。接着来处理k线。为了防止未完成的k线造成的假性信号,这里我们决定舍弃最新的一根k线进行信号的计算。在策略初始阶段,obj.isFirst为true,将倒数第二根k线之前的数据添加到obj.rData中,赋值PreBarTime为倒数第二根k线时间戳,然后定义obj.isFirst为false。接下来,只有倒数第二根k线更新,我们再进行添加。这里不要忘了,也要更新PreBarTime。
K线获取好以后,接下来就要参考Pine语言,进行涨停信号的判断。我们来看下Pine语言的源代码,第一部分是快速和慢速过滤器的设置,这里需要过滤器filter参数,快速滤波器周期fastLength,和慢速周期slowLength参数,我们设置好。具体v1,和v2的计算,Pine语言使用ema内置函数计算出来的,在Javascript语言中,使用TA.EMA可以计算。第二部分,是Hull移动平均值,通过构造hma3函数进行计算。这里我们需要定义一个参数,length。Pine语言函数里面使用到了wma进行计算,Javascript中talib函数有内置函数,我们可以直接使用。我们来进行hma3函数的编写,这里定义两个参数_src和_length。定义p为_length除以2,接着计算不同周期的WMA值,定义为p_3,p_2和p_1。接着来计算ret,作为最后一个wma计算使用的数组,使用p_3,p_2和p_1中的每个元素,因为在Javascript中,talib.WMA计算使用的数组中不允许出现空值,所以当空值最多的p_1数组不为空,这里定义为大于0的时候,我们再进行添加。然后最后计算hmares进行返回。这里的hma3计算确实比较麻烦一点,大家需要注意一下。回到主逻辑,这里的第二部分,使用构造的函数,填写参数rData和hma周期,进行hma数值的计算。
第三部分,是线性回归信号的判断,这里也可以使用内置函数talib.LINEARREG,参数填写为k线数据,周期len,我们再添加一个参数len。最后的offset偏移值为0。
最后是涨停信号的判断了,如果linear_reg上穿hma均线,判断为buy,这里定义倒数第二根linear_reg小于hma均线,最新linear_reg大于hma均线。不要忘了还有过滤器的设置,如果过滤器为真,还需要判断快速滤波器v1是否大于v2,过滤器为假的话,直接判断buy信号就可以。
因为想实现不同品种的合约涨停信号的计算和信息的推送,是在k线更新的那一刻完成的,所以涨停信号的计算移动到当k线更新的时候,这一部分进行计算。另外还需要设置当k线没有更新的时候,我们需要及时的将buySignal设置为假,这样就可以避免在buySignal为true的k线周期内,重复的推送信息。
最后return obj这样就完成了第一部分DetectManager的设置。接下来来到我们的主函数,首先,这里我们需要定义参数Instruments,放入我们感兴趣的品种名称。回到主函数,这里将合约列表Instruments进行分割成arr,然后定义detectlist列表,用来存放信号计算的逻辑对象,接着使用for循环,首先进行正则化处理,然后使用DetectManager.New构造不同合约的逻辑对象,添加进入detectlist列表中。
接着进入主循环,创建变量recommandlist推荐列表变量,使用for循环进行不同合约的信号判断,首先使用Detect方法,接着Show()方法获取信号判断的结果,当信号判断为真,就是res的第二个值,将合约的名称添加进入recommandlist推荐列表当中。
如果recommandlist推荐列表不为空,证明该时刻判断某些品种出现了涨停信号,这个时候我们进行信息的及时推送。在TV上有一个报警机制,当我们设置的信号触发的时候,可以发送APP提醒或者通过webhook进行推送。在优宽平台,我们也可以实现这个功能。
推送设置在账号设置里面,可以选择APP,邮箱和webhook推送,这样当今日推荐的涨停品种信号触发的时候,可以设置推送到我们指定的app,邮箱或者webhook,帮助我们及时接受信息。APP,邮箱我们都很熟悉,这里通过设置webhook接口,可以帮助我们将最新的推送消息发送到我们设定的渠道,这里有一篇调用钉钉接口实现机器人推送消息的,大家有兴趣可以实践一下;同时,我们还可以利用python创建一个服务,用来接收优宽的推送消息。推送渠道设置好以后,在我们的代码中,当推荐列表不为空,我们打印当前的时间,推荐的品种。最后加上@符号就可以完成推送。
这里我们也可以设置状态表进行展示,创建tblRecommand,当有品种判断信号为真,添加进入tblRecommand进行展示。结尾部分,设置未连接状态的状态栏展示,和Sleep。
以上呢,就是多品种涨停鉴定策略的代码编写。我们可以回测运行一下,根据回测日志,可以看到使用黑色三兄弟,在8月1号到8月31号,我们获取到了29次涨停信号的提醒。我们会发布这个策略代码到策略广场,大家有兴趣可以更换不同的参数和合约品种进行更多的尝试!
我们也建立了相应的实盘,这里我们稍微修改了下代码。每次K线更新,会打印检查的品种和结果,这样可以让我们实时了解策略的运行进度,并且当有信号触发的时候,会发送到APP和邮箱,作为交易的参考。这样呢,我们就可以实现多品种涨停信号的监测和推送。其实,这样类似的应用场景还有很多,欢迎大家提出来,我们会热心的帮你实现!
视频参考链接:
《热门Pine语言策略--抓住那个涨停板!》
《升级商品期货多品种海龟交易策略以及回测说明》
9.基于TradingView信号执行交易
在TradingView上有很多可以选择使用的指标、策略、代码等,这些都可以在TradingView上直接运行,可以画线、计算、显示交易信号等。并且TradingView有实时的价格数据、充足的K线数据方便各种指标计算。TradingView上这些脚本代码叫做PINE语言,唯独一点不太方便的就是在TradingView上进行实盘交易。虽然在优宽上已经支持PINE语言,也可以实盘运行。但是也有TradingView的铁粉还是希望以TradingView上的图表发出的信号去下单交易,那么这个需求也可以通过优宽来解决。
大家都知道某些商品是具有外盘的,这些外盘合约对国内期货价格的变动会有显著的影响。比较有名的比如新加坡的铁矿石合约,美国的原油和农产品合约,英国的贵金属合约等。这些合约的交易时间是和国内不同步的,所以国内的交易老手经常会参考外盘行情,获取信息差的利润。然而,这些外盘数据,在国内平台进行获取和信号计算,是比较困难的。然而在TradingView上,涵盖基本所有国家的金融数据,可以实现实时的数据获取和信号计算。所以,我们可以在TradingView上建立一个外盘信息推送机制,可以让国内商品期货的实盘实时接受外盘的信息,进而进行一些策略的运行。
讲解完具体的需求场景,我们来看下具体的解决方案。整个方案中涉及4个主体,简单来说分别是:
| 编号 | 主体 | 描述 |
|---|---|---|
| 1 | TradingView | TradingView上运行着PINE脚本,可以发出信号,访问优宽的扩展API接口 |
| 2 | 优宽平台 | 管理实盘、可以在实盘页面发送交互指令、也可以通过扩展API接口让优宽平台发送交互指令给托管者上的实盘策略程序 |
| 3 | 托管者软件上的实盘程序 | TradingView信号执行策略实际运行起来的程序 |
| 4 | 交易所 | 实盘上配置的交易所,托管者上的实盘程序直接发送请求下单的交易所 |
具体的实现步骤,我们大致介绍下:
- TradingView上编写脚本进行运行,这个脚本进行交易数据获取和信号计算。
- 在优宽上配置API KEY,填写到TradingView的webhook接口,用来给优宽发指标信号。需要注意的是,这里需要TradingView账号至少是PRO会员级别。
- 实盘编写「TradingView信号执行策略」,用来接收TradingView的信号,对接真实的市场进行交易操作。
首先,我们来看怎样通过TradingView向优宽实盘发送消息?我们打开TradingView,这里我们挑选的品种是布伦特原油。布伦特原油是国际上最重要的原油定价基准之一,其价格波动会对全球原油市场产生广泛的影响。当布伦特原油价格变动较大时,国内原油期货市场可能会受到价格传导的影响,导致国内原油期货价格也出现相应的波动。所以这里我们建立一个测试策略,使用布伦特原油的交易信号去进行国内原油的交易操作。这个测试策略很简单,根据k线是否连续上涨或者下跌去判断做多还是做空的信号。这里我们定义两个参数,consecutiveBarsUp 和 consecutiveBarsDown初始值都为3,然后统计连续上涨ups,和连续下跌dns的数值。在非历史bar的情况下,当ups大于consecutiveBarsUp参数,定义action为long,然后使用strategy.order进行下单,这里我们将comment定义为action;对于连续下跌的情况也是一样,当dns小于consecutiveBarsDown,定义action为short。这里我们只是判断上涨还是下跌的趋势,然后将action信息推送到我们国内优宽原油期货交易实盘,根据交易的趋势进行相应的交易操作。我们来看下怎样设置推送?
点击左边的闹钟按钮,点击创建警报,这里我们可以选择很多的指标和策略的警报,这里我们选择刚创建好的策略,到期时间无限制,然后消息这里我们只返回strategy.order.comment,这里的comment就是定义的action信息。
{
"Action":"{{strategy.order.comment}}",
}
然后第二栏这里设置webhook url,就是当警报触发时,向指定的url发送post的请求。我们来看下这里的webhook url怎样填写?我相信到这里大家一定有疑问,TradingView的信息是怎样传送的,传送的信息实盘怎样接受。关于第一个问题,TradingView其实是把消息发送给优宽的扩展API接口的。API接口我们在前面的课程中介绍过,那次课程我们使用扩展API实现了实盘的定时启停。这次我们要使用的扩展API接口是CommandRobot,来实现消息的接受。所以webhook的设定格式是这样的:
// 目前已经不支持这种方式:https://www.youquant.com/api/v1?access_key=xxx&secret_key=yyy&method=CommandRobot&args=[实盘ID,""],需要编码,例如:
https://www.youquant.com/api/v1?access_key=xxx&secret_key=yyy&method=CommandRobot&args=%5B实盘ID%2C%22%22%5D
我们来看下怎样填写:这里的ApiKey,也就是xxx和yyy,在账号设置Api接口这里获取。这里的CommandRobot是调用的方法,最后的参数args填写实盘的id,最后的空字符代表我们要发送的消息。
但是相信让大家自己拼凑填写有时候总会出现小小的失误,其实这些字符的拼接我们可以在实盘策略里面进行完成。我们来看下这个实盘策略应该怎样编写。
javascript
var BaseUrl = "https://www.youquant.com/api/v1" // 优宽扩展API接口地址
var RobotId = _G() // 当前实盘ID
var Success = "#5cb85c" // 成功颜色
var Danger = "#ff0000" // 危险颜色
var Warning = "#f0ad4e" // 警告颜色
var buffSignal = [] // 保存信息
var symbol = 'sc2311' // 定义合约
var q = $.NewTaskQueue();
function createManager() {
var self = {}
self.tasks = []
self.newTask = function(signal) {
var task = {}
task.Action = signal["Action"]
task.error = null
task.finished = false
Log("创建任务:", task)
self.tasks.push(task)
}
self.pollTask = function(task) {
exchange.SetContractType(symbol)
var pos = exchange.GetPosition()
if (task.Action == "long"){
Log('当前仓位:', pos)
if(pos.length == 0) {
q.pushTask(exchange, symbol, "buy", 1)
Log('开多', Success)
}else if(pos[0].Type == PD_SHORT || pos[0].Type == PD_SHORT_YD){
q.pushTask(exchange, symbol, "closesell", 1)
Log('平空', Success)
}else if(pos[0].Type == PD_LONG || pos[0].Type == PD_LONG_YD){
Log('已有多头仓位', Warning)
}
}
if (task.Action == "short"){
if(pos.length == 0) {
q.pushTask(exchange, symbol, "sell", 1)
Log('开空', Success)
}else if(pos[0].Type == PD_LONG || pos[0].Type == PD_LONG_YD){
q.pushTask(exchange, symbol, "closebuy", 1)
Log('平多', Success)
}else if(pos[0].Type == PD_SHORT || pos[0].Type == PD_SHORT_YD){
Log('已有空头仓位', Warning)
}
}
q.poll()
task.finished = true
}
self.process = function() {
var processed = 0
if (self.tasks.length > 0) {
_.each(self.tasks, function(task) {
if (!task.finished) {
processed++
self.pollTask(task)
}
})
if (processed == 0) {
self.tasks = []
}
}
}
return self
}
var manager = createManager()
function HandleCommand(signal) {
// 检测是否收到交互指令
if (signal && $.IsTrading(symbol)) { // 交易时间收到交互指令
Log('是否正在交易:', $.IsTrading(symbol))
Log("收到交互指令:", signal)
} else if (signal && !$.IsTrading(symbol)){ // 非交易时间收到交互指令
Log("非交易时间")
return
} else { // 没有收到时直接返回,不做处理
return
}
objSignal = JSON.parse(signal)
buffSignal.push(objSignal)
// 创建任务
manager.newTask(objSignal)
}
function main() {
// 已经不支持:Log("WebHook地址:", "https://www.youquant.com/api/v1?access_key=" + YouQuant_AccessKey + "&secret_key=" + YouQuant_SecretKey + "&method=CommandRobot&args=[" + RobotId + ',+""]', Danger)改为以下方式
Log("WebHook地址:", "https://www.youquant.com/api/v1?access_key=" + YouQuant_AccessKey + "&secret_key=" + YouQuant_SecretKey + "&method=CommandRobot&args=%5B" + RobotId + '%2C+%22%22%5D', Danger)
while (true) {
if(exchange.IO('status')){
LogStatus("已经连接CTP, " + new Date())
try {
// 处理交互
HandleCommand(GetCommand())
// 处理任务
manager.process()
// 状态栏表格显示
if (buffSignal.length > maxBuffSignalRowDisplay) {
buffSignal.shift()
}
var buffSignalTbl = {
"type" : "table",
"title" : "信号记录",
"cols" : ["Time", "Action"],
"rows" : []
}
for (var i = buffSignal.length - 1 ; i >= 0 ; i--) {
buffSignalTbl.rows.push([_D(), buffSignal[i].Action])
}
LogStatus(_D(), "\n", "`" + JSON.stringify(buffSignalTbl) + "`")
Sleep(1000)
} catch (error) {
Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
Sleep(1000)
}
}else{
LogStatus('未连接CTP', _D())
}
Sleep(1000);
}
}
在策略开头,我们定义BaseUrl,代表优宽扩展API接口地址,RobotId可以通过_G获取,这在前面恢复策略进度我们有提到过,接着定义三种颜色,分别代表成功,危险和警告的颜色,定义buffSignal保存获取到的信息,symbol是原油期货主力合约,q是交易类库多品种管理对象。
第一部分是我们来定义createManager,用来处理推送过来的signal对应的交易操作。具体来说,createManager函数创建了一个任务管理器对象。函数内部定义了一个self对象,该对象包含以下属性和方法:
- tasks:任务数组,用于存储待处理的任务。
- newTask方法:首先定义接收信号,创建任务的方法newTask。在该方法数中,接受一个信号对象作为参数。当参数从TradingView传达以后,创建一个新的task变量,然后定义任务的Action(操作)属性,error(错误信息)属性和finished(是否已完成)属性,并将定义好的task加入任务数组中。
- pollTask方法:然后对给定的任务对象执行相应的交易操作,定义pollTask方法。根据任务的Action属性判断是买入还是卖出操作,然后根据持仓类型,调用q.pushTask方法添加相应的交易任务到交易队列中。使用pushTask不要忘记q.poll()。最后将任务的finished属性标记为true,表示任务已完成。
- process方法:最后建立process方法用于处理任务队列中的任务。通过遍历任务数组,对未完成的任务调用pollTask方法进行处理。若所有任务都已完成,则将任务数组清空。
函数最后返回self对象。
这里通过调用createManager函数manager变量,这样可以通过manager变量来管理和执行任务队列。
因为国内外开盘时间的不一致,所以我们接受到的信息是不能直接使用的。HandleCommand函数的作用是接受和处理交互指令。它接受一个参数signal,该参数表示收到的交互指令。函数首先检测是否收到了有效的交互指令,并且当前处于交易时段。如果不处于交易中或者没有收到有效的交互指令,则直接返回,不进行任何处理。如果收到了有效的交互指令并且处于交易状态,函数将JSON解析交互指令,将其转换为对象objSignal。然后将objSignal添加到buffSignal数组中,表示记录交互指令。
接下来,函数调用manager.newTask(objSignal)方法创建一个新的任务。这样可以将交互指令转化为对应的任务,并添加到任务管理器的任务队列中,以便后续处理。
最后的main函数,在该函数中,会进行以下操作:
首先我们输出WebHook地址和交易类型信息,这里就要根据提供的参数信息完成webhook字段的拼接。
信号的接受和处理方法已经设置完成了,在进入主循环之前,我们需要设置几个参数:优宽_AccessKey和优宽_SecretKey,maxBuffSignalRowDisplay表示最大信号显示行数。
接着我们进入主循环:我们首先完成webhook url的拼接。这里的三个参数是优宽 的ApiKey的两部分,和实盘的ID。
在大致的框架列好以后,这里我们使用try和catch结构捕获任何异常错误,打印相关错误信息。
这里首先调用HandleCommand()函数获取并处理交互指令,TradingView传送的信息会发送到交互信息里面,使用GetCommand()就是获取交互命令字符串。
然后调用任务管理器manager的process()方法来处理任务队列中的任务。
当然我们也要记录信号,将收到的信号记录添加到buffSignalTbl状态栏表格进行显示,并根据设定的最大显示行数进行处理。
根据设定的休眠间隔时间,使用Sleep()函数暂停一段时间。
这样呢,我们就简单设置好了使用优宽接收TradingView传递的实时信息进行交易的策略,我们创建实盘运行一下。可以看到,首先返回webhook的信息,我们需要填写的TradingView的栏中。这时候TradingView才可以传递消息。当实盘接受到信号,打印收到交互任务,并创建任务,并根据现有的仓位进行相应的交易操作,如果是在非交易时间段接收到了任务,直接进行返回。这样我们就可以使用TradingView的信息实现跨平台的交易操作。
我们这里的策略设计比较简单,主要是为了教学的目的。其实TradingView上有很多新颖的指标和策略,大家都可以动手尝试下。如果大家有好的想法,想让我们帮忙实现,也欢迎留言评论区,我们将为大家热心解答!
视频参考链接:
《另一种TradingView信号执行策略方案》
10. 浅谈量化交易中的过拟合问题
你有没有听过这样的传闻,传说有一种特殊类型的策略,每年只需要少数的参数修改,就可以实现高达200%+的收益,今天就要为大家带来这个神奇的策略中的一种,EMV(简易波动策略)策略。
简易波动(Ease of Movement Value)反映的是价格、成交量、人气的变化,它是一种将价格与成交量变化相结合的技术,它通过衡量单位成交量的价格变动,形成一个价格波动指标。当市场人气聚集,交易活跃时提示买入信号;当成交量低迷,市场能量即将耗尽时提示卖出信号。
简易波动EMV根据等量图和压缩图的原理设计而成,它的核心理念是:市场价格仅在发生趋势转折或即将转折时,才会消耗大量能量,外在表现就是成交量变大。当价格在上升的过程中,由于推波助澜的作用,不会消耗太多的能量。虽然这个理念与量价同升的观点相悖,但的确有其独特的地方。
如果大家不相信的话,我们来使Javascript语言做一个试试看。
javasript
function main(){
var emvList = []
var isFirst = true
var PreBarTime = 0
var symbol = 'SA888'
$.CTA(symbol, function(st) {
var r = st.records
if(isFirst){
PreBarTime = r[r.length - 2].Time
isFirst = false
}else {
if(PreBarTime != r[r.length - 2].Time){
PreBarTime = r[r.length - 2].Time
//指标计算 交易执行
var A = (r[r.length - 2].High + r[r.length - 2].Low)/2
var B = (r[r.length - 3].High + r[r.length - 3].Low)/2
var C = r[r.length - 2].High - r[r.length - 2].Low
var EMV = (A - B) * C / r[r.length - 2].Volume * 1000
emvList.push(EMV)
var smaEMV = TA.SMA(emvList, period)
var buySignal = smaEMV[smaEMV.length - 2] < N1 && smaEMV[smaEMV.length - 1] > N1
var sellSignal = smaEMV[smaEMV.length - 2] > N2 && smaEMV[smaEMV.length - 1] < N2
if (st.position.amount <= 0 && buySignal) {
Log("当前持仓", st.position);
return st.position.amount < 0 ? 2 : 1
} else if (st.position.amount >= 0 && sellSignal) {
Log("当前持仓", st.position);
return st.position.amount > 0 ? -2 : -1
}
}
}
})
}
策略的原理基于EMV指标和其简单移动平均线的计算,定义了一个基本的交易策略:当EMV指标突破其买入阈值时,执行买入操作;当EMV指标跌破卖出阈值时,执行卖出操作。
这里面有三个参数,分别是EMV的窗口长度,买入信号EMA阈值和卖出信号EMA阈值。话说ChatGpt由1750亿个参数就可以模拟众生,今天我们就来试试调试这三个参数来实现年化200%+的梦想。
因为该策略是根据突破点位不断地进行开平仓的操作,所以这里我们选择使用CTA框架函数进行交易的操作,该函数接受一个参数 symbol 和一个回调函数,回调函数的参数是一个表示交易状态的对象 st,st包含了期货合约的综合数据,包括k线,账户和仓位等信息。
这里首先,我们定义三个变量,分别是一个空数组 emvList,用于存储计算出的 EMV指标;isFirst,代表是否是策略的起始状态;preBartime,表示先前K线的时间。
将变量 r 赋值为 st.records。因为我们EMV指标使用到了volume成交量这个数据,而只有当一根k线真正的走完,这一k线周期的成交量才算固定。所以这里我们建立了一个类似pine语言的收盘价模型。其实我们前面也用到过,这里我们正式介绍下它的名字。根据倒数第二根k线的时间戳是否更新,我们来确定是否进行交易指标的计算和具体交易操作的执行。
具体的来说,在策略起始状态,isFirst为真,将preBartime定义为倒数第二根k线的时间戳;然后伴随策略进度,判断最新的k线是否更新。在判断时间戳更新后,首先更新preBartime,然后:
第一步计算倒数第二根k线的最高价和最低价的平均值作为 A 值;第二步计算倒数第三根k线的最高价和最低价的平均值作为 B 值;然后使用计算倒数第二根k线的价格范围,也就是最高价减去最低价作为 C 值;最后根据公式计算 EMV 值,并将计算出的 EMV 值添加到 emvList 数组中。
利用技术分析库(TA)中的简单移动平均线内置函数(SMA),计算 emvList 数组的简单移动平均线值,存储在变量 smaEMV 中。这样呢,可以对噪音信号做了一些平滑。
信号计算完毕,接着就要根据策略判断买入和卖出信号:如果倒数第二个 smaEMV值小于 N1 但是最新的smaEMV值大于 N1,表示突破买入阈值,设置 buySignal 为真,发出买入信号;与此相反,如果倒数第二个移动平均线值大于 N2 并且最新的移动平均线值小于 N2,表示跌破卖出的阈值,那么设置 sellSignal 为真,发出卖出信号。
根据买入和卖出信号以及持仓状态进行决策,这里我们很熟悉了,根据CTA框架的特点,使用持仓量判断return的数值:多头信号下,持仓为空,return 1;如果持有空仓,首先平掉空仓,再开多仓,return 2。空头信号出现的时候,相反的逻辑处理就可以了。
这样我们的策略就定义完了,确实不复杂,下面我们来看策略的效果。在回测页面,我们设置原始的参数,均线周期为15,买入阈值和卖出阈值都设置为0,回测时间定为2022年完整的一年,跑一下看下结果。可以看到,使用1小时为策略周期,针对于纯碱主力合约,我们的策略收益为10252,而这仅仅是一手的收益。当然这并不是这个策略的极限,不知道调一下参数是否会有别的惊喜呢。
这里设置均线周期调参最小值5,最大值是30,步长是1;N1最小值是0,最大值是2,步长0.1;N2最小-2,最大0,步长一样。优宽平台支持多个参数同时调参,当然时间会稍微长一点,大家也可以按顺序分别调整参数。最后我们看下我们最优的参数结果,窗口周期为29,买入阈值为0.2,卖出阈值为-1.3,让我们看下效果。
年化收益变为21101,确实很神奇。简简单单三个参数的改变,就可以实现年化率百分之二百+,但是这个策略一直有效吗,让我们把时间换为今年的时间段,2023年1月到2023年9月间,可以看到收益变为了-4500。
人生的大起大落真是刺激,本以为找到了可以一本万利的无风险策略,结果却是黄粱一梦。看来“All models are wrong, but some are useful”是真实的。问题究竟出在哪里呢?
答案是过拟合。过拟合(overfitting)是在,在机器学习模型中,模型过于强调训练数据的细节和噪声,结果导致它在新的、未见过的数据上的泛化性能较差。也就是说,过拟合本质上是对训练数据集过度拟合,使得模型在测试数据上表现不佳。有没有觉得我们在第一次调参后的数据过于绝对了,买入阈值为0.2,卖出阈值为-1.3,这意味着这一年确实是多头主导的年份,小小的跌幅不用怕,只有达到-1.3巨额的跌幅才是真的应该转变空头方向。并且2022年的价格最低点是2162,最高点是3268,价格差距1000点。
在2023年,如果经常做期货的朋友都知道3月15号,纯碱开始大跌,一直从2800点跌到1500点左右,算是价格腰斩。也就是从那时开始,可以看到我们的策略收益开始由正转负,一致持续负收益到5月31号,这也是我们公认的2023年商品期货走势的拐点。从这个时候起,我们的策略收益也开始逐步回血,到9月份已经收回了13000元左右的回撤。
所以呢,过拟合并不是仅仅出现在使用过多参数的模型中,使用少量的参数也可以出现过拟合的现象。在量化交易中,过拟合是一个普遍存在的问题。有些策略即使在历史数据上表现得很好,但在实际市场上表现不佳,因为过度适应了历史数据的特点而失去了泛化能力。我相信这些比较理论性的解决方法,大家都听说过很多次。但是,最重要的还是,我们要去了解这个市场,这个世界唯一不变的就是一直在改变,想要一个永久万能的模型也是不太可能的,即使一个专业工业性的大模型,三到五个月的时间也会进行一次更新和淘汰,所以对于我们来说,需要通过实践和不断地试验,针对于当前市场和当前品种,找出局部的最优解已经很好了,大家加油。
视频参考链接:
《EMV指标策略》
11.商品期货Orderflow订单流策略(上)
在电子交易兴起之前,要想了解成交量是如何在K线上分配的是一件很难的事情。如今科技的发展给带给我们一种前所未有的市场分析方式,大部分软件都已经支持以Order Book方式向我们提供价格和成交量数据,用来了解价格上涨或下跌背后的原因。
在二级交易市场中,影响价格变化的因素是纷繁复杂的,并且每一个因素影响价格变化的权重都不一样,以至于很难从传统技术分析图形中推导出价格行为,因为相对于价格和成交量来说,技术分析图形相对抽象和滞后。而OrderFlow订单流工具的横空出世,使得市场更加通透。
OrderFlow订单流分类
OrderFlow订单流有很多种分类,包括:
成交明细(Sales Details):这个我们最为熟悉,它是期货市场中一种记录每笔交易细节的信息报告。它提供了每个合约的交易价格、交易量以及订单执行的时间戳等详细数据。通过成交明细,我们可以获得更细致的市场交易情绪和活动信息,有助于进一步分析市场的买卖力量、支撑阻力区域以及潜在的价格变动趋势。我们可以观察成交明细来确定市场的流动性、看涨或看跌力量,以及潜在的支撑和阻力水平。此外,成交明细还可以用于验证交易策略、识别潜在的市场信号,并帮助交易者做出更明智的交易决策。

市场深度数据:这个我们也比较熟悉。它是用来显示市场上委托买单和卖单的价格和数量信息的一种数据。它提供了有关特定交易品种的当前买卖订单的详细情况,可以展示出不同价格水平上的委托量。通过观察市场深度数据,我们可以了解到当前市场上的买卖力量分布情况,以及在不同价格水平上的供需情况。这对于市场分析和制定交易策略非常重要,因为它可以帮助交易者判断支撑位和阻力位的价格水平,并预测市场的可能走势。但是市场深度数据是市场中尚未成交的订单流数据,这些数据就是各交易软件中常见的五档行情(如上图所示)。做过交易的都知道,这些数据通常变化无常,有时候突然来一个大单,又突然凭空消失,存在很强的欺骗和诱导作用。

成交量分布(VP):成交量分布(Volume Profile,简称VP)是一种用于衡量市场交易活跃度和价格区间的市场分析工具。它通过统计和显示特定时间段内的各个价格水平上的成交量信息。成交量分布通常以柱状图的形式展示,横轴表示价格区间,纵轴表示成交量。每个价格区间上的柱子高度表示该价格区间上的成交量。通过这种方式,交易者可以清晰地看到市场上不同价格水平的成交活动情况。根据成交量分布图,我们可以获得支撑位和阻力位预测区间;其次可以了解交易活跃度和流动性情况。但是呢,成交量分布具有一定的滞后性,成交量分布图是基于历史数据生成的,只能反映过去一段时间的市场情况。随着时间的推移,市场情况可能发生变化,导致成交量分布图的有效性降低。

足迹图(Footprint Chart):足迹图(Footprint Chart)是一种用于显示市场交易活动的特殊图表类型。它提供了更为详细和精确的成交价与成交量之间的关系,帮助交易者更好地理解市场的买卖压力以及价格变动。在足迹图中,每个价格水平都有一个指示器来表示该价格上的成交量。通常,买入成交量位于价格上方,卖出成交量位于价格下方。这种显示方式可以让我们看到市场上的买盘和卖盘活动,并评估哪一方力量更强。

足迹图可以提供以下关键信息:
成交量分布:足迹图显示了不同价格水平上的成交量分布情况。通过观察不同价格区间上的成交量,交易者可以了解到相应价格水平上的买入和卖出活动强度。
买卖压力:足迹图可帮助交易者判断买卖压力的分布情况。如果在特定价格水平上的买入成交量较高,可能表示买盘压力较大,可能导致价格上涨;而若卖出成交量较高,则可能意味着卖盘压力较大,可能导致价格下跌。
交易情绪:足迹图可以提供一定的交易情绪指示。例如,如果看到大量买盘活动并且价格上涨,可能表示市场参与者对价格有较强的偏好和买入需求。相反,如果出现大量卖盘活动且价格下跌,可能表示市场参与者更倾向于卖出,并且情绪较为悲观。
足迹图也是有一些缺点的,例如足迹图展示了每个价格级别上的买卖交易量,其中可能包含大量的小额交易,这些交易可能是市场噪音或者无意义的交易。这可能导致交易者对信息的过度解读或者误判市场趋势。
以上就是orderflow四种数据的介绍。其实在市场中,充斥着各种各样的信息,好的坏的、真的假的,这些信息就像荆棘一样错纵交织,导致这些消息很难被理解,很难用正确的逻辑推导。作为散户,资金和消息的弱势群体,在很多情况下只能被动的接受主力的安排,所以我们需要使用技术的工具抽取出价格波动的真正原因。
作为散户的我们使用肉眼观察Orderflow数据,获得的信息在一定程度上是片面和不完整的,因此做出的交易决策可能缺乏完整的依据。幸好,有了量化分析的帮助,我们可以从宏观视角去捕捉交易的每一刻变化,进而描绘出整体的趋势脉络。因此,本节课程,我们将要学习在优宽量化交易平台,尝试探索实现orderflow数据构建和实施交易策略。
构建成交量分布图
本节课呢,让我们来构建这个成交量分布图。在JavaScript语言中,并没有内置函数可以帮助我们快速获取结果。但是,我们可以利用目前所学的知识,手动编写代码来实现这个功能。
javascript
var chart = {
__isStock: false,
title: {
text: '成交量分布图',
},
xAxis: {
title: {
text: 'Price'
}
},
yAxis: {
title: {
text: 'Volume'
}
},
series: [{
type: 'column',
data: []
}]
}
function main() {
var isFirst = true
var priceCount = {}
var preVolume = 0
while (true) {
if(exchange.IO('status')){
LogStatus('已连接CTP', _D())
exchange.SetContractType('SA888')
var ticker = exchange.GetTicker()
var price = ticker.Last
var volume = ticker.Volume
if (volume < preVolume) {
Log('更新时间到')
priceCount = {}
isFirst = true
preVolume = 0
}
if(isFirst){
preVolume = volume
isFirst = false
}else{
if (!priceCount.hasOwnProperty(price)) {
priceCount[price] = volume - preVolume
} else {
priceCount[price] += volume - preVolume
}
preVolume = volume
}
var pricecountlist = []
for (var key in priceCount) {
pricecountlist.push([parseInt(key), priceCount[key]])
}
chart.series[0].data = pricecountlist
Chart(chart)
Sleep(1000)
}else{
LogStatus('未连接CTP', _D())
Sleep(1000)
}
}
}
来到策略编辑页面。第一步,我们来构建chart对象,这里将x轴和y轴分别命名为Price和Volume,series中将图表类型定义为column柱状图,data定义为空值,我们将在主函数进行填充。
接着我们来定义主函数,还是我们的固定框架,大家在写策略的时候,可以先把这个框架列好,然后进行填充。这里设置的合约是螺纹钢的主力合约。我们前面的准备工作已经完成了,现在我们来想下,我们应该获取什么数据,向图表里进行填充。K线数据吗,K线数据反映的是一段时间内价格的汇总变化,所以无法反应每一个价格具体的成交量。所以这里选择ticker数据,ticker数据可以反应价格的具体变化以及相对应的成交量。好了,目标数据我们选好了。但是直接拿ticker数据向图表里填充显然是不合适的。因为Ticker数据中的成交量是今日开盘累计的成交量,所以对于某一价格的成交量对于需要使用本时刻的成交量减去上一时刻的成交量;其次我们想要呈现的成交量分布是某一个具体开盘日的成交量数据,所以一天过后,我们需要及时的对储存的数据进行清空。
带着这两个问题,继续我们的代码编写,在while循环体外,首先设置三个变量,isFirst,是否第一个获取ticker数据;priceCount,用来储存价格成交量键值对;preVolume,先前ticker的成交量;在while循环中,获取当前ticker数据,然后获取最新成交价作为price,成交量作为volume。接着就要进行数据的填充了,如果是第一次获取ticker数据,先前的preVolume是没有的。所以将preVolume定义为第一个ticker的成交量,然后将isFirst设置为false。然后具体的价格和对应的成交量可以正常更新了。这里的priceCount我们用来储存价格和数量。如果priceCount没有某一price键,我们首先priceCouut添加该键,并将该键的值定义为本时刻的volume减去前一时刻的preVolume。如果存在该键的话,进行成交量的累加。一个ticker数据更新完成后,将preVolume定义为最新的volume。
注意:因为策略开盘的时候,ticker返回的成交量都是巨大的,不能真实反应某一价格的成交量,所以我们决定舍弃第一个ticker的数据。
成交量数据处理完毕了,接下来我们来看怎么进行数据的重置。volume是一个累加的数值,所以最新时刻的volume一定是大于等于上一时刻的,当最新时刻volume小于上一个时刻的时候,证明volume更新了,这里我们提升“更新时间到”,然后将priceCount,isFirst和preVolume进行重置。
好了,目前我们两个问题都解决了。接下来,我们就要将获取到的数据进行一下格式的整理,因为column需要获取的格式是数组的形式,每个数组的元素包含价格和对应的成交量,我们创建pricecountlist空的数组,然后将priceCount里的键值对进行添加,这里的键是字符型的,要进行一下转换。
最后将chart.series[0].data定义为pricecountlist,使用Chart进行画图。
我们来看下回测的结果,定义时间横跨两个交易日,这里需要使用的是实盘级tick。点击回测。螺纹钢的开盘时间是晚上九点,但是第一个ticker数据返回是8点59分,日志信息显示晚上8点59分,准时进行了数据的更新。这里的成交量分布图看起来是一个完美的正态分布,对应K线图,确实符合螺纹钢最近的震荡行情。所以,我们利用我们的一些统计学知识,在前提判断行情为震荡行情时,使用成交量最高的价格作为均线,然后使用95%的置信区间作为正常的波动范围,如果价格下穿或者上穿这个范围,我们进行相应的开多或者开空的交易操作。当然,并不是每日的交易日都是这样符合正态分布的,有时候会呈现偏态或者多峰分布,我们需要具体问题具体分析。
对统计学感兴趣的小伙伴,一看到这个分布一定会有很多交易策略的想法迸发出来,大家可以动手尝试一下,当然也可以留言评论区,我们呢,也会热心的帮你实现,我们一起来实现共同进步!
视频参考链接:
《CTA策略之orderflow订单流策略(1)》
12.商品期货Orderflow订单流策略(下)
上节课我们学习了Orderflow订单流之成交量分布的画图方法,本节课我们来继续学习足迹图的画法和相关的交易策略。
传统的K线有开盘价、最高价、最低价、收盘价等四个价格,然而K线仅仅代表这个时间段内的价格变化情况,比如小时线代表了一个小时内的价格变化情况。而足迹图则是根据Tick数据,提供了K线时间段内发生的具体细节,包括K线每个价格的多头和空头成交量,可以很清晰的看见在这一根k线柱中,具体价格多空成交的订单。
例如我们来看这个足迹图,每一根K线都有一个独立的Delta结构数据,在Delta结构数据方框中,最上方是这根K线总的成交量,最下方是这根K线所有多头成交量和空头成交量的差,中间则是这根K线每个价格多头成交量和空头成交量数据。通过将K线拆分成更详细的可视化数据,从而帮助我们理解价格变动的机制。

构建足迹图
今天我们首先利用代码实现一个足迹图(Footprint Chart)K线图表。
首先提醒一下大家哈,这部分的画图代码确实比较复杂,尤其是图表对象的构建,不过大家也不用太纠结。如果大家对图表对象中哪些属性不太清楚,可以询问chatGPT直接获取答案。当我们需要足迹图的时候,我们可以直接拿过来使用。不过这里重要的是,我们要理解足迹图中,对于ticker数据的处理,其中关键的是买方数量和卖方数量的获取逻辑,这样有助于我们更深入地理解价格和买卖数量的变动关系。
因为足迹图K线图表画图是比较复杂的,这个策略代码分为两个部分。第一部分是足迹图的构造函数对象FootPrintConstructer,第二部分是main主函数。
首先我们来进行第一部分FootPrintConstructer的编写,它接受一个参数period(代表K线周期)。在函数内部,创建了一个空对象self,并将该对象返回。这样,在使用FootPrintConstructer构造函数创建新对象时,可以通过访问对象的属性来操作和修改对象的数据。
接着我们创建self的属性和方法。首先我们利用Chart函数创建了一个图表对象,并将其赋值给self.c。在Chart图表中,第一个chart用于设置图表的一些基本样式和行为;plotOptions用于设置图表中绘图选项;tooltip用于设置图表的工具提示样式;series用于设置图表的数据系列;yAxis用于设置图表y轴(垂直方向)的样式和选项;navigation用于设置图表的导航按钮样式和选项。
在创建完图表后,代码调用self.c的reset方法来清空图表数据。
接下来是我们的重点,对于ticker数据的处理和图表数据的填充。首先初始化一些变量。其中,self.pre用来记录上一个数据,初始值为null;self.records用于存储K线数据,初始为空数组;这里定义feed方法,用于处理传入的数据,并更新图表和记录。首先,判断是否有上一个数据。如果self.pre为空,则将当前ticker赋值给self.pre。
这里为什么要使用self.pre先前数据呢,主要是为了判断买卖的动作。首先定义action为空。然后,根据最新数据的Last价格与上一个ticker数据的买价和卖价进行比较,确定买卖动作。如果最后价格大于等于上一个ticker卖价sell,则标记action为'buy';如果最后价格小于等于上一个ticker的买价buy,则标记action为'sell';
如果先前数据存在两跳以上间隙的情况下,当前last数据位于两跳的间隙,那么前两种条件就是不满足的。我们需要使用当前ticker的买价和卖价,如果当前数据最新价格大于等于最新的sell卖价,action定义为buy;如果最新价格小于当前买价,action定义为sell。当然,当前的买价和卖价也是可能存在两跳以上间隙的,如果最新的价格位于中间,那么这两个条件也是不符合的,我们定义action为both。表示同时存在买入和卖出。
当然我们也要考虑每日开盘,volume数据重置的情况 ,当判断最新的volume小于Volume的情况,需要把先前的pre.Volume设置为0。
当判断好action动作以后,接下来我们就要来判断amount的属性了。如果action标记不为空字符串,并且amount大于0,证明有交易正在发生,我们就要进行相应的逻辑处理。首先定义epoch变量,通过计算当前的时间戳并取整,这里是向下取整,我们要统计一根完整k线内ticker数据的详细变化。然后定义bar和pos变量,分别定义为null和undefined,这两个变量分布是我们画图需要的数据,和图表数据的更新参数。
如果records长度为0或者最新records时间戳小于epoch,代表k线更新,这个时候需要重置bar数据,将bar里的time属性更新为epoch,data属性清空,高开低收定义为当前最新价,然后把bar添加到records数组中。
重置bar以后,在当前k线进行的时候,我们需要对bar中的一些属性进行更新,将bar定义为当前k线数据,然后更新当前bar的最高价,最低价和收盘价,这里的pos是更新参数,在以前chart画图函数我们有提到过,当定义为-1的时候,表示最新的k线数据还没有固定,这时候就要不断的更新最新的k线数据。
这里的bar数据包括时间戳epoch,高开低收,表示k线的数据这里定义完成了。但是汇总的总体变化不能反应在这一根k线中,具体的ticker变化,以及对应的每个ticker价格对应的买方和卖方的成交量,我们将定义k线中具体ticker对应的成交量了,就是k线中这里要显示的数据,一个价格后面,是对应的卖方数量和买方数量。这个价格就是每一个ticker的最新价,ticker.Last,然后每一个最新价格对应的买方数量,和卖方数量,我们将这个数据存储在bar的data属性中。
所以这里我们首先进行初始化,如果bar.data[ticker.Last]未定义的话,为bar.data[ticker.Last]添加属性,将buy和sell数量分别定义为0。
还记得我们前面定义的action属性吗,这个时候根据价格变化判断出来的action,我们使用到这里判断amount的具体属性。这里有一种特殊情况,当action为both的时候,将买入和卖出的数量分别定义为当前的amount除以2。然后我们统计sellVol卖方数量,和buyVol数量,初始值定义为0,接着使用for循环进行累加获取各自方向的总和。这里使用tips保存生成的文本内容,这里我们来定义第一行内容,当前k线发生的总交易量,使用买方总量加上卖方总量。
接着我们来定义各个价格级别上的买卖对比信息,通过Object.keys(bar.data)将bar对象中的键放到一个数组中。然后,使用.sort()对数组进行排序,并使用.reverse()颠倒数组中的顺序,就是倒序排列。接下来,使用.forEach()方法对数组中的每个元素执行一个函数操作。这个函数接受一个参数p,表示当前遍历到的数组元素。在函数内部,通过p作为键访问bar.data对象,获取对应键p下的sell和buy属性的值,分别赋给变量pSell和pBuy。
接着,使用if语句判断pSell和pBuy的大小关系。如果pSell大于pBuy,说明卖方数量大于买方数量,表示价格下跌,将箭头符号' ▼ '赋值给变量arrow。如果pSell小于pBuy,说明买方数量大于卖方数量,表示价格上涨,将箭头符号' ▲ '赋值给变量arrow。如果pSell等于pBuy,表示买卖数量相等,将菱形符号' ♦ '赋值给变量arrow。
这样就可以将每个价格级别p、买方数量pBuy、箭头符号arrow和卖方数量pSell拼接成一行字符串,并添加到tips变量中。我们还要显示买方数量和卖方数量的差值,通过这一行进行定义。这样呢,我们的足迹图数据就定义完成了。最后添加到self.c图表中,这里的数据包括k线图的时间戳,高开低收,还有足迹图数据tips,最后的pos代表图表的更新参数。当一个ticker数据处理完成以后,需要将当前ticker定义为self.pre。这样足迹图构造函数FootPrintConstructer就定义完成了。
在主函数中,搭建固定的框架,然后在while循环体外,使用FootPrintConstructer构造函数,创建footPrint对象,参数填写60000,也就是一分钟。在while循环体内,设置合约,获取ticker数据,如果成功获取到Tick数据,使用footPrint的feed方法开始处理数据,创建足迹图。在模拟回测的时候,这里要选择实盘级tick,不然这里出现的足迹图是不能反应真实的tick成交量的。
足迹图交易策略
下面我们来看使用足迹图进行交易的策略。理论上成交量是先行于价格的,买卖双方成交量的多少是原因,价格的变化是结果。所以在大多数情况下,量增价涨是一种常态,大部分K线都保持这种规律,而买卖均衡与价格背离却是一种偶然。接下来我们利用买卖均衡与价格背离这种现象,来尝试进行交易策略的构建。以下是策略逻辑:
- 多头开仓:如果当前无持仓,并且收盘价大于开盘价,但是主动买量小于主动卖量
- 空头开仓:如果当前无持仓,并且收盘价小于开盘价,但是主动买量大于主动卖量
- 多头平仓:如果有多头持仓,并且利润超过100(止盈),或者损失超过-100(止损)。
- 空头平仓:同样如果有空头持仓,当利润超过100,或者损失超过-100进行平仓。
我们在原有的代码基础上继续进行补充就可以,首先需要在feed方法上面定义arr空数组,lastTime为0;然后在进行图表绘图以后,使用push方法将一个对象添加到数组arr中,该对象包含了开盘价、收盘价和买卖量差。如果数组arr的长度大于2,则使用arr.shift()方法移除数组的第一个元素,以保持数组的长度为2。使用exchange.GetPosition()方法获取当前持仓信息,并初始化持仓量holdAmount和盈利金额profit的变量。
如果存在持仓信息,则根据持仓类型(多头或空头),更新持仓量holdAmount的值为正值还是负值,并将盈利金额profit赋值为当前持仓的盈利金额。如果当前K线的时间与上一次记录的时间不同,则表示进入了新的K线周期。
首先将lastTime定义为bar.time,然后根据数组arr中保存的最新数据,获取买卖量差volDiff,开盘价lastOpen、收盘价lastClose,利用收盘价减去开盘价的差作为价格差priceDiff。
接下来就到了交易信号的判断和具体交易操作的执行了,这里我们使用交易类库,设置单品种管理对象p:
- 如果没有持仓且出现正的价格差和负的买卖量差,则打印信息并执行多头开仓操作。
- 如果没有持仓且出现负的价格差和正的买卖量差,则打印信息并执行空头开仓操作。
- 如果持有多头仓位且盈利金额超过100或亏损金额超过-100,则打印信息并执行多头平仓操作。
- 如果持有空头仓位且盈利金额超过100或亏损金额超过-100,则打印信息并执行空头平仓操作。
javascript
/*backtest
start: 2023-09-19 09:00:00
end: 2023-09-26 15:00:02
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
mode: 1
args: [["symbol","rb888"]]
*/
var p = $.NewPositionManager()
var FootPrintConstructer = function(period) {
var self = {} // 创建一个对象
self.c = Chart({ // 创建Chart图表
chart: {
zoomType: 'x', // 缩放类型,设置为'x'表示只能在横向上进行缩放
backgroundColor: '#272822', // 背景颜色
borderRadius: 5, // 边框圆角大小
panKey: 'shift', // 按住shift键可平移图表
animation: false, // 是否开启动画效果
},
plotOptions: {
candlestick: {
color: '#00F0F0', // 蜡烛图的颜色
lineColor: '#00F0F0', // 蜡烛图线条的颜色
upColor: '#272822', // 上涨蜡烛图的颜色
upLineColor: '#FF3C3C' // 上涨蜡烛图线条的颜色
},
},
tooltip: {
xDateFormat: '%Y-%m-%d %H:%M:%S, %A', // x轴(时间)的格式
pointFormat: '{point.tips}', // 每个数据点的显示格式,{point.tips}表示从数据点的tips属性获取内容
borderColor: 'rgb(58, 68, 83)', // 边框颜色
borderRadius: 0, // 边框圆角大小
},
series: [{
name: exchange.GetName(), // 获取交易所名称
type: 'candlestick', // 蜡烛图
data: [] // 空数组
}],
yAxis: {
gridLineColor: 'red', // 网格线颜色
gridLineDashStyle: 'Dot', // 网格线样式,Dot表示虚线
labels: { // 标签样式,文本颜色
style: {
color: 'rgb(204, 214, 235)'
}
}
},
navigation: { //设置buttonOptions,包括按钮大小、符号的大小和位置、符号的边框宽度等
buttonOptions: {
height: 28,
width: 33,
symbolSize: 18,
symbolX: 17,
symbolY: 14,
symbolStrokeWidth: 2,
}
}
})
self.c.reset() // 清空图表数据
self.pre = null // 用于记录上一个数据
self.records = []
arr = []
lastTime = 0
self.feed = function(ticker, symbol) {
if (!self.pre) { // 如果上一个数据不为真
self.pre = ticker // 赋值为最新数据
}
var action = '' // 标记为空字符串
if (ticker.Last >= self.pre.Sell) { // 如果最新数据的最后价格大于等于上一个数据的卖价
action = 'buy' // 标记为buy
} else if (ticker.Last <= self.pre.Buy) { // 如果最新数据的最后价格小于等于上一个数据的买价
action = 'sell' // 标记为sell
} else {
if (ticker.Last >= ticker.Sell) { // 如果pre.Buy和pre.Sell存在两跳间隙的情况下,ticker.Last处于中间,最新数据的最后价格大于等于最新数据的卖价
action = 'buy' // 标记为buy
} else if (ticker.Last <= ticker.Buy) { // 如果pre.Buy和pre.Sell存在两跳间隙的情况下,ticker.Last处于中间,最新数据的最后价格小于等于最新数据的买价
action = 'sell' // 标记为sell
} else { //如果ticker.Buy和ticker.Sell存在两跳间隙的情况下,ticker.Last处于中间
action = 'both' // 标记为both
}
}
// reset volume 重新开盘重置
if (ticker.Volume < self.pre.Volume) { // 如果最新数据的成交量小于上一个数据的成交量
self.pre.Volume = 0 // 把上一个数据的成交量赋值为0
}
var amount = ticker.Volume - self.pre.Volume // 最新数据的成交量减去上一个数据的成交量
if (action != '' && amount > 0) { // 如果标记不为空字符串,并且amount大于0,证明有交易正在发生
var epoch = parseInt(ticker.Time / period) * period // 计算K线时间戳并取整
var bar = null
var pos = undefined //画图更新参数
if (
self.records.length == 0 || // 如果K线长度为0或者最后一根K线时间戳小于epoch
self.records[self.records.length - 1].time < epoch
) {
//Log('重置bar')
bar = {
time: epoch,
data: {},
open: ticker.Last,
high: ticker.Last,
low: ticker.Last,
close: ticker.Last
}
self.records.push(bar) // 把bar添加到records数组中
} else {
//Log('更新bar')
bar = self.records[self.records.length - 1] // 当前k线数据
bar.high = Math.max(bar.high, ticker.Last) // 当前K线的最高价与最新数据最后价格的最大值
bar.low = Math.min(bar.low, ticker.Last) // 当前K线的最低价与最新数据最后价格的最小值
bar.close = ticker.Last // 最新数据的最后价格
pos = -1
}
if (typeof bar.data[ticker.Last] === 'undefined') { // 如果数据为空
bar.data[ticker.Last] = { // 添加属性
buy: 0,
sell: 0
}
}
if (action == 'both') { // 如果标记等于both
bar.data[ticker.Last]['buy'] += amount/2 // buy累加
bar.data[ticker.Last]['sell'] += amount/2 // sell累加
} else {
bar.data[ticker.Last][action] += amount // 标记累加
}
var sellVol = 0
var buyVol = 0
for (var i in bar.data) {
sellVol += bar.data[i].sell
buyVol += bar.data[i].buy
}
tips = '<b>◉ ' + (sellVol + buyVol) + '</b>'
Object.keys(bar.data) // 将对象里的键放到一个数组中
.sort() // 排序
.reverse() // 颠倒数组中的顺序
.forEach(function(p) { // 遍历数组
pSell = bar.data[p].sell
pBuy = bar.data[p].buy
if (pSell > pBuy) { // 卖方数量大于买方数量,下跌
arrow = ' ▼ '
} else if (pSell < pBuy) { // 买方数量大于卖方数量,上涨
arrow = ' ▲ '
} else {
arrow = ' ♦ '
}
tips += '<br>' + p + ' → ' + pBuy + arrow + pSell
})
tips += '<br>' + '<b>⊗ ' + (buyVol - sellVol) + '</b>'
self.c.add( // 添加数据
0, {
x: bar.time,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
tips: tips
},
pos
)
arr.push({
'open': bar.open,
'close': bar.close,
'diff': buyVol - sellVol
})
if (arr.length > 2) {
arr.shift()
}
var position = exchange.GetPosition()
var holdAmount = 0
var profit = 0
if (position.length > 0) {
if (position[0].Type == PD_LONG || position[0].Type == PD_LONG_YD) {
holdAmount = position[0].Amount
} else {
holdAmount = -position[0].Amount
}
profit = position[0].Profit
}
if (bar.time != lastTime) {
lastOpen = arr[0].open
lastClose = arr[0].close
diff = arr[0].diff
lastTime = bar.time
priceDiff = lastClose - lastOpen
volDiff = diff
if (holdAmount == 0 && priceDiff > 0 && volDiff < 0) {
Log('多开')
p.OpenLong(symbol, 1)
}
if (holdAmount == 0 && priceDiff < 0 && volDiff > 0) {
Log('空开')
p.OpenShort(symbol, 1)
}
if (holdAmount > 0 && (profit > 100 || profit < -100)) {
Log('多平')
p.CoverAll()
}
if (holdAmount < 0 && (profit > 100 || profit < -100)) {
Log('空平')
p.CoverAll()
}
}
}
self.pre = ticker // 重新赋值
}
return self // 返回对象
}
function main() {
var footPrint = FootPrintConstructer(60000) // 创建一个对象
while (true) { // 进入循环模式
if(exchange.IO("status")){
LogStatus("行情和交易服务器连接成功, " + _D())
exchange.SetContractType(symbol)
var ticker = exchange.GetTicker() // 获取交易所Tick数据
if (ticker) { // 如果成功获取到Tick数据
footPrint.feed(ticker, symbol) // 开始处理数据
}
Sleep(3000)
}else{
LogStatus("正在等待与交易服务器连接, " + _D())
Sleep(3000)
}
}
}
这样就完成了交易策略代码部分的编写,我们使用螺纹钢主力合约回测一下,这里设置回测的日期为最近的一周。根据回测的结果显示,我们一共进行了55笔交易,其中平仓盈亏是630元,但是手续费占据了很大部分,最后的预估收益为161元。
以上呢,就是足迹图的构建和使用足迹图进行的交易策略。其实足迹图可以使用的地方还有很多,大家可以尝试探索一下,有疑问的话,可以留言评论区,我们会热心解答!
视频参考链接:
《CTA策略之orderflow订单流策略(2)》
13.商品期货仓位管理工具:马丁策略
今天我们不谈策略,来讲一下商品期货交易中的仓位管理。初入期货市场的我们,总是希望把把盈利,开始的时候,总是以轻仓试探,当期货软件的数字一旦变红,我们马上就卖出;当尝到盈利甜头的我们,开始后悔为什么只下了一手,于是我们开始加大杠杆,梭哈进场,这时候的盈利和亏损都是成倍放大的。运气不好,亏损出现。这时候的我们又开始后悔,为什么要梭哈入场,明明可以轻仓,拥有更多时间等待价格回归,到如今我们的选择只有两条,割肉或者补仓。这时候,仓位管理的重要性就凸显出来。
毕竟每个量化交易系统不是完美的,可以对每一笔未来的方向有着准确的判断。不同的交易策略适用于不同的行情,比如上节课我们讲到的网格策略,就是特别适合于震荡的行情,当单边行情出现的时候,如果我们不及时止损,结果可能只有爆仓。所以我们的交易系统在按照交易逻辑运行策略的同时,还需要对资金和仓位做一些更好的准备和管理。
最近在知乎上看到这样的提问,大概率加上仓位管理,可以保持长期盈利吗?题主通过统计分析,构建出了一个正期望策略进行开仓的同时,准备使用马丁策略管理仓位,回答的答案也很是直白,马丁策略的最终结果是爆仓,马丁策略永远不要使用。今天呢,我们就来了解一下这个神奇的仓位管理工具,马丁策略。
马丁格尔策略最早起源于18世纪的法国,不过那个时候它多被用于赌桌上面,之后没过多久就在欧洲广为人知。在理论金融学里面,它有一个更熟悉的名字,鞅。理论上这是一种胜率接近于100%的策略,直到现在在很多交易市场都有它的身影,如:外汇、期货及数字货币市场。然而它真地是万能的吗?我们首先来了解下它的原理。
其实马丁格尔既不是交易策略,也不是交易机制,而是一种资金管理方式。其原理很简单:交易者每次亏损一定额度,就把下一次下单量加倍,直到盈利时把下单量恢复到初始值。如此一来,只需要盈利一次,不仅可以收回之前的亏损,还能获得第一次下单量的收益。显而易见这是一个逆势翻倍加仓的资金管理方式。
现假设有一枚正反两面一样重的硬币,不断的抛硬币,出现正面和反面的概率约等于50%,接下来我们用抛这枚硬币打赌,最初的下注金额是1元,如果出现正面赢1元,如果出现反面赔1元。理论上,硬币出现正反的概率是一样的,因为每次出现的结果相互独立不受影响,即50%。
根据马丁策略原理,每次赔钱时就把下注金额调整为上次下注金额的2倍,只需要赢一次就可以挽回之前的所有损失。但当连续亏损时,也将会输得一无所有。如果本金只有10元,第一次下注1元,出现反面亏损1元,账户余额为9元;第二次下注2元,出现反面亏损2元,账户余额为7元;第三次下注4元,出现反面亏损4元,账户余额为3元;这时就没有足够的资金下注了。
期货市场与赌场是不同的,期货的涨跌并不是完全随机赌大小,真实的金融交易市场要比赌场更加复杂。如果将马丁格尔策略用在期货交易中,一旦市场按照反方向趋势行情运行,后面随着行情的发展,头寸翻倍增加会越来越大,风险也随之加大。本节课呢,我们就来使用马丁策略应用于商品期货市场,看看是真的战无不胜还是爆仓离场。
我们首先来编写一个简单的马丁格尔策略,今天我们来使用sublime编辑器进行编写策略。最近有同学联系我们,说好不容易在网页编辑器写好的策略,结果因为忘记保存,结果全部代码丢失。其实呢,优宽也是支持在远程编辑器编写策略,并同步到优宽策略库里的。我们可以这样设置,首先我们需要编写一个策略,可以是空白的,然后保存;接着点击进入,右上角我们下载这个策略到本地;下载完成,这个策略用sublime打开,接着回到网页界面,点击远程编辑,需要安装一个插件。这里我们选择sublime plugin,下载这个插件到本地;然后点击进入sublime的packages,复制进入到库中。然后回到页面,点击更新密钥。复制这个密钥到本地策略的第一行,然后我们就可以在本地编辑器写策略了,点击保存,可以发现同步保存到我们的网页编辑器里了。这样就可以实现远程的策略编辑。
javascript
/*backtest
start: 2023-05-01 00:00:00
end: 2023-08-01 10:01:48
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
*/
var profits = 10
var unit = 1
var bei = 2
function main() {
exchange.SetContractType(symbol)
while (true) {
var depth = exchange.GetDepth();
if (!depth) return;
var ask = depth.Asks[0].Price;
var bid = depth.Bids[0].Price;
var position = exchange.GetPosition()
if (position.length == 0) {
var redom = Math.random()
if (redom < 0.5) {
exchange.SetDirection("sell")
exchange.Sell(bid, unit, "开空")
}
if (redom > 0.5) {
exchange.SetDirection("buy")
exchange.Buy(ask, unit, "开多")
}
}
if (position.length > 0) {
var type = position[0].Type;
var profit = position[0].Profit;
var amount = position[0].Amount;
if (type == PD_LONG || type == PD_LONG_YD) {
if (profit > profits) {
exchange.SetDirection("closebuy")
exchange.Sell(bid, amount, "多头止盈,当前盈利:" + profit)
unit = 1
}
if (profit < -profits) {
unit = unit * bei
exchange.SetDirection("buy")
exchange.Buy(ask, unit, "多头加仓,当前盈利:" + profit)
}
}
if (type == PD_SHORT || type == PD_SHORT_YD) {
if (profit > profits) {
exchange.SetDirection("closesell")
exchange.Buy(ask, amount, "空头止盈,当前盈利:" + profit)
unit = 1
}
if (profit < -profits) {
unit = unit * bei
exchange.SetDirection("sell")
exchange.Sell(bid, unit, "空头加仓,当前盈利:" + profit)
}
}
}
Sleep(1000 * 60 * 60 * 24)
}
}
在策略开头,我们首先定义一些变量:profits表示盈利目标,我们设置为10,当盈利大于10的时候,我们进行止盈,而当盈利小于-10的时候,我们需要进行加仓;至于具体加仓的数量,需要根据初始的交易数量,逐步乘以加仓的倍数,这里设置初始的unit是1,bei表示加仓倍数,是2,表示每次加仓的数量是当前持仓数量的两倍。
在main函数中,首先设置交易合约类型,这里我们设置了外部的参数,方便使用不同的合约进行测试。
进入主循环,获取市场深度信息。
对于马丁开仓的方向,这里我们决定使用抛硬币模拟随机数的方式。如果当前没有持仓(position.length == 0),则根据一个随机数生成的值来决定开仓方向。如果随机数小于0.5,则设置交易方向为卖出(开空),并以当前买一价(bid)卖出指定数量的合约。如果随机数大于0.5,则设置交易方向为买入(开多),并以当前卖一价(ask)买入指定数量的合约。
对于离场,这里我们只有止盈离场,不存在止损离场。当出现盈利为负的时候,我们按照倍数进行加仓。如果当前有持仓(position.length > 0),就要根据持仓的类型判断操作。如果是多头持仓,就要判断当前盈利是否达到设定的盈利目标。如果盈利超过盈利目标,则设置交易方向为平多(卖出),以当前买一价卖出持仓数量的合约,并将单位数量unit重置为1。如果盈利低于负的盈利目标,则将单位数量乘以加仓倍数,设置交易方向为买入(加仓),以当前卖一价买入加仓数量的合约。
如果持仓类型为空头,则与多头持仓相反的操作。
最后,通过Sleep函数设定间隔时间,这里设置为每天执行一次交易操作。
这样一个简单的马丁策略就设置好了。我们点击保存,可以发现网页页面更新上了最新编写的策略。我们回测运行一下,时间设置为今年的5月到8月。
从回测结果可以看到,确实没有亏损,最后的盈利是35168元。但是收益曲线并不像是传说的那样曲线上升。我们看下回测日志,看下最大加仓次数,结果发现,最大加仓次数是是6,一共持有128个仓位,然后保证金不足就无法开仓了,确实冒了很大的风险。我们的初始资金是100W,燃油的保证金还算是比较低,当我们选择原油或者铁矿石合约的话,爆仓的情况相信会更容易发生。
这里提醒一下,我们我们每次开仓的选择是随机的方向,所以模拟回测的结果可能会不一致,我们再次运行一下。很不幸,这次最后的结果是亏损。
当然还有别的同学会有疑问,既然亏损加仓的时候容易出现爆仓,那我盈利加仓的时候可以吗,当亏损一旦出现,我就立即离场。这其实就是反向马丁策略的设计原理。我们在原有策略的基础上,可以改动试下。这里我们添加一个全局变量,preprofit,初始值为0。然后在这里当今日的盈利profit减去昨日的盈利preprofit大于设置的盈利阈值profits的时候,我们进行加倍的开仓;而一旦最新的盈利小于昨日的盈利,我们就要及时的进行止损。对于多头和空头,这里的处理逻辑是一样的。
javascript
/*backtest
start: 2023-05-01 00:00:00
end: 2023-08-01 10:01:48
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
*/
var unit = 1
var profits = 10
var bei = 2
var preprofit = 0
function main() {
exchange.SetContractType(symbol)
while (true) {
var depth = exchange.GetDepth();
if (!depth) return;
var ask = depth.Asks[0].Price;
var bid = depth.Bids[0].Price;
var position = exchange.GetPosition()
if (position.length == 0) {
var redom = Math.random()
if (redom < 0.5) {
exchange.SetDirection("sell")
exchange.Sell(bid, unit, "开空")
}
if (redom > 0.5) {
exchange.SetDirection("buy")
exchange.Buy(ask, unit, "开多")
}
preprofit = 0
}
if (position.length > 0) {
var type = position[0].Type;
var profit = position[0].Profit;
var amount = position[0].Amount;
if (type == PD_LONG || type == PD_LONG_YD) {
if (profit - preprofit > profits) {
unit = unit * bei
exchange.SetDirection("buy")
exchange.Buy(ask, unit, "多头加仓,当前盈利:" + profit)
}
if (profit - preprofit < -profits) {
exchange.SetDirection("closebuy")
exchange.Sell(bid, amount, "多头止损,当前盈利:" + profit)
unit = 1
}
}
if (type == PD_SHORT || type == PD_SHORT_YD) {
if (profit - preprofit > profits) {
unit = unit * bei
exchange.SetDirection("sell")
exchange.Sell(bid, unit, "空头加仓,当前盈利:" + profit)
}
if (profit - preprofit < -profits) {
exchange.SetDirection("closesell")
exchange.Buy(ask, amount, "空头止损,当前盈利:" + profit)
unit = 1
}
}
preprofit = profit
}
Sleep(1000 * 60 * 60 * 24)
}
}
另外,还需要注意一点的是,当我们开仓完成,我们需要重置preprofit为0,方便持有盈利的判断。同样的使用燃油为目标合约,选择相同的时间段进行回测,可以看到,这时候确实不存在爆仓的风险了,只不过收益曲线下跌的很流畅。其实也可以理解,因为我们开仓的成功率是一半一半,而当盈利出现,我们继续选择加仓,在很多情况下,利润都会回撤,我们选择止损平仓,所以策略的收益比较不理想。
这节课我们选择使用马丁策略作为反面教材,验证了我们常挂在嘴边的“浮亏加仓”和“浮盈加仓”。但是无论是浮亏加仓还是浮盈加仓,都需要谨慎考虑仓位管理因素。马丁策略在一定程度上,可以定义为扛单,重点是我们有多少的资本去应对亏损,当我们在亏损的时候,继续选择去加仓,如果等不到趋势反转,这在一定情况下,确实会造成爆仓。所以我们在进行手动交易和量化交易的时候,对于仓位和资金的风险管理应该放在首要的位置。
对于仓位管理方面,以上有几个因素大家可以参考下:
资金管理:基于账户资金和交易目标,设定合理的头寸规模和资金使用计划,避免过度杠杆和过大的头寸。
风险控制:设置止损和止盈规则,控制亏损和保护盈利。合理控制仓位规模和加仓倍数,避免过度风险和波动性。
多样化投资组合:根据市场和行业的不同情况,可以分散投资于不同品种或不同策略,降低单一风险。
定期回顾和优化:定期检查和评估仓位管理和交易策略的效果,根据市场变化做出相应的调整和优化。
当然这些理论的建议大家都了解。我们都知道,相信一个理性人,确实厌恶亏损是我们的本能。如果在手动交易中,我们确实无法做到理性的割肉。那么在量化策略中,我们可以做到更系统的仓位和资金,用来确保长期盈利能力和资金安全。
视频参考链接:
14.商品期货自适应网格策略设计
在商品期货交易中,趋势跟踪、日内短线、手工炒单、套利、高频是大家比较常见的几种交易方法,但是除了这些方法之外,网格交易也是一种不错的选择,商品期货量化交易也是可以使用网格策略的,那具体怎么操作呢?本篇我们就用优宽量化(youquant.com)交易平台来实现一个简单的商品期货网格策略。
网格交易又称渔网交易,它也是量化交易的一种策略,简单的说,网格交易是利用行情震荡波动来赚钱的交易方法,在价格不断上下波动中,通过价格波动的上下区间布置网格低吸高抛获取利润。网格交易不依赖人的主观思考,完全是一种机械行为,比较适合用量化的方式进行交易,利用价格波动在网格区间内低买高卖,通过反复循环差价赚取利润,这种赚取差价的获利方式,可以参照下面这个图片:
网格交易本质上是一种空间变时间的玩法,其秉持的原则是“仓位策略比择时策略更重要”。简单的网格是以某个价位为基准点,当价格上涨戓下跌一定的点数或者一定的比例,挂N手数量空单戓多单,每一个格子即是盈利点位,但通常并不设置止损,当价格朝向与持仓有利的方向运动并达到网格点位时获利平仓,并且在该点位挂同样的买单戓卖单。这样这些交易订单就像渔网一样阵列,在行情的波动中来回盈利。
对于传统的网格策略,首先需要根据历史数据确定网格的上限和下限,然后根据品种的波动率情况设置网格的宽度,还要根据自己的资金实力设计网格的数量,网格数量越多,则需要加仓的点位就越多,以及计算好补仓资金份额,防止潜在的风险导致破网。
无论行情是上涨还是下跌,它可以平均开仓和平仓价格,这种交易方法不会增加风险,反而会降低风险。对于已经平仓的交易都是正收益,资金曲线相对稳定,这也是网格策略的优点之一。另外还有一个优点就是:网格策略不需要(要求)对市场方向做出正确的判断,这对于懒人或者对市场方向不是太敏感的交易者来说节省了很多时间和精力。
但是针对于商品期货市场,其巨大的风险性造成传统的网格策略在使用的时候,存在一定的缺陷:
(1)重复开仓和加仓风险:网格交易策略通常会在价格下跌时买入并设定不同的价格区间,而当价格连续下跌时,可能会造成频繁的买入操作,进而增加了持仓成本和风险。如果价格持续下跌,网格交易策略可能会导致不断加仓并承受更大的损失。
(2)无法适应剧烈行情:在剧烈行情的市场中,网格的间距可能会被迅速击穿,从而网格失效。
但是网格策略在商品期货市场中真的完全无法使用吗,当然不是。到目前为止呢,我们学习了很多的策略设计和仓位管理的知识,今天我们就来尝试一下改造原始的网格策略,看它能否变得温顺一点。
首先,我们来想一下这两个问题的解决方案,关于重复开仓和加仓的风险,如果大家看过前面的海龟交易策略,这个问题很好解决,我们可以设置一个最大持仓数量,当持仓数量达到上限,策略就不在开仓;
第二个问题,我们需要了解下网格设计的原理,在传统的网格策略中,网格区间是根据历史价格走势构建出来的,因此呢,存在一定的历史局限性,并且网格区间是在策略初始的时候构建出来的,不能随着价格的趋势发生变化。针对于这一个问题,我们可以设置一个更加灵活的网格区间,伴随价格的走势,我们可以不断地调整。
javascript
/*backtest
start: 2023-01-03 09:00:00
end: 2023-10-09 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
*/
var obj = $.NewPositionManager(); // 使用优宽量化交易类库
var band = []; // 定义全局变量 band
function bandCal(records){ //band计算函数
var ma = TA.MA(records, maPeriod)[records.length - 1]
var std = talib.STDDEV(records, stdPeriod)[records.length - 1]
var array = [-1.96, -0.85, -0.53, 0.53, 0.85, 1.96];
// 获取网格区间分界线
var band = array.map(num => _N(ma + num * std, 2))
return band
}
function onTick(records) {
// 此处用来获取持仓信息
var positions = _C(exchange.GetPosition) // 获取持仓数组
var position_amount = 0
for (var i = 0; i < positions.length; i++) { // 遍历持仓数组
if (positions[i]['Type'] === PD_LONG || positions[i]['Type'] === PD_LONG_YD) {
position_amount = 1 * positions[i].Amount // 将position_long标记为正数
} else if (positions[i]['Type'] === PD_SHORT || positions[i]['Type'] === PD_SHORT_YD) {
position_amount = -1 * positions[i].Amount // 将position_short标记为负数
}
}
// 根据价格落在(-inf,-1.96],(-1.96,-0.85],(-0.85,-0.53],(-0.53,0.53],(0.53,0.85], (0.85,1.96], (1.96, Inf) 的区间范围来获取最新收盘价所在的价格区间
var grid = null
var close_01 = records[records.length - 1].Close;
if (close_01 > band[5]) {
grid = 6
} else if (close_01 > band[4]) {
grid = 5
} else if (close_01 > band[3]) {
grid = 4
} else if (close_01 > band[2]) {
grid = 3
} else if (close_01 > band[1]) {
grid = 2
} else if (close_01 > band[0]) {
grid = 1
} else {
grid = 0
}
// 若无仓位且价格突破则按照设置好的区间开仓
if (!position_amount) {
if (grid == 6 || grid == 0) {
band = bandCal(records) // 重新计算band
return('更新band')
}
if (grid > 3) {
obj.OpenShort(symbol, 1); // 以市价单开空仓到仓位
Log('空仓grid', grid)
}
if (grid < 3) {
obj.OpenLong(symbol, 1); // 以市价单开多仓到仓位
Log('多仓grid', grid)
}
}
// 持有空仓的处理
if (position_amount < 0) {
// 突破区间,进行止损,重新计算band
if (grid == 6) {
Log('止损平仓grid', grid)
obj.CoverAll(symbol) // 以市价单全平空仓
band = bandCal(records) // 重新计算band
}
// 等于3为在中间网格,平仓
else if (grid <= 3) {
Log('止盈平仓grid', grid)
obj.CoverAll(symbol) // 以市价单全平空仓
band = bandCal(records) // 重新计算band
}
// 大于3为在中间网格的下方,加仓
else if (grid > 3 && -position_amount < maxPos) {
obj.OpenShort(symbol, 1) // 以市价单调空仓到仓位
}
}
// 持有多仓的处理
if (position_amount > 0) {
// 突破区间,进行止损,重新计算band
if (grid == 0) {
Log('止损平仓grid', grid)
obj.CoverAll(symbol) // 以市价单全平多仓
band = bandCal(records) // 重新计算band
}
// 等于3为在中间网格,平仓
else if (grid >= 3) {
Log('止盈平仓grid', grid)
obj.CoverAll(symbol) // 以市价单全平空仓
band = bandCal(records) // 重新计算band
}
// 小于3为在中间网格的下方,加仓
else if (grid < 3 && position_amount < maxPos) {
obj.OpenLong(symbol, 1) // 以市价单调多仓到仓位
}
}
}
function main() {
var isFirst = true
var preBartime = 0
while (true) { // 进入循环模式
if(exchange.IO("status")){
LogStatus("行情和交易服务器连接成功, " + _D())
_C(exchange.SetContractType, symbol)
var records = _C(exchange.GetRecords)
if(isFirst){
band = bandCal(records)
isFirst =false
preBartime = records[records.length - 1].Time
}else{
if(preBartime != records[records.length - 1].Time){
preBartime = records[records.length - 1].Time
onTick(records)
}
}
}else{
LogStatus("正在等待与交易服务器连接, " + _D())
}
Sleep(3000)
}
}
我们来使用代码来进行实现。首先我们设置两个全局变量,obj交易类库单品种控制对象,band网格区间。接下来我们计算网格区间,定义bandCal函数,参数是k线records。相对于以往固定的使用5或者10作为网格价格的间隔,这里我们使用一些统计学知识,决定使用正态分布来决定价格的分布区间。我们价格价格的波动在一段范围内是符合正态分布的,因此使用均值加上标准差倍数乘以标准差,可以表示价格的分界线。这里我们选用了标准差倍数正负0.53代表,均值附近40%的波动区间,正负0.85代表均值附近60%的波动区间,正负1.96代表95%的波动区间。所以我们的网格区间,是以移动平均值作为均值,价格的不同分布概率作为网格的区间。
至于具体均值和标准差的计算,我们使用一定周期窗口内的数值,所以这里我们设置两个参数,maPeriod均线周期和stdPeriod标准差周期。
参数设置好以后,我们使用了TA.MA和talib.STDDEV分别来计算对应参数的均值和标准差。标准差是一种衡量价格波动性的指标,它可以帮助确定价格的波动范围。
然后,代码定义了一个包含6个数值的数组array,这些数值分别是[-1.96, -0.85, -0.53, 0.53, 0.85, 1.96]。这些数值就是我们刚提到的,代表了统计学上的正态分布中的标准差倍数。
接下来,代码使用了map函数对array数组中的每个元素进行遍历,并通过计算ma + num * std得到网格区间的分界线。这里的num就是array数组中的每个元素。
最后,代码使用了_N函数将计算得到的网格区间分界线保留两位小数,并将结果作为一个数组返回。这样呢,我们的网格区间就定义好了,当我们每次完成一笔交易或者价格突破关键点位的时候,我们可以使用这个函数进行band数组的更新。
第二个我们来定义onTick函数,这段代码是一个简单的网格交易策略的实现。首先获取当前的持仓信息,并初始化变量position_amount为0,用于记录持仓数量。接着遍历持仓数组,判断持仓类型是多仓(包括PD_LONG和PD_LONG_YD)还是空仓(PD_SHORT和PD_SHORT_YD),然后根据持仓类型判断持仓数量的正负,并存入position_amount变量中。
接下来根据价格落在我们设置的网格的区间范围,就是变量band存储的阈值来划分,来获取最新收盘价所在的价格区间grid。我们的假设是,在一定时间范围,价格是正态分布波动的,当价格偏离均值一定范围,我们进行相应的开多或者开空操作,当价格回到均值范围,我们进行止盈平仓;当然也有单边情况发生的时候,就是价格的波动超过了95%的置信区间,在一定程度上,代表着均值的转移,这个时候我们就要进行及时的平仓,并进行新的band的计算。
具体来说,grid一共有7个值。当grid等于3的时候,在中间正负0.53的标准差倍数,代表40%的正态分布的概率区间,这一区间内,我们不进行开仓,只进行止盈平仓的操作;当价格在1和2,我们进行开多的操作;当价格在4和5,进行开空的操作;在grid等于0或者6,我们就要及时的更新band,进行止损。
这里我们首先使用代码来判断当前价格所在的grid区间。
然后进行入场信号的判断,如果当前没有持仓,首先需要判断是否需要重新计算band,因为在很多情况下,当价格突破一定的阈值,阻力位就变成了支撑位,在很长时间内,grid一直是6,策略不会进行任何的操作,会错过很多的交易机会,所以我们要重新计算band,然后return,重新计算grid;接着当grid大于3的时候,我们使用市价单开空仓;当grid小于3,使用市价单开多仓。这样就完成了入场的操作。
下面进行加仓和出场的操作,出场包括止盈和止损。当持仓空仓position_amount小于0,首先我们判断止损,当grid等于6,代表突破上限,使用CoverAll以市价单全平空仓,并重新计算band;接下来是止盈的操作,如果grid回到3或者以下,我们就要及时的进行止盈,然后重新更新band;
当然如果没有达到止盈或者止损位,我们也可以跟随市场趋势,进行加仓的操作,当判断如果grid依然大于3,进行加仓开空;但是不要忘了,我们单向持仓是有一定数量限制的,所以添加条件当-position_amount小于maxPos最大持仓数量。
多仓的处理逻辑也是一样,只是grid的条件数值需要改一下。这样onTick函数就定义好了。
最后我们来定义主函数。网格策略作为一个低频趋势策略,我们想使用onBar机制运行,就是只有在k线更新的时候我们进行策略的运行。所以在while循环体外,我们首先定义isFirst,代表策略是否第一次运行,preBartime,用来判断k线的更新,。
下面设置while循环体。当判断是第一次运行策略,isFirst为真,首先需要计算band网格区间,然后定义isFirst为false,preBartime为当前k线的时间;
然后当判断isFirst为false的时候,我们判断最新的k线时间戳是否等于preBartime,如果k线更新,赋值preBartime为最新的k线时间,然后开始执行网格策略的ontick函数。以上呢,我们制定的的网格策略就定义完成。
接下来,我们回测运行一下。这里我们需要挑选合适的品种,网格策略适用于波动率低,趋势不明显的品种,是震荡行情获利法宝,这就要求我们对期货品种有个略微的把握,比如工业品尤其是黑色系更容易走出趋势行情不适合网格策略,反而农产品往往在一个价格区间内波动适合网格策略。另外,我们设置的网格设计理念是正态分布,确实更加适合震荡的行情,所以这里我们选择有名的稳定品种,玉米。根据回测的日志,可以看到这个策略确实取得了不错的收益。我们看下收益概览中回撤的时间,主要集中于四月中旬到五月中旬,和六月中旬到七月中旬,对照一下k线的走势,确实这两段时间是分别一个单边下降和上涨的行情,因此策略的收益出现较多的回撤。而在其他时间,当行情逐渐稳定下来,我们看到收益是逐渐上升的。
最近很多同学反应,在回测系统运行流畅的策略,到了实盘中经常会报很多的bug,其实在回测页面,我们也可以模拟实盘的环境,这里我们选择容错模式回测,我们来看下。容错模式就是模拟实盘环境,各种接口出现报错,去测试我们的策略模型是否足够健壮去处理这些bug,我们看下这里报错,这里的错误类型主要是没有连接交易所或者获取数据失败,所以这里我们使用_C重复尝试,再继续运行一下,可以看到没有运行错误了。这样呢,在容错模式下运行没有bug的模型,在实盘中一般不会出现较大的问题。
我们的课程是实战性课程,仅仅是运行策略肯定是不能满足我们的要求的。我们需要加上状态变量的恢复,这里我们需要记录的状态变量,是band,还记得吗,使用_G函数;还有策略运行状态的展示,包括当前交易品种,持仓方向,均价,数量,盈亏,止盈次数和止损次数,并且还有收益的展示,这些我们在前面的课程中都有提到过,这里不给大家重复的讲解了,如果大家对源码感兴趣的话,可以到我们的文库教程,这里包括了教程的文本讲解和原始的代码,大家可以复制运行一下,或者进行一下改进,欢迎大家进行更多的尝试。
javascript
/*backtest
start: 2023-01-03 09:00:00
end: 2023-09-30 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["symbol","c888"],["maxPos",3],["stdPeriod",30]]
*/
var obj = $.NewPositionManager(); // 使用优宽量化交易类库
var band = []; // 定义全局变量 band
var pos_dir = '--'
var pos_amount = '--'
var pos_price = '--'
var pos_profit = '--'
var pos_margin = 0
var stopprofit = 0
var stoploss = 0
function bandCal(records){
var ma = TA.MA(records, maPeriod)[records.length - 1]
var std = talib.STDDEV(records, stdPeriod)[records.length - 1]
var array = [-1.96, -0.85, -0.53, 0.53, 0.85, 1.96];
// 获取网格区间分界线
var band = array.map(num => _N(ma + num * std, 2))
return band
}
function onTick(records) {
// 此处用来获取持仓信息
var positions = _C(exchange.GetPosition) // 获取持仓数组
var position_amount = 0
for (var i = 0; i < positions.length; i++) { // 遍历持仓数组
if (positions[i]['Type'] === PD_LONG || positions[i]['Type'] === PD_LONG_YD) {
position_amount = 1 * positions[i].Amount // 将position_long标记为正数
} else if (positions[i]['Type'] === PD_SHORT || positions[i]['Type'] === PD_SHORT_YD) {
position_amount = -1 * positions[i].Amount // 将position_short标记为负数
}
}
// 根据价格落在(-inf,-1.96],(-1.96,-0.85],(-0.85,-0.53],(-0.53,0.53],(0.53,0.85], (0.85,1.96], (1.96, Inf) 的区间范围来获取最新收盘价所在的价格区间
var grid = null
var close_01 = records[records.length - 1].Close;
if (close_01 > band[5]) {
grid = 6
} else if (close_01 > band[4]) {
grid = 5
} else if (close_01 > band[3]) {
grid = 4
} else if (close_01 > band[2]) {
grid = 3
} else if (close_01 > band[1]) {
grid = 2
} else if (close_01 > band[0]) {
grid = 1
} else {
grid = 0
}
// 若无仓位且价格突破则按照设置好的区间开仓
if (!position_amount) {
if (grid == 6 || grid == 0) {
band = bandCal(records) // 重新计算band
_G('band', band)
return('更新band')
}
if (grid > 3) {
obj.OpenShort(symbol, 1); // 以市价单开空仓到仓位
Log('空仓grid', grid)
}
if (grid < 3) {
obj.OpenLong(symbol, 1); // 以市价单开多仓到仓位
Log('多仓grid', grid)
}
}
// 持有空仓的处理
if (position_amount < 0) {
// 突破区间,进行止损,重新计算band
if (grid == 6) {
Log('止损平仓grid', grid)
obj.CoverAll(symbol) // 以市价单全平空仓
band = bandCal(records) // 重新计算band
stoploss += 1
}
// 等于3为在中间网格,平仓
else if (grid <= 3) {
Log('止盈平仓grid', grid)
obj.CoverAll(symbol) // 以市价单全平空仓
band = bandCal(records) // 重新计算band
stopprofit += 1
}
// 大于3为在中间网格的下方,加仓
else if (grid > 3 && -position_amount < maxPos) {
obj.OpenShort(symbol, 1) // 以市价单调空仓到仓位
}
}
// 持有多仓的处理
if (position_amount > 0) {
// 突破区间,进行止损,重新计算band
if (grid == 0) {
Log('止损平仓grid', grid)
obj.CoverAll(symbol) // 以市价单全平多仓
band = bandCal(records) // 重新计算band
stoploss += 1
}
// 等于3为在中间网格,平仓
else if (grid >= 3) {
Log('止盈平仓grid', grid)
obj.CoverAll(symbol) // 以市价单全平空仓
band = bandCal(records) // 重新计算band
stopprofit += 1
}
// 小于3为在中间网格的下方,加仓
else if (grid < 3 && position_amount < maxPos) {
obj.OpenLong(symbol, 1) // 以市价单调多仓到仓位
}
}
_G('band', band)
}
function main() {
var preBartime = 0
var initAccount = _C(exchange.GetAccount)
// 读取保存网格数据;
band = _G('band')
// 保存网格数据为空;
if (band == null) {
Log('无存储网格数据')
_C(exchange.SetContractType, symbol)
records = _C(exchange.GetRecords)
if(records.length < 20) return
band = bandCal(records)
Log(band)
_G('band', band)
Log('已储存新的网格数据')
}else{
Log('载入已保存的网格数据')
}
while (true) { // 进入循环模式
if(exchange.IO("status")){
LogStatus("行情和交易服务器连接成功, " + _D())
// 订阅合约,并且取得优宽量化平台当前周期的所有收盘价
_C(exchange.SetContractType, symbol)
var records = _C(exchange.GetRecords)
if(preBartime != records[records.length - 1].Time){
preBartime = records[records.length - 1].Time
onTick(records)
}
var tblStatus = {
type: "table",
title: "持仓信息",
cols: ["合约名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏", "止盈次数", "止损次数"],
rows: []
}
var statusPos = _C(exchange.GetPosition) // 获取持仓数组
if(statusPos.length != 0){
pos_dir = statusPos[0]['Type'] === PD_LONG || statusPos[0] === PD_LONG_YD ? '多头' : '空头'
pos_amount = statusPos[0]['Amount']
pos_price = statusPos[0]['Price']
pos_profit = statusPos[0]['Profit']
pos_margin = statusPos[0].Margin
}else{
pos_dir = '--'
pos_amount = '--'
pos_price = '--'
pos_profit = '--'
pos_margin = 0
}
tblStatus.rows.push([symbol, pos_dir, pos_price, pos_amount, pos_profit, stopprofit, stoploss])
lastStatus = '`' + JSON.stringify([tblStatus]) + '`'
LogStatus(lastStatus)
var accountInfo = _C(exchange.GetAccount)
var curprofit = accountInfo.Balance + pos_margin - initAccount.Balance
LogProfit(curprofit, "权益", '&')
}else{
LogStatus("正在等待与交易服务器连接, " + _D())
}
Sleep(3000)
}
}
最后,我们来稍微讲下策略的优化思路。任何策略是有适用范围的,网格策略也不例外,商品期货属于杠杆交易,尤其是网格交易有逆势加仓的特点,风险是比较大的。所以判断走势和控制风险是网格策略两个重要的优化方向。在本节课我们讲解的网格策略中,我们的设计原理是正态分布,意味着对震荡的行情有了一个初始的假设;而当出现单边行情的时候,正态分布可能失效,我们可以尝试非正态分布,比如对数正态分布和伽马分布等一些偏态的分布去进行拟合;第二点,关于风险控制,一个可以优化的思路是我们可以利用主力合约与次主力合约的价差或者有相关性的跨品种价差做网格策略,相比于单品种价差的波动是相对稳定的。
以上呢,都是我们可以做的一些探索,大家如果有好的想法,也可以提出来,我们一起努力来学习,共同进步。
视频参考链接:
《用python中的Pandas库实现一个商品期货网格策略》
15.浅谈期货交易中的资金流分析
价格不是上就是下,长期而言,价格的涨跌概率应各是50%,那么要正确预测未来的价格,就需要实时获取影响价格的全部因素,然后给每个因素一个正确权重,最后作出客观理性分析。要把影响价格的全部因素罗列出来,可能会写满整个屏幕。
市场是真的无法预测吗?也不是,所有的宏观因素和微观因素都已经反映到价格上了,也就是说价格是全部因素相互作用的结果。我们只需要分析价格,就可以做出一个完整的交易策略。
那么影响价格的原因是什么呢?我们可能会总结,因为:国家对相关产业政策扶持、原产地又又下暴雨了、国际贸易战、MACD金叉了、别人都买了等等,当然这些也许都没错。事后看,总能找出推动价格上涨的理由。
但是,作为散户的我们,信息在很多时候只能延迟被动的接受,并且分析的方向并不一定正确。那么有没有一种方法可以实时的了解价格的涨跌原因呢?
其实,价格的涨跌类似于水涨船高。这里的水就是资金的推动,盘面上,如果买的人多过卖的人,价格就会上涨。反之,如果卖的人多过买的人,价格就会下跌。有了这个概念,我们就可以根据资金净流向流动反映出来的供求关系,对未来价格的走势给出合理的预期。
但是在很多情况下,趋势是由大资金导向把控的。所以很多炒期货的同学,经常会关注于龙虎榜的数据,每日在收盘以后查看各大机构在空头和多头的开仓和平仓数量,并以此来推测价格的未来走势。但是这个龙虎榜数据是在收盘以后两个小时左右发布的,具有一定的滞后性。并且也无法观测区分大资金和小资金的资金走势。
但是呢,拥有量化分析的工具。我们就可以实时的收集数据,进行整理,分类和汇总,从而实时的展示各品种在各个时间段的开多,开空,平空和平多的数量,并且可以依据仓位变动的大小,大致区分是大资金的交易,还是小资金的交易。今天呢,我们就试着根据实时的数据,做一个大资金和小资金的资金流的实时汇总和展示。
我们首先来分析一下一个分时数据的截图,可以看到有时间,价格,现量,仓差和性质。现量是交易量,仓差是持仓量的变化。这个数据是在交易时间段,实时更新的,我们可以获取到,并由此汇总这个数据,分别计算出来多头和空头的资金流向。
这里的大资金和小资金力度的区分呢,我们初步制定的规则,当仓差大于阈值,我们定义为大资金的变化,小于阈值,定义为小资金的资金变化。当然这是一个初步的参考,也有很多小资金账户在同一时间汇总交易的数量大于阈值,并且也有很多大资金会进行拆单,将一个大单子进行拆分,从而避免价格的大量波动,达到自己理想的成交价格。所以我们的资金流分析只是一个初步的尝试,大家可以在这个基础上,进行更仔细的划分。
javascript
function getTypeName(zcl, jylx, vol){
if(zcl > 0 && (Math.abs(zcl) == vol)){
return '双开'
}else if(zcl < 0 && Math.abs(zcl) == vol){
return '双平'
}else if(jylx == 1 && zcl < 0){
return '多平'
}else if(jylx == 2 && zcl < 0){
return '空平'
}else if(jylx == 2 && zcl > 0){
return '多开'
}else if(jylx == 1 && zcl > 0){
return '空开'
}else if(jylx == 2 && zcl == 0){
return '多换'
}else if(jylx == 1 && zcl == 0){
return '空换'
}
return ''
}
function main() {
var url = "数据源头url";
var isFirst = true
while(true){
var t = new Date() // 获取当前时间对象
var hour = t.getHours() // 获取当前小数:0~23
var minute = t.getMinutes() // 获取当前分钟:0~59
//重置数据
if(isFirst){
var openLongSumBig = 0
var openShortSumBig = 0
var closeLongSumBig = 0
var closeShortSumBig = 0
var openLongSumSmall = 0
var openShortSumSmall = 0
var closeLongSumSmall = 0
var closeShortSumSmall = 0
var dataList = [] //交易记录id保存列表
isFirst = false
}else{
if(hour == 20 && minute == 59){
Log('新的交易日,重置数据')
var openLongSumBig = 0
var openShortSumBig = 0
var closeLongSumBig = 0
var closeShortSumBig = 0
var openLongSumSmall = 0
var openShortSumSmall = 0
var closeLongSumSmall = 0
var closeShortSumSmall = 0
var dataList = []
}
}
if($.IsTrading(symbol)){
}else{
LogStatus('非交易时间段')
}
if($.IsTrading(symbol)){
var response = HttpQuery(url);
// 提取响应内容中的 JSON 数据部分
var startIndex = response.indexOf("(") + 1
var endIndex = response.lastIndexOf(")")
var jsonContent = response.substring(startIndex, endIndex)
var info = JSON.parse(jsonContent)
// 格式化并输出结果
if(info.mx){
for (var i = 0; i < info.mx.length; i++) {
var v = info.mx[i]
var price = v.p
var time = _D(v.utime * 1000)
var amount = Math.abs(v.zcl)
var type = getTypeName(v.zcl, v.jylx, v.vol)
var id = v.utime + v.vol + v.zcl
if (!dataList.includes(id)) {
dataList.push(id)
// 输出结果
Log('时间:', time, '价格:', price, '数量:', amount, '类型:', type)
if (type == '多开'){
amount >= threshold ? openLongSumBig += amount : openLongSumSmall += amount
}
if (type == '空开'){
amount >= threshold ? openShortSumBig += amount : openShortSumSmall += amount
}
if (type == '多平'){
amount >= threshold ? closeLongSumBig += amount : closeLongSumSmall += amount
}
if (type == '空平'){
amount >= threshold ? closeShortSumBig += amount : closeShortSumSmall += amount
}
if (type == '双开'){
if(amount >= threshold){
openLongSumBig += amount/2
openShortSumBig += amount/2
}else{
openLongSumSmall += amount/2
openShortSumSmall += amount/2
}
}
if (type == '双平'){
if(amount >= threshold){
closeLongSumBig += amount/2
closeShortSumBig += amount/2
}else{
closeLongSumSmall += amount/2
closeShortSumSmall += amount/2
}
}
var capTable = {type: 'table', title: '仓位操作汇总信息', cols: ['类型', '大资金仓位数量', '小资金仓位数量'], rows: [
['多开',openLongSumBig,openLongSumSmall],
['空开',openShortSumBig,openShortSumSmall],
['多平',closeLongSumBig,closeLongSumSmall],
['空平',closeShortSumBig,closeShortSumSmall]]}
var treTable = {type: 'table', title: '仓位类型汇总信息', cols: ['类型', '大资金仓位数量', '小资金仓位数量'], rows: [
['多头',openLongSumBig - closeLongSumBig ,openLongSumSmall - closeLongSumSmall ],
['空头',openShortSumBig - closeShortSumBig,openShortSumSmall - closeShortSumSmall]]}
LogStatus('`' + JSON.stringify(capTable) + '`\n' + '`' + JSON.stringify(treTable) + '`')
$.PlotMultLine("资金走势", "大资金走势", openLongSumBig + openShortSumBig - closeLongSumBig - closeShortSumBig,v.utime * 1000)
$.PlotMultLine("资金走势", "小资金走势", openLongSumSmall + openShortSumSmall - closeLongSumSmall - closeShortSumSmall,v.utime * 1000)
$.PlotMultLine("趋势分析", "大资金多头趋势", openLongSumBig - closeLongSumBig,v.utime * 1000)
$.PlotMultLine("趋势分析", "小资金多头趋势", openLongSumSmall - closeLongSumSmall,v.utime * 1000)
$.PlotMultLine("趋势分析", "大资金空头趋势", openShortSumBig - closeShortSumBig,v.utime * 1000)
$.PlotMultLine("趋势分析", "小资金空头趋势", openShortSumSmall - closeShortSumSmall,v.utime * 1000)
}
}
}else{
Log('暂时无法获取数据')
}
}else{
LogStatus('非交易时间段')
}
Sleep(10)
}
}
话不多说,进入我们的代码。这次我们获取数据的方式是,从网页上获取期货交易的分时数据,然后进行汇总分析。分时数据提供的渠道有很多,这里我们挑选一个渠道,定义url变量。然后我们定义isFirst,代表第一次运行这个策略。
然后在while循环体中,我们就要开始分时数据的收集,整理和汇总。因此我们想统计每个交易日的具体资金流变化,所以在每天晚上的8点59分,我们需要定时重置清空我们的数据。获取当前时间对象t,然后获取具体小时和分钟。然后开始定义重置数据的模块。
如果是第一次运行策略,当isFirst为真。我们定义一些变量来汇总大资金和小资金的多头和空头的信息,包括openLongSumBig大资金开多,openShortSumBig开空,
closeLongSumBig多平,和closeShortSumBig空平,对于小资金这里的变量后缀改为small就可以。
然后我们还需要定义一个空的列表,dataList,因为每次返回的分时数据中可能存在已经收集过的数据,需要做一个重复过滤筛选,所以定义一个类似交易记录id的变量。最后isFirst定义为false。
当isFirst为假,在每日的20点59分,我们重置以上的这些变量。
接着我们要开始收集我们的数据了。我们都知道不同的品种具有不同的开盘时间,在非开盘时间,是没有成交记录的。我们这里使用交易类库的IsTrading函数,只有正在交易的时候,才会开始收集数据,在非交易时段,输出状态信息'非交易时间段'。
在交易时间段内,使用HttpQuery获取分时数据的信息,我们对原始获取的数据进行一下正则处理,提取响应内容中的 JSON 数据部分,然后使用JSON.parse进行解析。这样分时数据的初始文本内容我们就获取完成。
数据是保持在infomx当中,当然并不是每时每刻都可以获取到数据的,由于一些特殊的故障,当没有获取到infomx,策略就会报错停止运行。所以这里进行一个容错的处理。如果获取到分时数据,infomx,就开始分条处理;如果没有获取到,打印'暂时无法获取数据'。
返回的分时数据是一个个时间戳对应的交易记录,使用for循环,分别提取每条交易记录的价格,时间戳,返回的是utime时间戳,我们需要乘以1000,持仓数量,也就是仓位资金的变化,是zcl属性,具有正负之分,这里我们统一为正值。xvol,代表的是交易量;type,具体的交易性质,包括多开,多平,空开,空平,多开,多换,空开,和空换这八种类型,原有的分时数据是不提供这个字段的,我们需要一个函数进行转换。
getTypeName函数,具有三个参数,zcl: 表示数据中的 zcl 字段,表示持仓量;jylx: 表示数据中的 jylx 字段,当jylx 为1,代表空头,当jylx为2,代表多头;vol: 表示数据中的 vol 字段,表示成交量。在这个函数中,我们就要根据是多头还是空头,交易量和持仓量的变化,确定交易性质。
如果 zcl 持仓量变化大于 0,且 zcl 的绝对值等于 vol交易量,则返回 '双开'。
否则,如果 zcl 小于 0,且 zcl 的绝对值等于 vol,则返回 '双平'。
否则,如果 jylx 等于 1,且 zcl 小于 0,返回 '多平'。
否则,如果 jylx 等于 2,且 zcl 小于 0,返回 '空平'。
否则,如果 jylx 等于 2,且 zcl 大于 0,返回 '多开'。
否则,如果 jylx 等于 1,且 zcl 大于 0,返回 '空开'。
否则,如果 jylx 等于 2,且 zcl 等于 0,返回 '多换'。
否则,如果 jylx 等于 1,且 zcl 等于 0,返回 '空换'。
如果以上条件均不满足,则返回空字符串 ''。
这里还需要注意的是,其实这里我们获取的交易性质,是汇总不同交易量后的持仓量变化的推断,因为在一条信息中,交易量包含不同的开多,开空,平空和平多的交易操作,最后引起的仓位变化,我们定义为这一时刻的交易的性质。
比如这里的交易量是101手,最后引起的持仓量变化是-19手,这101手交易量可能包含很多开多,开空,平多和平空不同类型的操作,但是最后引起的仓位数量是下降,价格变化是下跌,所以我们定义为多平,在仓位资金的流出定义为-19。
接着我们合成一个id,作为此条交易信息的标识,使用时间戳加成交量加仓差。
接着就要对资金进行分类处理了,如果dataList不包含id,证明该条交易记录没有处理过,那么push到dataList中。然后打印该条交易记录信息。
接着根据type类型,分类统计amount到大小资金流的不同交易方向中。
举例示范一下,如果type是多开,使用三元表达式判断,如果amount大于阈值threshold,累加到openLongSumBig中,如果小于阈值,累加到openLongSumSmall中。
对于空开,空平和多平,处理的逻辑也是一样的。对于双开和双平,我们使用amount除以2,分别累加到不同方向。而对于多换和空换,由于持仓没有变化,所以我们不记录统计。
接下来就要进行图表的展示了,我们分别统计仓位操作汇总信息,包括大资金和小资金的,具体开多,开空,平多和平空的数量;
还有仓位类型汇总信息,分别统计不同资金在多头和空头的汇总信息,例如多头仓位,使用不同资金的开多仓位减去平多仓位。
最后,我们使用画图展示,不同资金的走势,利用开多仓位加上开空仓位,减去平多仓位和平空仓位;
趋势分析,分别就不同类型的资金的多头趋势和空头趋势进行计算展示。
然后我们建立实盘就可以运行了。
在实盘中可以看到,在交易时间段,状态栏和图表会实时的展示,大小不同类型资金,汇总交易操作数量,和资金走势具体情况。通过实盘交易的观察和资金流分析,可以更加清晰地了解不同类型资金在市场中的博弈情况和趋势。另外,根据资金流分析的结果,我们可以对市场趋势有一个更宏观的认识和了解。
此外,对于不同类型资金的趋势分析,我们也可以使用量化的角度更深入地研究大资金和小资金对价格的推动作用。比如,当大资金流入市场时,是否意味着市场将会持续上涨?或者当小资金占据市场主导时,市场是否会出现频繁的短期波动?因此,在实战中,资金流分析是非常有用的工具,可以帮助我们更好地把握市场机会和趋势,制定更科学合理的交易策略。
相对于以往,我们讲述的都是经典的策略原理和具体的代码设计,本节内容我们更像是一个试验性的探索。因为在我们的学习过程中,在我们感觉掌握到一定知识后,总是会试图对于交易的理念和想法使用程序化表达出来。本节课,我们就参考了股票资金流的分类算法,构建出了期货交易中的资金流分析。当然本设计并不是十分的完善,希望本节课作为一个启发性的课程,大家可以对这个算法提出更好的优化方法,我们也会积极采纳,共同学习,共同进步。
视频参考链接:
16.优宽量化交易平台的合约代码设置
你知道吗,优宽作为专业的量化交易平台,不仅支持商品期货市场,期权市场和股票市场也是支持的。本节课呢,我们将要学习具体怎样在优宽平台设置不同类型的期货,期权和股票的合约。
期货合约设置
首先我们来看期货合约的设置。由于商品期货合约存续的特殊性,为满足回测的使用需求,针对每一个品种提供主力连续合约和指数合约,其主要是根据当前时间段内有效的商品期货合约数据人工合成。其中:
主力连续合约:由该期货品种不同时期的主力合约(价格和成交量)直接拼接而成,代码以888结尾,例如rb888。合约首次上市时, 以当日收盘同品种持仓量最大者作为从第二个交易日开始的主力合约。如果同品种其他合约持仓量在收盘后超过当前主力合约1.1倍时, 则在第二个交易日进行主力合约切换。
指数合约:由该期货品种所有正在交易的合约,以持仓量加权平均计算。
在回测中,我们就可以使用主力连续合约或者指数合约设置不同的期货品种。这主要是在回测系统中使用更加方便。但是在实盘使用的时候,当我们使用主力或者指数合约下单,交易所是无法识别的,这里我们使用调试工具对接真实的市场进行仿真交易,当我们使用指数合约下单,返回 Not select symbol的错误。
在实盘中,我们需要设置具体的合约名称。针对于不同交易所的具体合约,期货合约代码规则可以总结为这张图中展示的这样。
| 交易所 | 具体合约规则 | 主力连续合约 | 指数合约 | 具体合约 |
|---|---|---|---|---|
| 中国金融期货交易所 | 品种代码(大写) + 交割年份(2位) + 交割月份(2位) | IF888 | IF000 | IF2201 |
| 上海期货交易所 | 品种代码(小写) + 交割年份(2位) + 交割月份(2位) | ag888 | ag000 | ag2201 |
| 上海国际能源交易中心 | 品种代码(小写) + 交割年份(2位) + 交割月份(2位) | bc888 | bc000 | bc2201 |
| 郑州商品交易所 | 品种代码(大写) + 交割年份(1位) + 交割月份(2位) | AP888 | AP000 | AP201 |
| 大连商品交易所 | 品种代码(小写) + 交割年份(2位) + 交割月份(2位) | a888 | a000 | a2201 |
| 广州期货交易所 | 品种代码(小写) + 交割年份(2位) + 交割月份(2位) | lc888 | lc000 | lc2405 |
中国金融期货交易所:合约代码由品种代码(大写)+ 交割年份(2位)+ 交割月份(2位)组成。例如具体合约为IF2201。
上海期货交易所,上海能源期货交易所,大连商品期货交易所和广期所,广期所是最新成立的,这四个交易所它们的合约命名的规则都是一样的:合约代码由品种代码(小写)+ 交割年份(2位)+ 交割月份(2位)组成。例如具体合约为ag2201。
比较特殊的是郑州商品交易所:合约代码由品种代码(大写)+ 交割年份(1位)+ 交割月份(2位)组成。例如具体合约为AP201。
根据不同的交易所和品种,合约代码会有一定的差异,但通常都遵循这种命名规则。
如果我们想获取所有产品的列表数据,可以使用products参数。需要注意,这个函数只能在实盘中使用。
可以看到,一共返回了151个期货品种,不过这里有一些特殊合约,比如efp合约等,我们只需要关注不加特殊后缀的合约。
javascript
function main() {
while (!exchange.IO("status")) {
LogStatus("正在等待与交易服务器连接, " + new Date())
}
Log("开始获取所有产品")
var products = _C(exchange.IO, "products")
Log("产品合约列表获取成功")
for (var i = 0; i < products.length; i++) {
Log(products[i].ProductID)
}
}
可以看到,一共返回了153个期货品种,不过这里有一些特殊合约,比如efp合约等,我们只需要关注不加特殊后缀的合约。
如果我们想获得所有合约的列表数据,可以使用instruments参数。
javascript
function main() {
while (!exchange.IO("status")) {
LogStatus("正在等待与交易服务器连接, " + new Date())
}
Log("开始获取所有合约")
var instruments = _C(exchange.IO, "instruments")
Log("合约列表获取成功")
var len = 0
for (var instrumentId in instruments) {
len++
}
Log("合约列表长度为:",len)
}
可以看到,一共有30844个,这里面不仅包括期货,也包括期权的合约。所以在优宽平台,针对于不同的期货品种,数据的获取和使用都是很方便的。
期权合约设置
接着我们来看下期权合约代码的设置。商品期权是一种很好的商品风险规避和管理的金融工具,大宗商品的风险无处不在,其中最重要的是价格风险,那么我们就可以利用商品期权的各种套期保值工具对冲交易,来规避这种风险。商品期货期权的交易需要期货公司开通相关权限。
商品期货期权合约基本格式:
标的期货合约 + 看涨 / 看跌 + 行权价
由于交易所的标准合约命名规则并不相同,且对大小写敏感,各交易所合约格式可能有差别,以下是具体交易所对应的合约格式。当我们设置不同交易所期权合约代码,可以对照看下。
| 交易所 | 代码格式 |
|---|---|
| 上期所 | 小写 + 四个数字 + C/P + 行权价 |
| 郑商所 | 大写 + 三个数字 + C/P + 行权价 |
| 中金所 | 大写 + 四个数字 + -C-/P- + 行权价 |
| 大商所 | 小写 + 四个数字 + -C-/P- + 行权价 |
| 上能源 | 小写 + 四个数字 + -C-/P- + 行权价 |
| 广期所 | 小写 + 四个数字 + -C-/P- + 行权价 |
这里的大写C是call option的简写,代表看涨, P是put option的简写,代表看跌。
需要注意的是并不是所有的期货品种都具有期权的。比如玻璃和纯碱都不具有期权合约。
下面我们看下在优宽进行期权合约的查找和订阅,例如我们想查询当前的原油合约,首先和刚才一样设置instruments,然后在InstrumentName查找是否包含sc,原油的期货合约代码,并且包含大写C,或者大写P,最后返回当前所有正在交易的期权合约。
javascript
function main(){
var productsForFind = null
while(true){
if(exchange.IO("status")){
LogStatus(_D(), "已经连接CTP !")
var ret = exchange.IO("instruments")
ret.forEach(function(product) {
// Log(product)
// 这里设置要查的名字,sc铁矿石合约
if (product.InstrumentName.indexOf("sc") != -1 && (product.InstrumentName.indexOf("P") != -1 || product.InstrumentName.indexOf("C") != -1)) {
Log(product, "#FF0000")
productsForFind = product
}
})
break
} else {
LogStatus(_D(), "未连接CTP !")
}
Sleep(1000)
}
Log(productsForFind, "#FF0000")
}
如果想设置具体的期权合约,并获取正在交易的ticker数据,可以这样设置。同样使用SetContractType,合约代码为i2311-P-840。代表选择了铁矿石23年11月份的看跌合约,行权价是840。然后日志信息里返回当前正在交易的合约明细和ticker数据。
javascript
function main() {
// 判断商品期货行情和交易连接是否正常
while(!exchange.IO("status")) {
Sleep(1000);
}
// 设置合约代码
Log('合约明细', exchange.SetContractType("i2311-P-840"));
Log('ticker数据', exchange.GetTicker())
}
股票合约的设置
最后我们来看下股票合约的设置。在优宽平台,不仅支持A股,还支持港股和美股的数据获取,策略验证和回测。支持的交易所包括中泰XTP模拟交易,和富途牛牛的实盘交易、模拟盘交易。如果大家对股票的量化交易感兴趣的话,我们后续也会为大家推出一些股票类的量化交易课程。
下面我们来看下具体股票合约的订阅方式,A股分为上交所,深交所和北交所,目前北交所的股票是暂时不支持的,我们后续会进行添加。订阅合约的时候,在股票代码需要具体交易所的后缀。
上交所股票后缀需要加上SH,例如我们这里获取茅台的ticker数据。在回测页面,平台需要选为股票证券。
javascript
function main() {
var info = exchange.SetContractType("600519.SH") // 设置为股票600519.SH即贵州茅台
Log(info)
Log(exchange.GetTicker()) // 获取股票贵州茅台当前的价格信息
}
深交所股票后缀是SZ,这里展示获取万科股票的数据。
javascript
function main() {
var info = exchange.SetContractType("000002.SZ") // 设置为股票000002.SZ即万科
Log(info)
Log(exchange.GetTicker())
}
港股的后缀是HK。
javascript
function main() {
var info = exchange.SetContractType("00700.HK") // 设置为腾讯控股
Log(info)
Log(exchange.GetAccount())
Log(exchange.GetTicker())
}
美股的后缀是UK。
javascript
function main() {
var info = exchange.SetContractType("AAPL.US") // 订阅合约代码AAPL.US
Log(info) // 打印订阅的合约的详细数据
var ticker = exchange.GetTicker() // 获取AAPL.US 合约的行情数据
Log(ticker) // 打印获取到的数据
}
这样我们就可以使用股票的数据进行一些量化策略的编写和验证,并且可以连接交易所的模拟账户,对接真实的市场,进行仿真实盘的演练。
以上呢,就是我们在优宽平台订阅不同类型合约的实际操作展示。可以发现,这里可以使用进行量化探索的地方很多,而且重点是数据获取和回测模拟,全部都是免费的。我们呢,可以在文库中找到一些策略的源码,比如这里的股票双均线,网格策略,我们都可以挑选不同的品种进行尝试。如果大家在学习的过程中,遇到了问题,也欢迎留言,我们会为大家热心解答~
视频参考链接:
17.期权基础知识介绍
上一节课,我们学习了怎样在优宽平台设置期权和股票合约的明细,有不少的同学非常感兴趣。特别是对于期货和期权的对冲策略,询问我们能不能讲解一下怎样在优宽平台进行实现,本节课,我们就给大家安排。
不熟悉期权的小伙伴确实对这个名词陌生又畏惧,因为相对于期货来说,期权相当于一个期货的衍生品。所以,它的玩法也更多。这是大商所铁矿石的期权合约列表,可以看到,针对于不同月份的合约,包括近月和远月,不同的价格,从低到高依次排列,不同的交易类型,包括认购期权和认沽期权。另外,期权还有不同的交易方向,包括买入期权和卖出期权,不同期权的盈利方式,还有到期时间,包括美式和欧式期权,这些名词压上来,大家肯定都是一头雾水。但是本节课呢,我们将对刚才讲到的名词简单进行一下梳理,帮助大家更全面的认识期权。
期权类型
期权是作为标的资产期货的衍生品而存在的,所以它的价格变化和期货存在紧密的关系。首先,我们来了解一下期权的类型。
看涨期权(认购期权):行权方有权力在未来以约定的价格(行权价格)购买标的资产。
看跌期权(认沽期权):行权方有权力在未来以约定的价格(行权价格)卖出标的资产。
一个期权的交易操作,包括买入和卖出。但是和期货不一样的是,买入期权并不一定是多头,而卖出期权也并一定是空头。要知道,理论上一笔期权合约是同时被买入和卖出的,也就是预期相反的两人成为了同一合约的对手方。买入看涨期权意味着标的资产上涨到行权价之后可以选择行权或卖出,就是用更低的价格获得合约或者交易套利,所以看涨期权的买方是典型的多头,反之卖方就是是空头;另一方面,看跌期权的买方则是希望通过标的资产下跌获利的,所以是空头,而身为其对手方的看跌期权卖方便是多头。
所以总结一下就是,买入看涨和卖出看跌都是多方,而卖出看涨和买入看跌都是空方。
| 期权类型 | 交易操作 |
|---|---|
| 看涨期权(认购期权) | 买入(多头) | 卖出(空头) |
| 看跌期权(认沽期权) | 买入(空头) | 卖出(多头) |
我们简单说明一下各种期权类型的风险和收益,也就是关于盈利和亏损的上下限,又来献出这幅经典的图像。

对于买入看涨期权来说,我们的亏损最多就是权利金全部,而如果伴随标的资产的价格增高,盈利是无限的;对于卖出看涨期权的盈利和收益和买入看涨期权刚刚相反,它的盈利最多就是权利金,亏损是无限的;
对于买入看跌期权,盈利最多是标的资产价格变为0,亏损最多就是保证金;对于卖出看跌期权,恰恰相反。
作为一个买方,开仓的权利金亏损完是不需要补仓的,但是对于卖方来说,当亏损出现,还需要及时的进行补足保证金。从一方面来看,卖出期权的风险更大,为什么要当卖方呢?当然并不能仅仅从盈利大小来看,盈利期望的大小还需要乘以概率。巴菲特老爷子,就曾经是股指期货看跌期权的卖方,结果伴随美国股票指数的上升,赚得盆满锛满。
期权价值
下面来了解一下期权的计价方式,期权是有「虚实」之分的,具体指的是实值期权和虚值期权。实值期权,也称价内期权,是指期权的行权价格低于标的资产价格的看涨期权,或是行权价格高于标的资产价格的看跌期权;虚值期权,也称价外期权,是指期权的行权价格高于标的资产价格的看涨期权,或是行权价格低于标的资产价格的看跌期权。
| 期权类型 | 实值期权 | 虚值期权 |
|---|---|---|
| 看涨期权 | 行权价格低于标的资产 | 行权价格高于标的资产 |
| 看跌期权 | 行权价格高于标的资产 | 行权价格低于标的资产 |
我们举例说明一下,对于行权价为3600的螺纹钢看涨期权,当价格点位为3700点时,该期权为「实值期权」,意味着合约交割日如果维持价格不变的话可以以行权价3600便宜买入3700点位的指数;而如果价格点位低于行权价3600点,该期权为「虚值期权」,此时行权就会直接亏损。
但是没有实值的期权就不值钱了吗?当然不是的,期权的价值包括内在价值,也就是实值部分,还包括时间价值,可以等待价格变化达到盈利的点位。
期权价值 = 内在价值 + 时间价值

我们看这张图,这就是标准的T型报价图。中间是行权价格,从低到高排列,左边是认购期权,右边是认沽期权。期权的价值,也就是这里的最新价格,是内在价值和时间价值的和。最新标的资产价格是909,对于认购期权来说,当最新的价格大于行权价格,内在价值是正数,是实值期权;然后随着行权价格升高,内在价值降低直至为0,开始变为虚值期权,这时候的期权的价值开始由时间价值占主导的位置;对于右边的认沽期权,内在价值和时间价值走向刚好相反。
javascript
function main() {
var futureInfo = _C(exchange.SetContractType, 'rb2311')
var futureTick = _C(exchange.GetTicker)
var optionInfo = _C(exchange.SetContractType, 'rb2311C3600')
var optionTick = _C(exchange.GetTicker)
Log('标的资产信息', "#FF0000")
Log('标的资产合约:', futureInfo.InstrumentName)
Log('标的资产价格:', futureTick.Last)
Log('期权信息', '#00FF00')
Log('期权合约:', optionInfo.InstrumentName)
Log('期权价格:', optionTick.Last)
Log('期权行权价格:', optionInfo.StrikePrice)
Log('期权内在价值:', Math.max(0, futureTick.Last - optionInfo.StrikePrice) ) //对于认沽期权,内在价值等于,行权价格减去标的资产价格和0的最大值
Log('期权时间价值:', optionTick.Last - (Math.max(0, futureTick.Last - optionInfo.StrikePrice))) //时间价值
}
我们在优宽平台展示一下期权价格的内在价值和时间价值的计算,方便大家更了解期权的计价方式。由于回测系统没有期权数据,这里我们使用调试工具,对接市场获取真实数据。首先我们获取标的资产信息rb2311,然后获取标的资产的最新价格tick数据;接下来获取期权rb2311C3600的信息,获取它的tick数据。接下来,我们就要来计算了。
首先,打印标的资产的合约名称和价格;接下来打印期权合约名称,最新的价格和行权的价格,是StrikePrice属性;然后进行内在价值的计算。
- 内在价值:对于认购期权来说,内在价值是标的资产价格减去行权价格,当然如果是负数的话,我们就取0,所以使用math.max获取;对于认沽期权,是行权价格减去标的资产价格。
- 时间价值:期权的最新价格减去内在价值。
然后我们打印一下,当前的标的资产价格是3567;认购期权的行权价格是3600,使用3600去买3567的资产,肯定是亏得,所以内在价值是0;但是还具有时间价值,使用期权价格减去内在价值,等于14.5。
以上呢,就是期权价值各部分的计算方式。
期权的了结方式和盈利计算。
期权的了结方式包括平仓和行权。期权在开仓以后,是随时可以平仓的。

比如我们进行期权开仓以后,对已持有的期权仓位进行反向操作叫作平仓。比如我们卖出一手看涨合约,开仓均价是15.5,当最新价格跌到7.5的时候,不考虑手续费,盈利就是(15.5-7.5)*结算单位,比如是10,就是盈利为80块钱。这是我们最常见的了结方式。
还有了结方式是行权了结,就是期权买方行使权利,看涨期权买方以约定的价格买入约定数量的标的期货,看跌期权买方以约定的价格卖出约定数量的标的期货,期权卖方不能行权。期权是有行权日期的,但是对于我们普通的散户来说,我们需要拿一个期权直到行权日吗?当然不是必须的。期权分为美式期权和欧式期权,欧式期权必须要等到行权日才能了结,国内属于欧式期权的品种有:铜期权、黄金期权、上证50ETF期权和沪深300股指期权,其他商品类的期权都是美式期权。
最后的了结方式是放弃,期权买方仅能在到期日当天提交放弃申请。对期权认购方来说,如果亏损大于权利金,期权买方通常选择放弃行权。期权的了结方式比期货多一个方式,相对来说更加灵活。
了解完期权的整体概念以后,我们来在优宽平台实现期权的买卖的操作,因为期权包括认购期权和认沽期权,买卖的方向分为买入合约和卖出合约,并且同时具有开仓和平仓。有不少的同学容易搞混,我们使用代码实现一下。
javascript
function main() {
_C(exchange.SetContractType, 'rb2311C3600')
// 买入合约开仓
exchange.SetDirection('buy')
exchange.Buy(-1, 1)
Sleep(1000)
// 买入合约平仓
exchange.SetDirection('closebuy_today')
exchange.Sell(-1, 1)
Sleep(1000)
// 卖出合约开仓
exchange.SetDirection('sell')
exchange.Sell(-1, 1)
Sleep(1000)
// 卖出合约平仓
exchange.SetDirection('closesell_today')
exchange.Buy(-1, 1)
Sleep(1000)
}
我们使用认购期权展示下,也就是call option,合约包含字母C,分别进行买入合约开仓,买入合约平仓,卖出合约开仓和卖出合约平仓的工作。使用SetContreactType设置一个认购合约。对于交易的操作,和期货一样,首先设置不同的Direction,对于买入和卖出的开仓,分别设置为'buy'和'sell',然后使用交易函数exchange.Buy和exchange.Sell进行开仓操作;对于平仓,设置Direction为'closebuy_today'和'closesell_today',因为是今仓,所以这样设置。当是昨仓的时候,直接使用'closebuy'和'closesell'就可以。可以看到,买入合约类似期货多头开仓,卖出合约类似期货空头开仓;但是因为同时包括认购期权和认沽期权,我们不能简单概括为多头开仓和空头开仓。
整体来说,相对于期货来说,期权的涨跌幅更大,有时候会翻到几十或者上百倍,所以很多同学对此感到陌生又好奇,但是又没有系统的学习过,希望上面这些知识,可以帮助大家更清晰的理解期权的概念。如果大家还是感到很多疑问,可以留言我们的评论区或者后台,我们将会热心解答。
视频参考链接:
18.期权隐含波动率套利策略
上节课我们系统整理了期权的基础知识,作为课程讲解的第一部分基础内容,我们的教程希望更多的服务于小白用户,所以课程的基础知识的讲解更为细致,让我们在理解期权概念的基础上可以更好的学习相关的内容。
本节课,我们来学习有关期权交易的基本策略。期权作为期货的衍生品,有很多种玩法。最简单的就是在判断市场走势和波动率的基础上,单一的买入/卖出看涨期权或者看跌期权,这种需要对市场有着准确的判断,所以冒的风险也比较大;第二种是组合不同类型的期权,形成对冲或者套利策略。
我们大致介绍下使用期权组合的策略。
价差策略:包括垂直价差(到期日相同但是行权价不同的期权组合),水平价差(到期日不同但是行权价相同的期权组合),和对角价差(垂直价差和水平价差的结合,到期日和行权价都不同的期权组合),还有一种比率价差,是使用看涨期权或者看跌期权来合成比率价差的策略。
波动率策略:在预期价格有大幅波动,但是不确定上涨还是下跌的情况下可以使用。具体包括:跨式价差,宽跨式价差,条式策略和带式策略。
区间震荡策略:与波动率策略相对的就是区间震荡策略,当预期价格会在一个区间范围内波动,有明显的支撑位和压力位可以使用。具体有包括蝶式策略(包括看涨蝶式和看跌蝶式);鹰式策略(包括看涨鹰式和看跌鹰式)。
我们这里只是大致介绍了概念,大家需要在深入了解各个策略的使用场景以后,才能具体的开展应用。
本节课我们使用代码演示的是,使用隐含波动率进行套利的策略,相对于标的资产方向是难以预测的,波动率的走势相对更好推测,因为它的均值回归性更强。在标准的定义中,波动率就是指标的价格的波动程度。隐含波动率(Implied Volatility)是相对于历史波动率的(Historical Volatility),它是将市场价格中蕴含的对未来波动率的预期提取出的产物。
javascript
function calculateImpliedVolatility(optionPrice, underlyingPrice, strikePrice, riskFreeRate, timeToExpiration, optionType) {
const ACCURACY = 1e-6;
const MAX_ITERATIONS = 100;
let volatility = 0.5; // 初始的波动率猜测值
for (let i = 0; i < MAX_ITERATIONS; i++) {
const d1 = (Math.log(underlyingPrice / strikePrice) + (riskFreeRate + volatility * volatility / 2) * timeToExpiration) / (volatility * Math.sqrt(timeToExpiration));
const d2 = d1 - volatility * Math.sqrt(timeToExpiration);
let calculatedOptionPrice;
if (optionType === 'call') {
calculatedOptionPrice = underlyingPrice * cumulativeDistribution(d1) - strikePrice * Math.exp(-riskFreeRate * timeToExpiration) * cumulativeDistribution(d2);
} else if (optionType === 'put') {
calculatedOptionPrice = strikePrice * Math.exp(-riskFreeRate * timeToExpiration) * cumulativeDistribution(-d2) - underlyingPrice * cumulativeDistribution(-d1);
}
const difference = calculatedOptionPrice - optionPrice;
if (Math.abs(difference) < ACCURACY) {
return volatility;
}
const vega = underlyingPrice * Math.sqrt(timeToExpiration) * probabilityDensity(d1);
volatility -= difference / vega;
}
throw new Error('无法找到隐含波动率');
}
function cumulativeDistribution(x) {
const a1 = 0.31938153;
const a2 = -0.356563782;
const a3 = 1.781477937;
const a4 = -1.821255978;
const a5 = 1.330274429;
const L = Math.abs(x);
const K = 1 / (1 + 0.2316419 * L);
const cdf = 1 - 1 / Math.sqrt(2 * Math.PI) * Math.exp(-L * L / 2) * (a1 * K + a2 * K * K + a3 * Math.pow(K, 3) + a4 * Math.pow(K, 4) + a5 * Math.pow(K, 5));
if (x < 0) {
return 1 - cdf;
}
return cdf;
}
function probabilityDensity(x) {
return 1 / Math.sqrt(2 * Math.PI) * Math.exp(-x * x / 2);
}
function getIV(option, underlyingPrice){
var optionInfo = _C(exchange.SetContractType, option)
var strikePrice = optionInfo.StrikePrice
var optionPrice = _C(exchange.GetTicker).Last
var riskFreeRate = 0.035
var endDate = new Date(optionInfo.EndDelivDate.replace(/^(\d{4})(\d{2})(\d{2})$/, '$1-$2-$3'))
var today = new Date()
// 计算时间差(以天为单位)
var timeDiff = Math.ceil((endDate - today) / (1000 * 60 * 60 * 24))
var timeToExpiration = timeDiff/365
var optionType = option.match(/.[CP]./)[0][1] == 'C' ? 'call' : 'put'
if(optionPrice && underlyingPrice){
var iv = calculateImpliedVolatility(optionPrice, underlyingPrice, strikePrice, riskFreeRate, timeToExpiration, optionType)
return iv
}else{
Log(option, ': 正在计算中')
}
}
function main(){
var profitA = 0
var profitB = 0
var amountA = 0
var amountB = 0
var typeA = ''
var typeB = ''
var priceA = ''
var priceB = ''
var timeA = ''
var timeB = ''
var initAccount = _C(exchange.GetAccount)
while(true){
if(exchange.IO('status')){
var futureInfo = _C(exchange.SetContractType, underlyingSymbol)
var volumeMul = futureInfo.VolumeMultiple
var underlyingPrice = _C(exchange.GetTicker).Last
var ivA = getIV(hedgeOptionA, underlyingPrice)
Log('IVA', ivA)
var ivB = getIV(hedgeOptionB, underlyingPrice)
Log('IVB', ivB)
var pos = _C(exchange.GetPosition)
if(pos.length == 0){
profitA = 0
profitB = 0
amountA = 0
amountB = 0
typeA = ''
typeB = ''
priceA = ''
priceB = ''
timeA = ''
timeB = ''
var accountInfo = _C(exchange.GetAccount)
var totalProfit = accountInfo.Info.Balance - initAccount.Info.Balance
LogProfit(totalProfit)
if(ivA - ivB > ivEnterThreshold){
exchange.SetContractType(hedgeOptionB)
exchange.SetDirection('buy')
exchange.Buy(-1, hedgeAmount)
exchange.SetContractType(hedgeOptionA)
exchange.SetDirection('sell')
exchange.Sell(-1, hedgeAmount)
}else if(ivB - ivA > ivEnterThreshold){
exchange.SetContractType(hedgeOptionA)
exchange.SetDirection('buy')
exchange.Buy(-1, hedgeAmount)
exchange.SetContractType(hedgeOptionB)
exchange.SetDirection('sell')
exchange.Sell(-1, hedgeAmount)
}
}else{
for(var i = 0; i < pos.length; i++){
if(pos[i].ContractType == hedgeOptionA){
_C(exchange.SetContractType, hedgeOptionA)
callPutA = hedgeOptionA.match(/.[CP]./)[0][1] == 'C' ? '认购期权' : '认沽期权'
typeA = pos[i].Type == PD_LONG || pos[i].Type == PD_LONG_YD ? '买入期权' : '卖出期权'
amountA = pos[i].Amount
profitA = (typeA == '买入期权' && callPutA == '认购期权') || (typeA == '卖出期权' && callPutA == '认沽期权') ?
(_C(exchange.GetTicker).Last - pos[i].Price) * amountA * volumeMul :
(pos[i].Price - _C(exchange.GetTicker).Last) * amountA * volumeMul
priceA = pos[i].Price
timeA = pos[i].Type == PD_LONG || pos[i].Type == PD_SHORT ? '今仓' : '昨仓'
}else if(pos[i].ContractType == hedgeOptionB){
_C(exchange.SetContractType, hedgeOptionB)
callPutB = hedgeOptionB.match(/.[CP]./)[0][1] == 'C' ? '认购期权' : '认沽期权'
typeB = pos[i].Type == PD_LONG || pos[i].Type == PD_LONG_YD ? '买入期权' : '卖出期权'
amountB = pos[i].Amount
profitB = (typeB == '买入期权' && callPutB == '认购期权') || (typeB == '卖出期权' && callPutB == '认沽期权') ?
(_C(exchange.GetTicker).Last - pos[i].Price) * amountB * volumeMul :
(pos[i].Price - _C(exchange.GetTicker).Last) * amountB * volumeMul
priceB = pos[i].Price
timeB = pos[i].Type == PD_LONG || pos[i].Type == PD_SHORT ? '今仓' : '昨仓'
}
}
if(Math.abs(ivA - ivB) <= ivOutThreshold){
if(typeA == '卖出期权' && typeB == '买入期权'){
exchange.SetContractType(hedgeOptionA)
exchange.SetDirection(timeA == '今仓' ? 'closesell_today' : 'closesell')
exchange.Buy(-1, amountA)
exchange.SetContractType(hedgeOptionB)
exchange.SetDirection(timeB == '今仓' ? 'closebuy_today' : 'closebuy')
exchange.Sell(-1, amountB)
}else if(typeA == '买入期权' && typeB == '卖出期权'){
exchange.SetContractType(hedgeOptionA)
exchange.SetDirection(timeA == '今仓' ? 'closebuy_today' : 'closebuy')
exchange.Sell(-1, amountA)
exchange.SetContractType(hedgeOptionB)
exchange.SetDirection(timeB == '今仓' ? 'closesell_today' : 'closesell')
exchange.Buy(-1, amountB)
}
}
}
var optionAStatus = {
type: "table",
title: "期权A信息",
cols: ["期权名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏", '隐含波动率'],
rows: []
}
var optionBStatus = {
type: "table",
title: "期权B信息",
cols: ["期权名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏", '隐含波动率'],
rows: []
}
optionAStatus.rows.push([hedgeOptionA, typeA, priceA, amountA, profitA, ivA])
optionBStatus.rows.push([hedgeOptionB, typeB, priceB, amountB, profitB, ivB])
LogStatus('`' + JSON.stringify(optionAStatus) + '`\n' +
'`' + JSON.stringify(optionBStatus) + '`')
}else{
LogStatus('未连接交易所')
}
Sleep(1000)
}
}
具有一些共同属性的期权波动率的走势应该是一致的,比如说同一标的资产,同一到期时间,但是不同行权价格的期权。通过判断两个同类型期权的隐含波动率相对来说是否有高估和低估,并以“高卖低买”的形式进行交易。这种方法非常类似于股票或期货统计套利中的“配对交易”,只不过在此关注的指标并不是二者价格,而是二者的隐含波动率。当期权1的隐含波动率为20,期权2为15,当我们判断期权1与期权2之间的波动率差异偏高,我们可以设置做空期权1,买入期权2,预期二者波动率之间的差距会缩小,从而实现获利的目的。
首先我们进行的是隐含波动率的计算,这方面涉及到比较复杂的统计学知识,手写轮子是比较复杂的,我们可以使用chatGPT的帮助。输入提问“使用js手动计算隐含波动率”。可以看到,很快的为我们返回了代码,并具体解释各个函数的含义。我们复制到优宽平台。
为获取实时的数据并进行计算,这段代码我们需要修改一下。前面的指标计算的函数不需要改变。原始的代码这里是带入固定数值进行计算,我们需要实时的获取数值,所以这里我们定义getIV函数。我们看下IV计算需要的参数,一共有六个,分别是:
- optionPrice:期权价格
- underlyingPrice:标的资产价格
- strikePrice:行权价格
- riskFreeRate:无风险利率
- timeToExpiration:剩余时间
- optionType:期权类型
可以看到,只有underlyingPrice标的资产价格这个参数需要用到期货的实时价格,其他价格参数都是期权信息中可以获取到的价格,所以getIV函数定义两个参数:option,是期权的合约;underlyingPrice,对应标的资产实时价格,我们可以在主函数中进行获取。
接下来,我们在函数中获取各个参数,首先使用SetContractType获取期权合约的信息optionInfo,然后获取合约信息里的strikePrice行权价格,接着获取期权的实时价格,使用ticker的最新价格,这里都使用的是_C,防止获取不到信息。无风险利率,我们参考使用的是LPR,中国的贷款市场报价利率。剩余时间timeToExpiration,我们需要进行一下处理,因为期权信息里返回的只有EndDelivDate截止的具体日期,为计算距离今天的截止日期,我们使用正则表达式处理一下,获取timeToExpiration变量。最后的期权类型optionType,同样使用正则表达式,判断是call,还是put。这样iv计算的参数就获取完成了。
这里为了容错处理,没有获取到实时的数据。使用if判断在获取到期权价格和标的资产价格的时候,进行计算;否则返回信息,“正在计算中”。
这样隐含波动率指标的计算就定义完成,这里可以删除这段代码。隐含波动率指标的计算的结果大家可以放心的使用,因为我对比过大商所的期权计算工具,两者的差距并不大。
在进入主函数之前,我们需要设计几个策略参数,具体包括期权A和期权B的合约,对冲期权数量,标的资产,iv入场阈值和出场阈值。接下来进入我们主函数的设置。
固定框架安排上。然后,我们设置合约A和合约B的几个变量,用来记录仓位的信息,包括实时的收益,仓位数量,仓位类型,仓位均价,仓位时间,是今仓还是昨仓。同时为了计算实时的收益,我们首先获取初始账户的信息initAccount。
在while循环里,设置期货合约,获取期货的实时价格underlyingPrice,作为参数放入getIV函数中,分别计算期权A合约的隐含波动率ivA,和期权B的ivB。
在获取到两个合约的iv信息后,我们就可以根据两个iv值之间的差价做入场或者出场信号的判断。我们设定一个参数,ivEnterThreshold,iv入场阈值,如果两者之间的差值大于阈值大于该值,我们就进行卖出iv值较高的合约,同时买入iv值较低的合约;两个值之间的差距总是要回归的,在开仓以后,如果两个iv值之间的差距小于另一个参数ivOutThreshold的时候,我们就要及时的设置平仓。这就是利用波动率差距进行开平仓的逻辑。
在代码里,我们获取仓位信息pos。如果目前没有开仓,首先将期权A和期权B的仓位的信息重置为初始值,然后在判断ivA - ivB大于入场阈值的时候,进行空合约A,多合约B的操作;我们上节课提到过,期权的买卖和期货一样,需要先SetDirection,然后使用交易函数下单。如果ivB - ivA大于入场阈值,进行多合约A,空合约B的操作。
如果检查持有仓位,使用for循环,找到对应合约,分别给不同合约的不同属性进行赋值,需要注意的两点是,第一,因为认购期权和认沽期权同时具有买入和卖出的方向,所以这里将type属性定义为'买入期权'和'卖出期权';第二,期权的收益在仓位信息中是不返回的,我们需要手动的进行计算。这里我们需要判断是多头,包括买入认购期权和卖出认沽期权,还是空头,包括卖出认购期权和买入认沽期权;然后乘以仓位的数量,还需要乘以一手多少个单位,这个值我们可以在期货合约信息中获取到,在这里增加一个变量volumeMul,然后进行相乘,计算期权的实时收益。
在获取完成合约信息以后,在判断两个iv值的差距回归到正常的水平,也就是绝对值小于出场阈值,需要根据合约A和合约B的仓位类型,包括是卖出还是买入,今仓还是昨仓,设置正确的Direction,然后进行平仓的操作。
最后我们进行可视化的展示,对期权A和期权B的仓位信息和隐含波动率的信息,进行图表的绘制;
如果我们想展示实时的收益曲线,以往我们使用的都是利用实时的仓位净值减去初始的仓位净值。放在这使用反而不太合适,因为我们买入期权是支付权利金,卖出期权是收到权利金,所以资产净值会发生变化,因为权利金的变化会造成净值数值的不准确,所以我们可以选择在平仓以后,就是仓位列表为0的时候,进行收益的计算,确保不受到权利金的干扰。
最后设置程序休眠时间为1秒,这样我们的期权隐含波动率对冲策略就设置完成了。因为回测系统是不包含期权信息的,所以我们需要使用实盘进行策略的运行。创建实盘,选择rb2311合约作为标的资产,rb2311P3600和rb2311P3550,两个相同到期时间但是不同行权价格的认沽期权作为对冲合约,这里设置的参数,因为我们这里想快速的验证是否可以进行开平仓的操作,所以两个阈值设置的小一点。
我们实盘运行一下,可以看到首先计算隐含波动率,然后根据两者的差值,进行开平仓的操作,这样我们的隐含波动率对冲策略就完成了,不涉及到复杂的数理统计学知识,只需要了解策略的设计框架,我们就可以进行这一个属于专业量化领域的策略。
当然回归实际,这个策略只是一个简单的根据两个期权的隐含波动率不同进行套利的策略,其实这两个期权的选择可以结合市场的预测趋势,结合价差策略的不同类型,进行双重期权组合的套利,可以在一定程度上提高盈利概率。
视频参考链接:
19.期权Delta动态对冲策略
我们来简单介绍一下,期权定价模型一般使用的是B-S模型,期权价格根据【标的物价格】、【行权价】、【到期剩余时间】、【(隐含)波动率】、【无风险利率】确定价格。这部分涉及到比较复杂的数理统计学知识,因此这里需要我们需要做到的是知道这些术语的概念,以及怎样使用就可以,至于具体的推导方式我们不需要过于在意。
期权的风险敞口,也就是各种希腊字母,大家可以先大致了解这些字母的含义,再具体的代码设计中我们会具体讲解怎样使用这些字母。
| 希腊字母 | 定义 | 性质 |
|---|---|---|
| Delta | 衡量期权价格相对标的资产价格变动的敏感度,数学公式:Delta = 期权价格的变化 / 标的资产的变化。 | 看涨期权的 Delta>0,看跌期权的 Delta<0;看跌期权的 Delta = 看涨期权的 Delta - 1。 |
| Gamma | 衡量期权价格相对于标的资产价格变化速度(即加速度)的敏感性。 | Delta 的变化率。 |
| Theta | 衡量期权价格相对于到期时间变化的敏感性,通常都是负数。 | 当期权是平值和距离到期日不足30天时,时间衰减最快。 |
| Vega | 衡量期权价格相对于资产波动率变化的敏感性。 | 历史波动率 |
| Rho | 衡量期权价格相对于无风险利率变化的敏感性。 | 利率 |
| Zeta | 衡量期权价格每变动 1% 时,隐含波动率的变化百分比。 | 隐含波动率 |
我们看到因为期权和期货的价格变动往往存在一定程度的正相关性,所以常见的期权和期货组合对冲策略包括:
- Delta对冲:持有一定数量的标的资产或期货合约,并同时买入或卖出相应数量的期权合约达到Delta中性,用来抵消标的资产价格波动带来的损益。
- Vega对冲:通过同时持有认购期权和认沽期权来对冲隐含波动率的变化对投资组合价值的影响。
- 跨市套利:利用不同交易所或不同期限的期权和期货合约之间的价差,进行风险对冲和获利。
今天,我们要使用代码实现的策略是Delta对冲策略。策略原理是通过配平期权和期货的Delta,达到交易方向的风险中性。
Delta是期权风控核心参数,简单理解就是「标的资产价格」变动一个点,「期权价格」将变化Delta个点,也就是标的资产和权利金的联动比。Delta是期权价格关于标的价格的一阶偏导,可以理解是期权价格与标的资产价格关系曲线的斜率,这个参数客观反应了期权价格对于标的价格变化的敏感程度。
其实标的资产也具有Delta,对于多头恒定是1,对于空头恒定是-1。所以使用这个公式我们可以达到风险中性。
标的资产Delta * 标的资产数量 = 期权Delta * 期权数量 * -1
举例说明一下,假设当前螺纹钢看跌期权Delta = -0.5,这就意味着当标的资产螺纹钢多头期货合约每上涨1个点时,期权合约价格会下降0.5个点。怎样达到风险中性呢,我们配平这个Delta就可以。
标的资产螺纹钢多头期货合约Delta是1,标的资产数量也是1,看跌期权Delta是-0.5,所以获取到的期权数量是2,买入两份看跌期权就可以。那么有没有其他配平的方式呢,大家可以想一下。当我们也可以选择卖出两份Delta是0.5的看涨期权,但是卖出期权需要消耗的保证金较多,所以更多时候选择的是买入期权。
期权Delta伴随价格的涨跌是随时改变的,这个公式的平衡会被破坏,所以我们需要根据期权Delta的变化,对应的对期权进行加仓或者减仓,用来实时Delta的动态平衡。这对于套期保值的领域很是友好,在降低风险的情况下,实现自己初始的建仓目的。我们来看下具体的策略设计。
javascript
function callDelta(underlyingPrice, strikePrice, riskFreeRate, timeToMaturity, volatility) {
var d1 = (Math.log(underlyingPrice/strikePrice) + (riskFreeRate + 0.5 * volatility * volatility) * timeToMaturity) / (volatility * Math.sqrt(timeToMaturity));
return erf(d1 / Math.sqrt(2.0)) * 0.5 + 0.5;
}
function putDelta(underlyingPrice, strikePrice, riskFreeRate, timeToMaturity, volatility) {
var d1 = (Math.log(underlyingPrice/strikePrice) + (riskFreeRate + 0.5 * volatility * volatility) * timeToMaturity) / (volatility * Math.sqrt(timeToMaturity));
return erf((d1 / Math.sqrt(2.0))) * 0.5 - 0.5;
}
// erfc(x)是N(x)的补函数
function erfc(x) {
var z = Math.abs(x);
var t = 1 / (1 + z / 2);
var r = t * Math.exp(-z * z - 1.26551223 + t * (1.00002368 + t * (0.37409196 + t * (0.09678418 + t * (-0.18628806 + t * (0.27886807 + t * (-1.13520398 + t * (1.48851587 + t * (-0.82215223 + t * 0.17087277)))))))));
return x >= 0 ? r : (2 - r)
}
function erf(x) {
var sign = x < 0 ? -1 : 1;
return sign * Math.sqrt(1 - Math.pow(Math.E, -x*x*(4/Math.PI + 0.147*Math.abs(x))))
/ (1 + 0.5*Math.abs(x));
}
var hedgeAmount = null
function main(){
var underlyingType = ''
var underlyingPrice = 0
var underlyingAmount = 0
var underlyingProfit = 0
var hedgeType = ''
var hedgePrice = 0
var hedgeAmountNow = 0
var hedgeProfit = 0
while(!exchange.IO('status')){
Log('等待连接CTP')
Sleep(1000)
}
Log('开始建仓')
// 买入标的资产
_C(exchange.SetContractType, underlyingSymbol)
exchange.SetDirection("buy")
exchange.Buy(-1, openAmount)
while(true){
if(exchange.IO('status')){
var futureInfo = _C(exchange.SetContractType, underlyingSymbol)
var r = exchange.GetRecords(PERIOD_D1)
var underlyingPrice = r[r.length - 1].Close
var volumeMul = futureInfo.VolumeMultiple
var volatility = talib.STDDEV(r, 30)[r.length - 1]
var riskFreeRate = 0.0345
var optionInfo = exchange.SetContractType(hedgeOption)
var strikePrice = optionInfo.StrikePrice
var optionTicker = exchange.GetTicker()
var endDate = new Date(optionInfo.EndDelivDate.replace(/^(\d{4})(\d{2})(\d{2})$/, '$1-$2-$3'))
var today = new Date()
// 计算时间差(以天为单位)
var timeDiff = Math.ceil((endDate - today) / (1000 * 60 * 60 * 24))
var timeToMaturity = timeDiff/365
var delta = hedgeOption.match(/.[CP]./)[0][1] == 'P' ?
putDelta(underlyingPrice, strikePrice, riskFreeRate, timeToMaturity, volatility) :
callDelta(underlyingPrice, strikePrice, riskFreeRate, timeToMaturity, volatility)
var pos = _C(exchange.GetPosition)
if(pos.length == 0){
Log('标的资产未建仓')
}else if(pos.length == 1){
Log('开始使用期权合约进行对冲')
hedgeAmount = Math.ceil(Math.abs(pos[0].Amount / delta))
Log('初始对冲数量:', hedgeAmount)
exchange.SetContractType(hedgeOption)
exchange.SetDirection('buy')
exchange.Buy(-1, hedgeAmount)
}else if(pos.length == 2){
for(var i = 0; i < pos.length; i++){
if(pos[i].ContractType == underlyingSymbol){
underlyingPos = pos[i]
underlyingType = underlyingPos.Type == PD_LONG || underlyingPos.Type == PD_LONG_YD ? '多头' : '空头'
underlyingPrice = _N(underlyingPos.Price)
underlyingAmount = underlyingPos.Amount
underlyingProfit = underlyingPos.Profit
}
if(pos[i].ContractType == hedgeOption){
hedgePos = pos[i]
hedgeType = hedgePos.Type == PD_LONG ? '今仓' : '昨仓'
hedgePrice = _N(hedgePos.Price)
hedgeAmountNow = hedgePos.Amount
hedgeProfit = (hedgePos.Price - optionTicker.Last) * hedgePos.Amount * volumeMul
}
}
hedgeAmount = Math.ceil(Math.abs(underlyingAmount / delta))
if(hedgeAmountNow > hedgeAmount){
Log('对冲期权数量过多,卖出对冲期权')
exchange.SetContractType(hedgeOption)
exchange.SetDirection(hedgeType == '今仓' ? "closebuy_today" : "closebuy")
exchange.Sell(-1, hedgeAmountNow - hedgeAmount)
}
if(hedgeAmountNow < hedgeAmount){
Log('对冲期权数量不足,买入对冲期权')
exchange.SetContractType(hedgeOption)
exchange.SetDirection('buy')
exchange.Buy(-1, hedgeAmount - hedgeAmountNow)
}
}
var tblUnderStatus = {
type: "table",
title: "标的资产持仓信息",
cols: ["合约名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏"],
rows: []
}
tblUnderStatus.rows.push([underlyingSymbol,
underlyingType ,
underlyingPrice,
underlyingAmount,
underlyingProfit
])
var tblHedgeStatus = {
type: "table",
title: "对冲期权持仓信息",
cols: ["合约名称", "持仓类型", "持仓均价", "持仓数量", "持仓盈亏"],
rows: []
}
tblHedgeStatus.rows.push([hedgeOption,
hedgeType,
hedgePrice,
hedgeAmountNow,
_N(hedgeProfit,2)
])
LogStatus('`' + JSON.stringify(tblUnderStatus) + '`\n' +
'`' + JSON.stringify(tblHedgeStatus) +
'`\n实时Delta: ' + _N(delta,2) + ' 动态对冲数量:' + hedgeAmount)
var totalprofit = underlyingProfit + hedgeProfit
LogProfit(totalprofit)
if(timeDiff<=2){
var info = '期权剩余时间为:' + timeDiff + '日, 请及时处理期权对冲套利'
Log(info,'@')
}
}else{
LogStatus('未连接交易所')
}
Sleep(1000 * 2)
}
}
首先搭建好我们的固定框架。这里我们设置hedgeAmount对冲期权数量,为全局变量,初始值为空。然后在main函数中,设置变量标的资产和对冲期权的类型,开仓均价,数量和收益。
Delta动态对冲策略,更多的是实现套期保值的目的,也可以是实现保护利润或者防止亏损进一步扩大,相对于期货的锁仓,期权的权利金较低,可以减少资金的占比投入。这样可以在承担更小的成本的情况下,实现对冲和保护的效果。因此,第一步,我们首先进行建仓。这里需要设置策略的参数underlyingSymbol标的资产,underlyingAmount标的资产数量。在策略开头,在判断连接交易所的情况下,买入标的资产,实现一个建仓的模拟过程。因为我们标的资产是多头,所以想以较少的成本实现风险的对冲,这里可以选择买入认沽期权,所以设置策略参数hedgeOption,代表对冲期权合约名称。
和隐含波动率策略一样,我们需要进行Delta的计算,这也涉及到比较复杂的数学知识,同样可以询问chatGPT替我们完成。我们复制这段代码到我们的策略当中。我们看下这个指标需要的参数有五个,前面四个我们比较熟悉,最后一个volatility,需要注意的是标的资产的价格波动率(也称为实际波动率)。
我们在代码中计算这个指标。首先设置标的资产期货合约,使用GetRecords获取k线数据,参数PERIOD_D1,表示这里我们获取的是日级别的k线数据,因为volatility波动率计算的日级别的。一般使用的是30天的标的资产价格波动,使用talib.STDDEV内置函数,参数填写为30,使用索引获取最新的值。
然后设置变量underlyingPrice,代表标的资产的最新价格,这里也设置了volumeMul合约乘数,用来计算期权的收益。
其他指标的获取,包括无风险利率,行权价格,剩余时间,我们上节课都介绍过。这里将认购期权和认沽期权的Delta的计算函数是分开的,所以我们可以根据期权的名称进行获取。
然后获取仓位信息,开始进行Delta的动态对冲。首先在判断建仓没有完成的情况下,也就是仓位列表长度为0,返回未建仓的消息;如果建仓完成,仓位列表长度为1,我们开始使用期权合约进行对冲风险,首先计算需要对冲合约的数量hedgeAmont,使用标的资产数量除以delta,因为看跌期权的delta是负值,取绝对值,然后取上限,使用Math.ceil。接着设置方向为buy,进行买入数量为hedgeAmont的期权合约。
这里我们初始的使用期权对冲标的资产的涨跌风险就设置完成了,但是Delta作为一个动态的值,Delta的平衡可能会被打破,所以我们要时刻进行监视。在仓位仓位列表长度为2的情况下,分别获取标的资产和对冲期权的仓位信息,包括仓位类型,持仓均价,持仓数量和持仓收益,对于期货这些属性的计算我们都很熟悉;但是对于期权来说,这里的期权类型我们定义为今仓还是昨仓,方便下面对期权进行平仓;还有期权收益的计算,因为原始仓位字段没有返回,我们需要自己计算一下。对于认沽期权的买方,使用持仓均价减去期权的最新价,乘以持仓数量和合约乘数。
接下来,我们更新hedgeAmount变量。接着我们就要进行判断,使用现在持有的期权数量hedgeAmountNow,和需要的期权数量hedgeAmount进行比较。如果现有期权的持仓数量大于需要的持仓数量,我们就进行减仓,卖出多余的期权,数量是hedgeAmountNow - hedgeAmount;相反,如果数量不足,我们就需要进行补仓,补仓数量是hedgeAmount - hedgeAmountNow。在卖出期权的时候,我们需要判断是今仓还是昨仓,然后设置对应的Direction。这样我们的Delta动态调整就完成了。
最后一部分进行可视化展示,首先定义两个表格,实时展示标的资产和对冲期权持仓的状态。这里我们也进行了收益的展示,对标的资产和对冲期权收益进行汇总展示。
因为Delta动态对冲的目标并不是直接获利,而是通过对冲风险来保护现有头寸或利润,所以更多的基于套期保值的目的。所以在期权即将到达行权日期的时候,我们需要及时的发出信息。这里判断剩余时间不到两天的时候,发送提醒对冲期权合约即将到期,请及时处理,这里我们使用“@”符号,方便推送到设置的APP或者邮箱当中。
Delta对冲策略的频率应该设置的大一点,防止Delta变化过快,造成频繁的期权开仓平仓,造成手续费的浪费,这里我们为了教学的展示,休息间隔设置为2秒。
这样我们的一个Delta对冲策略就设置完成了,我们建立实盘看下。初始的实盘参数标的资产合约rb2311,买入数量设置为10,因为标的资产是多头,因此对冲期权选择的是一个认沽期权rb2311P3600,行权价是3600。根据日志信息,在策略起始阶段,我们首先建仓,然后根据期权的Delta值,进行相应的期权对冲合约的买入,接着就是Delta动态调整和需要的期权的调仓。这里呢,也展示了标的资产和对冲期权的持仓状态信息,还有实时展示了Delta和动态对冲数量。
我们的策略设计展示了一个模拟建仓以后,使用Delta对冲策略进行风险控制的策略设计。需要注意的是,这个策略只是为了教学的展示,实际上这个策略可以优化的地方还有很多,比如这里我们初始的标的资产和对冲期权都是手工进行参数选择的。还记得我们前面讲过的交互控件吗,这里使用交互控件进行初始的建仓过程,然后根据Delta的变化,发送消息可以选择手动的进行对冲期权的加仓或者加仓。大家可以根据自己想法进行进一步的优化。
视频参考链接:
20.RSI多头短线择时策略
最近有小伙伴联系我们,说他在量化论坛里看到了一个很热门的策略,但是又苦于量化系统和实盘配置的麻烦。在学习完我们前面的课程以后,非常喜欢优宽平台“即开即用”的特性。于是他询问我们能不能在我们平台实现这个策略,我们的答案当然就是“立即安排”。
我们首先来看下这个策略的介绍,这个策略的名称是“RSI多头短线择时策略”。RSI指标我们都非常熟悉,相对强弱指标(Relative Strength Index,简称RSI)是一种常用的技术分析指标,用于衡量市场的超买和超卖情况,以及确认价格趋势的强度和转折点。它常用的方法就像文章里介绍的一样,在RSI超过70时卖出资产,当RSI跌破30时买入资产。另外,RSI也可以用作动量趋势指标,比如在上升趋势中,RSI通常在40到80之间波动,在下降趋势中在20到60之间波动。所以,这篇文章的大佬就围绕RSI介绍了三个多头短线择时策略,并且最后组合上述三个策略中的核心信号,构建一个新的RSI多信号集成策略,并通过回测取得了良好的效果。
话不多说,我们直接开始。第一个策略「RSI经典策略」。它的核心思想是在市场情绪处于相对低迷时入场,所以它的开仓条件是RSI小于15,只做多头的方向,离场条件是今日收盘价高于昨日高点。我们看下初始化代码,原始的代码是基于python语言的,首先定义RsiStrategy类对象,里面包含了进行rsi策略的变量定义和方法,可以看到首先定义rsi相关的变量和参数,接着定义on_init函数,用来进行策略初始化,第二个函数on_bar定义策略的执行逻辑,理解起来并不是复杂,我们来看使用JavaScript语言在优宽平台能否编写的更为简便一点。
注意:我们只是借鉴了策略的交易逻辑,至于具体的开仓手数等代码细节我们并没有完全复刻。并且代码中有一些细节并没有处理完善,比如止损操作,所以我们的代码将完善这些细节。
打开策略编辑页面,固定框架安排下。首先设置策略的目标合约,我们使用策略参数的形式。在主代码中,在while循环体外,设置计算RSI指标窗口rsiWindow是2,rsi阈值rsiThreshold是15,最大持仓周期maxHolding是9,也就是如果持仓的天数超过了9天,但是还没有达到平仓的条件,这就说明遇到了单边的趋势,我们需要及时止损,这里我们还设置了开仓时间记录变量openTime,为了交易的简便,我们使用交易类库的单品种控制对象p。
接着在主函数中,首先设置固定框架。接着订阅合约,获取k线数据。然后使用k线数据计算rsi指标,周期设置为rsiWindow参数,定义昨日高点preHigh,定义当前时间nowTime变量。接着定义入场信号,当最新rsi值小于rsi阈值。
最后是我们交易操作的设置了。首先获取仓位信息pos,在持仓为空,仓位列表长度为0,并且开仓信号成立,使用OpenLong开一手多仓;平仓信号是当持有多仓,仓位列表长度是1,并且k线最新价格大于昨日最高价,或者持仓天数大于等于9,说明遇到了单边趋势要及时止损,这里设置当前的时间减去开仓记录的时间大于等于9日的毫秒数,使用交易类库Cover函数进行平仓。
仅用29行,我们就实现了这一个策略代码。原始的代码中进行回测,需要我们下载固定时间的数据,然后导入python。作为一个“即开即用”的工作,我们不需要下载额外的数据,在回测系统设置好时间和品种直接可以开始回测。我们使用今年呈现强烈多头趋势的白糖品种试一下。可以看到,这个策略在这波上升浪中频繁的进行开平仓的交易,确实取得了一定的收益。但是这个策略适用于所有品种吗?我们换一个螺纹钢主力合约试一下,从今年1月到10月,可以看到策略收益是-4000年,这也是因为这个策略只做多头方向有关,今年整体螺纹钢从年初的4400点下降到目前的3600点,确实策略亏损的次数出现的更多。
下面,我们来看第二个策略。RSI区域动量策略。这个策略包括两个指标:
- RSI多头范围:RSI过去N天内在40到100之间波动。
- RSI多头动量:RSI的极值高点在N天内大于70
这个策略相当于一个双重认证,当RSI必须在过去的N天内在40到100之间波动的时候,并且在这期间出现了一个大于动量阈值70的高点。所以具体的交易逻辑是:
- 当RSI多头范围和多头动量条件都成立时,开仓。
- 当RSI多头范围和多头动量条件都不再成立时,平仓。
原始的策略中多头范围设置的区间变量是100,但是对于目前的交易环境多空头转换的很快,所以我们将多头范围区间设置为30。
我们来看下具体代码的编写,首先设置策略参数变量,rsiPeriod,rsi计算周期,rsiWindow,rsi多头范围,rsi_lower和rsi_upper是回看周期内最低和最高阈值,rsi_highest是rsi动量阈值。接下来进入主函数,设置合约,获取k线,计算rsi值,周期设置为rsiPeriod参数,然后取回看范围rsiList,可以使用数组的slice函数,参数填写-rsiWindow,取到回看周期内的rsi值。
接着进行多头范围和多头动量的判断。在原始的python代码里,由于没有使用封装,所以这部分的设置显得较为复杂一点,我们看下能否编写的更加简单一点。多头范围是判断在回看周期内rsi的值都需要在一定范围,可以使用数组的every函数,判断每一个值是否大于最低值并且小于最高值;接着多头动量,判断回看周期内是否有一个值大于多头动量阈值。可以使用数组的some函数。可以看到,使用array的一些函数可以很方便的帮我们进行一些数组的操作,省去了需要for或者while循环的轮子。
接下来就到了开平仓的操作了,获取当前仓位,如果持仓为空,并且两个信号为真,进行开多仓的操作;如果判断持有仓位,在出现两个信号中有一个不成立的情况下,我们及时的进行平仓。
这就是第二个策略的编写,我们同样使用白糖进行一下回测,可以看到相对于第一个策略,这个策略策略采取的操作更少,但是取得了8000多元的收益。这波涨势抓的是不错的。换一个品种,螺纹钢,不出意外的出现意外,收益为-1200多元。
第三个策略,RSI-IBS 策略,这个策略在rsi指标之外,新加了一个指标IBS,表示内部K线强度),指标计算公式如下:
(Close - Low) / (High - Low)
IBS指标的波动范围从0到1,测量收盘价相对于日内高低点的位置,较低的值被认为是看涨的,而较高的值则是短期看跌的,IBS的基本假设是市场具有均值回归的特性。
策略交易逻辑为:
- 开仓:IBS低于0.25、RSI低于45
- 平仓:当日收盘价高于昨日收盘价
我们来看下具体代码的编写,同样的首先设置参数rsiWindow是21,rsiThreshold是45,增加一个指标ibs_entry是0.25。在while循环里,在rsi指标计算之外,增加ibs指标的计算。然后我们就可以进行开仓信号的判断了,在rsi值小于rsiThreshold,并且ibs小于0.25情况下,定义longSignal。
接着进行交易的操作,满足开仓条件进行入场;对于平仓的逻辑是今日的收盘价大于昨日,或者持仓天数超过9天。我们来看下回测的结果。
同样的使用白糖,可以看到相对于第一个策略在整体的时间段都有开平仓的操作,第二个策略主要在上升浪的过程中进行开平仓的操作,这个策略是在上升浪之外,也就是更多的在震荡的时间,进行一些开平仓的操作。这时候我们聪明的小脑瓜开始活动了,如果我们合成第二个第三个策略,是不是会取得更好的效果呢?
其实原始策略的最后一部分就是合成使用这三个策略的指标进行开平仓的操作,但是我们看到第一个策略信号很容易触发,所以可能会进行更多的无效操作,所以我们来自作主张,只合成第二个第三个指标,看看策略的运行效果。
javascript
/*backtest
start: 2023-01-03 09:00:00
end: 2023-10-25 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["symbol","SR888"]]
*/
function rsiRangeMom(r) {
var rsiPeriod = 14
var rsiWindow = 30
var rsi_lower = 40
var rsi_upper = 100
var rsi_highest = 70
var rsi = TA.RSI(r, rsiPeriod)
var rsiList = rsi.slice(-rsiWindow)
var rangeSignal = rsiList.every(value => value >= rsi_lower && value <= rsi_upper)
var momSignal = rsiList.some(value => value > rsi_highest)
var longSignal = rangeSignal && momSignal
var coverSignal = rangeSignal == false || momSignal == false
return [longSignal, coverSignal]
}
function rsiIbs(r) {
var rsiPeriod = 21
var rsiThreshold = 45
var ibs_entry = 0.25
var rsi = TA.RSI(r, rsiPeriod)
var preClose = r[r.length - 2].High
var ibs = (r[r.length - 1].Close - r[r.length - 1].Low) / (r[r.length - 1].High - r[r.length - 1].Low)
var longSignal = rsi[rsi.length - 1] < rsiThreshold && ibs < ibs_entry
var coverSingal = r[r.length - 1].Close > preClose
return [longSignal, coverSingal ]
}
function main() {
var maxHolding = 9
var openTime = null
var p = $.NewPositionManager()
var initAccount = exchange.GetAccount()
while(true){
if(exchange.IO('status')){
LogStatus('已连接交易所')
exchange.SetContractType(symbol)
var r = exchange.GetRecords()
var nowTime = r[r.length - 1].Time
var rsiRangeMomIndicator = rsiRangeMom(r)
var rsiIbsIndicator = rsiIbs(r)
var pos = exchange.GetPosition()
Log(rsiRangeMomIndicator[0],rsiRangeMomIndicator[0])
Log(rsiIbsIndicator[0],rsiIbsIndicator[1])
if(pos.length == 0 && (rsiRangeMomIndicator[0] == true || rsiIbsIndicator[0] == true)){
p.OpenLong(symbol, 1)
openTime = nowTime
} else if(pos.length == 1 && ((rsiRangeMomIndicator[1] == true && rsiIbsIndicator[1] == true) || nowTime - openTime >= 1000*60*60*24*maxHolding)){
p.Cover(symbol, 1)
openTime = null
}
var posStatus = {
type: "table",
title: "持仓信息",
cols: ["合约名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏"],
rows: []
}
var posDir = pos.length > 0 ? (pos[0].Type == PD_LONG || pos[0].Type == PD_LONG_YD) ? '多头' : '空头' : ''
var posPrice = pos.length > 0 ? pos[0].Price : 0
var posAmount = pos.length > 0 ? pos[0].Amount : 0
var posProfit = pos.length > 0 ? pos[0].Profit : 0
posStatus.rows.push([symbol, posDir, posPrice, posAmount, posProfit])
lastStatus = '`' + JSON.stringify([posStatus]) + '`'
LogStatus(lastStatus)
var accountInfo = exchange.GetAccount()
var totalProfit = accountInfo.Balance - initAccount.Balance
LogProfit(totalProfit)
}else{
LogStatus('等待连接交易所')
}
Sleep(1000)
}
}
根据原始的策略原理:这里我们来定义如果第二第三个策略开仓信号任意成立一个,我们进行开仓的操作;对于平仓,我们的信号判断是,如果第二第三个策略平仓信号同时成立的话,进行平仓的操作。所以我们第一步就是需要计算两个策略的开仓信号和平仓信号。将两个策略的变量一起放入主函数,会有些杂乱,并且两个策略的参数名是一致的,但是参数值是不一致的,所以我们可以定义两个函数,用来返回两个策略的开平仓信号。这里来分别定义rsiRangeMom和rsiIbs函数,参数都是k线。在两个函数体内,我们根据两个策略的内容,删掉交易操作的部分,计算出开仓信号和平仓信号,最后使用return进行返回。需要注意的是,第三个策略的最大持仓时间,需要在开仓后才能统计,所以我们将这部分放在主函数中进行定义。
在主函数中,我们进行开平仓的操作。如果持仓为空,并且两个策略的开仓信号任意成立一个,进行入场;对于平仓,如果两个平仓信号成立,或者持仓时间达到上限,我们进行平仓。同样的使用白糖,我们进行策略的验证。可以看到,组合策略吸收了两个策略的优点,在趋势和震荡时间段,同时具有交易的操作,所以盈利部分也获得了一定的增加。
当然这个策略,作为实盘肯定是不足的。我们可以加上一些可视化的展示。加上持仓状态的展示和实时的收益展示。这就是我们可以运行的实盘策略,我们跑下看看。
以上呢,就是我们根据热门策略,在优宽平台使用JS语言实现,通过这几个策略的编写,可以发现无论是数据获取,代码编写和策略运行和回测展示,在优宽平台似乎更加方便一点。当然如果大家有好的建议,也欢迎大家留言评论区,我们会积极的改进,大家有好的想改编的策略,也欢迎投稿,我们会热心帮大家实现~
视频参考链接:
21.行情收集(Mysql数据库)
最近接到大家的反馈,怎样保存实盘的k线数据到数据库当中,从而搭建属于自己的量化数据库。解决方法呢,也很简单,我们可以创建一个实盘数据收集器,用来保存我们的实时数据,这样我们可以在本地连接我们的数据库,进而获取数据进行数据分析和策略探索的一些工作。今天我就来教大家怎样进行实现。
本节课我们的示例是在服务器上实现的,如果你的托管者在本地,那么我相信实现起来更为容易。我们的实时数据获取是在托管者上的,如果想在本地获取,那么我们需要一个中间的媒介,其实优宽拥有专门的数据库工具,DBExec,可以很方便的使用sql语言进行数据的存储和管理。但是有不少熟悉mysql的小伙伴想使用mysql进行搭建,今天呢,我们就以mysql为工具建立一个数据收集器。其实实现起来并不是十分的复杂,但是针对于我们的初学者用户,实现起来中间确实有一些波泽,所以本节课进行实操展示,大家可以参考学习一下。
首先我们需要在服务器端安装mysql软件。这里的服务器我使用的是轻量服务器,基本的交易操作和数据存储都是满足的。由于服务器系统和mysql软件版本有很多,大家可能在安装的过程中出现种种的坑。经过我多次踩坑,最后为大家推荐的教程是这篇。Mysql安装这里面的坑很少,大家一步步安装就可以。
第二部分是远程访问的设置。我们需要进入mysql中进行管理员权限的更改,让远程也可以访问数据库。
最后一部分是服务器安全组的设置,作用是放通mysql的端口,但是这篇文章使用的是标准的服务器,最后一部分安全组的设置开放Mysql的外接端口,在轻量服务器中没有相关的设置,我们需要在这里的防火墙设置中进行端口的放通。防火墙这里添加规则,点击mysql端口。以上呢,就是我们需要安装mysql和设置远程访问的大致步骤。
我们现在可以在远程测试下连接数据库。这里我使用的是python语言,因为在python语言进行数据的清洗,处理和分析都是很方便的。这里我们需要下载一个pymysql的包,安装成功后。打开jupyter notebook,首先import mysql.connector,然后创建 MySQL 连接对象,这里的user和password使我们刚才在mysql中创建的管理员的账户和密码,host填写为服务器的公网地址。然后创建MySQL游标对象,执行 SQL 查询,这里我们要写sql语句,'show databases'展示所有的数据库,然后执行sql语句,接着打印查询到的数据库的名称,成功打印后,证明我们的远程数据库连接成功。这里最后加上游标的关闭和数据库的关闭,防止资源的过渡占用。
python
import mysql.connector
# 创建 MySQL 连接对象
cnx = mysql.connector.connect(user='????', password='????',
host='????')
# 创建 MySQL 游标对象
cursor = cnx.cursor()
# 执行 SQL 查询
query = 'show databases'
cursor.execute(query)
for row in cursor:
print(row)
cursor.close()
cnx.close()
Mysql数据库就基本搭建好了,接下来我们在实盘中需要创建策略,实时的搜集数据,并保存到服务器的Mysql数据库当中。这样在本地,我们可以连接服务器上的数据库获取实时数据进行分析。因为有些数据在回测系统中是不提供的,比如期权的数据,我们需要实时的搜集。
python
import pymysql
# 创建 MySQL 连接对象
cnx = pymysql.connect(
host='????', # 数据库主机地址
user='????', # 数据库用户名
password='????'
)
# 创建 MySQL 游标对象
cursor = cnx.cursor()
# 创建一个新的databases
cursor.execute("CREATE DATABASE IF NOT EXISTS my_database")
# 在新的databases中创建table来保存期货的k线数据,K线数据格式{'Time': 1673884800000, 'Open': 2127.0, 'High': 2148.0, 'Low': 2127.0, 'Close': 2148.0, 'Volume': 2150.0, 'OpenInterest': 3742.0},分割成列进行保存
cursor.execute("USE my_database")
cursor.execute("CREATE TABLE IF NOT EXISTS future_kline ( \
id INT AUTO_INCREMENT PRIMARY KEY, \
timestamp BIGINT NOT NULL, \
open FLOAT NOT NULL, \
high FLOAT NOT NULL, \
low FLOAT NOT NULL, \
close FLOAT NOT NULL, \
volume FLOAT NOT NULL, \
open_interest FLOAT NOT NULL)")
# 在新的databases中创建table来保存期权的k线数据,K线数据格式{'Time': 1673884800000, 'Open': 2127.0, 'High': 2148.0, 'Low': 2127.0, 'Close': 2148.0, 'Volume': 2150.0, 'OpenInterest': 3742.0},分割成列进行保存
cursor.execute("USE my_database")
cursor.execute("CREATE TABLE IF NOT EXISTS option_kline ( \
id INT AUTO_INCREMENT PRIMARY KEY, \
timestamp BIGINT NOT NULL, \
open FLOAT NOT NULL, \
high FLOAT NOT NULL, \
low FLOAT NOT NULL, \
close FLOAT NOT NULL, \
volume FLOAT NOT NULL, \
open_interest FLOAT NOT NULL)")
def main():
preBarTime = 0
while True: # 执行策略逻辑循环
if exchange.IO("status"): # 检测是否与期货公司服务器连接,登录成功
LogStatus(_D(), "已经连接")
exchange.SetContractType("SA401") # 设置要操作的合约,这里设置为SA401,也可以做成参数,由策略参数上进行设置
futureR = exchange.GetRecords(PERIOD_M5)
exchange.SetContractType("SA401P1500") # 设置要操作的合约,这里设置为SA401,也可以做成参数,由策略参数上进行设置
optionR = exchange.GetRecords(PERIOD_M5)
Log(_D(futureR[-1].Time/1000), '期货时间')
Log(_D(optionR[-1].Time/1000), '期权时间')
if len(futureR) < 2 or len(optionR) < 2:
continue
newfutureR = futureR[-2]
newoptionR = optionR[-2]
if newfutureR.Time == newoptionR.Time and preBarTime != newfutureR.Time and preBarTime != newoptionR.Time:
Log('更新时间到')
Log(_D(newfutureR.Time/1000), '期货')
Log(_D(newoptionR.Time/1000), '期权')
preBarTime = newfutureR.Time
# 如果获取到futureR 和 optionR,添加到新创建的两个表中
cursor.execute("USE my_database")
cursor.execute("INSERT INTO future_kline (timestamp, open, high, low, close, volume, open_interest) VALUES (%s, %s, %s, %s, %s, %s, %s)", \
(newfutureR.Time, newfutureR.Open, newfutureR.High, newfutureR.Low, newfutureR.Close, newfutureR.Volume, newfutureR.OpenInterest))
cnx.commit()
cursor.execute("USE my_database")
cursor.execute("INSERT INTO option_kline (timestamp, open, high, low, close, volume, open_interest) VALUES (%s, %s, %s, %s, %s, %s, %s)", \
(newoptionR.Time, newoptionR.Open, newoptionR.High, newoptionR.Low, newoptionR.Close, newoptionR.Volume, newoptionR.OpenInterest))
cnx.commit()
else:
LogStatus(_D(), "未连接") # 如果未连接上期货公司服务器,在机器人状态栏上显示时间,和未连接信息
Sleep(1000*10)
接着我们来写策略创建我们的行情收集器。在这个策略里面,我们需要连接本地数据库,创建数据表,获取实时数据并写入数据表。这里使用到了一些基本的python语言,如果大家感到有一点陌生的话,可以询问chatgpt的帮助。
进入我们的代码,在开头,是同样的步骤,import pymysql,稍微提醒一下,这是服务器上的python环境,所以这个包我们在服务器上需要重新安装一下,其实我们安装很多我们常用的包到我们的服务器当中,比如机器学习或者深度学习的包,这样我们也可以结合深度学习进行策略的编写。回到本节课,这里创建 MySQL 连接对象和游标对象,然后我们需要创建一个新的databases。接下来,就要使用这个databases来实时保存数据。这里为了举例,我们保存下期货和相应期权的k线数据。首先执行sql语句"USE my_database",然后在my_database中创建future_kline和option_kline表,具体表中每栏的设置,我们可以根据返回的k线属性,包括timestamp时间戳,高开低收四个价格,成交量和持仓量,具体的每一列的数据属性,还有NOT NULL表示不允许出现空值。这里还设置了每一行数据的id,作为主key,是一个自增的数字。这样初始的数据库连接和表的设置就完成了,接下来到主函数我们进行实时数据的搜集和写入。
和Javascript语言一样,定义main主函数,还有我们的固定框架,在while循环前,定义preBarTime是先前k线的时间,然后分别设置合约,进行k线数据的获取,定义为futureR和optionR变量,因为CTP协议对期权数据返回的速度比较慢,这里我们设置了k线的周期是五分钟PERIOD_M5,是足够我们进行数据分析的。这里我们收集的是已经完成的k线,也就是倒数第二根k线,所以在开头我们需要判断期货或者期权数据的长度是否大于2,否则跳过本次循环。然后定义变量newfutureR和newoptionR作为期货k线和期权k线倒数第二根的数据,是已完成的k线数据。
接着我们就要数据的收集了,因为返回的时间不一致,所以我们需要判断两根k线的时间戳是否相等,然后判断两根k线的时间戳是否不等于prebartime,证明k线更新了,这三个条件都需要满足以后,我们进行数据的写入,首先将prebartime定义为最新k线的时间戳,然后将这根k线的数据写入表中,这里执行insert语句进行对应属性数值的写入,然后使用commit提交事务进行永久保存。
最后我们设置程序的休眠时间是10秒,因为收集的数据是5分钟的k线,所以可以设置的大一点。总体来说,这份代码并不复杂,结合了sql语句和k线属性的特点,一些地方稍加注意就可以。
我们运行实盘看一下,这里是我们实盘的日志,可以看到等到k线更新,执行写入的工作。这样我们的行情收集器就设置好了。
接下来我们在本地验证下数据库的写入工作。回到我们刚才的代码中,这里需要填写需要的databse,是我们刚才创建完成的my_database,然后这里执行"SHOW TABLES",可以看到我们刚才创建的两个表,然后打开其中的一个期权k线表,可以看到最新导入的数据,我们可以把这个数据转换为dataframe,然后画一个k线图看下。
python
import mysql.connector
cnx = mysql.connector.connect(user='????', password='????',
host='????',
database='my_database')
# 创建游标对象
cursor = cnx.cursor()
cursor.execute("SHOW TABLES")
# 获取查询结果
tables = cursor.fetchall()
tables
cursor.execute("SELECT * FROM option_kline")
# 获取查询结果
results = cursor.fetchall()
results
cursor.close()
cnx.close()
接下来我们在本地验证下数据库的写入工作。回到我们刚才的代码中,这里需要增加一个参数database,填写我们刚才创建完成的my_database,然后打开其中的一个期权k线表,可以看到最新导入的数据,一共有86根k线,我们可以把这个数据转换为dataframe,然后画一个k线图看下。另外,我们也可以进行很多有趣的数据分析工作,大家有兴趣都可以探索一下。
其实行情收集器的应用场景有很多,通过建立自己的量化数据库,我们可以给多个实盘提供数据源,另外可以让实盘启动时,当对需要的K线BAR数量达到一定要求的时候,我们可以直接使用行情收集器里的数据,我们再也不用担心实盘起始的时候K线BAR数量不足了。
这样呢,我们就完成了利用实盘进行实时的数据获取和写入工作,希望可以满足那位小伙伴的疑问。为了教学讲解,这里的sql语言可能写的简单,如果各位有sql大佬,也欢迎进行优化。下节课,我们就将使用我们收集的数据作为自定义数据源,在优宽平台进行回测,有感兴趣的小伙伴不要错过。
视频参考链接:
22.回测系统自定义数据源实践
上节课我们手把手教大家实现了一个行情收集器,那么收集的行情数据接下来怎么使用呢?当然是用于回测系统了,这里依托于优宽量化交易平台回测系统的自定义数据源功能,我们就可以直接把收集到的数据作为回测系统的数据源,这样我们就可以让回测系统应用于任何我们想回测历史数据的市场了。注意是任何数据哦,包括外汇,商品现货,股票等等。
因此,我们可以给「行情收集器」来一个升级!让行情收集器同时可以作为自定义数据源给回测系统提供数据。下面我们来看具体怎样实现。
python
import pymysql
import _thread
import json
import math
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse
def url2Dict(url):
query = urlparse(url).query
params = parse_qs(query)
result = {key: params[key][0] for key in params}
return result
class Provider(BaseHTTPRequestHandler):
def do_GET(self):
try:
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
dictParam = url2Dict(self.path)
Log("自定义数据源服务接收到请求,self.path:", self.path, "query 参数:", dictParam)
databaseName = dictParam["database"]
tableName = dictParam["table"]
# 连接数据库
Log("连接数据库服务,获取数据,数据库:", databaseName, "表:", tableName)
# 创建 MySQL 连接对象
cnx = pymysql.connect(
host=????, # 数据库主机地址
user=????, # 数据库用户名
password=????,
database=databaseName
)
# 创建 MySQL 游标对象
cursor = cnx.cursor()
# 获取到表数据
cursor.execute("SELECT * FROM " + tableName)
# 获取查询结果
results = cursor.fetchall()
# 要求应答的数据
data = {
"schema" : ["time", "open", "high", "low", "close", "vol"],
"data" : [],
"detail": {'PriceTick': 1, 'VolumeMultiple': 10, 'ExchangeID': 'CZCE', 'LongMarginRatio': 0.12, 'ShortMarginRatio': 0.12, "InstrumentID": 'SA888'}
}
for item in results:
# 将数值部分添加到data.data中的列表
data["data"].append([item[1], item[2], item[3], item[4], item[5], item[6]])
Log("数据:", data, "响应回测系统请求。")
# 写入数据应答
self.wfile.write(json.dumps(data).encode())
cursor.close()
cnx.close()
except BaseException as e:
Log("Provider do_GET error, e:", e)
def createServer(host):
try:
server = HTTPServer(host, Provider)
Log("Starting server, listen at: %s:%s" % host)
server.serve_forever()
except BaseException as e:
Log("createServer error, e:", e)
raise Exception("stop")
# 创建 MySQL 连接对象
cnx = pymysql.connect(
host='????', # 数据库主机地址
user='????', # 数据库用户名
password='????' # 数据库密码
)
# 创建 MySQL 游标对象
cursor = cnx.cursor()
# 创建一个新的databases
cursor.execute("CREATE DATABASE IF NOT EXISTS my_database")
# 在新的databases中创建table来保存期货的k线数据,K线数据格式{'Time': 1673884800000, 'Open': 2127.0, 'High': 2148.0, 'Low': 2127.0, 'Close': 2148.0, 'Volume': 2150.0, 'OpenInterest': 3742.0},分割成列进行保存
cursor.execute("USE my_database")
cursor.execute("CREATE TABLE IF NOT EXISTS future_kline ( \
id INT AUTO_INCREMENT PRIMARY KEY, \
timestamp BIGINT NOT NULL, \
open FLOAT NOT NULL, \
high FLOAT NOT NULL, \
low FLOAT NOT NULL, \
close FLOAT NOT NULL, \
volume FLOAT NOT NULL, \
open_interest FLOAT NOT NULL)")
# 在新的databases中创建table来保存期权的k线数据,K线数据格式{'Time': 1673884800000, 'Open': 2127.0, 'High': 2148.0, 'Low': 2127.0, 'Close': 2148.0, 'Volume': 2150.0, 'OpenInterest': 3742.0},分割成列进行保存
cursor.execute("USE my_database")
cursor.execute("CREATE TABLE IF NOT EXISTS option_kline ( \
id INT AUTO_INCREMENT PRIMARY KEY, \
timestamp BIGINT NOT NULL, \
open FLOAT NOT NULL, \
high FLOAT NOT NULL, \
low FLOAT NOT NULL, \
close FLOAT NOT NULL, \
volume FLOAT NOT NULL, \
open_interest FLOAT NOT NULL)")
def main():
preBarTime = 0
Log('测试')
try:
# _thread.start_new_thread(createServer, (("localhost", 9090), )) # 本机测试
_thread.start_new_thread(createServer, (("0.0.0.0", 9090), )) # VPS服务器上测试
Log("开启自定义数据源服务线程", "#FF0000")
except BaseException as e:
Log("启动自定义数据源服务失败!")
Log("错误信息:", e)
raise Exception("stop")
while True: # 执行策略逻辑循环
if exchange.IO("status"): # 检测是否与期货公司服务器连接,登录成功
LogStatus(_D(), "已经连接")
exchange.SetContractType("SA401") # 设置要操作的合约,这里设置为SA401,也可以做成参数,由策略参数上进行设置
futureR = exchange.GetRecords(PERIOD_M5)
exchange.SetContractType("SA401P1500") # 设置要操作的合约,这里设置为SA401P1500,也可以做成参数,由策略参数上进行设置
optionR = exchange.GetRecords(PERIOD_M5)
Log(_D(futureR[-1].Time/1000), '期货时间')
Log(_D(optionR[-1].Time/1000), '期权时间')
if len(futureR) < 2 or len(optionR) < 2:
continue
newfutureR = futureR[-2]
newoptionR = optionR[-2]
if newfutureR.Time == newoptionR.Time and preBarTime != newfutureR.Time and preBarTime != newoptionR.Time:
Log('更新时间到')
Log(_D(newfutureR.Time/1000), '期货')
Log(_D(newoptionR.Time/1000), '期权')
preBarTime = newfutureR.Time
# 如果获取到futureR 和 optionR,添加到新创建的两个表中
cursor.execute("USE my_database")
cursor.execute("INSERT INTO future_kline (timestamp, open, high, low, close, volume, open_interest) VALUES (%s, %s, %s, %s, %s, %s, %s)", \
(newfutureR.Time, newfutureR.Open, newfutureR.High, newfutureR.Low, newfutureR.Close, newfutureR.Volume, newfutureR.OpenInterest))
cnx.commit()
cursor.execute("USE my_database")
cursor.execute("INSERT INTO option_kline (timestamp, open, high, low, close, volume, open_interest) VALUES (%s, %s, %s, %s, %s, %s, %s)", \
(newoptionR.Time, newoptionR.Open, newoptionR.High, newoptionR.Low, newoptionR.Close, newoptionR.Volume, newoptionR.OpenInterest))
cnx.commit()
else:
LogStatus(_D(), "未连接") # 如果未连接上期货公司服务器,在机器人状态栏上显示时间,和未连接信息
Sleep(1000*10)
程序开头导入了一些需要使用的模块,包括 pymysql、_thread、json、math、http.server 和 urllib.parse。这里面有几个包,给大家稍微解释一下作用。_thread:这是 Python 的内置模块,用于支持多线程编程。它提供了一些函数,如 start_new_thread(),用于创建新的线程并执行指定的函数。http.server:内置模块,用于创建基于 HTTP 协议的服务器。它提供了一些类,用于处理 HTTP 请求和启动 HTTP 服务器。urllib.parse:这是 Python 的内置模块,用于解析 URL。它提供了一些函数,如 urlparse() 和 parse_qs(),用于解析 URL 字符串,并提取其中的各个部分,如协议、主机、路径、查询参数等。
下面我们定义第一个函数。url2Dict这个函数接收一个 URL 字符串作为参数,并返回一个包含查询参数的字典。具体来说,它使用 urlparse 函数解析 URL,然后使用 parse_qs 函数将查询参数解析为字典。最后,它返回一个包含查询参数的字典。当我们向数据库发送请求url的时候,这个函数帮助我们进行解析。我们来看下url具体包含的参数包括品种名,交易所,价格精度,数量精度,bar周期(毫秒),深度档数,是否需要分笔数据,开始时间和结束时间。当然我们也可以根据数据库类型的区别,定义别的参数,例如在本例中,我们定义了数据库名称和具体的表名称,方便我们在数据库中获取到指定的数据。
接着我们定义一个名为 Provider 的类,它继承自 BaseHTTPRequestHandler 类,可以方便地创建自定义的 HTTP 服务器,而不需要从头开始编写处理 HTTP 请求的逻辑。
在 do_GET 方法中,首先发送一个 HTTP 响应头,指定响应状态码为 200,200是HTTP 协议中一个常见的状态码,表示请求成功。然后设置响应内容的 MIME 类型为 application/json。然后解析 URL 中的查询参数,并将其转换为字典形式。
然后,打印日志信息,包括 self.path 和 dictParam 的值,用于调试和记录请求的路径和查询参数。
之后连接数据库,获取对于参数需求的数据。首先MySQL进行 数据库的连接,这里的参数包括主机地址,用户名,密码,和数据的名称,这里将数据库的名称定义为需求的参数名称databaseName,需求的数据库连接好以后,接下来执行一条 SELECT 查询语句,这里的参数名是我们的第二个参数,tableName,这样我们就可以从指定的数据库和表中获取我们需求的数据。
但是获取到的数据是我们需求的格式吗,我们需要进行一下处理。我们来看下回测系统需求的数据格式。在API文档里,可以看到需求的字段包括三个,schema是指定data数组里列的属性,data是具体数据的数值,还有detail,是商品期货的品种需要提供的属性,具体包括PriceTick,VolumeMultiple,ExchangeID,LongMarginRatio,ShortMarginRatio和InstrumentID。所以在代码里我们首先创建好需求的格式,其中 schema 键对应一个列表,表示数据的字段名,这是规定好的,包含时间戳,高开低收,和交易量数据;data 键对应一个列表,表示数据的具体内容;第三个detail键对应期货的属性信息,这里我们定义为纯碱的期货信息,PriceTick是1,合约乘数是10,交易所是郑商所,做多和做空保证金比率是12%,还有合约名称。
接着我们进行data数据的填写。这样我们返回数据的格式和具体内容就填写完成了。
最后,将构造好的数据以 JSON 格式写入 HTTP 响应体,并发送给客户端。查询完成以后,进行游标和数据库连接的关闭。
如果在整个过程中发生了异常,将捕获异常并打印错误日志。
总之,这段代码实现了一个简单的自定义数据源服务,通过接收 HTTP GET 请求,连接到 MySQL 数据库,并将查询结果以 JSON 格式返回给请求方。
下面是createServer(host) 函数:这个函数接收一个主机地址作为参数,并创建一个 HTTP 服务器。它使用 HTTPServer 类创建服务器对象,并指定处理请求的类为 Provider。然后,它调用 serve_forever 方法启动服务器。
这样我们的Http服务器设置已经完成了。下面我们需要在主函数中启动一个线程来创建 HTTP 服务器。这里使用 _thread.start_new_thread() 方法来创建一个新的线程,用于运行 createServer() 函数。createServer() 是我们定义的函数,用于启动 HTTP 服务器并监听指定的地址和端口。
在这里,通过传入一个包含地址和端口的元组参数 (("0.0.0.0", 9090), ),来指定 HTTP 服务器监听的地址和端口。其中,0.0.0.0 表示监听所有的网络接口,9090 是指定的端口号。
在启动线程之前,使用 try...except 语句来捕获可能发生的异常。如果启动线程失败,则会打印错误信息,并抛出一个 Exception 异常,以终止程序的执行。
最后,如果线程成功启动,则会打印一条日志信息,表示自定义数据源服务线程已经开启。
使用这样多线程的方式来启动 HTTP 服务器,可以让程序在后台持续运行,并且不会阻塞主线程的执行。这样就可以实现同时处理多个客户端请求的功能。
以上这些工作呢,就是行情收集器的升级版需要添加的模块,再加上我们上节课讲过的内容,就可以实时的将收集的数据返回到需求的端口中。其实呢,这里http框架如果大家不太熟悉的话,可以了解这里的url参数设置和数据库参数的设置,其余的框架大家照搬就好。
实盘建立好以后,下面我们打开一个测试策略,进行回测。策略代码很简单,首先设置合约,打印合约的信息,然后GetRecords打印k线数据。在回测界面,更多参数这里设置指定数据源,这里的url是这样编写的,包含我们需要的两个参数,database和table。因为我们获取的k线周期是5分钟,所以这里k线的周期和底层k线的周期都设置为5分钟。还有回测时间的设置,需要是我们收集到的数据中包含的一段时间,否则会返回错误。这里的精度设置为0,否则会返回小数。
我们来验证一下,点击开始回测。可以看到回测系统利用获取到的k线进行画图的展示,对比默认的数据源可以发现是一致的。在实盘日志里,可以看到具体的数据提供流程。另外,最重要的就是使用自定义的数据源进行策略回测的功能,这里我们定义买入一手多仓,可以看到回测系统自动为我们进行了收益的统计和指标的展示。
这样就可以让服务器上的实盘自己收集K线数据,而我们可以随时获取这些收集的数据直接在回测系统回测了。当然我们的课程属于抛砖引玉,各位大神还可以继续扩展,例如支持多品种、多市场数据收集等等功能,可以真正的搭建一个属于自己的量化数据库。
视频参考链接:
《手把手教你给行情收集器升级回测自定义数据源功能(数字货币版)》
23.经典剥头皮策略JavaScript语言版
成为一名成功的交易者,一个重要方法是寻找到适合我们的交易技巧或策略,例如日内交易、剥头皮交易、波段交易等。每一种交易方法都会带来不同程度的风险,它们对交易技巧的要求也有所不同。如果你是位忙碌又想规避风险的交易者,持仓时间较短,想从几个小型的交易中寻求稳定且持续的利润,那么最适合你的就是剥头皮交易了
剥头皮交易策略(Scalping Strategy)是一种短期交易策略,旨在通过迅速进出市场来捕捉小幅价格波动获取利润。这个策略通常适用于高流动性的市场,如外汇市场或流动性较强的期货市场。剥头皮策略的核心思想是利用市场的短期波动性,寻找短暂的价格变动,并迅速进出仓位以获取利润。
剥头皮交易者会多次获得小额的利润,且不会持续持有盈利的仓位,这为的是在收益出现时及时把握住收益。换而言之,其目标是通过大量盈利的仓位而非少数大幅盈利的成功交易来实现成功的交易策略。剥头皮交易依靠的是降低敞口风险的理念,由于在每笔交易上实际所花的时间相当短,所以能够降低不利事件导致大幅波动后所面临的风险。此外剥头皮交易者认为,相比幅度较大的波动,幅度较小的波动较容易能够把握住并且发生的频率较高。
尽管存在一些困难,剥头皮交易非常适合交易新手。如果掌握了这个策略,那么我们可以进行超级短线交易。成为一名剥头皮交易者,并不需要对基本面分析有深入的了解。所以这种机械化的交易机制,非常适合使用量化语言进行表达。
剥头皮策略其实相当于是一个交易机制,并不是单指使用某种具体的指标和策略。总体来说,剥头皮交易的分析模式可以大致分为三组:
-
- 保守型剥头皮交易
在这种模式下,交易者可以等待较长时间以获取最佳的入场点。对于保守的日内交易策略,会使用各种技术分析工具,例如移动平均线、图表形态式、技术指标和震荡指标、支撑和阻力水平等。
-
- 中性剥头皮交易
剥头皮交易者在一个小时内进行几十次交易。这里剩下的时间用于市场分析很少,更多地依靠反应和直觉。交易者会监控订单流、市场对新闻的反应以及上涨和下跌市场价格行为的动态。
-
- 激进型剥头皮交易
在这种情况下,头寸在开仓几秒钟后立即平仓。这种交易策略旨在获取几个点的利润。
而具体的剥头皮策略使用的指标有很多,比如随机指标策略,移动平均线策略,抛物线转向指标策略和相对强弱指标策略等等。今天我们介绍的剥头皮策略来源于油管的一位博主,这个策略使用场景是外汇市场,我们看能否改造下,适用于商品期货市场。
我们首先大致介绍下这个策略的交易逻辑。这个策略依靠大小两个周期确定具体的入场点位,止盈和止损系数确定具体的出场点位。在期货市场中,有一句老手经验,“大周期定方向,小周期定买卖。”在这个策略当中,我们使用小时级别EMA均线确定大周期的趋势,如果大周期快线大于慢线,并且为了预防突发的情况,最新k线收盘价也要大于慢线,我们定义大周期为多头;相反情况下如果大周期快线小于慢线,并且最新收盘价小于慢线,我们定义为空头。这样大周期我们就确定完成。
下面来看小周期具体交易的确定,小周期使用到了三根均线,这里也要确定小周期的方向,如果快线大于中线,中线大于慢线,确定小周期为多头;相反情况,快线小于中线,中线小于慢线,确定为空头。那么怎么确定具体的交易点位呢,在期货交易中,有一个词叫做“回踩”。如果前一根k线的最低价回踩到小周期的快线之下,但是不能超过慢线,我们可以下多单,挂单的价格是最近五根k线的最高价加上三个点位,因为这个价格一般大于目前的价格,根据交易机制,在一个较高的价格可以保证立即成交,所以在一定程度上可以提高当时交易成功的概率。对于做空也是一样,出现回踩,使用最近最近五根k线的最低价减去3个点位进行挂单。
这里面比较特殊的一点是重复挂单的处理,因为这里使用的是挂单方式等待入场,而不是直接入场,所以有时候会出现重复挂单,这个问题我们需要在程序中设计完善。
对于离场的设置,这里使用的是两个止盈区间和止损区间,但是在期货市场中经过测试,由于波动性强于外汇市场,所以止盈的区间远大于止损的区间,这种止盈止损的方法不太适合于期货市场,我们采用通过设置止盈系数和止损系数,当达到固定点位我们进行离场。
以上呢,就是这个剥头皮策略的整体逻辑。现在大家有没有想法使用代码自己实现一下?大家可以暂停,手动编写一下这个策略。下面呢,我们将讲解下在优宽平台使用Js语言进行一下实现。
打开策略编辑页面,固定框架安排。首先我们需要设置几个参数,包括目标合约,趋势判断周期,包括小时级快线和慢线周期,5分钟级快线,中线和慢线周期,还有交易参数,包括入场点周期,止盈和止损系数,这个策略里面的参数较多,所以我们将策略参数进行分组,可以很方便的在不同分组中找到我们需要的参数进行修改,还记得怎样进行分组吗?使用括号加问号的方式,里面进行分组的命名。这样就可以清晰的展示不同分组的参数了。
接着我们来设置主函数,首先初始化几个变量,包括小时级趋势指标,5分钟级趋势指标,和最终的判断指标,先前的挂单价格,和止盈和止损的价格。这里为了平仓的方便,使用交易类库单品种控制对象。
下面进入我们的主循环,设置好合约,我们需要进行两个不同时间级别趋势的判断,首先是小时级别的,
使用GetRecords获取k线,参数填写为PERIOD_H1,这里先做一个判断,如果收集的k线数目小于计算慢线需要的数量,跳过这次循环,等收集足够的数量以后,使用TA.EMA内置函数计算小时级别的快线和慢线,然后进行小时级别也就是大周期趋势的判断。
这里的判断条件是快线大于慢线,并且当前k线的收盘价也是需要大于慢线,定义大周期趋势为1,也就是多头;相反情况下,如果快线小于慢线,并且最新的收盘价小于慢线,定义趋势为空,赋值为-1。
接下来进行小周期的判断,设置参数为PERIOD_M5,然后获取5分钟级别,也就是小周期的快线,中线和慢线,接着判断小周期的趋势,当快线大于中线,并且中线大于慢线,定义小周期趋势为多;另一种相反情况,定义为空,赋值为-1。
两个周期的趋势判断好以后,进行总的趋势的判断,使用三元表达式,如果两个趋势都是多,那么总体趋势finalSignal也是多,如果两个都是空,那么判断总体趋势也是空。
接下来我们要进行下单的设置了,获取当前仓位,如果持仓为空,当趋势明显并且出现回踩情况的情况下,我们就要进行相应方向的开仓。第一种情况,如果总体趋势判断为多,回踩的判断是,如果当k线价格小于快线,但是也不能偏离的太多,所以还需要判断收盘价需要大于慢线,这时候定义为入场的信号。入场挂单的价格在原版的策略中,是最近5根周期的最高价加上三点,我们这里将这个值设置为一个参数,enterPeriod,计算指定周期内k线的最高价,可以使用TA.Highest内置函数,然后加上3点,定义为挂单价格,我们设置好方向,进行挂单。但是当挂单以后,不一定能迅速的成交,仓位依然为0,所以依然会进入这个条件,当信号重复出现的时候,这个时候会出现多次挂单,所以这里我们需要加一个判断,记录每次的挂单价格为preOrderPrice,如果preOrderPrice不等于orderPrice,我们再次进行挂单,这样就避免了多次挂单的情况。
对于空头的处理逻辑也是一致的,如果总体趋势为空,出现向上的回踩。设置挂单价格,在判断不是重复挂单的情况下,进行挂单。
挂单完成以后,我们来解决上一步留下的问题,解决多余挂单的问题。什么算是多余挂单呢?这个我们需要定义清楚,第一种情况,如果挂单成交,已经持有仓位,那么就需要取消所有的未完成的挂单;第二种情况,仓位没有成交,但是挂单价格发生了变化,所以导致挂了多手仓位,这时候我们需要取消先前的挂单;这个时候我们就只剩下了一手的挂单了,但是这时候如果总体趋势发生变化,我们需要及时的取消掉挂单,防止带来亏损。这就是我们需要取消挂单的三种情况。
我们来看下怎样使用代码进行实现。获取所有未完成的订单函数大家还记得吗?使用GetOrders获取到。
第一种情况,在判断挂单成交,也就是仓位列表长度不为空的情况下,如果这时候还存在挂单的话,取消所有的挂单;
第二种情况,如果没有持仓,但是持有多个挂单,我们需要删除前面的挂单,只保留最新的挂单,这个i的定义是挂单列表的长度减1,只保留最新的挂单,撤销掉前面多余的挂单;
第三种情况,如果未完成的挂单列表长度为1,我们做一个判断,使用order的Type属性和Offset属性,ORDER_TYPE_BUY代表买单,ORDER_OFFSET_OPEN代表开仓,但是如果趋势不是1多头的话,就要取消当前的挂单;另一种情况,如果Type是ORDER_TYPE_SELL代表卖单,Offset也要是开仓,如果finalSignal不是-1,取消当前挂单。
这里为了理解的方便,所以取消挂单的设计比较直接一点,这里可以优化简洁的地方有很多,大家可以设计的更加完善一点。
下面我们进入止盈止损的设计,这里就没有那么多的特殊情况了,根据仓位的type属性判断是多仓还是空仓,如果持仓为多头,并且最新的收盘价大于开仓的价格加上止盈区间参数,我们进行止盈,或者小于开仓价格减去止损区间,我们进行止损,使用Cover函数直接平仓;对于空头也是一样,如果最新价格小于止盈价格或者大于止损价格,进行平仓。但是,在持有仓位以后,不忘要了重置先前的挂单价格。
以上呢,就是剥头皮策略的交易逻辑部分的设置,我们回测跑下看下效果,这里我们使用纯碱主力合约,时间为1周,使用真实的tick数据,参数的设置为原始推荐的参数,我们看到一共取得了103元的收益,策略的平仓盈亏是860元,但是手续费差不多是600元,确实很符合这个策略的特性。
作为一个实践课程,策略仅能在回测系统运行肯定是不够的,我们需要一个实盘级别的策略,所以在策略的plus版本中,代码增加了状态栏的显示,包括趋势判断和持仓状态,还有止盈止损次数统计,并且还有收益曲线和大小周期均线的画图。我们实盘运行一下,可以看到一个标准的实盘级策略可以多方面的了解策略的运行状态。
需要注意的是,本策略重点是要来讲解策略主体的交易逻辑,所以对于参数的选择和优化我们并没有涉及太多,原始的参数是针对于外汇市场的,针对于期货市场,我们可以进行一下调参,并且策略讲解中对于止盈止损的设计提出了很多好的想法,大家也可以根据这个策略为模版,进行更一步的探索和优化。
完整版代码:
javascript
/*backtest
start: 2023-09-01 09:00:00
end: 2023-09-07 23:20:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["symbol","SA888"],["smallFastMA",12],["smallMiddleMA",16],["smallSlowMA",26],["enterPeriod",3]]
*/
function main() {
var initAccount = exchange.GetAccount()
var trendBigSignal = 0
var trendSmallSignal = 0
var finalSignal = 0
var preOrderPrice = 0
var orderDir = ''
var orderPrice = 0
var posPrice = 0
var posType = ''
var posProfit = 0
var stopProfitPrice = 0
var stopLossPrice = 0
var winCount = 0
var lossCount = 0
var curProfit = 0
var preProfit = 0
var p = $.NewPositionManager()
while(true){
if(exchange.IO('status')){
exchange.SetContractType(symbol)
var bigR = exchange.GetRecords(PERIOD_H1)
var fastMABig = TA.EMA(bigR, bigFastMA)
var slowMABig = TA.EMA(bigR, bigSlowMA)
if(bigR.length < 21){
continue
}
if(fastMABig[fastMABig.length - 1] > slowMABig[slowMABig.length - 1] && bigR[bigR.length - 1].Close > slowMABig[slowMABig.length - 1]){
trendBigSignal = 1
}else if(fastMABig[fastMABig.length - 1] < slowMABig[slowMABig.length - 1] && bigR[bigR.length - 1].Close < slowMABig[slowMABig.length - 1]){
trendBigSignal = -1
}
var smallR = exchange.GetRecords(PERIOD_M5)
var curPrice = smallR[smallR.length - 1].Close
var fastMASmall = TA.EMA(smallR, smallFastMA)
var middleMASmall = TA.EMA(smallR, smallMiddleMA)
var slowMASmall = TA.EMA(smallR, smallSlowMA)
if(fastMASmall[fastMASmall.length - 1] > middleMASmall[middleMASmall.length - 1] && middleMASmall[middleMASmall.length - 1] > slowMASmall[slowMASmall.length - 1]){
trendSmallSignal = 1
}else if(fastMASmall[fastMASmall.length - 1] < middleMASmall[middleMASmall.length - 1] && middleMASmall[middleMASmall.length - 1] < slowMASmall[slowMASmall.length - 1]){
trendSmallSignal = -1
}
var finalSignal = trendBigSignal == 1 && trendSmallSignal == 1 ? 1 : trendBigSignal == -1 && trendSmallSignal == -1 ? -1 : 0
var posInfo = exchange.GetPosition()
// 下单
if(posInfo.length == 0){
posPrice = 0
posType = ''
posProfit = 0
stopProfitPrice = 0
stopLossPrice = 0
if(finalSignal == 1 && smallR[smallR.length - 1].Close < fastMASmall[fastMASmall.length - 1] && smallR[smallR.length - 1].Close > slowMASmall[slowMASmall.length - 1]){
var orderPrice = TA.Highest(smallR, enterPeriod, 'High') + 3
if(orderPrice != preOrderPrice){
exchange.SetDirection('buy')
exchange.Buy(orderPrice, 1)
Log('先前价格:', preOrderPrice)
Log('挂单价格:', orderPrice)
preOrderPrice = orderPrice
orderDir = '多头'
}
}
if(finalSignal == -1 && smallR[smallR.length - 1].Close > fastMASmall[fastMASmall.length - 1] && smallR[smallR.length - 1].Close < slowMASmall[slowMASmall.length - 1]){
var orderPrice = TA.Lowest(smallR, enterPeriod, 'Low') - 3
if(orderPrice != preOrderPrice){
exchange.SetDirection('sell')
exchange.Sell(orderPrice, 1)
Log('先前价格:', preOrderPrice)
Log('挂单价格:', orderPrice)
preOrderPrice = orderPrice
orderDir = '空头'
}
}
}
var orderInfo = exchange.GetOrders()
// 取消挂单
if(posInfo.length != 0){
if(orderInfo.length > 0){
Log('已成交,取消先前挂单')
for(var i = 0; i < orderInfo.length; i ++){
exchange.CancelOrder(orderInfo[i].Id)
}
}
}else if(posInfo.length == 0){
if(orderInfo.length > 1){
Log('入场订单价格变化,取消先前挂单')
for(var i = 0; i < orderInfo.length - 1; i ++){
exchange.CancelOrder(orderInfo[i].Id)
}
}else if(orderInfo.length == 1){
if(orderInfo[0].Type == ORDER_TYPE_BUY && orderInfo[0].Offset == ORDER_OFFSET_OPEN && finalSignal != 1){
Log('多头趋势消失,取消挂单')
exchange.CancelOrder(orderInfo[0].Id)
}else if(orderInfo[0].Type == ORDER_TYPE_SELL && orderInfo[0].Offset == ORDER_OFFSET_OPEN && finalSignal != -1){
Log('空头趋势消失,取消挂单')
exchange.CancelOrder(orderInfo[0].Id)
}
}
}
// 止盈止损
if(posInfo.length != 0){
orderDir = 0
orderPrice = 0
preOrderPrice = 0
posPrice = posInfo[0].Price
posType = posInfo[0].Type == PD_LONG || posInfo[0].Type == PD_LONG_YD ? '多头' : '空头'
posProfit = posInfo[0].Profit
if((posInfo[0].Type == PD_LONG || posInfo[0].Type == PD_LONG_YD) ){
stopProfitPrice = posInfo[0].Price + stopProfit
stopLossPrice = posInfo[0].Price - stopLoss
}
if((posInfo[0].Type == PD_SHORT || posInfo[0].Type == PD_SHORT_YD) ){
stopProfitPrice = posInfo[0].Price - stopProfit
stopLossPrice = posInfo[0].Price + stopLoss
}
if((posInfo[0].Type == PD_LONG || posInfo[0].Type == PD_LONG_YD) &&
(smallR[smallR.length - 1].Close > posInfo[0].Price + stopProfit || smallR[smallR.length - 1].Close < posInfo[0].Price - stopLoss) ){
Log('入场价格', posInfo[0].Price)
Log('多头止盈价格: ', posInfo[0].Price + stopProfit)
Log('多头止损价格: ', posInfo[0].Price - stopLoss)
Log('多头离场')
p.Cover(symbol)
var curAccount = exchange.GetAccount()
curProfit = curAccount.Balance - initAccount.Balance
if(curProfit > preProfit){
winCount += 1
Log('盈利')
Log('收益', curProfit - preProfit)
}else if(curProfit < preProfit){
lossCount += 1
Log('损失')
Log('收益', curProfit - preProfit)
}
preProfit = curProfit
}
if((posInfo[0].Type == PD_SHORT || posInfo[0].Type == PD_SHORT_YD) &&
(smallR[smallR.length - 1].Close < posInfo[0].Price - stopProfit || smallR[smallR.length - 1].Close > posInfo[0].Price + stopLoss) ){
Log('入场价格', posInfo[0].Price)
Log('空头止盈价格: ', posInfo[0].Price - stopProfit)
Log('空头止损价格: ', posInfo[0].Price + stopLoss)
Log('空头离场')
p.Cover(symbol)
var curAccount = exchange.GetAccount()
curProfit = curAccount.Balance - initAccount.Balance
if(curProfit > preProfit){
winCount += 1
Log('盈利')
Log('收益', curProfit - preProfit)
}else if(curProfit < preProfit){
lossCount += 1
Log('损失')
Log('收益', curProfit - preProfit)
}
preProfit = curProfit
}
}
var tblTrend = {
type: "table",
title: "趋势判断信息",
cols: ["合约名称", "大周期趋势", "小周期趋势", '趋势判断'],
rows: []
}
var tblStatus = {
type: "table",
title: "策略运行状态信息",
cols: ["合约名称", "当前价格", "挂单方向", "挂单价格", "持仓价格", "持仓方向", "止盈价格","止损价格", "持仓盈亏", "止盈次数", "止损次数"],
rows: []
}
tblStatus.rows = []
tblStatus.rows.push([symbol, curPrice, orderDir, orderPrice, posPrice, posType, stopProfitPrice, stopLossPrice, posProfit, winCount, lossCount]);
tblTrend.rows = [];
tblTrend.rows.push([symbol,
trendBigSignal == 1 ? '多头' : trendBigSignal == -1 ? '空头' : '震荡',
trendSmallSignal == 1 ? '多头' : trendSmallSignal == -1 ? '空头' : '震荡',
finalSignal == 1 ? '多头' : finalSignal == -1 ? '空头' : '震荡']);
lastStatus = '`' + JSON.stringify([tblTrend]) + '`\n' + '`' + JSON.stringify([tblStatus]) + '`';
LogStatus(lastStatus)
var accountInfo = exchange.GetAccount()
var totalProfit = accountInfo.Balance - initAccount.Balance
LogProfit(totalProfit, '&')
$.PlotMultRecords("大周期" + symbol, "k线", bigR, {layout: 'single', col: 12, height: '600px'})
$.PlotMultLine("大周期" + symbol, "快线", fastMABig[fastMABig.length - 1], bigR[bigR.length - 1].Time)
$.PlotMultLine("大周期" + symbol, "慢线", slowMABig[slowMABig.length - 1], bigR[bigR.length - 1].Time)
$.PlotMultRecords("小周期" + symbol, "k线", smallR, {layout: 'single', col: 12, height: '600px'})
$.PlotMultLine("小周期" + symbol, "快线", fastMASmall[fastMASmall.length - 1], smallR[smallR.length - 1].Time)
$.PlotMultLine("小周期" + symbol, "中线", middleMASmall[middleMASmall.length - 1], smallR[smallR.length - 1].Time)
$.PlotMultLine("小周期" + symbol, "慢线", slowMASmall[slowMASmall.length - 1], smallR[smallR.length - 1].Time)
}else{
LogStatus('未连接交易所')
}
Sleep(400)
}
}
视频参考链接:
SIMPLE and PROFITABLE Forex Scalping Strategy!
24.协整关系进行配对交易Python版
配对交易是统计套利中的非常经典的策略。它是基于数理分析的策略中的一个典型例子。策略的准则很简单:假设一对期货合约A和B,它们具有高度相关的关系。比如是相同品种的跨期合约,或者使用同一种原料,例如螺纹钢和铁矿石,再或者是上下游的关系,比如玉米和玉米淀粉,因此在实时的走势上,他们经常呈现惊人的一致性,所以很多交易者会参考两个品种的相关关系做出交易的决策。但是相对于前面的课程我们使用价差进行统计的套利,当价差超过一定上限或者下限,我们预测价差会回复到均值状态,所以利用价差进行套利,但是这个价差并不是稳定的,所以有时候策略参数会失效,我们需要实时的调整价差的上下阈值参数。
不使用固定的上下阈值参数,怎样判断两者之间的动态相关关系呢?我们可以使用协整检验。由于许多关联交易标的价格相关性是非平稳的,这就给经典的回归分析方法带来了很大限制。实际应用中大多数时间序列其实都是非平稳的,通常采用差分方法消除序列中含有的非平稳趋势,使得序列平稳化后建立模型,比如使用ARIMA模型。但是变换后的序列限制了交易标的价格的范围,并且有时变换后的序列由于不具有直接的分析意义,使得化为平稳序列后所建立的时间序列模型不便于解释。1987年Engle和Granger提出的协整理论及其方法,为非平稳序列的建模提供了另一种途径。虽然一些经济变量的本身是非平稳序列,但是,它们的线性组合却有可能是平稳序列。这种平稳的线性组合被称为协整方程,且可解释为变量之间的长期稳定的均衡关系。
协整是一种比相关更微妙的关系。如果两个时间序列是协整的,那么一定存在它们的某个线性组合,围绕着其平均值在较小范围内波动。用数学的语言说,在所有的时间点上,这个线性组合构成的新随机变量服从相同的概率分布。所以协整可被看作这种均衡关系性质的统计表示。需要注意的是,协整和相关,概念上容易混淆,但它们并不相同。有时候会出现协整但不相关,或者相关但不协整。
两个具有协整关系的合约的走势并不是每一刻都是相同的,也有一些特殊的时刻出现偶尔的偏离,所以我们总能找到它们的价差高点和价差低点。反映在图像中就是它们合约价格曲线的距离。配对交易的技巧就是由合约A和合约B构造对冲组合。如果两个合约都保持同步涨幅或者跌幅,我们既不会赚钱也不会亏钱。如果两个合约的价差开始向历史平均值靠近,我们就能赚得收益。需要注意的是,这的价差并不是两个合约的价格简单相减,而是协整关系系数。
在大宗商品的套利交易中,具有具有强烈的协整关系的两个合约,运用Engle和Granger提出的这种协整理论及其方法进行量化分析是最好不过的了,这也是目前很多世界上的顶级量化团队在进行大宗商品同品种跨期套利时的理论基石。
具体在交易中,我们怎样实现这个策略逻辑呢?下面我们来讲下策略原理。
假设时间序列Xt和Yt服从协整的关系,所以可以建立这样回归的方程。
$Y_t = \beta X_t + \alpha + \epsilon$
我们策略重点,是关注回归系数$\beta$的变化。它不是一个固定不变的常数,我们允许在一个上下限波动;当$\beta$超出了上限的时候,说明Y太贵了,X太便宜了,我们应该Long合约X,Short合约Y;而当$\beta$超出了下限,我们进行相反的操作,Long合约Y,Short合约X。直到$\beta$回归到正常的范围,我们进行平仓。策略原理还是容易理解的,我们实践性的课程重点是怎样使用程序化的语言进行实现。
前面的课程我们使用到了pine语言,js语言,这次我们换换口味,使用python语言进行实现协整关系的配对交易策略。打开策略编辑页面,选择python语言。
其实使用js语言和python语言编写策略的步骤,并没有很大的区别,比如这里的策略参数的添加,输入变量,描述,类型,默认值点击添加可以,这个策略中添加了五个参数,包括两个合约A和B,滚动区间movingPeriod1和movingPeriod2,他们用来确定滚动的信号值,还有重要的阈值参数,用来确定信号值的上限和下限。回到代码部分,首先导入我们需要的库,包括statsmodels的coint函数,进行协整性检验,用于判断两个时间序列是否存在协整关系。pandas:用于处理和分析数据的库,提供了Series和DataFrame等数据结构和相关的方法。
首先我们定义一个函数,用来合成两个k线数组成为dataframe的形式,方便进行后续的协整检验和交易信号的计算。分别订阅两个合约,然后获取两个合约的k线数据,这是一个相对来说低频的策略,所以我们获取k线的频率是天,使用参数PERIOD_D1,然后将获取的两个收盘价数据保存成为dataframe的格式,这里使用到了pandas包。因为两个k线的长度可能不一致,在后续协整关系检验中可能出现问题,所以这里进行长度调整,截取两个k线中较小的k线长度。接着我们将两个数据进行合并,使用pandas的concat函数,列名命名为Close_01和Close_02。最后return进行返回。
接着我们设计进行协整检验的函数cointTest。在获取合成k线dataframe以后,我们直接使用statsmodel的coint函数进行协整检验,将合成k线的两列放入函数中进行协整的判断,这里我们只需要pv_coint值,另外两个值不需要,所以使用占位符。这个给大家介绍一下协整检验的返回值的意义。协整检验的原始假设的协整关系不存在,所以如果返回的p值小于0.05,代表原假设被推翻,协整关系存在,如果该值大于0.05,证明接受原假设,协整关系不存在。所以我们进行判断,如果该值小于0.05,定义返回1代表协整关系存在,如果大于0.05,返回0代表不存在。
下面定义我们的主函数,首先我们通过cointTest函数对合约对进行协整检验,如果返回值是1,打印信息'协整关系成立,开始运行策略',如果返回值不是1,代表我们挑选的合约对不存在协整关系,我们提出警告,需要更换合约。
接着我们的固定框架,while True建立循环,然后判断交易所的连接状态。
接着就是我们策略的主体部分,在判断协整关系存在的情况下,我们进行交易信号的计算。同样的获取合成k线的dataframe,定义为curDf变量。
这里交易信号的计算过程是参考知乎大神的,首先使用收盘价的比率计算ratios作为beta的参考值。
然后使用特征工程的方法,动态的计算出zscore_mv,也就是滑动的z值。传统的z值计算方法是这样的:
zScore = (ratios - ratios.mean()) / ratios.std()
但是它作为信号过于薄弱。因为它集中的是“整个时期”的关系;而我们交易必然是一个动态变化的过程。这里需要不断计算“一段时间”的ZScore,而不是“整个时期”的ZScore。所以我们滚动计算一段区间的ZScore。这里,选取了Ratios滚动movingPeriod1的均值、Ratios滚动movingPeriod2的均值,以及Ratios滚动movingPeriod2的标准差,计算真正的信号ZScore_moving。这里的movingPeriod1和movingPeriod2可以在回测系统作为参数进行调参,获取最佳的参数值。
因为zscore_mv是一个数组,所以我们获取倒数第二根,也就是完成k线的值,作为今天交易的信号,这里使用了pandas的iloc索引。
这样我们的动态交易信号就计算完成了,接着我们来获取持仓信息进行相应的交易操作。还记得js的交易类库吗,python也具有交易类库,使用ext.NewPositionManager()定义。在持仓为空的情况下,如果z值小于负的阈值,证明beta,也就是合约A除以合约B的比率较小,我们使用交易类库进行多合约A,空合约B的操作;相反情况如果z值大于阈值,进行空合约A,多合约B的操作。
有开仓必然有平仓,在判断持有仓位情况下,如果阈值回归到正常的水平,也就是z值的绝对值小于阈值除以2,我们进行两个仓位的平仓,直接使用CoverAll函数就可以。
因为这是一个低频日级别的策略,所以我们设置sleep休息时间是1日。以上呢,就是我们使用具有协整关系的两个合约进行配对交易的实例,我们回测运行一下效果。在回测页面,设置策略开始的时间是今年的3月份,到10月底,作为一个低频的策略,使用模拟的tick数据就可以。策略参数这里,我们使用原始的参数,滚动区间1是5,2是60,阈值是1。目标合约对时我们选择螺纹钢和热卷好兄弟,都是2311的合约,拥有相同的原材料铁矿石,并且一致的到期时间,所以它们的走势呈现高度的一致性。我们测试下,在日志信息里可以看到,都满足了协整关系,并且在今年3月份到10月份,取得了1600多元的收益,虽然不是太多,但是胜在比较稳定。
接下来,我们换一组合约,玉米和玉米淀粉,这两个处于上下游的关系,走势也呈现高度的一致性。我们跑下策略看看,可以看到,同样满足了协整关系的检验,并且取得了一定的收益。
当然这作为一个实盘的策略肯定是不能满足的,和js语言一样,python策略也可以加上两个合约的状态栏展示,还有收益的展示,这样呢,也可以实时的展示策略的运行状态。
最后温馨提示一下,本节课的教程作为教学内容,为大家展示了使用协整关系进行配对检验的策略设计。其实这个策略可以优化的地方还有很多,比如交易信号zscore的更准确的估计,可以使用卡尔曼滤波或者遗传算法进行优化,大家有兴趣都可以探索修改一下!
代码完整版:
python
'''backtest
start: 2023-03-02 09:00:00
end: 2023-10-31 23:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["symbolA","rb2311"],["symbolB","hc2311"],["movingPeriod1",7],["movingPeriod2",55]]
'''
from statsmodels.tsa.stattools import coint
import pandas as pd
import json
def concatRecords():
# 协整关系检验
_C(exchange.SetContractType, symbolA)
records_A = _C(exchange.GetRecords, PERIOD_D1)
close_01 = pd.Series([record.Close for record in records_A])
_C(exchange.SetContractType, symbolB)
records_B = _C(exchange.GetRecords, PERIOD_D1)
close_02 = pd.Series([record.Close for record in records_B])
# 对close_01和close_02进行长度调整
min_length = min(len(close_01), len(close_02))
close_01 = close_01[-min_length:]
close_02 = close_02[-min_length:]
# 将close_01和close_02整合成一个DataFrame
df = pd.concat([close_01.reset_index(drop=True), close_02.reset_index(drop=True)], axis=1)
df.columns = ['Close_01', 'Close_02']
return df
def cointTest():
# 协整关系检验
concatDf = concatRecords()
# 协整判断
_, pv_coint, _ = coint(concatDf.Close_01, concatDf.Close_02)
if pv_coint <= 0.05:
return 1
elif pv_coint >= 0.05:
return 0
# 主函数
def main():
p = ext.NewPositionManager()
init_Account = _C(exchange.GetAccount)
cointRes = cointTest()
if cointRes == 1:
Log('协整关系成立,开始运行策略',"#FF0000")
else:
Log('合约A和合约B不存在协整关系,请重新更换合约对',"#00FF00")
return
while True:
if exchange.IO("status"):
curDf = concatRecords()
# 交易信号的计算
ratios = curDf.Close_01 / curDf.Close_02
ratios_mavg1 = ratios.rolling(window=movingPeriod1, center=False).mean()
ratios_mavg2 = ratios.rolling(window=movingPeriod2, center=False).mean()
std = ratios.rolling(window=movingPeriod2, center=False).std()
zscore_mv = (ratios_mavg1 - ratios_mavg2) / std
zscore_mv_value = zscore_mv.iloc[-2]
positions = _C(exchange.GetPosition)
if len(positions) == 0:
posTypeA = ''
posPriceA = 0
posProfitA = 0
posTypeB = ''
posPriceB = 0
posProfitB = 0
if zscore_mv_value < -threshold:
Log('Z值过小,多合约A,空合约B')
p.OpenLong(symbolA, 1)
p.OpenShort(symbolB, 1)
if zscore_mv_value > threshold:
Log('Z值过大,空合约A,多合约B')
p.OpenLong(symbolB, 1)
p.OpenShort(symbolA, 1)
if len(positions) != 0:
for i in range(len(positions)):
if (positions[i]['ContractType'] == symbolA):
posTypeA = "多头" if (positions[i]['Type'] == PD_LONG) or (positions[i]['Type'] == PD_LONG_YD) else "空头"
posPriceA = positions[i]['Price']
posProfitA = positions[i]['Profit']
elif (positions[i]['ContractType'] == symbolB):
posTypeB = "多头" if (positions[i]['Type'] == PD_LONG) or (positions[i]['Type'] == PD_LONG_YD) else "空头"
posPriceB = positions[i]['Price']
posProfitB = positions[i]['Profit']
if abs(zscore_mv_value) <= threshold/2 :
p.CoverAll()
Log('价格回归,平掉所有仓位')
tblAStatus = {
"type" : "table",
"title" : "持仓信息A",
"cols" : ["合约名称", "持仓方向", "持仓均价", "持仓盈亏"],
"rows" : []
}
tblBStatus = {
"type" : "table",
"title" : "持仓信息B",
"cols" : ["合约名称", "持仓方向", "持仓均价", "持仓盈亏"],
"rows" : []
}
tblAStatus["rows"].append([symbolA, posTypeA, posPriceA, posProfitA])
tblBStatus["rows"].append([symbolB, posTypeB, posPriceB, posProfitB])
lastStatus = f"`{json.dumps(tblAStatus)}`\n`{json.dumps(tblBStatus)}`\nZscore_MV: {zscore_mv_value}"
LogStatus(lastStatus)
accountInfo = _C(exchange.GetAccount)
curProfit = accountInfo.Balance - init_Account.Balance
LogProfit(curProfit, "&")
else:
LogStatus(_D(), "未连接CTP !")
Sleep(1000* 60 * 60 * 24)
视频参考链接:
25.基于优宽本地回测引擎搭建量化系统
前面的课程我们讲到了在优宽平台使用自定义的数据源进行回测,有的同学就反应既然可以使用自定义的数据源,那么有没有自定义的回测引擎呢,身为一个专业的量化平台,大家的要求肯定都会满足,本节课呢,我们介绍一下怎样在本地使用优宽回测引擎,其实本地回测引擎的功能和网页的回测系统基本上是一致的,除了不能满足实盘交易的功能,我们可以有意思的工作,比如全品种历史数据的下载,可视化分析和策略的回测。本节课我将带领大家使用这个从0开始搭建属于你自己的量化回测系统。
我们打开API文档,这里介绍了优宽量化交易平台开源了JavaScript语言和Python语言的本地回测引擎。我们点击python语言的,看一下对应模块的介绍。打开页面,可以看到这里有详细的安装步骤和简单的例子。我们复制这行代码到终端进行回测引擎的安装。安装成功以后,我们复制这个实例到python的编辑器里面。我们首先看下这个代码,在代码开头是回测参数的配置,这里面设置了策略起始和结束的时间,k线周期,还有交易所的设置,这里就相当于在网页端保存回测设置的结果,然后我们导入优宽回测引擎模块,接下来初始化回测引擎。这样我们就可以编写我们的策略了,第一步使用GetAccount打印了账户信息,第二步打印了ticker的信息,最后打印回测结果,并画图展示回测的结果。我们运行一下,看下返回结果。
'''backtest
start: 2018-02-19 00:00:00
end: 2018-03-22 12:00:00
period: 15m
exchanges: [{"eid":"Bitfinex","currency":"BTC_USD","balance":10000,"stocks":0}]
'''
from youquant import *
task = VCtx(__doc__) # initialize backtest engine from __doc__
print(exchange.GetAccount())
print(exchange.GetTicker())
print(task.Join(True)) # print backtest result
task.Show() # or show backtest chart
可以看到,和网页的函数功能是一致的,首先返回了包括余额等的账户的信息,接下来返回的是ticker数据,包括时间戳,高开低收,买价和卖价,交易量和持仓量,接下来backtest result返回的是,策略运行对应的,每日收盘价,账户余额,持有仓位,手续费和净值。最后是回测图表的展示,包括定义的回测时间段内收益的变化等,用来动态展示策略的运行效果。
当然这里的展示还不够全面,我们将使用两个例子来展示优宽本地回测引擎的强大。第一个例子,我们用来获取某一期货品种的历史数据。数据的获取,是编写量化策略开始的第一步,相对于其他历史数据的获取方式,有的需要付费的账户,有的需要繁杂的api设置,还有的需要下载数据到本地,然后上传才能进行数据分析,使用youquant本地回测引擎。这些烦恼都可以解决。只需要开启回测引擎,就可以获取到和网页端一样的历史数据。
注意:优宽的回测数据起始的时间是从2016年开始的,当然这已经足够,毕竟太过久远的数据并没有太大的参考意义。
我们来看下数据获取的具体方法。首先我们进行回测时间的设置,为了省去手工设置,填写参数的烦恼,我们可以到优宽的回测页面,选择最早的时间,2016年,然后到最近的时间,2023年11月1号,选择商品期货,点击添加。然后到策略编辑页面,点击保持回测设置,这样我们回测的参数就设置好了。我们复制到python当中。
'''backtest
start: 2020-01-01 09:00:00
end: 2023-11-02 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
'''
task = VCtx(__doc__) # 初始化引擎
rlist = []
rlisttime = []
prebartime = 0
while True:
try:
exchange.SetContractType('FG888')
r = exchange.GetRecords()
if r[-1].Time != prebartime:
for i in range(len(r) - 1):
if r[i].Time not in rlisttime:
rlist.append(r[i])
rlisttime.append(r[i].Time)
prebartime = r[-1].Time
else:
continue
except:
print('数据读取完成')
break
同样的,我们首先启动回测引擎。接下来我们获取k线数据,和网页上的回测系统一样,在策略运行的期间,使用GetRecords会持续不断的获取k线数据,我们想获得的k线数据是不包括重复时间戳的,所以设置三个变量,第一个rlist用来保持k线数据,第二个rlisttime用来保存k线的时间戳,第三个是prebartime,是前一根k线bar的时间戳,只有最新k线更新,我们进行k线的保存。
这里呢,我们不需要定义main函数了,使用while True循环,首先设置合约,当然需要设置一个在2016年已经上市的合约,这里我们设置玻璃的主力合约,然后使用GetRecords获取k线数据。接着判断如果k线的最新一根时间戳不等于prebartime的时候,我们使用for循环,到倒数第二根k线的位置,是已经完成的k线,接着判断如果rlisttime不包含该根k线时间戳的时候,使用append进行添加,同样的rlisttime也添加相应的时间戳,这里还需要重新赋值prebartime为最新的时间戳。这里我们使用try和except的框架包住我们的策略,在try中运行策略,如果运行完毕,在except中打印数据读取完成,直接break。
这样,我们需求的数据就获取完成,我们看下获取到的数据,是每日的k线数据,包括高开低收,成交量和持仓量。
我们可以使用pandas包将这个数据转换为dataframe的格式,方便进行下一步的数据分析和画图的展示。通过将时间转为index索引,我们就获取到了标准的时间序列分析所需要的格式。
我们可以python丰富的画图工具进行k线的绘图展示,这里我们使用了两个画图的函数,进行k线图的绘制。可以看到从2020年到2023年玻璃的整体走势,平均的价格是1500元左右,2021年上升到高峰3000元,大家有兴趣可以做很多的数据分析工作。
接下来,我们来讲解第二个例子,使用本地的回测引擎,测试在本地运行量化策略。我们在策略广场中找到这个经典的MACD策略。复制到这里。这里我们需要做一些小小的改动。首先重新设置回测引擎。然后在while true里面,加上try和except的结构。最后我们运行这个main函数。稍微等待一下,等到返回策略运行完成,代表回测结束。
'''backtest
start: 2019-01-01 00:00:00
end: 2021-01-01 00:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
'''
task = VCtx(__doc__) # 初始化引擎
mp = 0 # 定义一个全局变量,用于控制虚拟持仓
# 程序主函数
def onTick():
_C(exchange.SetContractType, "rb000") # 订阅期货品种
bar = _C(exchange.GetRecords) # 获取K线数组
if len(bar) < 100: # 如果K线数组长度太小,就直接返回跳过
return
macd = TA.MACD(bar, 5, 50, 15) # 计算MACD值
dif = macd[0][-2] # 获取DIF的值
dea = macd[1][-2] # 获取DEA的值
depth = exchange.GetDepth()
ask = depth['Asks'][0]['Price']
bid = depth['Bids'][0]['Price']
global mp # 全局变量,用于控制虚拟持仓
if mp == 1 and dif < dea:
Log('多平信号成立,挂单价格为:', bid)
exchange.SetDirection("closebuy") # 设置交易方向和类型
id = exchange.Sell(bid, 1) # 平多单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == -1 and dif > dea:
Log('空平信号成立,挂单价格为:', ask)
exchange.SetDirection("closesell") # 设置交易方向和类型
id = exchange.Buy(ask, 1) # 平空单
mp = 0 # 设置虚拟持仓的值,即空仓
if mp == 0 and dif > dea:
Log('多开信号成立,挂单价格为:', ask)
exchange.SetDirection("buy") # 设置交易方向和类型
id = exchange.Buy(ask, 1) # 开多单
mp = 1 # 设置虚拟持仓的值,即有多单
if mp == 0 and dif < dea:
Log('空开信号成立,挂单价格为:', bid)
exchange.SetDirection("sell") # 设置交易方向和类型
id = exchange.Sell(bid, 1) # 开空单
mp = -1 # 设置虚拟持仓的值,即有空单
def main():
while True:
try:
onTick()
Sleep(1000)
except:
print('策略运行完成')
break
if __name__ == "__main__":
main()
print(task.Join(True)) # print backtest result
task.Show() # or show backtest chart
怎样查看策略回测的效果呢,使用我们开头讲过的task.Join和task.Show,运行一下。可以看到策略的实时的资金变化,还有收益的曲线。这个策略确实不太理想哈,赔到最后只剩1600多元了,确实是一个真实的量化效果。我们经常可以看到,使用MACD指标一年翻几十倍的爆炸标题视频,现在呢,我们就可以使用这个策略使用优宽本地回测引擎,针对于我们关注的品种,真实的验证传闻策略的真假。
我们在网页界面回测一下,看看是否得出同样的回测效果。点击复制并进行在线回测,点击开始回测,但是这里的初始金额需要和本地的一致,改为10000元,可以看到回测的结果,最后的净值也是1600多元,证明本地回测的结果是可信的。当然这只是一个试验的策略,大家可以在本地继续优化它,直到做出来一个满意的策略。
怎么样,有没有很心动,一行命令安装一个模块,就可以搭建一个属于自己的专属量化交易系统,省去了大量的环境配置的操作。通过这样的方式,等到调试完成一个成熟的策略,我们就可以搭建实盘进行运行,成为真正的量化交易者,大家快动手尝试起来吧!
视频参考链接:
26.多因子策略介绍
在相对一段长的时间内,“指标无用论”一致是量化交易者中的箴言。因为,不管多么神奇的指标,针对于不同的品种和走势,总有失效的时刻,出现巨大的回撤。但是,一个指标没有用,难道所有的指标都没有用吗?多因子模型以另一种角度给出了答案。通过结合多个指标,构建成为不同的因子,通过综合这些因子,多因子模型可以更全面地捕捉市场的特征,降低单一指标失效时的风险。例如,如果某个因子在特定市场条件下表现不佳,其他因子可能仍然有效,从而有助于平衡和稳定投资组合的表现。这种多样化的方法有助于降低投资风险,提高收益的稳定性。
因此,近年来多因子策略成为商品期货CTA(Commodity Trading Advisor)策略的重要组成部分,特别是在工业界应用级大模型中。这些策略以多个因子为基础,如市场趋势、波动性和成交量等,以期望获得稳定的回报。
从本节课开始,我们将尝试在优宽平台搭建一个多因子的模型,这项工作的开发难度是巨大的。因此,在每一步的探索过程中,我们一起来见证每一次的尝试和进取,大家有好的想法可以留言评论区进行互动,共同参与这项工作当中,我们一起来共同学习,共同进步。
多因子模型原理
多因子模型的发展历史可以追溯到资本市场中的Fama-French三因子模型和Carhart四因子模型等。这些模型通过引入不同的因子,如市值、账面市值比率、动量和市场风险等,来解释资产回报。不过这些模型都针对的是股票市场,针对于期货市场,因子会有所不同。
多因子模型的核心理念在于,资产的回报可以通过多个因子来解释。这些因子可以是与市场相关的,也可以是特定于特定资产类别的。在商品期货CTA策略中,通常涵盖以下几个因子:
-
技术指标因子:包括移动平均线、相对强弱指标(RSI)、随机指标(KDJ)等用于衡量市场趋势和超买超卖情况的技术指标。
-
波动率因子:通过历史波动率、ATR(平均真实波动范围)等指标来衡量市场的波动性,以识别市场风险水平。
-
成交量因子:关注交易量的大小和变化情况,如成交量的变动率、成交量的移动平均等,用于捕捉市场的流动性和交易活跃度。
-
市场情绪因子:包括情绪分析、舆情分析、新闻面情感评估等,用于捕捉市场参与者的情绪和预期。
-
基本面因子:考虑供需关系、季节性因素、宏观经济数据等基本面因素对商品价格的影响。
多因子模型的目标是将这些因子相互整合,从而预测资产未来的表现。这可以通过为每个因子分配适当的权重,然后将它们合并来实现。
当然,这些因子并不是固定的,最近某家研报甚至提出了9大因子的框架。几乎每家量化投资公司都会有自己的因子库。做不同频段策略会用到不同频段的因子,比如盘口因子,宏观因子,另类因子等等。建因子库的原因是无论做股票中性alpha,smart beta,主观,商品期货cta,统计套利,期现套利策略;都可以把初始信号转化为回报去做大规模的遍历,和跨周期的回测。而如果你想成为专业的量化分析师,入职公司的头两个月就是挖掘有效的因子,各种方法构建都可以!
在中国商品期货市场,多因子模型的研究呈现迅猛的态势,但越来越多的学者和从业者开始在这个领域展开研究。一些研究表明,基于趋势、波动性和成交量等因子的多因子策略在中国商品期货市场中表现出色,为CTA策略提供了有效的工具。在工业领域,有不少研报提供了商品期货多因子模型的搭建理念,比如中信和华泰的研报,水平质量都相当高,大家有空都可以学习一下。
多因子模型搭建框架
构建多因子模型的框架是策略开发的第一步。在商品期货CTA策略中,多因子框架通常包括以下几个关键组成部分:
-
数据获取:首先,需要获取历史市场数据,包括价格、成交量和其他相关因子数据。这些数据通常来源于交易所或专业数据供应商。
-
因子选择:基于策略目标和市场特征,选择适当的多因子。这通常需要行业专业知识和经验。
-
因子计算:编写代码来计算所选因子的值,并通过一定的算法进行因子的筛选和组合。
-
信号生成:基于因子值生成交易信号,确定买入、卖出或持有的决策。
当然在交易完成过后,我们也要进行风险管理。
-
风险管理:确保对资产组合的风险进行有效控制,包括设置止损和仓位管理策略。
-
回测和优化:使用历史数据进行回测,识别模型的弱点并进行策略的优化。
-
实时交易:一旦策略在回测中表现出色,就可以将其应用到实际市场中进行交易。
这里面的每一步都可以单独拿出来作为一篇或者几篇文章,我们将在后续的系列里一步步进行展示。
多因子模型的优缺点
多因子模型具有多方面的优势,包括:
-
多元化:多因子模型整合多个因子,降低了单一因子模型的风险。
-
稳定性:多因子模型通常能够在不同市场条件下表现稳定,减小了风险。
-
回报潜力:通过综合多个因子,多因子策略可以提供更高的回报潜力。
然而,多因子模型也存在一些缺点:
-
复杂性:搭建和维护多因子模型通常需要更多的工作,包括数据处理和编程。
-
过度拟合:如果不小心选择和管理因子,多因子模型可能会过度拟合历史数据,导致在实际市场中表现不佳。
-
成本:多因子模型通常需要更多的计算资源和数据,这可能增加了成本。
这些缺点呢,远远盖不过一个稳定收益的大模型的吸引。多因子模型框架搭建起来确实比较困难,在网上呢,也很难找到成品的策略框架可以直接拿过来使用。想想也是合理,作为公司的盈利法宝,模型和因子当然都是不可泄露的。但是,优宽作为专业的开放式平台,我们很乐意提供更多的量化知识,帮助大家构建自己的多因子模型。我们的教学策略可能会比较简单,大家可以在此基础上,添加进入自己的想法,作为一个专业的量化人,构建出来属于自己的因子库和多因子模型。
当然心动不如行动,不管多大的困难,我们也要从第一行代码敲起来。这节课呢,我们就尝试首先搭建一个多因子模型的框架。
作为一个多因子模型,其实在搭建框架之外,我们做的最多的事情就是尝试因子的有效性,因此各个模块函数需要保持复用性,就是在多因子模型中也可以稳定使用。所以呢,这次我们决定使用模板类库的方式,构建各个模块函数,这样呢,即使我们使用单因子或者多个因子去尝试因子的有效性,都是可以的。
为了理解的方便,我们使用最简单的一个个模块函数的方式,方便大家理清每个板块函数的编写方式,当然,熟悉Python的小伙伴可以使用面向对象的编程,将我们的各个模块构造成类的方式,提高代码的稳定性。
首先,我们选择目标合约,这里需要注意的是,我们的目标都是主力的合约,而不同的合约的主力月份是有区分的,所以我们建立一个字典索引,包含不同合约的代码和主力月份。
第二部分,我们定义getTarList函数,用来根据日期获取最新的主力合约代码,这里面需要处理的细节有很多。比如合约代码的大小写,还要根据日期绝对主力合约的年份和月份拼写,郑商所呢,数字只有3位,其他所有四位,比如2024年1月的合约,郑商所表达为“大写代码加401”,其他所是“小写代码加2401”。另外,我们需要根据月份定义,比如大部分合约不能进入交割月,还有其他的合约在交割月的前一个月就会到期,比如23年燃油的1月份合约,到期时间是22年年尾的12月27日,这些特殊的情况我们都需要考虑到。
第三部分,getMainData,获取主力日级别k线,方便后续因子,比如波动率,展期收益率,技术指标ATR,macd等的计算。
第四部分,获取外部因子getOutFactor板块,这里是需要使用k线之外的指标,我们需要在网上进行查询使用。
第五部分,因子计算calFactor。这方面设计的函数可太多了,各个因子的计算都可以编写成为一个单独的函数。我们在后续的因子介绍中将逐步完善。
第六部分,因子处理和组合proFactor。各个不同的因子当然不是直接拿过去使用的,我们需要对因子进行标准化的处理,挑选出来合适的因子进行合成,然后我们针对于不同的品种进行打分,确定对不同品种应该进行的交易操作。
第七部分,交易信号的确定,这里定义成了groupFactor多空组判断。
下面呢是,交易函数trade,我们根据上一步的交易信号获取到多空组列表以后,我们就要判断持仓品种的仓位类型和多空组的判断是否一致,如果一致就继续保持仓位类型,如果不一致,就要进行相应的平仓和相反方向开仓的操作。
因为我们建立的是一个实盘级别的策略,所以移仓换月的函数也是必不可少。
当然,以上的各个板块并不是确定的部分,我们后续在探索的过程中,需要根据各个不同的特殊情况,再进行模块函数的增加和改动。工作量确实不少,大家一起加油。
结语
对于我们而言,从0开始搭建一个多因子模型的框架,因子的有效性和策略的收益我们可以不必那么多的关注,重点在于从“打地基”开始树立多因子的整体框架,我们一步步来,让子弹慢慢飞!
27.展期收益率单因子模型
在商品期货市场,多因子模型是用于解释价格波动和预测市场走势的重要工具。多因子模型的一种简单模式就是单因子模型。本节课程将深入介绍单因子模型,并以展期收益作为单因子来探讨其概念、计算方法以及如何搭建模型筛选多空品种。作为多因子模型的基础,理解单因子模型是掌握多因子模型的关键步骤。
- 单因子模型:多因子模型的基础
上节课我们介绍了多因子模型的具体概念。它假设资产价格的变动是由多个因子引起的,如市场因子、价值因子、动量因子等。对于我们初始来说,一开始就搭建这么复杂的模型肯定是不适合的。所以我们决定从单因子模型入手。单因子模型则是多因子模型的最基本形式,只考虑一个因子对资产价格的影响。在这节课程中,我们将介绍展期收益作为单因子,探讨如何使用它来分析期货市场。
- 展期收益是什么?
这节课我们要使用的单因子是展期收益。展期收益(Roll Yield)是期货市场中的一种单因子,表示期货合同的价格与现货市场价格之间的差异。它通常反映了市场对未来价格变动的预期。当对某品种未来价格比较悲观的时候,近期期货价格高于远期期货价格时,展期收益为正,称为远月合约贴水,或现货升水;相反比较乐观的时候,近期期货价格低于远期期货价格,展期收益为负,称为远月合约升水,或现货贴水。
展期收益的计算通常针对不同交割日期的期货合同,以分析市场对不同交割日期的价格预期。展期收益可以通过以下简单的公式来计算:
展期收益(年度) = Log(近期合约期货价格/ 远期合约期货价格)/间隔月份 * 12
展期收益率有一个特性——单调性,强者恒强,弱者恒弱。按照专业机构提供的交易逻辑:不同合约的展期收益率排名可在一定程度上体现“多强空弱”,这里的“强”、“弱”概念可以认为是目前该品种趋势的判断,所以我们的交易逻辑就是从展期收益率的角度做到“顺势而为”。按照“多强空弱”中的理念,我们做多展期收益率最高的品种,做空展期收益率最低的品种,从而根据展期收益率排名构建相应的交易策略。
以上呢,就是展期收益率的介绍。当然我们的重点还是通过这个策略是实现单因子模型,我们来看怎样使用代码进行实现。
这个单因子策略是作为多因子模型准备的,因此各个模块函数需要保持复用性,就是在多因子模型中也可以稳定使用。所以呢,这次我们决定使用模板类库的方式,构建各个模块函数,这样呢,即使我们使用单因子或者多个因子去尝试因子的有效性,都是可以的。
第一个模块我们来补充getTarList函数,但是需要额外注意的一点是,这里我们不仅要获取主力合约,还要获取次主力合约,所以这里面定义mainList代表主力合约列表,nextList定义次主力合约列表。然后我们使用轮询在代码和具体主力月份的字典里,找到当前的主力合约代码和次主力合约代码,首先获取当前时间,使用“_D()”,然后获取当前的年份和月份,接着就要处理不同的情况了。当月份是12月的时候,我们更换主力合约,因为下一年的第一个主力合约即将到期,所以我们更换主力合约为下一年的第二个主力,次主力呢,更换为下一年的第三个。这里面需要对大小写进行不同的处理,如果是大写,年份保留2位,大写保留1位。然后针对于第一个主力到第二个主力,第二个到第三个,到三个到12月份,我们进行轮换的处理,这种方法确实比较复杂一点,但是理解起来更加简单。有的同学可能会询问为什么不使用代码加上888,然后获取代码的instrumentid呢,因为系统的主力合约是根据交易量和持仓量决定的,所以有时候非主力合约会成为不在列表里的主力合约,造成次主力合约判断出现错误。
python
# 获取主力/次主力合约代码
def getTarList():
mainList = [] # 主力合约列表
nextList = [] # 次主力合约列表
for commodity, monthList in commodity_contracts.items():
curTime = _D()
curYear = curTime[2:4]
curMonth = curTime[5:7]
if int(curMonth) == 12:
if commodity.isupper():
mainID = commodity + str(int(curYear[1])+1) + str(monthList[1])
nextID = commodity + str(int(curYear[1])+1) + str(monthList[2])
else:
mainID = commodity + str(int(curYear)+1) + str(monthList[1])
nextID = commodity + str(int(curYear)+1) + str(monthList[2])
elif int(curMonth) >= int(monthList[0]) and int(curMonth) < int(monthList[1]) - 1:
if commodity.isupper():
mainID = commodity + str(curYear[1]) + str(monthList[1])
nextID = commodity + str(curYear[1]) + str(monthList[2])
else:
mainID = commodity + str(curYear) + str(monthList[1])
nextID = commodity + str(curYear) + str(monthList[2])
elif int(curMonth) >= int(monthList[1]) - 1 and int(curMonth) < int(monthList[2]) - 1:
if commodity.isupper():
mainID = commodity + str(curYear[1]) + str(monthList[2])
nextID = commodity + str(int(curYear[1])+1) + str(monthList[0])
else:
mainID = commodity + str(curYear) + str(monthList[2])
nextID = commodity + str(int(curYear)+1) + str(monthList[0])
elif int(curMonth) < 12:
if commodity.isupper():
mainID = commodity + str(int(curYear[1])+1) + str(monthList[0])
nextID = commodity + str(int(curYear[1])+1) + str(monthList[1])
else:
mainID = commodity + str(int(curYear)+1) + str(monthList[0])
nextID = commodity + str(int(curYear)+1) + str(monthList[1])
mainList.append(mainID)
nextList.append(nextID)
return [mainList, nextList]
第二个getMainData,这个函数功能是获取不同列表,不同周期的k线数据,所以这里增加两个参数,周期和合约列表。这里呢,在使用getTarList函数后,第一个索引就是主力合约的列表,第二个索引是次主力合约的列表,作为参数放入这个函数当中,然后使用轮询的方式,获取倒数第二条记录,是完成的k线,添加进入DataFrame当中,进行返回就可以获取到主力和次主力的k线数据。
python
# 获取主力日级别k线,方便后续因子计算
def getMainData(PERIOD, contractList):
df = pd.DataFrame(columns=["Instrument", "InstrumentId", "Time", "Open", "High", "Low", "Close", "Volume"])
for codeID in contractList:
info = exchange.SetContractType(codeID)
code = re.search(r'([A-Za-z]+)\d+', codeID).group(1)
records = exchange.GetRecords(PERIOD)
latest_record = records[-2] # 获取倒数第二条记录
curR = {
"Instrument": code,
"InstrumentId": codeID,
"Time": latest_record['Time'],
"Open": latest_record['Open'],
"High": latest_record['High'],
"Low": latest_record['Low'],
"Close": latest_record['Close'],
"Volume": latest_record['Volume']
}
# 将字典转换为DataFrame,并添加到主DataFrame中
new_row = pd.DataFrame([curR])
df = df.append(new_row, ignore_index=True)
return df
在获取完成主力和次主力k线数据以后,第三个板块就是要计算展期收益率了。展期收益率的公式是这样的,所以我们也使用轮询的方式对上两个板块中获取的数据,针对于不同的合约进行依次的计算。这里需要注意的是,针对于间隔月份的计算,因为同处于一年的主力合约,可以直接使用两个月份之差进行计算,如果跨年,那么次主力的月份就要加上12。
python
# 计算展期收益率
import math
def calRollover():
mainData = getMainData(PERIOD_D1, getTarList()[0])
nextData = getMainData(PERIOD_D1, getTarList()[1])
rolloverList = [] # 用于存储展期收益率的列表
for i in range(len(mainData)):
mainLine = mainData.iloc[i]
nextLine = nextData.iloc[i]
mainFuture = mainLine['InstrumentId']
nextFuture = nextLine['InstrumentId']
mainDate = re.findall(r"\d+\d*", mainFuture)
nextDate = re.findall(r"\d+\d*", nextFuture)
if int(nextDate[0][-2:]) > int(mainDate[0][-2:]):
diffMonth = int(nextDate[0][-2:]) - int(mainDate[0][-2:])
else:
diffMonth = int(nextDate[0][-2:]) + 12 - int(mainDate[0][-2:])
rollOver = math.log(mainLine['Close'] / nextLine['Close']) / diffMonth * 12
rolloverList.append(rollOver)
rolloverDf = pd.DataFrame({
"InstrumentId": mainData['InstrumentId'],
"RollOver": rolloverList
})
return rolloverDf
这里因为只有一个因子,所以因子处理和因子合成的板块这里暂缺。
多空组判断groupFactor,这里定义两个参数,factorDf因子的dataframe,factor代表具体使用的因子。这里只有展期收益的因子,根据正负性分为做多组和做空组。
python
def groupFactor(factorDf, factorName):
positive_df = factorDf[factorDf[factorName] > 0].sort_values(by=factorName)
negative_df = factorDf[factorDf[factorName] < 0].sort_values(by=factorName)
positive_codes = positive_df['InstrumentId'].tolist() # 做多组
negative_codes = negative_df['InstrumentId'].tolist() # 做空组
return[positive_codes, negative_codes]
对不同的品种进行分类完毕以后,我们就要进行交易的操作了。这里不仅仅是根据多空组的判断进行做多还有做空的操作。因为不同品种可能在在这两组之间进行切换,所以当品种切换组别的时候,我们进行平仓,在另外方向开仓的处理。
在获取仓位信息以后,在策略起始阶段,判断仓位列表长度为0,直接根据多空组的列表进行相应的操作。
后续判断持仓长度不为0,获取各个品种的持仓type,是多头还是空头,然后判断如果该品种不再做多组的时候,打印信息该品种需要被移动到做空组,首先进行多头的平仓,然后进行空头的开仓。这里为了交易的进行,都是设置了一个比较容易成交的价格。对于空头组的处理也是一致的,如果某品种不再属于空头组,进行空头平仓,然后就像多头开仓。
python
# 多空组买卖
def trade(pos_group, neg_group):
posInfo = exchange.GetPosition()
if len(posInfo) == 0:
for i in range(len(pos_group)):
exchange.SetContractType(pos_group[i])
curPrice = exchange.GetRecords()[-1].Close + 5
exchange.SetDirection('buy')
exchange.Buy(curPrice, 1)
for i in range(len(neg_group)):
exchange.SetContractType(neg_group[i])
curPrice = exchange.GetRecords()[-1].Close - 5
exchange.SetDirection('sell')
exchange.Sell(curPrice, 1)
Log('多因子策略开始,建仓完成', "#FF0000")
else:
for i in range(len(posInfo)):
if posInfo[i].Type == PD_LONG or posInfo[i].Type == PD_LONG_YD:
if posInfo[i].ContractType not in pos_group:
Log('合约', posInfo[i].ContractType, '移仓至持空组')
exchange.SetContractType(posInfo[i].ContractType)
curPrice = exchange.GetRecords()[-1].Close - 5
exchange.SetDirection('closebuy')
exchange.Sell(curPrice, 1)
curPrice = exchange.GetRecords()[-1].Close - 5
exchange.SetDirection('sell')
exchange.Sell(curPrice, 1)
else:
if posInfo[i].ContractType not in neg_group:
Log('合约', posInfo[i].ContractType, '移仓至持多组')
exchange.SetContractType(posInfo[i].ContractType)
curPrice = exchange.GetRecords()[-1].Close + 5
exchange.SetDirection('closesell')
exchange.Buy(curPrice, 1)
curPrice = exchange.GetRecords()[-1].Close + 5
exchange.SetDirection('buy')
exchange.Buy(curPrice, 1)
还有最后一个板块,移仓的处理。首先获取主力合约列表,获取字母代码部分,方便我们根据品种的代码找到最新的主力合约,然后获取仓位的信息,如果仓位列表长度不为0,使用轮询的的方式,判断如果该品种不再主力列表当中,同样获取该品种的代码部分,对应索引找到该品种的主力合约。然后就进行平掉旧合约,开仓新合约,这里需要品种的type进行,保持该品种的多头和空头趋势。然后再次获取一下仓位的信息,如果都是主力合约,打印信息进行说明。
我们单因子模型需要的模块函数就定义完成了,然后我们使用ext加上函数名称的格式,定义这些模块函数,这样在我们就可以在策略编写中使用这个模版类库的交易函数了。下面呢,我们就进行一下测试。
python
# 移仓
def posTrans():
mainList = getTarList()[0]
codeList = [''.join(filter(str.isalpha, item)) for item in mainList]
prePos = exchange.GetPosition()
if len(prePos) != 0:
for i in range(len(prePos)):
if prePos[i].ContractType not in mainList:
mainCode = re.search(r'([A-Za-z]+)\d+', prePos[i].ContractType).group(1)
index = codeList.index(mainCode)
mainID = mainList[index]
Log('旧合约', prePos[i].ContractType, '需要被更换为', mainID)
if prePos[i].Type == PD_LONG or prePos[i].Type == PD_LONG_YD:
# 平掉旧合约
exchange.SetContractType(prePos[i].ContractType)
curPrice = exchange.GetRecords()[-1].Close - 5
exchange.SetDirection('closebuy')
exchange.Sell(curPrice, 1)
# 开仓新合约
exchange.SetContractType(mainID)
curPrice = exchange.GetRecords()[-1].Close + 5
exchange.SetDirection('buy')
exchange.Buy(curPrice, 1)
else:
exchange.SetContractType(prePos[i].ContractType)
curPrice = exchange.GetRecords()[-1].Close + 5
exchange.SetDirection('closesell')
exchange.Buy(curPrice, 1)
exchange.SetContractType(mainID)
curPrice = exchange.GetRecords()[-1].Close - 5
exchange.SetDirection('sell')
exchange.Sell(curPrice, 1)
# 移仓完成后再次判断
afterPos = exchange.GetPosition()
if len(prePos) != 0:
all_in_main_list = all(afterPos[i].ContractType in mainList for i in range(len(afterPos)))
if all_in_main_list:
Log('所有合约都是主力合约', "#00FF00")
我们设置一个策略样例进行检验,固定框架安装上,首先第一步进行展期收益率的计算,然后划分做多组和做空组,在进行交易之前,首先进行移仓的处理,确保最新的仓位都是主力合约,接着进行对应的交易操作。这样呢,就是一个单因子模型的主体交易部分。当然为了实盘的运行,我们需要加上策略图表的展示,还有收益的展示。
这里比较特殊的一点是,因为我们想固定天数间隔运行一次策略,而策略的实时收益展示,我们是想及时更新的,所以这里计算了主操作的时间间隔,如果主操作的间隔大于固定周期,进行主策略的运行;而策略的实时展示,是一直在进行的。
设置休眠时间是10天,我们试着运行一下。在回测结果里,可以看到,首先进行建仓的处理,然后每隔10天,进行一下交易的操作,在品种对应组别更换的时候,进行相应的平仓还有相反方向开仓的处理。当接近主力合约更换的月份,我们进行移仓换月的处理。
python
def main():
# 记录上次执行的时间
last_operation_timeBig = 0
isFirst = True
initAccount = exchange.GetAccount()
while True:
current_timeBig = int(time.time())
# 获取时间间隔(秒)
if isFirst:
time_intervalBig = 24 * 60 * 60 * 10
isFirst = False
else:
time_intervalBig = current_timeBig - last_operation_timeBig
# 执行多空组买卖之前的操作(每隔10天执行一次)
if time_intervalBig >= 24 * 60 * 60 * 10:
if exchange.IO("status"):
# 移仓
ext.posTrans()
# 计算展期收益率
callRollover = ext.calRollover()
# 划分组
positive_codes = ext.groupFactor(callRollover, 'RollOver')[0]
negative_codes = ext.groupFactor(callRollover, 'RollOver')[1]
# 多空组买卖
ext.trade(positive_codes, negative_codes)
last_operation_timeBig = current_timeBig
else:
Sleep(1)
positions = exchange.GetPosition()
if len(positions) != 0:
longContract = []
longPrice = []
longProfit = []
shortContract = []
shortPrice = []
shortProfit = []
for i in range(len(positions)):
if (positions[i]['Type'] == PD_LONG) or (positions[i]['Type'] == PD_LONG_YD):
longContract.append(positions[i]['ContractType'])
longPrice.append(positions[i]['Price'])
longProfit.append(positions[i]['Profit'])
else:
shortContract.append(positions[i]['ContractType'])
shortPrice.append(positions[i]['Price'])
shortProfit.append(positions[i]['Profit'])
tblAStatus = {
"type" : "table",
"title" : "持多组",
"cols" : ["合约名称", "持仓均价", "持仓盈亏"],
"rows" : []
}
tblBStatus = {
"type" : "table",
"title" : "持空组",
"cols" : ["合约名称", "持仓均价", "持仓盈亏"],
"rows" : []
}
for i in range(len(longContract)):
tblAStatus["rows"].append([longContract[i], longPrice[i], longProfit[i]])
for i in range(len(shortContract)):
tblBStatus["rows"].append([shortContract[i], shortPrice[i], shortProfit[i]])
lastStatus = f"`{json.dumps(tblAStatus)}`\n`{json.dumps(tblBStatus)}`"
curAccount = exchange.GetAccount()
curProfit = curAccount.Balance - initAccount.Balance
LogProfit(curProfit, "&")
LogStatus(lastStatus)
Sleep(1)
本节的课程呢,是一个单因子的示范策略。在后续课程中,我们将继续探讨多因子模型,包括如何选择和组合不同的因子,以构建更强大的模型来分析和预测商品期货市场的走势。了解单因子模型是深入理解多因子模型的第一步,当然后续课程的难度也会逐步的增加,让我们慢慢来。
视频参考链接:
28.高阶中心矩类因子
前面两节课程,我们介绍了多因子模型的概念,以及使用展期收益率进行了单因子模型的构建,初步建立起了多因子模型的框架。从本节课开始,我们将逐步的完善多因子模型,包括多个不同类型因子的引入,因子有效性的判断,因子的处理和合成等。
概念介绍
相对于股票市场,不同的股票对应的是实体的公司,所以有一些固定的因子,比如市盈率,市净率,年度收益等等。可以作为股票的因子直接拿过来使用。对于期货市场,因子的计算大多数的时候需要我们手动的计算和获取。但是即使针对于同一种因子,使用不同的计算方法,所获得的因子有效性也是具有显著区别的。这里的计算方法的差别有很多,比如k线的获取频率,因子的窗口周期,因子的标准化的方法等等。本节课呢,我们就介绍一种比较简单的因子计算方法,由此呢,来看出因子的一些计算差异。
本节课我们要介绍的一种因子类型,是高阶中心矩类因子。它是价格数据的直接生成品,因此计算起来并不复杂。但是作为一种参数类型,学会计算方式,将有助于我们计算其他与其相关的因子类型。另外,上节课我们只是使用了展期收益率一个因子,所以分组判断groupFactor和交易的trade函数设计的不是太完善,不能适用于所有的因子,所以这节课我们也要将这两个函数进行完善。
首先我们介绍一下高阶中心矩类因子:
-
波动率因子:在商品期货市场中,波动率因子被用来衡量商品价格的波动性。它反映了商品价格变动的幅度,是价格变动序列的二阶中心矩。在实际应用中,波动率因子可以用来预测商品期货价格的变动趋势和风险程度。一般来说,波动率因子越大,意味着价格变动越剧烈,风险也相应较高。因此,在投资决策中,对波动率因子的分析和理解非常重要。一般来说,我们需要根据自己的风险承受能力和投资目标来决定是做多波动率较高的商品期货还是做多波动率较低的商品期货。如果我们希望追求较高的收益,同时也有较高的风险承受能力,那么可以选择做多波动率较高的商品期货。相反情况下,如果希望保持相对稳健的投资策略,那么可以选择做多波动率较低的商品期货。
-
偏度因子:偏度因子是商品期货收益率序列的三阶中心矩,用于衡量价格变动的不对称性。在商品期货市场中,偏度因子的绝对值越大,意味着价格变动的离散程度越大,价格波动的分布曲线越有可能出现长拖尾现象。如果价格变动表现为右偏,意味着价格下跌的概率大于上涨的概率;反之,如果价格变动表现为左偏,意味着价格上涨的概率大于下跌的概率。所以研究发现偏度和收益率之间往往呈现负相关性,说明偏度因子也可以看作是一种反向指标。

- 峰度因子:峰度因子是商品期货收益率序列的四阶中心矩,用于衡量价格变动的峰态。直观来说,峰度反映了价格变动分布的峰部尖度。正态分布的峰度为3,如果价格变动的分布峰度大于3,称为“尖峰态”;如果峰度小于3,则称为“低峰态”。对于尖峰分布来说,其尾部集中了比正态分布更多的数据量,这些数据往往是过大或过小的离群值。因此,尖峰分布往往伴随着“肥尾”现象。同理,厚峰分布的数据更多地分布在均值附近,其尾部更细。与偏度相比,峰度与波动率的关系更为紧密。波动率衡量的是随机变量偏离均值的离散程度,但并没有表明该离散程度主要归属于随机变量的哪些值。而峰度则衡量了随机变量的离散程度主要是来自峰部还是尾部。和波动率一样,峰度因子也是一个反向指标。

总之,高阶中心矩类因子在商品期货市场中具有重要的应用价值。通过对波动率因子、偏度因子和峰度因子的分析和理解,我们可以更好地把握商品期货市场的动态和风险特征,为投资决策提供有力的支持。
计算方式
下面我们来看下计算的公式。高阶中心矩类因子是需要一定时间段内的数据的,所以我们需要设置回看周期参数,计算指定的回看周期内的高阶中心矩类因子的数值。
第一个,波动率。其中\(ret_i\)为 5 分钟收益率,\(𝑖 = 1, 2,3\)为5分钟收益率的序列,\(\mu_{5\text{min}}\)为回看期内所有 5 分钟收益率序列的均值。
\(sd_{\text{t}} = \sqrt{\mathbb{E}\left[(ret_{\text{i}} - \mu_{5\text{min}})^2\right]}\)
第二个,偏度因子值。在上面公式基础上,除以\(\sigma_{5\text{min}}\),它是波动率,然后是3次方。
\(skew_{\text{t}} = \mathbb{E}\left[\left(\frac{ret_{i} - \mu_{5\text{min}}}{\sigma_{5\text{min}}}\right)^3\right]\)
第三个,和偏度因子值的计算公式几乎一致,只是作为更高阶,这里要改成4次方。
\(kurt_{\text{t}} = \mathbb{E}\left[\left(\frac{ret_{i} - \mu_{5\text{min}}}{\sigma_{5\text{min}}}\right)^4\right]\)
相对于以往使用价格数据,这里使用收益率作为\(ret_i\)价格变化的统计。收益率消除了不同期货品种价格水平的影响,使得不同资产之间更容易进行比较。在许多金融模型中,收益率通常被假定为服从正态分布,这简化了数学模型的运算。而价格的分布通常不是正态分布。另外,收益率提供了对资产风险的更直观测量。投资者通常更关心的是他们的投资在一段时间内的表现,而不仅仅是资产的绝对价格水平。
我们来看下使用代码怎么实现。在上节课模版类库的基础上,定义calCMF函数,这个函数里面定义两个参数,PERIOD代表k线周期,backPERIOD代表回看周期。在函数里面,首先获取主力合约列表,使用上节课讲到的getTarList函数。我们对三个指标的计算是使用的收益率数据。这里使用轮询的方式,对每个合约先取k线数据,这里取得k线数据的范围是从-backPERIOD-2到-1,最后一根是倒数第二根,是完成的k线。这样呢,我们获取了backPERIOD+1根k线,然后我们使用最新收盘价除以上一根收盘价,再减去1就是收益率序列,可以获取backPERIOD个收益率数据。这里重要的一点是需要对收益率进行标准化。因为我们假设收益率是符合正态分布的,所以我们使用正态分布标准化。接下来,三个指标的计算就比较简单了。可以使用pandas包的三个函数,std(),skew()和kurt(),获取到各个品种的结果最后进行返回就可以。
python
# 计算高阶矩阵因子
def calCMF(PERIOD, backPERIOD):
contractList = getTarList()[0]
df = pd.DataFrame(columns=["InstrumentId", "Std", "Skew", "Kurt"])
for codeID in contractList:
info = exchange.SetContractType(codeID)
records = exchange.GetRecords(PERIOD)[-backPERIOD-2: -1]
recent_returns = [records[i + 1].Close / records[i].Close - 1 for i in range(len(records) - 1)]
standardized_returns = (recent_returns - np.mean(recent_returns)) / np.std(recent_returns) # 收益率标准化
std_dev = pd.Series(standardized_returns).std()
skewness = pd.Series(standardized_returns).skew()
kurtosis = pd.Series(standardized_returns).kurt()
# 将结果添加到 DataFrame
df = df.append({
"InstrumentId": codeID,
"Std": std_dev,
"Skew": skewness,
"Kurt": kurtosis
}, ignore_index=True)
return df
怎样判断因子的有效性呢,可以使用分层回归法。其实和我们上节课在单品种中使用的,展期收益率为正数进行做多,展期收益率为负数进行做空类似。但是这里根据待测试的因子排序,将期货品种分为N组来进行分组回测,使用固定的周期来进行调仓的操作。 如果情况理想,N组品种的收益率会呈现较好的单调性,单调递增或递减,且每一组的收益差距较大。这样的因子体现为较好的区分度。假如第一组收益最高,最后一组收益最低,那么做多第一组组合并做空最后一组组合,然后比较使用不同因子获得的收益率和夏普比率,来判断不同因子的有效性。
还记得上节课我们是怎样使用groupFactor对展期收益率进行分组的吗,对展期收益率为正的做多,对展期收益率为负的做空,但是有时候两者的数目不是一致的,可能存在很大的差异。另外对于波动率因子和峰度因子都是正数,所以不能按照符号进行区分。所以,我们重新改写下这个groupFactor函数。这里首先将因子进行排序,这里我们分成四组,获取各组的数量。分数排名前25%的为第一组,最后25%的第二组。这里我们使用pandas包的head函数,定义因子前25%为head_group,使用tail函数获取最后25%,最后将两个组的合约代码进行返回。
python
# 多空组判断
def groupFactor(factorDf, factorName):
sortDf = factorDf.sort_values(by=factorName, ascending=False)
# 取前25%和最后25%的数据
total_count = len(sortDf)
group_count = int(total_count * 0.25)
head_group = sortDf.head(group_count) # 第一组取前25%
tail_group = sortDf.tail(group_count) # 第二组取最后25%
head_codes = head_group['InstrumentId'].tolist() # 第一组codes
tail_codes = tail_group['InstrumentId'].tolist() # 第二组codes
return [head_codes, tail_codes]
第二方面,对于trade函数,在前面一堂课中,我们对所有品种进行分类,然后进行交易的操作,如果该品种不再是持多组,那么该品种一定移动到了持空组,但是现在由于我们只做前25%和后25%,所以可能很多品种移动到了空闲组,就是不进行任何操作。所以我们也需要重新改写下函数。
在原来的函数基础上,首先获取目前持有多仓和空仓和合约列表,我们首先判断,当某品种已经具有仓位,但是不再是持多组或者持空组,我们第一步进行平仓。下面呢,再根据最新的需要做多或者做空的列表,开需要开的仓位,这里判断当前品种不再持仓当中,但是属于持多组或者持空组,我们进行相应仓位的开仓。
python
# 多空组买卖
def trade(pos_group, neg_group):
posInfo = exchange.GetPosition()
if len(posInfo) == 0:
for i in range(len(pos_group)):
exchange.SetContractType(pos_group[i])
curPrice = exchange.GetRecords()[-1].Close + 5
exchange.SetDirection('buy')
exchange.Buy(curPrice, 1)
for i in range(len(neg_group)):
exchange.SetContractType(neg_group[i])
curPrice = exchange.GetRecords()[-1].Close - 5
exchange.SetDirection('sell')
exchange.Sell(curPrice, 1)
Log('多因子策略开始,建仓完成', "#FF0000")
else:
longList = [pos.ContractType for pos in posInfo if pos.Type == PD_LONG or pos.Type == PD_LONG_YD] # 已经开多仓
shortList = [pos.ContractType for pos in posInfo if pos.Type == PD_SHORT or pos.Type == PD_SHORT_YD] # 已经开空仓
# 检查已经开的多仓,如果在需要开多仓的列表中,则保留,否则平仓
for pos_contract in longList:
if pos_contract not in pos_group:
# 进行平仓操作
Log(pos_contract, '多余多仓进行平仓')
exchange.SetContractType(pos_contract)
curPrice = exchange.GetRecords()[-1].Close - 5
exchange.SetDirection('closebuy')
exchange.Sell(curPrice, 1)
# 检查已经开的空仓,如果在需要开空仓的列表中,则保留,否则平仓
for neg_contract in shortList:
if neg_contract not in neg_group:
# 进行平仓操作
Log(neg_contract, '多余空仓进行平仓')
exchange.SetContractType(neg_contract)
curPrice = exchange.GetRecords()[-1].Close + 5
exchange.SetDirection('closesell')
exchange.Buy(curPrice, 1)
# 开需要开的多仓
for pos_contract in pos_group:
if pos_contract not in longList:
# 进行开仓操作
Log(pos_contract, '需要开多的进行开多仓')
exchange.SetContractType(pos_contract)
curPrice = exchange.GetRecords()[-1].Close + 5
exchange.SetDirection('buy')
exchange.Buy(curPrice, 1)
# 开需要开的空仓
for neg_contract in neg_group:
if neg_contract not in shortList:
# 进行开仓操作
Log(neg_contract, '需要开空的进行开空仓')
exchange.SetContractType(neg_contract)
curPrice = exchange.GetRecords()[-1].Close - 5
exchange.SetDirection('sell')
exchange.Sell(curPrice, 1)
这样是一种平仓的方法,另外还有一种方法,就是如果持仓品种不再属于做多组或者做空组的时候,我们不是直接进行平仓,而是等到落到相反组别的时候,再进行平仓。这两种平仓方法哪种更好,大家可以根据自己的想法去进行试验。
然后,我们验证各个因子的有效性的时候,在分组判断groupFactor函数中,如果是负向指标,比如偏度和峰度,所以这里positive_codes是groupFactor函数返回的第二个索引,negative_codes是第一个索引。这里我们使用Skew因子做一下层次回归法的因子验证,设置好代码,我们可以进行回测,获取到不同因子的收益率和夏普比率,比较下因子的有效性。
python
'''backtest
start: 2021-11-02 09:00:00
end: 2022-11-30 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
'''
import json
import time
import numpy as np
def main():
# 记录上次执行的时间
last_operation_timeBig = 0
isFirst = True
initAccount = exchange.GetAccount()
while True:
current_timeBig = int(time.time())
# 获取时间间隔(秒)
if isFirst:
time_intervalBig = 24 * 60 * 60 * holdPeriod
isFirst = False
else:
time_intervalBig = current_timeBig - last_operation_timeBig
# 执行多空组买卖之前的操作(每隔10天执行一次)
if time_intervalBig >= 24 * 60 * 60 * holdPeriod:
if exchange.IO("status"):
# 移仓
ext.posTrans()
# 计算展期收益率
resRollover = ext.calRollover()
# 计算CMF
resCMF = ext.calCMF(PERIOD_M5, backPeriod)
# 划分组
positive_codes = ext.groupFactor(resCMF, 'Skew')[1]
negative_codes = ext.groupFactor(resCMF, 'Skew')[0]
# 多空组买卖
ext.trade(positive_codes, negative_codes)
last_operation_timeBig = current_timeBig
else:
Sleep(1)
positions = exchange.GetPosition()
if len(positions) != 0:
longContract = []
longPrice = []
longProfit = []
shortContract = []
shortPrice = []
shortProfit = []
for i in range(len(positions)):
if (positions[i]['Type'] == PD_LONG) or (positions[i]['Type'] == PD_LONG_YD):
longContract.append(positions[i]['ContractType'])
longPrice.append(positions[i]['Price'])
longProfit.append(positions[i]['Profit'])
else:
shortContract.append(positions[i]['ContractType'])
shortPrice.append(positions[i]['Price'])
shortProfit.append(positions[i]['Profit'])
tblAStatus = {
"type" : "table",
"title" : "持多组",
"cols" : ["合约名称", "持仓均价", "持仓盈亏"],
"rows" : []
}
tblBStatus = {
"type" : "table",
"title" : "持空组",
"cols" : ["合约名称", "持仓均价", "持仓盈亏"],
"rows" : []
}
for i in range(len(longContract)):
tblAStatus["rows"].append([longContract[i], longPrice[i], longProfit[i]])
for i in range(len(shortContract)):
tblBStatus["rows"].append([shortContract[i], shortPrice[i], shortProfit[i]])
lastStatus = f"`{json.dumps(tblAStatus)}`\n`{json.dumps(tblBStatus)}`"
curAccount = exchange.GetAccount()
totalProfit = sum(position['Profit'] for position in positions)
totalMargin = sum(position['Margin'] for position in positions)
curProfit = curAccount.Balance - initAccount.Balance + totalProfit + totalMargin
LogProfit(curProfit, "&")
LogStatus(lastStatus)
Sleep(1)
另外如果我们想测试参数的最优组合,比如这里的高阶因子类的回看周期,我们想分别定义为300,400和500,然后持仓周期我们也设置为参数,这里定义为1天,10天和30天,测试下最优的参数组合配置。为了比较不同参数组合的效果,我们不需要重新编写函数来呈现具体的结果,还记得前面讲过的函数的调参吗,这里我们设定下参数的范围,然后看下具体参数组合的策略收益。
本节课呢,我们计算了高阶中心类的因子,并且随着因子类型的扩充,我们重新完善了groupFactor和trade函数,使我们的模型更具适用性。另外,现在我们使用的是各个模块函数的方式,所以有些重复变量调用的冗余,这一部分呢,我们将在各个模块函数设计的初步完善的时候,进行统一的封装。在后续的课程中,我们将伴随模型设计的完善,一步步的优化各个函数。这是一个探索的过程,大家有好的想法可以留言评论区,我们也会根据大家的想法进行优化。
视频参考链接:
《数字货币因子模型》
《中信期货-期货多因子系列(一):动量及高阶矩因子在商品期货截面上的运用》
29.因子有效性的检验方法
多因子模型是一种通过多个因子来解释和预测市场变动的模型。在这些因子中,每一个因子都代表着市场的一种特定特征或趋势。然而,并不是所有的因子都是有效的,也就是说,它们对市场的解释和预测能力并不是完全相同的。因此,在进行多因子分析之前,对单因子进行有效性检验就显得尤为重要。
单因子有效性检验的主要目的是确定每个因子是否能够独立地解释市场的变动。如果某个因子无法通过有效性检验,那么它就不能被纳入到多因子模型中。通过这样的筛选过程,我们可以确保最终的模型只包含对市场变动有显著影响的因子,从而提高模型的预测能力和解释能力。
上节课我们使用了分层回归法。本节课,我们首先补充下因子的平稳性检验方法;然后介绍其他两种方法,对商品期货市场的单因子进行有效性检验,分别是t检验和RankIC的方法。
在课前首先声明下哈,因子有效性验证的方法确实很多,并且使用的参数和具体的代码,也都有很大的差异。我们的课程,也是根据专业机构的研报,试着做一些探索性的工作。如果有哪里讲得不足或者欠缺,欢迎大家提出更多意见。
因子平稳性检验
首先,我们来看因子的平稳下检验。因子平稳性检验是在使用因子模型进行资产定价或投资组合管理时的一项重要步骤。平稳性指的是时间序列数据的统计特性在不同时间段内保持稳定。对于因子而言,平稳性是确保因子对资产或投资组合的解释能力和预测能力的关键因素。如果因子的相关性很高,说明这个因子的持续性会很好,强者恒强。关于因子平稳性的检验方法有很多,其中使用最多的是单位根检验,它是检验时间序列数据是否具有单位根(也就是非平稳性)的方法之一。ADF检验和PP检验是常用的单位根检验方法。如果在检验中无法拒绝原假设(存在单位根),那么表明因子可能是非平稳的。
这里我们来看怎样实现,首先需要获取数据。这里为了计算的方便,我们设置合约列表为8个。在策略开头,创建一个空的factor_df,用来保存k线数据,和前面我们讲过的四个因子,包括展期收益率,波动率,偏度和峰度。这里我们设置用来进行因子平稳性检验的日期设置为200天。在while循环里,我们来收集数据,200天的数据,设置循环条件是因子列表长度小于合约列表长度8乘以200。然后,我们使用各个模块函数来获取k线,展期收益率,和高阶中心矩类因子,以合约名称为key进行merge合并,最后将新收集到的数据进行concat纵向合并。
数据收集完成以后,这里我们使用的是ADF方法进行因子平稳性的检验。在多因子模型中,通常是分别对每个品种的因子进行ADF检验,而不是将所有品种的因子合并一起做检验。这是因为每个品种的价格走势和因子可能受到不同的影响,因此需要对每各品种的因子进行独立的平稳性检验。
这里我们对八个品种的四个因子分别做adf检验,首先导入需要的包,然后使用两个轮询不同的因子,和不同的品种,然后打印对应的p值到日志当中。综合来看,RollOver和std中具有较多的品种的p值大于0.05,代表不符合平稳性检验,而Skew和Kurt所有的因子都符合平稳性检验。当然这里我们只是展示一下具体平稳性检验的操作,伴随策略周期的改变,因子的平稳性检验结果可能会发生相应的变化。
python
'''backtest
start: 2022-11-01 09:00:00
end: 2023-11-01 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["holdPeriod",1]]
'''
import json
import time
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
def main():
# 记录上次执行的时间
last_operation_timeBig = 0
isFirst = True
# Initialize factor_df
factor_df = pd.DataFrame(columns=["Instrument", "InstrumentId", "Time", "Open", "High", "Low", "Close", "Volume", "RollOver", "Std", "Skew", "Kurt"])
while len(factor_df) < 200 * 8:
current_timeBig = int(time.time())
# 获取时间间隔(秒)
if isFirst:
time_intervalBig = 24 * 60 * 60 * holdPeriod
isFirst = False
else:
time_intervalBig = current_timeBig - last_operation_timeBig
# 执行多空组买卖之前的操作(每隔holdPeriod执行一次)
if time_intervalBig >= 24 * 60 * 60 * holdPeriod:
if exchange.IO("status"):
# 获取k线
rData = ext.getMainData(PERIOD_D1, ext.getTarList()[0])
# 计算展期收益率
resRollover = ext.calRollover()
# 计算CMF
resCMF = ext.calCMF(PERIOD_M5, backPeriod)
# 合并数据
new_df = pd.merge(rData, resRollover, on='InstrumentId', how='inner').merge(resCMF, on='InstrumentId', how='inner')
factor_df = pd.concat([factor_df, new_df], ignore_index=True)
# 更新时间
last_operation_timeBig = current_timeBig
Sleep(60*60*2)
factors = ['RollOver', 'Std', 'Skew', 'Kurt']
for factor in factors:
Log(f"ADF Test for {factor}:")
for instrument in factor_df['Instrument'].unique():
subset = factor_df[factor_df['Instrument'] == instrument][factor]
result = adfuller(subset)
Log(f"Instrument {instrument}: p-value = {result[1]}")
回归法(T检验)
接下来我们进行因子有效性的检验。回归分析,也称为t检验。t检验是一种常用的统计检验方法,大多数情况下,用于比较两组数据的均值是否存在显著差异,也可以用于在回归分析中,计算自变量的显著性。多因子策略中因子有效性检验的回归法,主要是通过将因子 T 期的因子暴露与 T 1期的收益率进行线性回归,得到的回归系数即为该因子对收益率的影响系数。同时,还能通过t-test,获得该因子对于收益率的显著度水平,从而从量化的角度鉴别该因子的有效性。这里可以一次回归多个因子,或者单独回归。
我们来看怎样使用代码进行实现。这里需要导入进行线性回归的包。
在上一步收集数据的基础上,这里我们需要根据收盘价计算收益率,代表因变量。我们要分品种计算收益率,所以设置groupby为Instrument,然后进行收益率的标准化。基期的收益率是为0的,因为我们想使用T期的因子数据预测T+1的收益率数据,所以需要将收益率进行前移。使用shift(-8),然后删除空值。
接下来我们定义收益率为因变量,提取四个因子定义为自变量,自变量同样进行标准化的处理。这样我们的数据基本就处理完毕了,接下来进行线性回归检验。这里我们设置DataFrame来存储各个因子的显著性的结果。然后使用for循环遍历自变量,将各个因子分别与收益率进行线性回归分析,并记录该因子对应的系数值,t值和p值。这里我们使用状态栏进行展示。结果表明,仅有展期收益率的因子对各品种的收益率具有显著的影响作用。
python
'''backtest
start: 2022-11-01 09:00:00
end: 2023-11-01 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["backPeriod",70],["holdPeriod",1]]
'''
import json
import time
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
def main():
# 记录上次执行的时间
last_operation_timeBig = 0
isFirst = True
initAccount = exchange.GetAccount()
# Initialize factor_df
factor_df = pd.DataFrame(columns=["Instrument", "InstrumentId", "Time", "Open", "High", "Low", "Close", "Volume", "RollOver", "Std", "Skew", "Kurt"])
while len(factor_df) < 200*8:
current_timeBig = int(time.time())
# 获取时间间隔(秒)
if isFirst:
time_intervalBig = 24 * 60 * 60 * holdPeriod
isFirst = False
else:
time_intervalBig = current_timeBig - last_operation_timeBig
# 执行多空组买卖之前的操作(每隔10天执行一次)
if time_intervalBig >= 24 * 60 * 60 * holdPeriod:
if exchange.IO("status"):
# 获取k线
rData = ext.getMainData(PERIOD_D1, ext.getTarList()[0])
# 计算展期收益率
resRollover = ext.calRollover()
# 计算CMF
resCMF = ext.calCMF(PERIOD_M5, backPeriod)
# 合并数据
new_df = pd.merge(rData, resRollover, on='InstrumentId', how='inner').merge(resCMF, on='InstrumentId', how='inner')
factor_df = pd.concat([factor_df, new_df], ignore_index=True)
last_operation_timeBig = current_timeBig
Sleep(60*60*2)
factor_df['Returns'] = factor_df.groupby('Instrument')['Close'].pct_change() # 计算收益率
factor_df.Returns = factor_df.Returns.apply(lambda x: (x - factor_df.Returns.mean()) / factor_df.Returns.std()) # 收益率标准化
factor_df['Returns'] = factor_df['Returns'].shift(-8) # 收益率前移
factor_df = factor_df.dropna() # 去除空值
# 定义因变量和自变量
y = factor_df['Returns'] # 收益率
X = factor_df.iloc[:, 8:12] # 4个因子
X_standardized = X.apply(lambda col: (col - col.mean()) / col.std(), axis=0) # 因子标准化
# 创建DataFrame来存储结果
significant_factors_df = pd.DataFrame(columns=['Factor', 'Coefficient', 'Absolute_t_value', 'P_value'])
# 遍历自变量
for ind_var in X_standardized.columns:
model = sm.OLS(y, X_standardized[ind_var]) # 添加截距项
results = model.fit()
# 将结果添加到DataFrame中
significant_factors_df = significant_factors_df.append({
'Factor': ind_var,
'Coefficient': results.params[ind_var],
'Absolute_t_value': abs(results.tvalues[ind_var]),
'P_value': results.pvalues[ind_var]
}, ignore_index=True)
# 状态栏展示
table = {
"type" : "table",
"title" : "回归分析结果",
"cols" : ["Factor", "Coefficient", "Absolute_t_value", "P_value"],
"rows" : []
}
for i in range(len(significant_factors_df)):
table["rows"].append(significant_factors_df.iloc[i].astype(str).tolist())
# 显示结果
LogStatus('`' + json.dumps(table) + '`')
需要注意的是,这里我们使用的是一次性回归所有的时间,我们也可以按照固定周期进行回归,获取到连续的t值,然后统计一下t值的均值是否大于2,代表是否显著;另外还可以统计t值大于2的占比,用来表示t值的稳定性。
这里关于其他因子不显著的原因,有多方面的原因,可是是因为波动率,偏度和峰度受到回看周期参数的影响,另外,这也是由于回归因子验证法固有的缺陷,只能验证因子对于收益率的线性关系。我们可以根据所学的统计学知识进行非线性方程的验证。
RankIC因子验证性方法
接下来我们来介绍RankIC(Rank Information Coefficient)和RankIC_IR(Rank Information Coefficient Information Ratio)因子验证性方法。
RankIC(排名信息系数): RankIC度量了因子值的排名与未来期(通常是下一期)的收益率之间的相关性。它是通过计算因子值的排名和未来期收益率的排名之间的相关系数来定义的。RankIC的范围在-1到1之间,越接近1表示因子值的排名与未来收益率呈正相关,越接近-1表示负相关,而接近0表示几乎没有相关性。
RankIC_IR(排名信息系数信息比率): RankIC_IR是RankIC的信息比率,它衡量了因子预测的相对表现。RankIC_IR是RankIC的均值除以其标准差,因此它提供了一个考虑到因子预测稳定性的度量。通常,高的RankIC_IR值表示因子在预测未来收益方面具有较好的表现。
这些指标的评估有助于确定在多因子模型中使用哪些因子对于预期收益的预测是有效的。
我们来看下怎样使用代码进行计算,这里需要导入rankdata的包。开始的数据处理,因子和目标变量的提取和回归法基本一样。
这里首先定义一个空的dataframe,用来保存各个因子的rankic值。接下来使用轮询,分别计算各个因子和收益率的RankIC值。这里我们不是统一的计算一个RankIC值,而是按照周期计算一个连续的RankIC值,这里我们设置RankIC的周期是800,步长是8,也就是RankIC计算的周期是100天,每隔一天计算一次,保存到rankic_df各个因子的列中。然后我们对各个因子取平均,就是该因子的RankIC均值,使用均值除以标准差,就是RankIC_IR值。
根据返回的结果,一般认为RankIC大于0.03时,因子值跟下一期收益率的相关性较为明显;RankICIR大于0.5时,因子稳定获取超额收益的能力较强。可以看到展期收益率和偏度的RankIC的绝对值大于0.03,并且它的是RankIC_IR值大于0.5,证明这两个因子对预测收益具有一定的预测作用。
python
'''backtest
start: 2022-11-01 09:00:00
end: 2023-11-01 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["holdPeriod",1]]
'''
import json
import time
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
from scipy.stats import rankdata
def main():
# 记录上次执行的时间
last_operation_timeBig = 0
isFirst = True
initAccount = exchange.GetAccount()
# Initialize factor_df
factor_df = pd.DataFrame(columns=["Instrument", "InstrumentId", "Time", "Open", "High", "Low", "Close", "Volume", "RollOver", "Std", "Skew", "Kurt"])
while len(factor_df) < 200*8:
current_timeBig = int(time.time())
# 获取时间间隔(秒)
if isFirst:
time_intervalBig = 24 * 60 * 60 * holdPeriod
isFirst = False
else:
time_intervalBig = current_timeBig - last_operation_timeBig
# 执行多空组买卖之前的操作(每隔持仓周期执行一次)
if time_intervalBig >= 24 * 60 * 60 * holdPeriod:
if exchange.IO("status"):
# 获取k线
rData = ext.getMainData(PERIOD_D1, ext.getTarList()[0])
# 计算展期收益率
resRollover = ext.calRollover()
# 计算CMF
resCMF = ext.calCMF(PERIOD_M5, backPeriod)
new_df = pd.merge(rData, resRollover, on='InstrumentId', how='inner').merge(resCMF, on='InstrumentId', how='inner')
factor_df = pd.concat([factor_df, new_df], ignore_index=True)
last_operation_timeBig = current_timeBig
Sleep(60*60*2)
factor_df['Returns'] = factor_df.groupby('Instrument')['Close'].pct_change()
factor_df.Returns = factor_df.Returns.apply(lambda x: (x - factor_df.Returns.mean()) / factor_df.Returns.std())
factor_df['Returns'] = factor_df['Returns'].shift(-8)
factor_df = factor_df.dropna()
# 提取因子和目标变量
factors = factor_df[['RollOver', 'Std', 'Skew', 'Kurt']]
factors = factors.apply(lambda col: (col - col.mean()) / col.std(), axis=0)
returns = factor_df['Returns']
# 保存rankic值
rankic_df = pd.Dataframe()
for factor in factors.columns:
iclist = []
for i in range(0,len(factors)+1-800,8): # 步长8
current_factors = factors[factorl.ilocli:i+800]
current_returns = returns.iloc[i:i+800]
factor_ranks = rankdata(current_factors)
returns_ranks = rankdata(current_returns)
rankic_values = np.corrcoef(factor_ranks,returns_ranks)[0,1]
iclist.append(rankic_values)
rankic_df[factorl=iclist
rankic_means = rankic_df.mean()
rankic_ir = rankic_df.mean()/rankic_df.std()
但是需要注意的是,我们的预测周期是200天,所以拉长范围,因子的有效性会受到一定质疑。并且这些阈值通常是基于经验和实践得出的,并不是普适于所有情况。在具体的应用中,可以根据数据和具体问题的性质来调整这些阈值。同时,这些阈值的解释可能还受到市场环境、交易成本等因素的影响。因此,在使用这些规则时,需要谨慎考虑具体情境,并可以根据实际情况进行调整。
除此之外,还有一些机器学习方法也可以用于单因子检验,如支持向量机(SVM)、决策树(Decision Trees)、随机森林(Random Forests)等。这些方法可以通过学习历史数据来预测未来的市场变动,并可以用于评估因子的有效性。
最后在这里需要给大家说一声抱歉,由于确实期货多因子模型的资料比较少,使用的大部分都是来源于股票类的代码框架,再手动的编写代码移植到期货市场中,所以课程中代码整理和呈现会比较杂乱一点。在后期的课程中,我们也会不断优化,最后会呈现一个清晰明了可复用的实盘模型框架,请大家稍微等待一下。
视频参考链接:
《中信期货-金融工程专题报告:期货多因子系列(二),商品期货截面风格因子初探》
30.基本面因子特征工程处理
前面的课程,我们介绍了多因子模型中的展期收益率和高阶中心矩类因子。这些因子都是来源于期货的价格数据,属于技术指标。但是多因子模型的因子来源不是仅来自于量价指标,基本面因子,作为外部因子,在多因子模型中也发挥着重要的作用。
在期货交易市场,无论是私募机构还是普通散户,争论技术分析和基本面分析谁好谁坏,从来就没有停止过。技术分析者认为价格已经包含了一切,相信未来价格还以趋势方式演变,只关心图表上价格行为本身的变化,判断它能卖多少钱。基本面分析者认为真正价值最终将会反映在价格上,并不需要关心短期价格走势,更多的是分析影响价格背后的因素,判断它值多少钱。
作为期货交易的老手,不管技术指标使用的多么熟悉,但是总体的趋势判断还是要基于基本面分析的基础上。如果总体趋势判断出现问题,那么技术指标的应用缺乏实际的根基。
首先我们来看下影响期货基本面分析的因素,我们知道影响商品期货的三大因素中包括:宏观、品种、其他,每个因素下面大致可以分为以下的几类:
-
宏观
宏观政策
产业政策
政治因素
外汇汇率
经济周期
货币政策 -
其他
季节因素
天气因素
新闻事件
市场情绪 -
品种
升水贴水
供需关系
商品库存
产业利润
宏观经济数据包括宏观政策,产业政策,政治因素,外汇汇率,经济周期和货币政策,这些数据复杂多变,每天每时每刻,地球上有太多的经济数据公布,各国政界、央行、投行,官方的和非官方的。但是,使用宏观指标预测所有品种的变化是比较困难的,比如螺纹钢,热卷,玻璃等建材对应房地产的变化,玉米,豆类对应农业需要的变化,所以在多因子模型当中,我们暂且不使用宏观因子。
第二类包含的其他事件,更多的时候是一些比较突发的因素而引起的市场趋势的转变。但是对于散户来说,是消息接受的滞后一方,所以我们大多数时候只能被动的应对市场的突然转换。并且这类消息也比较难以量化,如果大家感兴趣,可以使用自然语言处理的技术抓取关键的新闻进行市场情绪指数的量化。
但是对于散户来说,基本面分析就真的没有办法了吗,也并不是。我们可以关注品种信息。在商品经济中,影响价格最关键的因素,就是供需关系。我们可以从第二类数据,升水贴水,供需关系,商品库存和产业利润中,找到目标期货品种对应的现货市场中库存和价格的实时变化。这些数据在官方的交易所每日都会公布,我们从这些公开的数据可以预判出相对的供需关系,从而判断期货未来的大概价值。
本节课呢,我们要尝试一下使用品种信息中的仓单和基差数据,验证一下基本面因子的有效性。
第一种我们来介绍仓单因子。影响商品现货价格的因素虽然有很多,但最终大都体现在供需关系上。如果买者多于卖者,价格就会上涨;如果卖者多于买者,价格就会下跌。国内的商品期货大致上可以分为:农产品和工业品。期货圈子中流传着这样一句话:“农产品看供给,工业品看需求。”农产品是刚需,需求是相对稳定的,决定价格主要看供给;工业品是下游需求带动的,再者国内基本都产能过剩,决定价格主要看需求。虽然,在实际操作中我们很难获取工业品的需求数据,也很难计算出农产品的供给数据。但是价格波动依存于供给与需求的相互作用,这种相互作用的结果就是库存。如果库存处于低位,说明市场供不应求,需求的力量大于供给的力量,未来价格看涨;如果库存处于高位,说明市场供大于求,供给的力量大于需求的力量,未来价格看跌。
仓单是库存的一个实时的指标,具体代表的是交易所的交割仓库入库现货后开具的标准仓单,它反映的是交易所公布的库存数量。仓单既可以注册也可以注销,当期货主力想要价格上涨时,会把持有的注册仓单注销掉,改变交易所公布的库存数量,来达到交割货物不足的假象,进而影响期货价格上涨的预期。当期货主力想要价格下跌时,会注册仓单,造成交割货物增多的假象,使得被动影响期货价格下跌。所以根据这个原理我们可以反推出在期货中的交易方向。
- 期货多头:如果仓单大量减少,说明期货价格低于现货价格,应该做多。
- 期货空头:如果仓单大量增加,说明期货价格高于现货价格,应该做空。
第二种我们使用基差因子。同样一个商品品种,在现货市场与期货市场的价格差,叫做基差。如果期货价格大于现货价格,我们称之为期货升水;如果期货价格小于现货价格,我们称之为期货贴水。无论是升水还是贴水,随着交割日期的临近,现货价格与期货价格都会趋于一致。所以我们可以做多贴水最多的合约,做空升水最高的合约,来实现现货价格和期货价格的一致。
前面的课程呢,当我们计算出来因子,是直接拿过来使用的,对于因子的处理是比较简单的,实际上一个实时动态的因子可能由于突发的情况或者趋势的短暂回调,出现小额的波动,而这种小额的波动可能造成交易趋势判断的假性信号,所以我们对不同类型的因子,需要更精细的思考和处理。
本节课,我们就以两种基本面因子作为示例,讲解一下对不同因子的处理方法,并使用前面两节课讲过的因子有效性检验方法,验证下不同因子处理方法的有效性。
仓单因子处理
第一个我们要处理的是仓单因子。这是我们从公共数据源获取的,包括8个比较活跃品种,从2020年11月到今年11月三年的仓单数据。因为不同品种存在季节效应,比如玻璃期货品种的金九银十的补库,会造成仓单短暂的大量减少,我们需要考虑这些季节性的变化。相对于直接使用仓单的每日变化作为因子直接放入模型中,我们尝试去对这个仓单数据进行一下特征的挖掘和构建。
这里我们参考专业的研报,使用这个公式进行仓单年同比增长率因子的构建。

\(C_i\)表示第 i 天的标准仓单数量,所以这个公式的分子是t-J到t日的平均仓单数量。
而分母是 t- 306 日到t-180日平均仓单数量。这里需要解释这些数字怎么来的,我们想要衡量的对象其实是仓单的同比增长率,而一年的交易天数是243,一个季度的交易天数是63,一年向前一个季度,是243加上63等于306,一年向后一个季度,是243减去63等于180,而180到306一共是126天,然后取平均。这样处理的年同比增长率能够降低季节性带来的影响。
另外,这里的参数是j,统计j日内的平均仓单,这里我们根据不同j值的变化导致的因子有效性检验的结果的区别,挑选出来最合适的j值。
当这个年同比仓单增长率同比增长较大时,我们进行做空,反之进行做多。所以这是一个反向的指标。
我们在代码中进行一下实现。有些同学经常会询问怎么在策略当中使用外部的数据,其实很简单,将回测系统使用的服务器布置在本地就可以。我们在本地布置好托管者以后,在模拟回测的界面,这里分发选择我们本地布置的托管者。然后在我们的策略当中,就可以访问本地的数据进行策略的编写。但是这里的python路径要设置对应哈,不然会访问不到对应文件里的数据。
python
'''backtest
start: 2022-02-23 09:00:00
end: 2023-11-18 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["holdPeriod",1]]
'''
import json
import time
import numpy as np
from scipy.stats import rankdata
# 导入仓单数据
receiptDf = pd.read_csv('receiptDf.csv')
# 仓单数据处理
# 计算基准(日级别仓单变化率)
receiptDf['wr_day'] = np.where((receiptDf['receipt'] == receiptDf['receipt_chg']) & (receiptDf['receipt'] != 0), 1,
np.where((receiptDf['receipt'] == receiptDf['receipt_chg']) & (receiptDf['receipt'] == 0), 0,
receiptDf['receipt_chg'] / (receiptDf['receipt'] - receiptDf['receipt_chg'])))
# 计算分母
# 创建一个空列表用于存储滑动均值
rolling_averages = []
# 遍历每个 'var' 分组
for var_group, var_group_df in receiptDf.groupby('Instrument'):
# 遍历每一天
for i in range(len(var_group_df)):
# 计算当前日期前 -306 到 -180 范围内的滑动均值
if i >= 306:
rolling_average = var_group_df['receipt'].iloc[i - 306: i - 180].mean()
else:
rolling_average = None # 对于前306天内的数据,无法计算范围内的滑动均值
rolling_averages.append(rolling_average)
receiptDf['rolling_averages'] = rolling_averages
# 计算因子
jList = [5, 22, 63, 126,243]
for j in jList:
rolling_window = receiptDf.groupby('Instrument')['receipt'].rolling(window=j, min_periods=j).mean().reset_index(level=0, drop=True)
receiptDf['wr_' + str(j)] = rolling_window / receiptDf['rolling_averages']
# 删除缺失值
receiptDf = receiptDf.dropna()
# 定义获取当日基本面数据函数
from datetime import datetime
def getReceiptData():
nowDate = int(datetime.strptime(_D()[0:10], "%Y-%m-%d").strftime("%Y%m%d"))
nowBasis = receiptDf[receiptDf.date == nowDate][['Instrument', 'wr_day', 'wr_5', 'wr_22', 'wr_63', 'wr_126', 'wr_243']]
return nowBasis
ext.getReceiptData = getReceiptData
# 因子有效性检验
def main():
# 记录上次执行的时间
last_operation_timeBig = 0
isFirst = True
# Initialize factor_df
factor_df = pd.DataFrame(columns=["Instrument", "InstrumentId", "Time", "Open", "High", "Low", "Close", "Volume", 'wr_day', 'wr_5', 'wr_22', 'wr_63', 'wr_126', 'wr_243'])
while len(factor_df) < 423:
current_timeBig = int(time.time())
# 获取时间间隔(秒)
if isFirst:
time_intervalBig = 24 * 60 * 60 * holdPeriod
isFirst = False
else:
time_intervalBig = current_timeBig - last_operation_timeBig
# 执行多空组买卖之前的操作(每隔持仓周期执行一次)
if time_intervalBig >= 24 * 60 * 60 * holdPeriod:
# 获取k线
rData = ext.getMainData(PERIOD_D1, ext.getTarList()[0])
rData.Instrument = rData.Instrument.str.upper()
# 获取基本面数据
resReceipt = ext.getReceiptData()
new_df = pd.merge(rData, resReceipt, on='Instrument', how='inner')
factor_df = pd.concat([factor_df, new_df], ignore_index=True)
last_operation_timeBig = current_timeBig
Sleep(60*60*2)
factor_df['Returns'] = factor_df.groupby('Instrument')['Close'].pct_change()
factor_df.Returns = factor_df.Returns.apply(lambda x: (x - factor_df.Returns.mean()) / factor_df.Returns.std())
factor_df['Returns'] = factor_df['Returns'].shift(-8)
factor_df[['wr_day', 'wr_5', 'wr_22', 'wr_63', 'wr_126', 'wr_243']] = factor_df[['wr_day', 'wr_5', 'wr_22', 'wr_63', 'wr_126', 'wr_243']].shift(8) # 仓单数据前移
factor_df = factor_df.dropna()
# 因子有效性检测
# ...
首先我们导入数据,第一步我们来计算初始的每日仓单变化率,作为基准比较不同j值下的仓单年同比增长率的表现差异。
这里有三种情况,第一种,昨日的仓单数量为0,今日的仓单数量完全是仓单的变化数量,定义这个值为1;第二种,昨日和今日的仓单数量都是0,定为为0;第三种,最常见,昨天和今天的仓单数量都不是0,使用仓单变化率除以昨天的仓单总量就可以。
接下来我们来计算年同比增长率,这里首先我们计算的是分母,由于各个品种的仓单量级可能存在差异,这里按照不同的合约分组进行处理,计算当前日期前 -306 到 -180 范围内的滑动均值。
下面我们处理分子的部分,。这里将备选的j值作为一个列表,定义为5(每周),22(每月),63(每季度),126(每半年),和243(每年)。然后使用公式进行计算不同j值的年同比增长率,作为不同的因子。
然后删除缺失值。
最后我们定义一个函数,定义获取当日仓单数据函数。这样在主函数当中就可以获取到当前策略日期对应的仓单因子了。
接下来,我们使用先前讲过的因子有效性检验的t检验,rankic,和分层回测法,对日仓单变量,和不同j值的年度仓单变化量进行检验。
在我们进行基本面因子有效性检验的时候,需要注意的一点是,未来函数。基本面数据一般都是在收盘过后公布的。比如是9点开盘,而今日的基本面数据是下午6点公布的。所以我们如果使用今日的基本面因子判断出现交易信号,实际上是提前知道了今日的基本面信息。所以我们在进行因子有效性检验的时候,需要对基本面数据进行前移。
factor_df[['wr_day', 'wr_5', 'wr_22', 'wr_63', 'wr_126', 'wr_243']] = factor_df[['wr_day', 'wr_5', 'wr_22', 'wr_63', 'wr_126', 'wr_243']].shift(8)
除去不同的仓单因子之外,这里还有一个可调的的参数,是持仓的天数,这样就可以使用不同的仓单因子和持仓天数的组合,挑选出来目标品种最优的参数配置。大家可以尝试一下。
基差因子处理
接下来我们进行基差因子的处理。相对于以往我们直接使用基差的变化率作为因子直接导入模型。这里我们将因子转换为动量因子,通过比较基差在不同时间点的变化来衡量期货市场中基差的趋势。
其实动量因子在多因子模型中使用的方面有很多。动量因子是量化投资中常用的一种技术指标,用于衡量因子指标在一定时间内的趋势或变动方向。动量因子的核心思想是过去的指标趋势可能在未来一段时间内继续,即走势有一定的延续性。通常,动量因子可通过计算一定时间窗口内的因子数值变动来衡量。
动量因子的基本计算公式为:

在这个公式中,"值"可以是价格、成交量、收益率等。动量因子的值为正时,表示因子的趋势向上;为负时,表示因子的趋势向下。动量因子的大小可用于衡量趋势的强度。
基差动量因子是动量因子的一种变体,专门用于期货市场。基差是指现货价格与期货价格之间的差异,而基差动量因子通过比较基差在不同时间点的变化来衡量期货市场中基差的趋势。基差动量因子同样具有正负和大小的特征,可以帮助投资者和交易者识别期货市场中基差趋势的方向和强度。这种因子的使用也受到市场供需、季节性因素和其他宏观经济因素的影响。这里我们也设置一个参数k,表示不同周期的动量因子值,并使用原始的基差,和不同参数k下的基差动量因子,检验不同因子的有效性。
python
'''backtest
start: 2020-11-18 09:00:00
end: 2023-11-18 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["holdPeriod",1]]
'''
import json
import time
import numpy as np
from scipy.stats import rankdata
# 导入基差数据
basis = pd.read_csv('basis.csv')
basis = basis.sort_values(by=['Instrument', 'date']) # 分组排序
basis = basis.fillna(method='ffill') # 处理缺失值
# 基差数据处理
# 计算基准(日级别基差变化率)
basis['basisrate'] = (basis.spot_price - basis.dominant_contract_price) / basis.spot_price
# 计算基差动量
# 创建一个空列表用于存储滑动均值
rolling_averages = []
# 设置不同参数K
kList = [5, 22, 63, 126,243]
for k in kList:
rolling_window = basis.groupby('Instrument')['basisrate'].rolling(window=k, min_periods=k).mean().reset_index(level=0, drop=True)
basis['br_' + str(k)] = (basis.basisrate - rolling_window) / rolling_window
# 删除缺失值
basis = basis.dropna()
# 计算基差动量
# 创建一个空列表用于存储滑动均值
rolling_averages = []
# 设置不同参数K
kList = [5, 22, 63, 126,243]
for k in kList:
rolling_window = basis.groupby('Instrument')['basisrate'].rolling(window=k, min_periods=k).mean().reset_index(level=0, drop=True)
basis['br_' + str(k)] = (basis.basisrate - rolling_window) / rolling_window
# 删除缺失值
basis = basis.dropna()
# 定义获取当日基本面数据函数
from datetime import datetime
def getBasisData():
nowDate = int(datetime.strptime(_D()[0:10], "%Y-%m-%d").strftime("%Y%m%d"))
nowBasis = basis[basis.date == nowDate][['Instrument','basisrate', 'br_5', 'br_22', 'br_63', 'br_126', 'br_243']]
return nowBasis
ext.getBasisData = getBasisData
def main():
# 记录上次执行的时间
last_operation_timeBig = 0
isFirst = True
# Initialize factor_df
factor_df = pd.DataFrame(columns=["Instrument", "InstrumentId", "Time", "Open", "High", "Low", "Close", "Volume", 'basisrate', 'br_5', 'br_22', 'br_63', 'br_126', 'br_243'])
while len(factor_df) < len(basis):
current_timeBig = int(time.time())
# 获取时间间隔(秒)
if isFirst:
time_intervalBig = 24 * 60 * 60 * holdPeriod
isFirst = False
else:
time_intervalBig = current_timeBig - last_operation_timeBig
# 执行多空组买卖之前的操作(每隔持仓周期执行一次)
if time_intervalBig >= 24 * 60 * 60 * holdPeriod:
# 获取k线
rData = ext.getMainData(PERIOD_D1, ext.getTarList()[0])
rData.Instrument = rData.Instrument.str.upper()
# 获取基本面数据
resBasis = ext.getBasisData()
new_df = pd.merge(rData, resBasis, on='Instrument', how='inner')
factor_df = pd.concat([factor_df, new_df], ignore_index=True)
last_operation_timeBig = current_timeBig
Sleep(60*60*2)
factor_df['Returns'] = factor_df.groupby('Instrument')['Close'].pct_change()
factor_df.Returns = factor_df.Returns.apply(lambda x: (x - factor_df.Returns.mean()) / factor_df.Returns.std())
factor_df['Returns'] = factor_df['Returns'].shift(-8)
factor_df[['basisrate', 'br_5', 'br_22', 'br_63', 'br_126', 'br_243']] = factor_df[['basisrate', 'br_5', 'br_22', 'br_63', 'br_126', 'br_243']].shift(8) # 基差数据前移
factor_df = factor_df.dropna()
# 提取因子和目标变量
factors = factor_df[['basisrate', 'br_5', 'br_22', 'br_63', 'br_126', 'br_243']]
factors = factors.apply(lambda col: (col - col.mean()) / col.std(), axis=0)
returns = factor_df['Returns']
在代码中,同样导入本地的数据,计算因子值。这里的数据具有缺失值,我们使用前向填充的方式,就是使用缺失值的前一个非缺失值来填充缺失值。基差是使用现货价格减去期货价格,然后再除以现货价格就是基差率,作为我们的基准指标。然后不同级别的基差动量因子,是当前的基差率减去一段时间内的滑动均值。这里我们首先计算过去时间段内的滑动均值,这里,参数k和仓单里的数值设置一样,同样使用5,22,63,126和243。使用当前的基差率,减去滑动均值,然后再除以滑动均值,就是基差的不同周期的动量因子。这样我们的基差动量因子就计算完成,接下来就可以对不同周期的基差因子进行有效性检验。
本节课呢,我们尝试使用外部的数据,也就是基本面因子导入多因子模型中。这里的关键还是对因子的处理,在数据分析科学中,特征工程,也就是因子处理,确实是一门高深的学问,除了高超的数据分析能力,还需要专业的知识去进行因子信息更有效的挖掘。本节课只是一个较为浅显的尝试,希望大家再此基础上,可以利用不同的基本面数据进行更多有趣的尝试。
视频参考链接:
31.量化交易模型中的因子处理
在上一节课中,我们介绍了单因子处理技术,但更多地关注于特征挖掘的部分。在获取并检验完因子有效性后,我们需要对每个因子进行进一步的处理。这包括处理缺失值、去除极端值、进行标准化、正交处理等。本节课详细展示了这些方法,以便大家更好地了解因子处理的细节和操作。
首先,我们介绍一下因子处理的常用技术和具体的实际意义。
缺失值处理:处理因子中的缺失值,可以采用填充技术,如均值、中位数或者利用其他因子的信息进行填充。当然,如果我们样本数量足够的话,另一种策略是删除包含缺失值的样本或因子。
极值处理:多因子可能受到异常值或极端值的影响,这可能导致模型的不稳定性和偏差。通过极值处理,我们可以使因子在一定范围内变化,提高模型对异常值的鲁棒性,使其更具有泛化能力。
标准化:多因子往往具有不同的量纲和方差,这可能使得一些因子对模型的贡献程度过大,而其他因子的贡献被相对忽略。标准化将因子的值转化为均值为0,标准差为1的标准正态分布,有助于比较不同因子的影响,并确保它们在模型中被平等对待。
共线性问题: 当多因子之间存在高度相关性时,模型可能会出现多重共线性问题,导致估计系数不准确,难以解释,以及模型不稳定。通过正交处理,可以将原始因子重新组合成一组新的因子,使得它们之间的相关性降低,减少多重共线性的影响。正交处理有助于提高因子之间的独立性,使得每个因子更加独立地解释模型的变化。这样可以更清晰地理解每个因子对模型的影响,提高模型的解释性。
总体而言,这些预处理步骤有助于提高多因子模型的稳定性、鲁棒性,减少异常值和共线性的影响,使模型更适用于真实的市场数据。
这节课我们选取在之前课程中表现较为突出的 4 个因子,分别是展期收益率,峰度,63日的基差动量,和日级别的仓单变化率,并加上价格和成交量的五日动量因子,这两个因子经过测试,也具有良好的因子有效性。本节课呢,我们将使用这六个准因子,作为因子处理的试验性展示,帮助大家理解因子处理技术,为后续的因子合成打下基础。
前面有些同学反应,在网页版的优宽进行数据的实时调试不太方便,本节课呢,我们将使用优宽的本地回测引擎,也可以很方便的获取和网页端一样的数据,并进行因子的检验和处理。
这里我们使用jupyter notebook,可以实时的查看数据的处理结果。在本地回测引擎,可以获得和网页端一样的数据,这里使用我们前面几节课编写好的函数,计算出来我们需要的因子数据,然后导出。接下来我们就可以利用收集完成的因子数据进行因子的处理。
关于因子处理的具体步骤,这里列好提纲。首先导入我们合成好的因子列表,我们打印数据看下,是从2022年的2月到今年的11月份,八个品种的6个因子的初始数据。我们的特征,就是这六个因子,我们的目标变量就是这个收益率。接下来,我们就要对这六个因子进行相应的处理。
python
import pandas as pd
factor_df = pd.read_csv('准因子.csv', index_col=0).reset_index(drop=True)
factor_df
在因子处理之前,我们首先看下各个因子的统计特征。这里我们定义获取各个因子的均值,标准差,最大值和最小值。可以看到这里有一些极值,我们后续需要处理一下。
python
features = factor_df.columns[2:]
# Calculate the required statistics for each feature
feature_stats = factor_df[features].agg(['mean', 'std', 'min', 'max'])
# Display the resulting DataFrame
print(feature_stats)
第一步是缺失值的处理,我们检查下各列的缺失值数量。这里的缺失值数据我们在因子计算中处理过,所以不存在缺失值。这一步骤我们可以省略。
python
factor_df.isnull().sum()
第二步,来去除极值。通常使用的办法是3倍绝对中位差法。3倍绝对中位差法是一种确定数据分布离散程度的方法,用于检测异常值。其基本思想是:如果数据集中有一个异常值,那么它将会使得整个数据集的波动性(即标准差)变大。因此,我们可以通过计算中位数与每个数据点之间的差的绝对值,来衡量数据的波动性,然后找到那些超过3倍中位绝对差的点,这些点可能就是异常值,我们将这些异常值进行替换。
python
def replace_outliers(series, factor=3.0):
median = series.median()
mad = series.mad()
upper_bound = median + factor * mad
lower_bound = median - factor * mad
return series.clip(lower=lower_bound, upper=upper_bound)
factor_df[features] = factor_df[features].apply(replace_outliers)
实现起来也并不复杂,编写一个函数,计算输入序列的中位数和平均绝对偏差,接着根据中位数和MAD计算上下界。最后使用clip上下界来剪裁序列中的值。超出上界的值被替换为上界,低于下界的值被替换为下界。
第三步,我们来进行标准化,这里我们可以应用sklearn的StandardScaler函数,可以方便的进行数据的正态标准化。
python
from sklearn.preprocessing import StandardScaler
# 使用StandardScaler进行标准化
scaler = StandardScaler()
factor_df[features]= scaler.fit_transform(factor_df[features])
最后,我们来进行因子的正交化处理。因子正交化有多种方式,目前市面上常用的有如下4种:回归取残差、Schmidt正交化、规范(Canonical)正交化、对称(Symmetric)正交化。这里面涉及到比较复杂的数理知识,这里给大家稍微解释一下。回归取残差这个方法涉及对其他因子进行回归,然后使用残差作为正交化后的因子。Schmidt 正交化可以把它看作一个逐步的过程,对每个因子进行操作,减去它在已经正交化的因子上的投影。结果就是一组正交的因子。规范(Canonical)正交化涉及将原始因子进行线性变换,转化为一组规范变量。然后在这些规范变量上进行正交化。对称(Symmetric)正交化涉及一种转化,旨在最大程度地保留信息的同时,使得正交后的因子的方差最大。
至于哪一种正交化的方法比较好,这里推荐的是对称正交化;它有很多良好的性质,比如它的前后因子的一一对应关系稳定,并且与因子的正交顺序无关,并且它是所有正交方法中,使得旋转前后因子间的距离最小的正交化方法,这就保证了正交化前后因子的相似性依然很高,信息损失小。
我们这里呢,就来实现一下对称正交化。首先,我们计算这六个因子之间的相关,可以看到,由于我们采用不同类型的特征,所以因子之间的相关性并不是很高,唯一比较高的两组变量是价格动量和基差的63日动量,相关系数为-0.13。如果数字不太敏感的话,我们可以画一下相关热力图。
python
# 计算相关矩阵
feature_df = factor_df[['RollOver', 'Kurt', 'br_63', 'wr_day', 'volumeMom','priceMom']]
correlation_matrix = feature_df.corr()
import seaborn as sns
import matplotlib.pyplot as plt
# Plotting the heatmap
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
plt.title('Correlation Heatmap')
plt.show()
下面呢,我们使用对称正交的方法演示一下,进行因子的正交处理。在这个函数里:
np.cov(factors_standardize.T.astype(float)) 首先计算了这些因子的协方差矩阵 (M)。
D, U = np.linalg.eig(M) 计算了协方差矩阵的特征值 (D) 和特征向量 (U)。特征值代表不同方向上的变化幅度,而特征向量表示相应的方向。
d = np.mat(np.diag(D**(-0.5))) 创建了一个对角矩阵 (d),其中每个特征值被取了-0.5次方。这实际上是在进行特征值的倒数运算。这种操作有助于调整正交化过程中的尺度,确保每个方向的变化幅度对最终结果的影响是均等的。
S = U * d * U.T 将归一化后的特征向量组合成正交化矩阵 (S)。这个矩阵 S 将用于将标准化后的因子转换为一组正交的(不相关的)因子。
factors_orthogonal_mat = np.mat(factors_standardize) * S 将正交化矩阵应用于标准化后的因子。
当然,这里的具体数学知识大家不用过于深究,我们可以定义成为函数的形式,在模型中直接使用。
python
import numpy as np
def orthogonalize_factors(factors_standardize):
# 计算协方差矩阵
M = (factors_standardize.shape[0] - 1) * np.cov(factors_standardize.T.astype(float))
# 特征值分解
D, U = np.linalg.eig(M)
# 转换为矩阵
U = np.mat(U)
# 对特征值取倒数并开方
d = np.mat(np.diag(D**(-0.5)))
# 获取过度矩阵S
S = U * d * U.T
# 获取对称正交矩阵
factors_orthogonal_mat = np.mat(factors_standardize) * S
# 转为DataFrame
factors_orthogonal = pd.DataFrame(factors_orthogonal_mat, columns=factors_standardize.columns, index=factors_standardize.index)
return factors_orthogonal
features_orthogonal = orthogonalize_factors(feature_df)
correlation_matrix = features_orthogonal.corr()
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
plt.title('Correlation Heatmap')
plt.show()
将我们的因子df放入函数中进行正交处理,然后将正交处理完成的df进行相关分析,可以看到各个因子的相关性降到了最低。
怎样检验因子的信息是否丢失呢,我们可以使用一种比较粗糙的办法。使用各因子作为自变量对因变量收益率进行回归分析,查看回归模型的显著水平和各因子的显著水平。如果正交后因子的显著性水平和整体回归模型的显著性水平与正交前相似,说明正交后因子保留了与因变量相关的信息。
python
## 正交前因子回归
import statsmodels.api as sm
independent_vars = feature_df[['RollOver', 'Kurt', 'br_63', 'wr_day', 'volumeMom', 'priceMom']]
dependent_var = factor_df['Returns']
independent_vars = sm.add_constant(independent_vars)
model = sm.OLS(dependent_var, independent_vars)
results = model.fit()
print(results.summary())
## 正交后因子回归
independent_vars = features_orthogonal
dependent_var = factor_df['Returns']
independent_vars = sm.add_constant(independent_vars)
model = sm.OLS(dependent_var, independent_vars)
results = model.fit()
print(results.summary())
这里我们建立两个回归方程,将原始的因子作为自变量,收益率作为因变量进行回归分析。打印回归结果发现,这个模型是显著的,F值小于0.05。这里给大家解释一下,回归模型的显著性通常通过F统计量(F值)来评估。F统计量是用于检验回归方程整体显著性的统计指标。在回归分析中,F统计量的计算涉及解释的方差和残差的方差之比。如果F统计量的p-value(显著性水平)小于设定的显著性水平(通常是0.05),我们可以拒绝零假设,认为整个回归模型是显著的。
我们查看回归分析的系数,结果发现交易量动量处于显著的水平。下面我们做一下正交后的因子作为自变量的回归检验,结果发现模型的显著性,小于0.05,还有各个因子的p值只有微小的差别。由此可以证明正交过后的因子保留了足够的信息。
以上呢,就是因子处理的一些基本方法。当然,我们的展示都是比较浅显的,对于真正的因子处理,需要进行更多的数理统计的分析,希望我们的课程可以带给大家一些启发。另外还有一些成熟的包帮助进行因子的处理,比如alphalens,大家有兴趣也可以尝试一下。下节课,我们将讲述因子合成,我们下节课再见。
32.多因子模型中的因子合成
在前面的课程中,我们完成了因子数据的获取、有效性检验以及因子的预处理工作。现在,我们迈向因子处理的最后一个环节,就是利用经过清洗的因子数据进行因子合成,帮助进行交易信号的判断。
在多因子模型中,因子合成是指将多个单独的因子组合成一个或多个综合因子的过程。这个过程的目的是创建一个更为综合、稳健、能够更好解释市场变化的因子组合分数,从而用于更准确的投资决策。
因子合成是构建多因子模型的关键步骤,其目的在于从多个因子中提取出关键信息。经过前期的工作,我们已经初步确定了因子数据的质量和可靠性。接下来,我们将运用这些因子数据,结合我们的交易策略和目标,进行因子合成,以进一步优化模型的性能。
在进行因子合成时,我们将考虑各个因子之间的相关性,权衡它们对最终合成因子分数的贡献。这个过程涉及到统计方法和机器学习技术,以确保我们选择的因子能够有效地捕捉市场的特定信号,从而提高多因子模型的预测能力。
我们首先介绍因子合成方法的理论基础。常见的方法有等权法、历史IC(半衰)加权法、历史收益率/IC加权法、最大化IC/IC_IR加权法、主成分分析(PCA)法。对于不同相关性及不同类别的因子,可考虑选用不同加权方式进行合成。
我们大致介绍下每种方法的理论基础。
-
等权法:这个最为简单,将所有待合成因子等权重相加,得到新的合成后因子。但是各个因子对于收益率的贡献并不是相同的,所以这种方法对于因子的权重划分过于简单。
-
历史因子收益率/IC加权法:这个其实是两种方法,只不过都使用到了历史的数据。收益率是使用回归模型中各个因子的回归系数,而IC是RankIC相关值。这个是将所有待合成因子,按照最近一段时期内历史因子收益率或者IC的算术平均值作为权重进行相加,得到新的合成后因子。这种方法表现好的因子权重更高。另外,关于历史因子收益率/IC,除了使用历史均值的方法,也可以使用半衰加权法进行各因子权重的处理。半衰加权每一期历史收益率或者RankIC的权重不同,将按照指数半衰权重进行加权。半衰加权的基本原则是距离现在越近的截面期权重越大、越远权重越小。这里存在一个参数——半衰期H,其意义为每经过H期(向过去前推H期),权重变为原来的一半,半衰期参数可取1,2,4等。
-
动态因子收益率/IC加权法:与历史相对的呢,就是动态因子收益率或者IC加权法,这里通过计算一段时间窗口周期内的因子收益率或者IC值,动态的给各个因子进行权重赋值。这样呢,可以提高因子权重赋值的动态有效性。
-
最大化IC_IR加权法:它是以历史一段时间的复合因子平均IC值作为对复合因子下一期IC值的估计,以历史IC值的协方差矩阵作为对复合因下一期波动率的估计,根据IC_IR等于IC的期望值除以IC的标准差,可以得到最大化复合因子IC_IR的最优权重解。这种方法涉及到比较复杂的数理知识,大家有兴趣可以研究一下。
-
主成分分析(PCA)法:PCA是数据降维的常用方法。PCA将一组相关性较高的n维数据投影到新的k维坐标上(N < k),这k维特征称为主成分,这些主成分之间是不相关的,以达到数据降维的目的。因子之间的相关性可能比较高,使用降维后的主成分作为合成后的因子。PCA法与前面所有方法的不同点在于,PCA只关注因子值矩阵本身的性质,与因子收益、因子IC等无关。从理论上来讲,它的优势在于,如果因子值矩阵的性质在不同截面期比较稳定,合成后因子稳定性比较好;它的劣势在于,由于合成过程不涉及因子收益等信息,合成后因子不具备明显的经济学含义,其在未来表现也不一定会优于待合成的几个单因子。
需要注意的是,在合成过程中,选用不同历史窗口期及不同因子合成数目,对合成效果都会有影响。
历史因子收益率加权法
首先我们介绍历史因子收益率加权法,这种方法通过线性回归结合历史因子收益率进行加权,从而预测未来期货品种收益率。具体步骤如下:
- 回归分析获取因子收益率:
- 在每个交易日\(t\),使用 period 个交易日前的因子值(因子暴露)作为自变量,期货品种收益率作为因变量进行带截距项的 OLS 回归。
\(R_t = \lambda_{t}C_{t} + \beta_{t-1}\lambda_{t-1} + \alpha_{t}\) - 获取当天的各个因子收益率\(\lambda_{t}\)。
- 累加因子收益率取平均:
- 通过滑动窗口周期的,获得不同时间段的因子收益率,然后累加各个因子的收益率取平均。
- 但是累加收益率的时候,我们可以考虑因子收益率有效性的约束条件。使用回归判断如果因子对应的系数p值小于0.1,证明因子对因变量有着较为显著的作用,那么累加这个因子的收益率,反之,如果因子对应的p值大于0.1,证明该因子在该阶段是无效的,那么将该因子的预期收益率定义为0,防止无效信息的干扰。
- 估计未来期望因子收益率:
- 使用得到的各个因子平均收益率 \(\lambda_{t}\),和今天的因子数值进行相乘,获取因子组合数值。
这种方法利用历史因子收益率对未来收益率进行预测,并结合了因子收益率有效性的约束条件,用来构建多空组合策略。
python
def history_linear_composite(df, window_size, step_size, selFeatures):
composite_factors = []
cumulative_coefficients = np.zeros(len(selFeatures)) # 因子收益率累加列表
cumulative_count = 0 # 次数统计
for i in range(window_size, len(df), step_size):
window_data = df.iloc[(i - window_size):i, :]
X = window_data[selFeatures]
y = window_data['Returns']
X = sm.add_constant(X)
model = sm.OLS(y, X).fit()
coefficients = model.params[1:]
p_values = model.pvalues[1:]
coefficients = np.where(p_values < 0.1, coefficients, 0)
cumulative_coefficients += coefficients
cumulative_count += 1
coefficientsMean = cumulative_coefficients / cumulative_count # 计算均值
for k in range(i, i + step_size):
composite_factor_value = np.dot(df.loc[k, selFeatures], coefficientsMean)
composite_factors.append(composite_factor_value)
return composite_factors
我们来看下代码实现。我们接着上节课的代码部分,在前期因子处理完成的基础上,我们进行使用历史因子收益率加权法的代码编写。
在通常情况下,我们并不使用所有的历史数据进行因子收益率的计算,而是通过滑动窗口周期的移动,计算窗口周期内因子的收益率系数进行累加,然后获取收益率历史均值。
这里定义线性组合函数history_linear_composite函数接受四个参数:df是包含因子和收益率的DataFrame,window_size是滑动窗口的大小,step_size 是滑动窗口的步长,selFeatures 是用于线性组合的因子的列名。
然后我们定义三个变量,分别是合成因子列表composite_factors,cumulative_coefficients因子收益率累加列表,和cumulative_count次数统计。
接着来定义滑动窗口循环,通过循环遍历数据集,每次取出一个大小为window_size 的滑动窗口,步长为 step_size。
下面就要提取滑动窗口数据,对于当前滑动窗口,从数据集df中提取自变量(X)和因变量(y),其中自变量是selFeatures中指定的因子,而因变量是收益率('Returns')。接着使用statsmodels库进行带有截距项的线性回归,得到回归系数(coefficients)以及每个系数的 p 值。
这里我们加一个约束条件,将回归系数按照对应的 p 值进行筛选,如果 p 值小于 0.1,证明该因子是有效的,那么保留系数,否则将系数置为 0。
然后我们进行因子收益率的累加,在除以累加的次数,就是各个因子的收益率均值。
各个因子的平均收益率系数计算完毕以后,我们就要来进行因子合成。对于未来一个步长周期内的每个数据点,计算组合因子值,就是将步长周期中的因子值,与历史因子收益率均值相乘并相加,这里涉及到了矩阵的算法,使用np.dot点乘可以实现,这样就可以得到未来一个步长窗口内的合成因子。通过不断使用轮询计算每一个步长周期的合成因子。这样各个时间点的合成因子我们就计算完成。
最后将所有计算得到的组合因子值存储在列表composite_factors中,并返回该列表。
函数定义完成以后,我们来进行因子合成的示范,这里通过定义好的窗口大小和步长,调用函数计算组合因子,并将计算结果存储在原始 df当中新的一列。需要注意的是,第一个滑动窗口周期内是不存在合成因子的,我们这里填补为空值。
下面我们来验证一下合成因子的有效性,这里我们通过合成因子和收益率之间的相关初步验证下,结果发现参数为22日,也就是一个月的合成因子与收益率的相关性较高,为0.03左右。按照历史收益率的均值,其实越接近后期的因子合成数值,应该越稳健,我们可以调取最后一个季度内的数据,进行相关分析。结果发现,相关系数达到0.05,确定有了一定的提升。
下面我们使用t检验验证下,结果发现t检验是显著的,合成因子对收益率具有显著的正向预测作用。这就是使用历史因子收益率加权法进行因子合成的演示,大家可以更换不同的参数和品种试验一下。另外,对于函数中因子的收益率系数,我们也可以使用半筛的方法进行不同权重的合成。
动态因子收益率加权法
python
def dynamic_linear_composite(df, window_size, step_size, selFeatures):
composite_factors = []
for i in range(window_size, len(df), step_size):
window_data = df.iloc[(i - window_size):i, :]
X = window_data[selFeatures]
y = window_data['Returns']
X = sm.add_constant(X)
model = sm.OLS(y, X).fit()
coefficients = model.params[1:]
p_values = model.pvalues[1:]
coefficients = np.where(p_values < 0.1, coefficients, 0)
for k in range(i, i + step_size):
composite_factor_value = np.dot(df.loc[k, selFeatures], coefficients)
composite_factors.append(composite_factor_value)
return composite_factors
这个其实和历史收益率计算的思路是一样的,只是这里不需要累加收益率进行取平均,直接使用最新的窗口周期内的数据进行回归,取因子的收益率。这里同样加一个约束条件,判断因子收益率p值是否小于0.1,证明该因子的有效性。最后使用因子的收益率和最新滑动周期内的因子相乘,就是合成的因子值。
我们来验证下合成因子的有效性,结果这里发现,使用63周期,也就是一个季度,合成因子和因变量收益相关性较高,达到0.048。另外,我们也可以使用t检验验证下,结果发现t检验是显著的,合成因子对收益率具有显著的正向预测作用。。
PCA方法
下面我们来看PCA因子合成的实现。PCA是将所有的因子进行主成分分析,挑选出来可以显著解释方差的主成分。由于待合成因子数量不会很多,一般第一主成分可以认为有较高的解释度,我们的展示也只取第一主成分作为合成后因子。根据推导过程,每个主成分都是原始因子的线性组合,因此PCA法合成因子的过程也会对应于一组因子权重。PCA在一般的编程软件上都有内置函数可以直接完成计算,我们不需要自己编写代码。我们来看下代码实现。
python
from sklearn.decomposition import PCA
def pca_composite(df, window_size, step_size, selFeatures):
composite_factors = [np.nan] * (window_size - step_size)
for i in range(window_size, len(df) + step_size, step_size):
window_data = df.iloc[(i - window_size):i, :]
X = window_data[selFeatures]
# 创建一个PCA对象,并指定要保留的主成分数量
pca = PCA(n_components=1)
# 对数据进行PCA变换
transformed_data = pca.fit_transform(X)
pcaValue = [val for sublist in transformed_data[-step_size:] for val in sublist]
for num in pcaValue:
composite_factors.append(num)
return composite_factors
首先调用sklearn的PCA函数,方便下面在函数内进行主成分分析。这个函数同样的为了因子的时效性,使用滑动窗口的形式,对滑动周期内的因子进行主成分分析,然后获取最新步长周期内的的PCA转换值作为最新的合成因子。
在函数体内,首先定义合成因子的列表,因为pca需要一定数据数量才能进行,在列表开头定义一些空值,长度为滑动窗口周期减去步长,接着我们就可以向这个列表添加合成因子。
这里我们选择滑动窗口周期内的因子数值作为X,然后调用内置函数PCA,创建一个PCA对象,并指定要保留的主成分数量是1,然后对数据进行PCA变换,使用pca.fit_transform函数,这里获取到的transformed_data,是一个双层嵌套形式的列表,所以使用这个列表表达式进行解套,然后获取最新的步长周期内的PCA转换因子分数,作为合成因子。这样我们的pca合成因子的函数就定义完成,可以看到,这里只使用到了因子的数据,并没有使用return的数据。
这里我们设置不同的滑动窗口周期参数验证下PCA因子转换的有效性,结果发现同样的使用63窗口周期,合成因子和收益率的相关系数较高,为-0.04,并且使用回归分析,结果发现t值也是显著的,证明合成因子是收益率的一个显著指标,注意这是一个负向指标,所以在多空分组中需要注意下。
以上呢,就是三种因子合成的方法介绍。当然我们的方法都比较简单,大家可以在此基础上,进行进一步的改造。最后一节多因子课程,我们将总结前面的模型,搭建出来一个实盘级别的大模型,大家稍微等待一下。
33.商品期货多因子实盘级策略(一)
大家好,前面的一系列课程我们从商品期货多因子策略概念讲解,单因子模型引入,高阶中心矩类因子计算,因子有效性检查,因子特征工程,因子处理和最后的因子合成,介绍了多因子模型的大概框架,模拟实现了多因子模型的各个环节和功能,今天就来兑现给大家的承诺,组合前面的课程,我们将要搭建一个实盘级别的多因子模型。
在这里需要首先注意的是,这个模型中使用的模型因子是,前期我们回测确定有效性的因子,所以在这个模型中,因子有效性的部分我们需要在实盘模型之外的回测系统加以验证。另外,因子的计算和合成需要在收集够一定因子量的基础上,所以我们首先提供一份历史的因子原始数据,建立一份初始的数据库。这份原始的数据包含3年的历史数据,我们可以采用回测系统对不同类型的因子进行特征工程的处理,然后确定因子的有效性,这样我们使用的因子就确定了下来。然后使用数据增量的方式,每天收集添加新的数据到数据库当中,开始因子的计算和合成,然后确定交易的信号,进行相关的交易操作。所以说呢,这个一个动态调整的模型,我们可以根据因子的有效性和模型的表现实时的调整因子,优化我们的模型。
首先介绍一下我们模型的架构,这里我们将模型分为模版类库和具体的实盘运行策略。模版类库中定义了多因子模型各个环节的方法函数,具体包括因子数据的收集,因子计算,因子处理,因子合成,上述的是因子处理的部分,接下来是交易部分的函数,包括移仓,多空组判断,和多空组买卖。
下面,我们来具体看一下。在模版类库里面,我们首先构建目标合约列表,包含了不同合约的主力月份,设置getTarList获取主力/次主力合约代码;getMainData用来获取合约k线,参数是合约symbol;calCMF计算高阶矩阵因子,参数是合约symbol和回看周期backPeriod,返回结果包括回看周期内五分钟级别收益率的标准差,偏度和峰度;momFactor用来获取动量因子,参数是factorDf因子数据,factor具体的因子,momPeriod动量窗口参数,calRollover计算展期收益率,参数是mainContract, nextContract,代表主力合约和次主力合约;getBasis和getReceipt用来获取基本面数据基差和仓差,参数为具体的日期和合约。这上面的几个函数都是用来获取因子的基本数据的,所以我们将这几个函数进行打包,定义成为getFactor函数,里面进行了各个因子的原始数据的获取,然后进行组合为dataframe的格式,最后进行返回,代表实时更新的因子数据。
因子原始数据获取完成以后,接下来我们进行因子的计算,在计算之前,我们进行缺失值的处理,我们在个别时候并不能获取所有品种的完整天数的数据,所以我们需要处理缺失值,使用前向填充的方法填充缺失值;下面进行基本面因子的计算,包括基差动量和年度仓单变化率。最后我们来处理因变量收益率,将收益率进行前移。这样我们的数据集处理完毕,可以理解为前面的因子数据都是自变量,最后的收益率是因变量。我们每日使用历史的自变量和因变量进行模型的拟合,可以认为是训练集;而最新的当日数据,因变量为空,是我们要测试的样本,可以认为是测试集,这样我们的初始因子计算就完成了。
接着我们来进行因子的处理,这里我们进行的因子处理包括标准化,使用norm_factor函数,参数为具体的因子factor,正交化orthogonalize_factors函数,参数是因子的dataframe,正交化这里选择的方法是信息损失最小的对称正交法,最后对所有的因子变量进行处理,使用proFactor函数。
因子处理完毕,下面一步进行因子的合成。这里我们选择因子合成的方法是历史收益率因子合成法,具体的参数包括因子dataframe,移动窗口周期,步长周期,和经过因子有效性检验的因子列表,在这个函数里我们进行合成因子的计算。定义好因子合成方法函数以后,编写getComposite函数,用来获取合成的因子,最后进行返回。这样我们的因子合成的函数就定义完成。
接下来是交易部分的函数了。具体的包括移仓,我们在前面的课程中有讲到过,当主力合约更新的时候,我们需要进行移仓换月。下面是多空组判断的函数,这里我们在前面课程的基础上,加了一个约束条件。原始的函数是根据合成的因子大小进行排序,取前25%进行开多,后25%进行平空。但是由于特殊的情况,合成的因子可能都为正值或者负值,所以可能只需要一个方向的操作,不需要另一个方向的操作。这里在判断头部组head_group获取合约名称的时候,返回合成因子分数大于0的合约名称;尾部组tail_group也一样,返回合成因子分数小于0的合约名称。
最后我们根据合成因子判断获取的做多或者做空合约的列表,进行多空组买卖了。这里在前面讲过的函数基础上,我们进行了一些细微部分的修改。我们先前的平仓逻辑是,如果某品种不在实时更新的做多或者做空列表的时候,我们直接进行平仓,可是这种交易的逻辑会造成频繁的开平仓。所以我们改变一下平仓的条件,首先获取持多仓和持空仓的列表,然后判断当一个方向的持仓合约,在另一个方向判断需要进行相反仓位的操作的时候,我们再进行平仓,这样就可以减少频繁开平仓的情况。当然这种情况下也会出现弊端,可能会持有一个亏损的仓位很长的时间。所以我们需要进行止损的操作,这里增加一个列表,判断实时出现亏损的仓位,我们直接进行平仓,避免判断失误的合约损失扩大。以上就是多因子模型中需要使用的各种需要的方法函数,可以发现都是我们前些课程讲过的各种方法函数,只是这里我们定义为模版类库的函数格式,这样在具体的实盘中我们就可以直接拿过来使用,减少代码的冗余,更加集中于实盘策略的实现。
python
import pandas as pd
import re
import datetime
import akshare as ak
import json
import time
import numpy as np
import warnings
import math
import statsmodels.api as sm
warnings.filterwarnings("ignore")
# 构建目标合约列表
commodity_contracts = {
# "hc": ['01', '05', '10'], # 热轧卷板
"rb": ['01', '05', '10'], # 螺纹钢
"sp": ['01', '05', '09'], # 纸浆
# "pg": ['01', '05', '09'], # LPG
"FG": ['01', '05', '09'], # 玻璃
"MA": ['01', '05', '09'], # 甲醇
"SA": ['01', '05', '09'], # 纯碱
# "pg": ['01', '05', '09'], # LPG
"TA": ['01', '05', '09'], # PTA
# "UR": ['01', '05', '09'], # 尿素
"eg": ['01', '05', '09'], # 乙二醇
# "eb": ['01', '05', '09'], # 苯乙烯
# "fu": ['01', '05', '09'], # 燃油
# "bu": ['01', '05', '09'], # 沥青
# "l": ['01', '05', '09'], # 聚乙烯
# "SF": ['01', '05', '09'], # 硅铁
# "p": ['01', '05', '09'], # 棕榈油
"pp": ['01', '05', '09'], # 聚丙烯
# "v": ['01', '05', '09'], # 聚氯乙烯
# "i": ['01', '05', '09'] # 铁矿石
}
# 获取主力/次主力合约代码
def getTarList():
mainList = [] # 主力合约列表
nextList = [] # 次主力合约列表
for commodity, monthList in commodity_contracts.items():
curTime = _D()
curYear = curTime[2:4]
curMonth = curTime[5:7]
if int(curMonth) == 12:
if commodity.isupper():
mainID = commodity + str(int(curYear[1])+1) + str(monthList[1])
nextID = commodity + str(int(curYear[1])+1) + str(monthList[2])
else:
mainID = commodity + str(int(curYear)+1) + str(monthList[1])
nextID = commodity + str(int(curYear)+1) + str(monthList[2])
elif int(curMonth) >= int(monthList[0]) and int(curMonth) < int(monthList[1]) - 1:
if commodity.isupper():
mainID = commodity + str(curYear[1]) + str(monthList[1])
nextID = commodity + str(curYear[1]) + str(monthList[2])
else:
mainID = commodity + str(curYear) + str(monthList[1])
nextID = commodity + str(curYear) + str(monthList[2])
elif int(curMonth) >= int(monthList[1]) - 1 and int(curMonth) < int(monthList[2]) - 1:
if commodity.isupper():
mainID = commodity + str(curYear[1]) + str(monthList[2])
nextID = commodity + str(int(curYear[1])+1) + str(monthList[0])
else:
mainID = commodity + str(curYear) + str(monthList[2])
nextID = commodity + str(int(curYear)+1) + str(monthList[0])
elif int(curMonth) < 12:
if commodity.isupper():
mainID = commodity + str(int(curYear[1])+1) + str(monthList[0])
nextID = commodity + str(int(curYear[1])+1) + str(monthList[1])
else:
mainID = commodity + str(int(curYear)+1) + str(monthList[0])
nextID = commodity + str(int(curYear)+1) + str(monthList[1])
mainList.append(mainID)
nextList.append(nextID)
return [mainList, nextList]
# 获取主力日级别k线,方便后续因子计算
def getMainData(symbol):
info = _C(exchange.SetContractType, symbol)
codeID = re.search(r'([A-Za-z]+)\d+', symbol).group(1)
records = exchange.GetRecords()
records.pop() # 删除最新未完成的k线
records_list = [] # Use a list to store individual records
for r in records:
curR = {
"Instrument": codeID,
"InstrumentId": symbol,
"Time": r['Time'],
"Open": r['Open'],
"High": r['High'],
"Low": r['Low'],
"Close": r['Close'],
"Volume": r['Volume']
}
records_list.append(curR)
# Convert the list of dictionaries to a DataFrame
recordsDf = pd.DataFrame(records_list)
return recordsDf
# 计算高阶矩阵因子
def calCMF(symbol, backPeriod):
info = exchange.SetContractType(symbol)
codeID = re.search(r'([A-Za-z]+)\d+', symbol).group(1)
records = exchange.GetRecords(PERIOD_M5)[-backPeriod - 2 : -1]
recent_returns = [records[i + 1].Close / records[i].Close - 1 for i in range(len(records) - 1)]
std_dev = pd.Series(recent_returns).std()
skewness = pd.Series(recent_returns).skew()
kurtosis = pd.Series(recent_returns).kurt()
return [std_dev, skewness, kurtosis]
# 获取动量因子
def momFactor(factorDf, factor, momPeriod):
factorDf['movingValue'] = factorDf[factor].rolling(window=momPeriod, min_periods = 1).mean()
factorDf['momValue'] = (factorDf[factor] - factorDf['movingValue']) / factorDf['movingValue']
return factorDf.iloc[-1, factorDf.columns.get_loc('momValue')]
# 计算展期收益率
def calRollover(mainContract, nextContract):
_C(exchange.SetContractType, mainContract)
mainRecords = exchange.GetRecords()[-1]
_C(exchange.SetContractType, nextContract)
nextRecords = exchange.GetRecords()[-1]
mainDate = re.findall(r"\d+\d*", mainContract)
nextDate = re.findall(r"\d+\d*", nextContract)
if int(nextDate[0][-2:]) > int(mainDate[0][-2:]):
diffMonth = int(nextDate[0][-2:]) - int(mainDate[0][-2:])
else:
diffMonth = int(nextDate[0][-2:]) + 12 - int(mainDate[0][-2:])
rollOver = math.log(mainRecords['Close'] / nextRecords['Close']) / diffMonth * 12
return rollOver
def getBasis(date, symbol):
max_retries = 10
for i in range(max_retries):
try:
basisDf = ak.futures_spot_price_daily(start_day=date, end_day=date, vars_list=[symbol])
if basisDf is None or basisDf.empty:
raise ValueError("No data available")
return [basisDf.spot_price[0], basisDf.dominant_contract_price[0]]
except (ValueError) as e:
if i < max_retries - 1: # 不在最后一次循环中抛出异常
Log(f"Failed to get data, retrying ({i+1}/{max_retries}). Error: {e}")
else:
Log(f"Failed to get data after {max_retries} attempts. Error: {e}")
return [None, None] # 如果没有成功获取数据,返回两个空值的列表
def getReceipt(date, symbol):
max_retries = 10
for i in range(max_retries):
try:
receiptDf = ak.get_receipt(start_day = date, end_day = date, vars_list=[symbol])
if receiptDf is None or receiptDf.empty:
raise ValueError("No data available")
return [receiptDf.receipt[0], receiptDf.receipt_chg[0]]
except (ValueError) as e:
if i < max_retries - 1: # 不在最后一次循环中抛出异常
Log(f"Failed to get data, retrying ({i+1}/{max_retries}). Error: {e}")
else:
Log(f"Failed to get data after {max_retries} attempts. Error: {e}")
return [None, None] # 如果没有成功获取数据,返回两个空值的列表
# 因子获取
def getFactor(symbol, nextsymbol, cmfBackPeriod, pricePeriod, volumePeriod):
# 获取k线数据
rData = getMainData(symbol)
# 获取展期收益率
rollover = calRollover(symbol, nextsymbol)
# 获取高阶矩类因子
cmfFactor = calCMF(symbol, cmfBackPeriod)
# 获取动量因子
priceMom = momFactor(rData, 'Close', pricePeriod)
volumeMom = momFactor(rData, 'Volume', volumePeriod)
# 获取基本面时间
timePre = _D(int(rData.Time.tolist()[-1])/1000)[0:10]
formatted_date = str(timePre.replace("-", ""))
# 获取基本面数据
upperSymbol = ''.join(filter(str.isalpha, symbol)).upper()
basisData = getBasis(formatted_date, upperSymbol)
receiptData = getReceipt(formatted_date, upperSymbol)
# 获取当前时间
timeNow = _D()[0:10]
dateNow = str(timeNow.replace("-", ""))
# 数据保存
curFactor = {
"Instrument": symbol,
"InstrumentId": upperSymbol,
"Time": dateNow,
"Close": rData.iloc[-1,rData.columns.get_loc('Close')],
"RollOver": rollover,
"Std": cmfFactor[0],
"Skew": cmfFactor[1],
"Kurt": cmfFactor[2],
"PriceMom": priceMom,
"VolumeMom": volumeMom,
"SpotPrice": basisData[0],
"ContractPrice": basisData[1],
"Receipt": receiptData[0],
"ReceiptCng": receiptData[1]
}
# 将字典转换为DataFrame,并添加到主DataFrame中
new_factor = pd.DataFrame([curFactor])
return new_factor
# 因子计算
def calFactor(factorData):
# 处理缺失值
dateList = factorData.Time.unique()
varList = factorData.InstrumentId.unique()
# 创建包含所有可能组合的 DataFrame
all_combinations = pd.MultiIndex.from_product([varList, dateList], names=['InstrumentId', 'Time'])
all_combinations = pd.DataFrame(index=all_combinations).reset_index()
merged_df = pd.merge(all_combinations, factorData, how='left', left_on=['InstrumentId', 'Time'], right_on=['InstrumentId', 'Time'])
merged_df = merged_df.sort_values(by=['InstrumentId','Time']) # 分组排序
# 填补缺失值
fillna_df = merged_df.fillna(method='ffill')
# 计算基差基本面因子
fillna_df['basis'] = fillna_df.SpotPrice - fillna_df.ContractPrice
fillna_df['basisrate'] = (fillna_df.SpotPrice - fillna_df.ContractPrice) / fillna_df.SpotPrice
# 设置不同周期K
periodList = [5, 22, 63, 126,243]
# 计算基差动量
for basisT in periodList:
rolling_window = fillna_df.groupby('InstrumentId')['basis'].rolling(window=basisT, min_periods=basisT).mean().reset_index(level=0, drop=True)
fillna_df['br_' + str(basisT)] = (fillna_df.basis - rolling_window) / rolling_window
# 仓单数据处理
fillna_df['wr_day'] = np.where((fillna_df['Receipt'] == fillna_df['ReceiptCng']) & (fillna_df['Receipt'] != 0), 1,
np.where((fillna_df['Receipt'] == fillna_df['ReceiptCng']) & (fillna_df['Receipt'] == 0), 0,
fillna_df['ReceiptCng'] / (fillna_df['Receipt'] - fillna_df['ReceiptCng'])))
receipt_rolling_averages = []
# 遍历每个 'var' 分组
for var_group, var_group_df in fillna_df.groupby('InstrumentId'):
# 遍历每一天
for dateI in range(len(var_group_df)):
# 计算当前日期前 -306 到 -180 范围内的滑动均值
if dateI >= 306:
rolling_average = var_group_df['Receipt'].iloc[dateI - 306: dateI - 180].mean()
else:
rolling_average = None # 对于前306天内的数据,无法计算范围内的滑动均值
receipt_rolling_averages.append(rolling_average)
fillna_df = fillna_df.sort_values(by=['InstrumentId','Time'])
fillna_df['receipt_rolling_averages'] = receipt_rolling_averages
# 计算年度仓单变化率
for receiptT in periodList:
receipt_rolling_window = fillna_df.groupby('InstrumentId')['Receipt'].rolling(window=receiptT, min_periods=receiptT).mean().reset_index(level=0, drop=True)
fillna_df['wr_' + str(receiptT)] = receipt_rolling_window / fillna_df['receipt_rolling_averages'] - 1
finalDf = fillna_df.dropna()
## 计算收益率
finalDf['Returns'] = finalDf.groupby('InstrumentId')['Close'].pct_change()
finalDf = finalDf.sort_values(by=['Time','InstrumentId']) # 分组排序
# 收益率前移
finalDf['Returns'] = finalDf['Returns'].shift(-8)
return finalDf
# 因子处理
def norm_factor(factor):
median_val = factor.median()
mad_val = (factor - factor.mean()).abs().mean()
factor_clip = np.clip(factor, median_val - 3 * mad_val, median_val + 3 * mad_val)
factor_norm = (factor_clip - factor_clip.mean()) / factor_clip.std()
return factor_norm
def orthogonalize_factors(factors_standardize):
# 计算协方差矩阵
M = (factors_standardize.shape[0] - 1) * np.cov(factors_standardize.T.astype(float))
# 特征值分解
D, U = np.linalg.eig(M)
# 转换为矩阵
U = np.mat(U)
# 对特征值取倒数并开方
d = np.mat(np.diag(D**(-0.5)))
# 获取过度矩阵S
S = U * d * U.T
# 获取对称正交矩阵
factors_orthogonal_mat = np.mat(factors_standardize) * S
# 转为DataFrame
factors_orthogonal = pd.DataFrame(factors_orthogonal_mat, columns=factors_standardize.columns, index=factors_standardize.index)
return factors_orthogonal
oriFeatures = ['RollOver','Std','Skew',
'Kurt', 'basisrate', 'br_5', 'br_22', 'br_63', 'br_126', 'br_243',
'wr_day', 'wr_5', 'wr_22', 'wr_63', 'wr_126', 'wr_243', 'PriceMom',
'VolumeMom']
allCol = ['RollOver','Std','Skew',
'Kurt', 'basisrate', 'br_5', 'br_22', 'br_63', 'br_126', 'br_243',
'wr_day', 'wr_5', 'wr_22', 'wr_63', 'wr_126', 'wr_243', 'PriceMom',
'VolumeMom','Returns']
# 因子处理
def proFactor(df):
df[oriFeatures] = orthogonalize_factors(df[oriFeatures].apply(norm_factor))
return df
selFeatures = ['RollOver','Std','Skew',
'Kurt', 'basisrate', 'br_5', 'br_22', 'br_63', 'br_126', 'br_243',
'wr_day', 'wr_5', 'wr_22', 'wr_63', 'wr_126', 'wr_243', 'PriceMom',
'VolumeMom']
# 合成因子
def history_linear_composite(df, window_size, step_size, selFeatures):
composite_factors = []
cumulative_coefficients = np.zeros(len(selFeatures)) # 因子收益率累加列表
cumulative_count = 0 # 次数统计
for i in range(window_size, len(df), step_size):
window_data = df.iloc[(i - window_size):i, :]
X = window_data[selFeatures]
y = window_data['Returns']
X = sm.add_constant(X)
model = sm.OLS(y, X).fit()
coefficients = model.params[1:]
p_values = model.pvalues[1:]
coefficients = np.where(p_values < 0.1, coefficients, 0)
cumulative_coefficients += coefficients
cumulative_count += 1
coefficientsMean = cumulative_coefficients / cumulative_count # 计算均值
for k in range(i, i + step_size):
composite_factor_value = np.dot(df.loc[k, selFeatures], coefficientsMean)
composite_factors.append(composite_factor_value)
return composite_factors
def getComposite(df, selFea):
df = df.sort_values(by=['Time','InstrumentId']) # 分组排序
df = df.reset_index(drop = True)
window_size = 63 * len(df['InstrumentId'].unique())
step_size = 1 * len(df['InstrumentId'].unique())
composite_factors = history_linear_composite(df, window_size, step_size, selFea)
df['HeCompositeFactor'] = [np.nan] * window_size + composite_factors
return df
# 移仓
def posTrans():
mainList = getTarList()[0]
codeList = [''.join(filter(str.isalpha, item)) for item in mainList]
prePos = exchange.GetPosition()
if len(prePos) != 0:
for i in range(len(prePos)):
if prePos[i].ContractType not in mainList:
mainCode = re.search(r'([A-Za-z]+)\d+', prePos[i].ContractType).group(1)
index = codeList.index(mainCode)
mainID = mainList[index]
Log('旧合约', prePos[i].ContractType, '需要被更换为', mainID)
if prePos[i].Type == PD_LONG or prePos[i].Type == PD_LONG_YD:
# 平掉旧合约
exchange.SetContractType(prePos[i].ContractType)
curPrice = exchange.GetRecords()[-1].Close - 4
exchange.SetDirection('closebuy')
exchange.Sell(curPrice, 1)
# 开仓新合约
exchange.SetContractType(mainID)
curPrice = exchange.GetRecords()[-1].Close + 4
exchange.SetDirection('buy')
exchange.Buy(curPrice, 1)
else:
exchange.SetContractType(prePos[i].ContractType)
curPrice = exchange.GetRecords()[-1].Close + 4
exchange.SetDirection('closesell')
exchange.Buy(curPrice, 1)
exchange.SetContractType(mainID)
curPrice = exchange.GetRecords()[-1].Close - 4
exchange.SetDirection('sell')
exchange.Sell(curPrice, 1)
# 移仓完成后再次判断
afterPos = exchange.GetPosition()
if len(prePos) != 0:
all_in_main_list = all(afterPos[i].ContractType in mainList for i in range(len(afterPos)))
if all_in_main_list:
Log('所有合约都是主力合约', "#00FF00")
# 多空组判断
def groupFactor(factorDf, factorName):
sortDf = factorDf.sort_values(by=factorName, ascending=False)
# 取前25%和最后25%的数据
total_count = len(sortDf)
group_count = int(total_count * 0.25)
head_group = sortDf.head(group_count) # 第一组取前25%
tail_group = sortDf.tail(group_count) # 第二组取最后25%
head_codes = head_group[head_group.HeCompositeFactor > 0]['Instrument'].tolist() # 第一组codes
tail_codes = tail_group[tail_group.HeCompositeFactor < 0]['Instrument'].tolist() # 第二组codes
return [head_codes, tail_codes]
# 多空组买卖
def trade(pos_group, neg_group):
posInfo = exchange.GetPosition()
if len(posInfo) == 0:
for i in range(len(pos_group)):
exchange.SetContractType(pos_group[i])
curPrice = exchange.GetRecords()[-1].Close + 4
exchange.SetDirection('buy')
exchange.Buy(curPrice, 1)
for i in range(len(neg_group)):
exchange.SetContractType(neg_group[i])
curPrice = exchange.GetRecords()[-1].Close - 4
exchange.SetDirection('sell')
exchange.Sell(curPrice, 1)
Log('多因子策略开始,建仓完成', "#FF0000")
else:
longList = [pos.ContractType for pos in posInfo if pos.Type == PD_LONG or pos.Type == PD_LONG_YD] # 已经开多仓
shortList = [pos.ContractType for pos in posInfo if pos.Type == PD_SHORT or pos.Type == PD_SHORT_YD] # 已经开空仓
lossList = [pos.ContractType for pos in posInfo if pos.Profit <= 0] # 亏损仓位
Log(lossList,'亏损仓位')
# 检查已经开的多仓,如果在需要开多仓的列表中,则保留,否则平仓
for pos_contract in longList:
if pos_contract in neg_group or pos_contract in lossList:
#if pos_contract not in pos_group:
# 进行平仓操作
Log(pos_contract, '多余多仓进行平仓')
exchange.SetContractType(pos_contract)
curPrice = exchange.GetRecords()[-1].Close - 4
exchange.SetDirection('closebuy')
exchange.Sell(curPrice, 1)
# 检查已经开的空仓,如果在需要开空仓的列表中,则保留,否则平仓
for neg_contract in shortList:
if neg_contract in pos_group or neg_contract in lossList:
#if neg_contract not in neg_group:
# 进行平仓操作
Log(neg_contract, '多余空仓进行平仓')
exchange.SetContractType(neg_contract)
curPrice = exchange.GetRecords()[-1].Close + 4
exchange.SetDirection('closesell')
exchange.Buy(curPrice, 1)
# 开需要开的多仓
for pos_contract in pos_group:
if pos_contract not in longList:
# 进行开仓操作
Log(pos_contract, '需要开多的进行开多仓')
exchange.SetContractType(pos_contract)
curPrice = exchange.GetRecords()[-1].Close + 4
exchange.SetDirection('buy')
exchange.Buy(curPrice, 1)
# 开需要开的空仓
for neg_contract in neg_group:
if neg_contract not in shortList:
# 进行开仓操作
Log(neg_contract, '需要开空的进行开空仓')
exchange.SetContractType(neg_contract)
curPrice = exchange.GetRecords()[-1].Close - 4
exchange.SetDirection('sell')
exchange.Sell(curPrice, 1)
ext.getTarList = getTarList
ext.posTrans = posTrans
ext.groupFactor = groupFactor
ext.trade = trade
ext.getFactor = getFactor
ext.calFactor = calFactor
ext.proFactor = proFactor
ext.getComposite = getComposite
其实综合来看,学过数据分析的朋友,是不是对上述的数据处理逻辑很熟悉,包括数据的获取,特征计算和处理,数据的清洗,还有模型的预测,以及最后信号的判断。只不过这里的数据是具体合约的时间流的数据,相信各位小伙伴也有数据分析的大拿,可以在此基础上进行更多数据探索和模型分析的工作,欢迎大家留言,一起展开探讨。下节课我们将讲解具体实盘部分的设置,我们下节课再见。
34.商品期货多因子实盘级策略(二)
上节课我们讲述了多因子模版类库中的各个模块函数,本节课我们就要结合实盘,运用各个模版类库中的函数进行一个实盘级多因子模型的搭建。多因子模型是需要一定的数据量才能开展模型训练的,要我们开启实盘等待收集足够的数据肯定是不合适的,所以我们可以利用回测系统进行因子收据的收集。然后在实盘中建立数据库导入我们收集完成的数据,然后伴随每日的更新,不断地将数据保存到数据库当中。这样呢,我们就可以实时的利用数据库收集的数据,进行一些因子有效性和模型检验的工作,不断优化我们的多因子模型。
这里首先为大家讲解一下怎样利用回测系统收集实盘的数据,也是为了验证我们模版类库中的因子数据的模版函数。这个代码编写并不复杂,首先我们要确定的是回测的时间,这里我们定义收集数据的时间是最近的三年,从2021年年初至今三年的时间,在这三年中,我们收集每一个交易日的因子数据。接着来定义策略的参数,这里我们分为两组,因子获取参数,里面包括高阶因子类,动量和数据获取的周期;因子组合参数组包括历史收益率窗口周期。
python
'''backtest
start: 2021-01-01 09:00:00
end: 2023-12-07 15:00:44
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
'''
import pandas as pd
import pymysql
def main():
# 创建 MySQL 连接对象
cnx = pymysql.connect(
host='IP', # 数据库主机地址
user='账户名', # 数据库用户名
password='密码'
)
# 创建 MySQL 游标对象
cursor = cnx.cursor()
# 创建数据库
cursor.execute("CREATE DATABASE IF NOT EXISTS tt_database")
cursor.execute("USE tt_database")
# 创建数据表
create_table_query = """
CREATE TABLE IF NOT EXISTS tt_table (
Instrument VARCHAR(255),
InstrumentId VARCHAR(255),
Time BIGINT,
Close FLOAT,
RollOver FLOAT,
Std FLOAT,
Skew FLOAT,
Kurt FLOAT,
PriceMom FLOAT,
VolumeMom FLOAT,
SpotPrice FLOAT,
ContractPrice FLOAT,
Receipt FLOAT,
ReceiptCng FLOAT
);
"""
cursor.execute(create_table_query)
# 记录上次执行的时间
last_operation_timeBig = 0
isFirst = True
while True:
current_timeBig = int(time.time())
# 获取时间间隔(秒)
if isFirst:
time_intervalBig = 24 * 60 * 60 * dataPeriod
isFirst = False
else:
time_intervalBig = current_timeBig - last_operation_timeBig
if time_intervalBig >= 24 * 60 * 60 * dataPeriod:
# 更新因子数据
symbolList = ext.getTarList()
contractLen = len(symbolList[0])
for k in range(contractLen):
if not ext.IsTrading(symbolList[0][k]):
continue
new_factor = ext.getFactor(symbolList[0][k], symbolList[1][k], cmfBackPeriod, pricePeriod, volumePeriod)
## 数据导入
insert_data_query = """
INSERT INTO tt_table
(Instrument, InstrumentId, Time, Close, RollOver, Std, Skew, Kurt, PriceMom, VolumeMom, SpotPrice, ContractPrice, Receipt, ReceiptCng)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_data_query, (
new_factor['Instrument'][0], new_factor['InstrumentId'][0], new_factor['Time'][0], new_factor['Close'][0],
new_factor['RollOver'][0], new_factor['Std'][0], new_factor['Skew'][0], new_factor['Kurt'][0], new_factor['PriceMom'][0],
new_factor['VolumeMom'][0], new_factor['SpotPrice'][0], new_factor['ContractPrice'][0], new_factor['Receipt'][0],
new_factor['ReceiptCng'][0]
))
cnx.commit()
last_operation_timeBig = current_timeBig
Sleep(60*60*10)
然后在主函数里,定义每隔一个数据获取周期,开启一次数据的收集。通过获取合约列表的长度,然后使用模版类库的getFactor函数,依次获取每个订阅合约的因子数据。这里增加了一个条件,如果该合约不在交易时间,直接跳过。这样就可以获取到原始的因子数据。
这些原始的数据当然需要进行保存,所以使用我们以前讲过的mysql数据保存方法。在策略开始打开mysql,创建数据库和数据表。然后在while循环里,每当新的一条数据更新,我们导入到数据库里面,不要忘了使用commit确定导入的操作。
这样我们就可以保存三年的原始数据到数据库里面,不仅可以在实盘里直接可以使用,也可以在实盘之外,利用我们前面课程讲过的因子有效性的验证方法,确定各因子的有效性然后导入模型,实现动态的优化模型。
下面我们来看实盘部分的设置。首先我们来看这里的参数设置,这里需要增加一组参数,代表持仓的周期holdPeriod。我们可以每日获取因子数据,但是调仓的时间可以自己确定。
python
import pandas as pd
import pymysql
import json
import time
import warnings
warnings.filterwarnings("ignore")
def main():
# 记录上次执行的时间
last_operation_time = 0
last_trade_time = 0
isFirst = True
if exchange.IO("status"):
Log('已连接CTP')
else:
Sleep(1000)
# 获取初始账户信息
initAccount = exchange.GetAccount()
while True:
if exchange.IO("status"):
current_sectime = int(time.time()) # 获取当前秒级别时间
cur_time = int(_D()[0:10].replace("-", ""))# 获取当前日级别时间
# 获取时间间隔(秒)
if isFirst:
time_intervalOpe = 24 * 60 * 60 * dataPeriod
time_intervalTra = 24 * 60 * 60 * holdPeriod
isFirst = False
else:
time_intervalOpe = current_sectime - last_operation_time
time_intervalTra = current_sectime - last_trade_time
# 数据库收集数据
if time_intervalOpe >= 24 * 60 * 60 * dataPeriod:
# 移仓
ext.posTrans()
# 创建 MySQL 连接对象
cnx = pymysql.connect(
host='???', # 数据库主机地址
user='???', # 数据库用户名
password='???'
)
# 创建 MySQL 游标对象
cursor = cnx.cursor()
cursor.execute("USE tt_database")
# 读取因子数据
select_data_query = "SELECT * FROM tt_table"
cursor.execute(select_data_query)
rows = cursor.fetchall()
column_names = [desc[0] for desc in cursor.description]
factordf = pd.DataFrame(rows, columns=column_names) # 获取数据库df
# 判断是否需要更新
if cur_time not in factordf.Time.tolist():
Log('每日更新数据#00ff00')
symbolList = ext.getTarList()
contractLen = len(symbolList[0])
for k in range(contractLen):
if not ext.IsTrading(symbolList[0][k]):
Log('非交易时间#00ff00')
continue
new_factor = ext.getFactor(symbolList[0][k], symbolList[1][k], cmfBackPeriod, pricePeriod, volumePeriod)
## 数据导入
insert_data_query = """
INSERT INTO tt_table
(Instrument, InstrumentId, Time, Close, RollOver, Std, Skew, Kurt, PriceMom, VolumeMom, SpotPrice, ContractPrice, Receipt, ReceiptCng)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_data_query, (
new_factor['Instrument'][0], new_factor['InstrumentId'][0], new_factor['Time'][0], new_factor['Close'][0],
new_factor['RollOver'][0], new_factor['Std'][0], new_factor['Skew'][0], new_factor['Kurt'][0], new_factor['PriceMom'][0],
new_factor['VolumeMom'][0], new_factor['SpotPrice'][0], new_factor['ContractPrice'][0], new_factor['Receipt'][0],
new_factor['ReceiptCng'][0]
))
cnx.commit()
cursor.close()
cnx.close()
last_operation_time = current_sectime ## 更新数据库保存时间
# 因子计算进行交易
if time_intervalTra >= 24 * 60 * 60 * holdPeriod:
# 创建 MySQL 连接对象
cnx = pymysql.connect(
host='???', # 数据库主机地址
user='???', # 数据库用户名
password='???'
)
# 创建 MySQL 游标对象
cursor = cnx.cursor()
cursor.execute("USE tt_database")
Log('调仓时间#ff0000')
## 因子计算
caldf = ext.calFactor(factordf)
Log('因子计算完成#ff0000')
## 因子处理
prodf = ext.proFactor(caldf)
Log('因子处理完成#ff0000')
## 因子合成
selFea = ['RollOver', 'Skew', 'br_63', 'wr_day', 'VolumeMom', 'PriceMom']
comdf = ext.getComposite(prodf, selFea)
finaldf = comdf[comdf.Time == cur_time]
Log('因子合成完成#ff0000')
## 多空组判断
positive_codes = ext.groupFactor(finaldf, 'HeCompositeFactor')[0]
negative_codes = ext.groupFactor(finaldf, 'HeCompositeFactor')[1]
Log('多空组判断完成#ff0000')
Log('做多组:', positive_codes)
Log('做空组:', negative_codes)
## 交易操作
ext.trade(positive_codes, negative_codes)
cursor.close()
cnx.close()
last_trade_time = current_sectime ## 更新交易操作时间
# 持仓状态展示
positions = _C(exchange.GetPosition)
if len(positions) != 0:
longContract = []
longPrice = []
longProfit = []
shortContract = []
shortPrice = []
shortProfit = []
for i in range(len(positions)):
if (positions[i]['Type'] == PD_LONG) or (positions[i]['Type'] == PD_LONG_YD):
longContract.append(positions[i]['ContractType'])
longPrice.append(positions[i]['Price'])
longProfit.append(positions[i]['Profit'])
else:
shortContract.append(positions[i]['ContractType'])
shortPrice.append(positions[i]['Price'])
shortProfit.append(positions[i]['Profit'])
tblAStatus = {
"type" : "table",
"title" : "持多组",
"cols" : ["合约名称", "持仓均价", "持仓盈亏"],
"rows" : []
}
tblBStatus = {
"type" : "table",
"title" : "持空组",
"cols" : ["合约名称", "持仓均价", "持仓盈亏"],
"rows" : []
}
for i in range(len(longContract)):
tblAStatus["rows"].append([longContract[i], longPrice[i], longProfit[i]])
for i in range(len(shortContract)):
tblBStatus["rows"].append([shortContract[i], shortPrice[i], shortProfit[i]])
lastStatus = f"`{json.dumps(tblAStatus)}`\n`{json.dumps(tblBStatus)}`\n当前时间: {cur_time}"
curAccount = _C(exchange.GetAccount)
totalProfit = sum(position['Profit'] for position in positions)
totalMargin = sum(position['Margin'] for position in positions)
curProfit = curAccount.Balance - initAccount.Balance + totalProfit + totalMargin
LogProfit(curProfit, "&")
LogStatus(lastStatus)
Sleep(1000 * 60 * 60)
回到代码部分,在策略开始导入策略运行需要的库,然后连接数据库;因为数据记录和交易的操作时间周期是不一致的,这里我们分别记录上次操作进行的时间,包括数据导入操作,last_operation_time,和last_trade_time交易的操作。然后定义isFirst代表是否第一次运行策略。这里还需要获取账户信息,方便后续的收益计算。
接着进入while循环,在判断连接CTP条件下,执行我们的多因子策略。这里我们获取两个时间戳,current_sectime当前秒级别时间,cur_time当前日级别时间。然后获取时间间隔,判断应该进行数据导入还是交易的操作。在策略开始的时候,我们是想直接运行的,所以初始的时间间隔定义为需要的时间间隔,然后伴随策略运行,两个时间间隔定义为当前时间减去上一次各自操作进行的时间。这样我们就可以根据两个时间循环进行相应的数据导入和交易的操作。
下面我们来定义具体的操作。第一个我们来进行数据更新的操作,和课程前面讲到的一致。这里定义如果数据导入的时间间隔,大于设置的数据收集的参数间隔,我们首先进行移仓的操作,然后读取数据库的数据,转换为dataframe,接着判断当前日级别的时间戳在数据库中是否已经存在,如果不存在进行更新导入。最后更新数据库保存时间。
第二个我们来进行因子计算的交易。如果时间间隔大于调仓的间隔,首先我们获取更新完成的数据,然后我们使用更新完成的数据,依次使用模版类库的函数进行因子计算,因子处理,因子合成,因子合成这里的特征,我们可以根据因子有效性的验证结果进行选择,然后我们可以获取到当日的合成因子数据。我们就是要利用这个合成因子使用模版类库的groupFactor函数,进行多空组的判断,在这个模型中,合成因子和收益率是正向的关系,所以设置第一组为做多组,第二组为做空组,如果计算出来的合成因子和收益率是负向的关系,这里做多组和做空组的索引是不一致的。最后根据两个合约列表进行相应的交易操作。交易操作的时间不要忘了更新。
当然,作为一个实盘,持仓状态的显示和收益的显示也是必须的,这些在前面的课程中我们都讲到过,这里不加以赘述了。
最后的验证时间来到了,我们建立一个实盘。这里选择编写好的多因子策略,选择托管者和仿真交易所。我们点击实盘运行。我们看下实盘的运行,可以看到实时的收益变化,以及具体做多组和做空组持仓均价和持仓的盈亏。
在日志信息了可以看到具体的策略操作。我们看到,策略首先判断持仓的合约是否都是主力合约,如果不是主力合约需要进行移仓换月的操作,接着策略收集今日更新的数据,导入到数据库里面,这样数据更新的操作就完成;然后利用更新完成的数据,依次进行因子计算,因子处理,因子合成,多空组判断的操作,接着利用多空组判断的合约列表,进行亏损仓位和多余仓位的平仓,和需求仓位的开仓操作。这样我们的多因子策略就可以实时运行起来。
以上呢,就是在优宽平台实现多因子策略的具体操作。这一系列的课程持续了一个月的时间,在这期间也遇到了很多的困难和挑战,非常感谢大家耐心的等待和评论区的鼓励。当然,这个模型并不是十分完善的,可以优化的细节还有很多,比如因子处理计算的各种策略参数和计算方法,大家可以根据自己的所学进行更加细化的研究。如果大家有问题,可以留言评论区,我们将会热心解答,大家一起加油,一起进步!
实盘公开地址:
35.商品期货多周期均线共振策略
最近有小伙伴在评论区留言,说希望讲解一下“多周期共振系统”。我们的回答当然是立即安排。首先给不了解这个策略的小伙伴们介绍一下。
商品期货的多周期均线共振系统策略是一种综合性的交易策略,它基于不同周期的均线共振现象来制定交易计划。这种策略的核心思想是利用不同周期的均线在同一时间点上形成的共振状态,来判断市场走势的强势或弱势,并以此为依据进行交易。
期货交易界老话。大级别上涨,小级别不配合,涨不动;小级别上涨,大级别不配合,涨不多的规律,只有多个周期的均线系统形成统一方向的趋势共振时,才可以形成持久的趋势行情,这也是多周期共振的理论认知。我们可以从图像的角度了解多周期共振,具体包括均线的多头排列和空头排列。
这里使用到了不同级别的k线,包括大周期,中周期,小周期,和不同滑动窗口的均线,包括快线,中线,和慢线。当不同级别的不同滑动周期的均线,均呈现金叉的信号向上扩散,则形成均线多头排列,均线多头排列代表上涨行情,可以视为多头开仓的信号;
与之相反的,当不同级别的不同滑动周期的均线,均呈现死叉的信号,往下扩散,则形成均线空头排列,均线空头排列代表下跌行情,可以视为空头开仓信号。
当多个周期均线都形成多头排列或者空头排列时,就形成了均线多周期共振。均线多周期共振表示当前多个周期的均线走势是相同的。均线在某种意义上代表着资金的走向,同时均线对价格的支撑或阻力作用也较为明显。由此呢,我们可以制定多周期均值共振的交易策略。
具体来说,该策略的应用包括以下步骤:
-
选择合适的周期参数:首先,投资者需要明确自己的交易习惯,选择适合自己的周期参数。长线交易可以选择月线、周线和日线三个周期共振;中线交易可以选择周线、日线和60分钟三个周期共振;短线交易可以选择日线、60分钟和15分钟三个周期共振。
-
判断共振状态:当不同周期的均线在同一时间点附近出现交叉或聚集时,就形成了均线共振。例如,月线、周线和日线的均线方向一致向上,表明市场处于多头趋势。此时,投资者可以制定相应的多头策略。
-
制定交易策略:根据共振状态和市场走势,制定相应的交易策略。在多头趋势中,投资者可以选择买入或持有期货合约;在空头趋势中,投资者可以选择卖出或观望期货合约。同时,投资者还需要注意不同周期的信号冲突和共振状态的变化,及时调整自己的交易策略。
-
观察进场信号:在确定了交易策略后,投资者需要密切关注市场走势,等待合适的进场信号。例如,在多头趋势中,投资者可以等待价格回调到重要的支撑位附近时,发出买入信号时再进场。
-
设定止损止盈:进场后,投资者需要设定合理的止损止盈位。止损位可以设置在重要的支撑或压力位附近,止盈位则可以参考目标盈利位或移动平均线等指标来确定。
-
跟踪持仓:进场后,投资者需要密切关注市场走势和持仓情况。如果市场走势与预期不符,投资者需要及时调整自己的交易策略或平仓离场。
其实有没有觉得很熟悉,这种策略很类似于我们前面讲过的剥头皮策略。那个策略当中,我们使用大小两个周期的均线共振判断趋势,然后根据小周期的回踩确定入场的信号。但是多周期均线共振使用两条以上的均线,更多的均线周期代表更多的趋势信号,我们就可以利用这些信号进行更多的操作。
本节课呢,我们就来编写一个,更加灵活的多周期均线共振策略。这个策略不仅可以适用于多种的k线周期;另外,我们还可以使用量化的方式定义多头排列和空头排列的程度。下面我们来具体讲解一下。
javascript
function main() {
var trendBigSignal = 0 //大周期趋势信号
var trendMidSignal = 0 //中周期趋势信号
var trendSmallSignal = 0 //小周期趋势信号
var prefinalSignal = 0 //先前趋势信号
var finalSignal = 0 //实时趋势信号
var p = $.NewPositionManager() //交易类库函数
while(true){
if(exchange.IO('status')){
// 订阅合约
var info = exchange.SetContractType(symbol)
// 大周期趋势判断
var bigR = exchange.GetRecords(1 * 60 * bigPeriod)
var popbigR = JSON.parse(JSON.stringify(bigR))
popbigR.pop()
if(bigR.length < bigSlowMA){
continue
}
var fastMABig = TA.MA(popbigR, bigFastMA)
var midMABig = TA.MA(popbigR, bigMidMA)
var slowMABig = TA.MA(popbigR, bigSlowMA)
if(fastMABig[fastMABig.length - 1] - midMABig[midMABig.length - 1] > diffBig && midMABig[midMABig.length - 1] - slowMABig[slowMABig.length - 1] > diffBig){
trendBigSignal = 1
}else if(fastMABig[fastMABig.length - 1] - midMABig[midMABig.length - 1] < -diffBig && midMABig[midMABig.length - 1] - slowMABig[slowMABig.length - 1] < -diffBig){
trendBigSignal = -1
}else{
trendBigSignal = 0
}
// 中周期趋势判断
var midR = exchange.GetRecords(1 * 60 * midPeriod)
var popmidR = JSON.parse(JSON.stringify(midR))
popmidR.pop()
var fastMAMid = TA.MA(popmidR, midFastMA)
var midMAMid = TA.MA(popmidR, midMidMA)
var slowMAMid = TA.MA(popmidR, midSlowMA)
if(fastMAMid[fastMAMid.length - 1] - midMAMid[midMAMid.length - 1] > diffMid && midMAMid[midMAMid.length - 1] - slowMAMid[slowMAMid.length - 1] > diffMid){
trendMidSignal = 1
}else if(fastMAMid[fastMAMid.length - 1] - midMAMid[midMAMid.length - 1] < -diffMid && midMAMid[midMAMid.length - 1] - slowMAMid[slowMAMid.length - 1] < -diffMid){
trendMidSignal = -1
}else{
trendMidSignal = 0
}
// 小周期趋势判断
var smallR = exchange.GetRecords(1 * 60 * smallPeriod)
var popsmallR = JSON.parse(JSON.stringify(smallR))
popsmallR.pop()
var fastMASmall = TA.MA(popsmallR, smallFastMA)
var midMASmall = TA.MA(popsmallR, smallMidMA)
var slowMASmall = TA.MA(popsmallR, smallSlowMA)
if(fastMASmall[fastMASmall.length - 1] - midMASmall[midMASmall.length - 1] > diffSmall && midMASmall[midMASmall.length - 1] - slowMASmall[slowMASmall.length - 1] > diffSmall){
trendSmallSignal = 1
}else if(fastMASmall[fastMASmall.length - 1] - midMASmall[midMASmall.length - 1] < -diffSmall && midMASmall[midMASmall.length - 1] - slowMASmall[slowMASmall.length - 1] < -diffSmall){
trendSmallSignal = -1
}else{
trendSmallSignal = 0
}
var finalSignal = trendBigSignal == 1 && trendMidSignal== 1 && trendSmallSignal== 1 ? 1 : trendBigSignal == -1 && trendMidSignal== -1 && trendSmallSignal== -1 ? -1 :0
var posInfo = exchange.GetPosition()
// 下单
if(posInfo.length == 0){
if(finalSignal == 1){
p.OpenLong(symbol, 1)
Log('开多仓')
}
if(finalSignal == -1){
p.OpenShort(symbol, 1)
Log('开空仓')
}
}
//止盈止损平仓
if(posInfo.length != 0){
profitLevel = info.VolumeMultiple * info.PriceTick * stopProfit //盈利线
lossLevel = -info.VolumeMultiple * info.PriceTick * stopLoss //亏损线
if(posInfo[0].Type % 2 == 0 && posInfo[0].Profit > profitLevel){
Log(posInfo[0].Profit)
p.Cover(symbol)
Log('平盈利多仓')
}
if(posInfo[0].Type % 2 == 0 && posInfo[0].Profit < lossLevel ){
Log(posInfo[0].Profit)
p.Cover(symbol)
Log('平亏损多仓')
}
if(posInfo[0].Type % 2 == 1 && posInfo[0].Profit > profitLevel){
Log(posInfo[0].Profit)
p.Cover(symbol)
Log('平盈利空仓')
}
if(posInfo[0].Type % 2 == 1 && posInfo[0].Profit < lossLevel ){
Log(posInfo[0].Profit)
p.Cover(symbol)
Log('平亏损空仓')
}
}
$.PlotMultRecords("大周期" + symbol, "k线", bigR, {layout: 'single', col: 12, height: '600px'})
$.PlotMultLine("大周期" + symbol, "快线", fastMABig[fastMABig.length - 1], bigR[bigR.length - 2].Time)
$.PlotMultLine("大周期" + symbol, "中线", midMABig[midMABig.length - 1], bigR[bigR.length - 2].Time)
$.PlotMultLine("大周期" + symbol, "慢线", slowMABig[slowMABig.length - 1], bigR[bigR.length - 2].Time)
$.PlotMultRecords("中周期" + symbol, "k线", midR, {layout: 'single', col: 12, height: '600px'})
$.PlotMultLine("中周期" + symbol, "快线", fastMAMid[fastMAMid.length - 1], midR[midR.length - 2].Time)
$.PlotMultLine("中周期" + symbol, "中线", midMAMid[midMAMid.length - 1], midR[midR.length - 2].Time)
$.PlotMultLine("中周期" + symbol, "慢线", slowMAMid[slowMAMid.length - 1], midR[midR.length - 2].Time)
$.PlotMultRecords("小周期" + symbol, "k线", smallR, {layout: 'single', col: 12, height: '600px'})
$.PlotMultLine("小周期" + symbol, "快线", fastMASmall[fastMASmall.length - 1], smallR[smallR.length - 2].Time)
$.PlotMultLine("小周期" + symbol, "中线", midMASmall[midMASmall.length - 1], smallR[smallR.length - 2].Time)
$.PlotMultLine("小周期" + symbol, "慢线", slowMASmall[slowMASmall.length - 1], smallR[smallR.length - 2].Time)
if (prefinalSignal == 0 && finalSignal == 1) {
Log("多头共振开启")
$.PlotMultFlag("小周期" + symbol, "多头信号", smallR[smallR.length - 2].Time, "flag test1", "多头共振开启")
}
if (prefinalSignal == 1 && finalSignal == 0) {
Log("多头共振结束")
$.PlotMultFlag("小周期" + symbol, "多头信号2", smallR[smallR.length - 2].Time, "flag test2", "多头共振结束")
}
if (prefinalSignal == 0 && finalSignal == -1) {
Log("空头共振开启")
$.PlotMultFlag("小周期" + symbol, "空头信号", smallR[smallR.length - 2].Time, "flag test3", "空头共振开启")
}
if (prefinalSignal == -1 && finalSignal == 0) {
Log("空头共振结束")
$.PlotMultFlag("小周期" + symbol, "空头信号2", smallR[smallR.length - 2].Time, "flag test4", "空头共振结束")
}
prefinalSignal = finalSignal
}else{
LogStatus('未连接交易所')
}
Sleep(400)
}
}
这节课我们使用Javascript语言。在策略开始,我们定义策略的参数,这个策略的原理并不是十分的复杂,关键在于策略参数的制定。这里我们定义四组参数,第一组是期货合约,第二到第四组分别是大周期,中周期和小周期的参数设置,具体包括k线周期,代表使用的是月线,日线还是具体的小时分钟线,快线,中线和慢线周期是各个级别的均线周期,有效间隔用来确定各个均线的距离,因为很多时候,不同周期的均线很可能只有很少的差距,可能会形成不明确的假性信号,所以我们可以把均线间隔作为一个变量,这样呢,只有各个均线间隔之间具有显著的差异,我们确定下来具体的交易信号。最后一组参数是平仓的设置,包括止盈参数和止损参数,当持有的仓位达到固定点数的时候,我们直接平仓。
回到代码部分,在策略开头,我们定义几个初始化变量,包括大周期/中周期/小周期趋势信号,还有先前趋势信号和实时的趋势信号,用来确定趋势的开启和结束。这里还定义单品种控制对象,使用交易类库可以更方便的进行交易的操作。
接着设置固定框架,while循环加上交易所连接判断。下面编写策略的主体逻辑,第一步订阅合约,然后就要进行不同周期的不同级别均线的计算了,这里首先获取不同级别的k线,还记得GetRecords可以填写参数吗,根据这个参数可以获取相应周期的k线,参数是以秒为单位的,所以1*60就是一分钟,然后乘以策略参数bigPeriod,如果定义bigPeriod为60,就可以获取到小时级别的k线。但是最后一根k线是不固定的,所以我们可以舍弃最后一根,但是在后续画图的时候,我们还是希望呈现实时的k线,所以这里我们可以使用深拷贝,定义popbigR变量。然后去除popbigR最后一根k线。我们就可以利用这个完整的k线进行后续的移动均线的计算。
均线的计算是有一点数量要求的,所以如果k线数量不足够,我们跳过本次循环。接着来计算大周期快线,中线,和慢线的移动均值,使用参数bigFastMA,bigMidMA和bigSlowMA。最后来判断大周期的趋势,这里不单单是判断快线,中线和慢线不重叠,我们可以增加约束条件,如果两条均线之间的差值大于某个值的时候,我们定义是有效的信号。这里定义如果快线减中线,中线减慢线都大于阈值,定义大周期趋势为多,赋值为1;相反情况下快线小于中线,中线小于慢线,赋值为-1,是空头趋势;另外的其他情况,定义为0,代表不存在明显的趋势。对于中周期和小周期的趋势判断,也是同样的思路。
三个周期的趋势定义完成,这里我们就来定义共振了。使用三元表达式,如果三个周期的趋势都为多,定义最终信号finalSignal也是1,如果三个周期趋势都为空,定义最终信号为-1,其他种情况,定义为0。这样多周期的均线共振就定义完成了。
接下来具体的开仓和平仓的思路大家可以自己发挥,可以使用顺势的信号,如果最终信号统一一致,我们确定入场信号,进行该方向的开仓;也可以增加额外条件,在最终信号判断一致的前提下,如果出现向上或者向下的回踩,我们再确定入场信号。
这里呢,为了操作演示,采用第一种的思路,如果出现相应信号,直接入场进行开仓。
对于平仓的逻辑,我们可以使用固定止盈止损或者移动止盈止损,本策略当中我们使用固定止盈止损,当实时仓位的利润达到盈利线或者亏损线,我们直接选择平仓。这里的盈利线和亏损线需要注意下,因为不同合约的单位和跳数都是不同的,例如螺纹钢每次跳动PriceTick是1点,合约单位VolumeMultiple是10吨,所以每个价格变化是10;而纸浆的跳动单位是2,玻璃的合约单位是20,所以这里设置盈利线和亏损线使用止盈或者止损单位乘以合约单位,再乘以价格跳数。
另外一点,对于仓位类型type类型的判断,相对于以往我们手动的定义type等于PD_LONG,PD_LONG_YD,PD_SHORT,PD_SHORT_YD,我们可以简单的表达,PD_LONG,PD_LONG_YD的值是0和2,是偶数,另外两种仓位类型是奇数,所以可以除以2,然后取余数的方法,如果余数为0,代表判断是多头持仓,如果余数是1,代表空头持仓,更加简便一点。
这样,我们的策略的交易逻辑就编写完成,最后我们来进行画图的展示。使用画线类库可以很方便的进行多个图表的展示,这里我们分别画出了大周期,中周期和小周期的k线,以及相应的快线,中线和慢线,可以看到使用的是原始的k线变量。
最后我们也可以加上多头信号和空头信号开启关闭的标志flag,如果先前的prefinalSignal和实时的finalSignal信号不一致,我们对应相应情况在小周期的k线图上可以实时的标注。
策略逻辑进行完成以后,最后将prefinalSignal赋值为实时的finalSignal。
我们来使用回测系统测试一下,首先设置策略参数,这里我们大周期定义为一个小时,所以是60分钟,中周期是30分钟,小周期是15分钟,快线,中线和慢线分别定义为5,10,和15。止盈和止损点数设置为2和5。我们来运行一下,可以看到这里呈现了不同周期的空线以及相应的均线,并且在小周期的K线图上,标注了多头和空头信号开启和关闭的提醒,然后我们进行相应的入场和出场的操作。
总之,商品期货的多周期均线共振系统策略是一种综合性的交易策略,它需要我们综合考虑不同周期的市场走势和自己的交易习惯来制定交易计划。同时,我们还需要注意风险管理、进场信号和持仓跟踪等方面的问题,以确保自己的交易能够取得成功。希望本节课的内容可以解答那位同学的疑问,大家有在量化交易中碰到的问题,都可以留言评论区,我们在力所能及的范围内,都会耐心解答。
36.浅谈贝叶斯定理在商品期货中的应用
大家如果在逛期货论坛的时候,经常会看到“跟跌不跟涨”的关键字。“跟跌不跟涨”的情况在商品期货中指的是,当两个品种的价格存在某种相关性时,当其中一个品种的价格下跌,另一个品种的价格也会随之下跌,但当其中一个品种价格上涨时,另一个品种的价格却并不一定上涨。
这种相关性背后的原因可能有很多,其中贝叶斯理论提供了一种解释。贝叶斯理论是一种概率论和统计学的理论,它描述了如何根据新的信息更新对某个事件或未知量的信念(或“概率”)。
在商品期货中,如果两个品种的价格受到相同的供需因素影响,那么它们的价格可能会存在一定的相关性。当这些因素发生变化导致其中一个品种的价格下跌时,另一个品种的价格也可能受到影响而下跌。这是因为市场参与者会根据新的信息调整他们对这两个品种价格走势的信念,从而导致价格的变动。
然而,如果两个品种的价格受到不同的供需因素或其他市场因素的影响,那么它们的价格可能并不会存在高度的相关性。在这种情况下,当其中一个品种价格上涨时,另一个品种的价格可能并不会跟随上涨。这是因为,市场参与者对这两个品种的供需状况或其他影响因素的信念,并没有发生改变。
因此,“跟跌不跟涨”的情况反映了商品期货市场中两个品种价格之间的相关性并不总是存在的,而是受到多种因素的影响。贝叶斯理论提供了一种解释这种相关性的理论基础。今天我们尝试从量化分析的角度理解一下背后的贝叶斯原理。
首先我们来了解一下贝叶斯理论。贝叶斯定理可能大家经常关注“概率”、“统计”、甚至“机器学习”方面的内容时会听到,关于这个公式的推导是比较复杂的,所以这里不做过多的介绍。我们来换个角度讲一点简单、容易的理解。
P(A|B) = P(B|A) * P(A) / P(B)
贝叶斯公式是这样的,这里的P(A)和P(B) 代表事件A和事件B发生的概率,而P(A|B) 是条件概率,条件概率的意思是当一个事件发生的时候,另一件事件发生的概率。所以P(A|B)代表发生事件B的情况下,事件A发生的概率。
这个公式看起来比较复杂,我们可以通过一个模型来理解。
我们假设在一个平面上有2个面积不同的圆,就是圆A,圆B。这两个圆有相交部分。然后这个平面上方开始随机位置掉落小球,一共掉落了N个小球。其中落到圆A中的小球个数为NA个,落入圆B中的小球有NB个,落入圆A、圆B相交部分的小球有NAB个。
那我们就可以计算出来:
小球落入圆A并且落入圆A、圆B相交部分的概率为:
P(B|A) = NAB / NA
小球落入圆B并且落入圆A、圆B相交部分的概率为:
P(A|B) = NAB / NB
以NAB值作为一个中介,我们可以得出:
P(B|A) * NA = NAB = P(A|B) * NB
P(B|A) * NA / N = P(A|B) * NB / N // 等式两边同时除以N。
P(B|A) * P(A) = P(A|B) * P(B) // NA / N 就是小球掉落到圆A的概率,NB / N 同理。
P(B|A) * P(A) / P(B) = P(A|B)
P(A|B) = P(B|A) * P(A) / P(B)
发现这个模型等式,最后变形之后就是贝叶斯定理公式。
上面的都是对于公式的推导,下面我们来看具体在商品期货中的应用。我们可以从历史数据获取到两个品种的阳线(收盘价大于开盘价),阴线(收盘价小于开盘价)信息,从而计算出来品种A和品种B的阳线或者阴线概率,以及两个品种同时为阳线或者阴线的概率。有了这些概率的信息,我们就可以利用这个公式:
P(B|A) * NA = NAB = P(A|B) * NB
在A是阳线的条件下,B是阳线的概率,也就是P(B|A),或者B是阳线的条件下,A是阳线的概率,也就是P(A|B),以上呢,我们就可以判断两个品种的“跟涨”情况。
相反情况下,我们也可以阴线的信息,判断出来“跟跌”情况。下面我们就来使用代码具体来实现。
javascript
function bayTest(pRa, pRb) {
// 检查
if (pRa.length < 100 || pRb.length < 100 || pRa[pRa.length - 1].Time != pRb[pRb.length - 1].Time) {
return
}
var ia = pRa.length - 1
var ib = pRb.length - 1
while (1) {
if (pRa[ia].Time != pRb[ib].Time) {
Log(ia, ib, _D(pRa[ia].Time), _D(pRb[ib].Time))
throw "检查不通过"
}
if (ia == 0 || ib == 0) {
break
}
ia--
ib--
}
// 数据的截取
var maxLen = Math.min(pRa.length, pRb.length)
ra = []
rb = []
for (var i = pRa.length - maxLen ; i < pRa.length ; i++) {
ra.push(pRa[i])
}
for (var i = pRb.length - maxLen ; i < pRb.length ; i++) {
rb.push(pRb[i])
}
// 阳线/阴线统计
var sumPlusA = 0 //品种A阳线数量
var sumPlusB = 0 //品种B阳线数量
var sumPlusAB = 0 //品种A和品种B同时为阳线数量
var sumMinusA = 0 //品种A阴线数量
var sumMinusB = 0 //品种B阴线数量
var sumMinusAB = 0 //品种A和品种B同时为阴线数量
for (var i = 0 ; i < ra.length ; i++) {
if (ra[i].Close > ra[i].Open) {
sumPlusA++
}else if(ra[i].Close < ra[i].Open){
sumMinusA++
}
if (rb[i].Close > rb[i].Open) {
sumPlusB++
}else if(rb[i].Close < rb[i].Open){
sumMinusB++
}
if (rb[i].Close > rb[i].Open && ra[i].Close > ra[i].Open) {
sumPlusAB++
}
if (rb[i].Close < rb[i].Open && ra[i].Close < ra[i].Open) {
sumMinusAB++
}
if (ra[i].Time != rb[i].Time) {
Log(_D(ra[i+1].Time), _D(rb[i+1].Time), i+1)
Log(ra.length, rb.length, _D(ra[i].Time), _D(rb[i].Time), maxLen, i, pRa.length, pRb.length)
throw "ra[i].Time != rb[i].Time"
}
}
// 计算贝叶斯概率
var pos_p_a = sumPlusA / ra.length //品种A阳线概率
var pos_p_b = sumPlusB / rb.length //品种B阳线概率
var pos_p_ba = sumPlusAB / sumPlusA //在品种A是阳线的情况下,品种B的阳线概率
// var pos_p_ab = sumPlusAB / sumPlusB
var pos_p_ab = pos_p_a * pos_p_ba / pos_p_b //在品种B是阳线的情况下,品种A的阳线概率
Log("跟涨:","pos_p_ab:", pos_p_ab, "pos_p_a:", pos_p_a, "pos_p_ba:", pos_p_ba, "pos_p_b:", pos_p_b)
var neg_p_a = sumMinusA / ra.length //品种A阴线概率
var neg_p_b = sumMinusB / rb.length //品种B阴线概率
var neg_p_ba = sumMinusAB / sumMinusA //在品种A是阴线的情况下,品种B的阴线概率
var neg_p_ab = neg_p_a * neg_p_ba / neg_p_b //在品种B是阴线的情况下,品种A的阴线概率
Log("跟跌:","neg_p_ab:", neg_p_ab, "neg_p_a:", neg_p_a, "neg_p_ba:", neg_p_ba, "neg_p_b:", neg_p_b)
return [[pos_p_ab, pos_p_a, pos_p_ba, pos_p_b],
[neg_p_ab, neg_p_a, neg_p_ba, neg_p_b]]
}
function main() {
while (1) {
if (exchange.IO("status")) {
exchange.SetContractType(symbolA)
var ra = exchange.GetRecords(PERIOD_D1)
exchange.SetContractType(symbolB)
var rb = exchange.GetRecords(PERIOD_D1)
var ret = bayTest(ra, rb)
$.PlotMultLine("跟涨情况", symbolA + "_" + symbolB, ret[0][0], new Date().getTime(),{layout: 'single', height: '600px'})
$.PlotMultLine("跟涨情况", symbolB + "_" + symbolA, ret[0][2], new Date().getTime(),{layout: 'single', height: '600px'})
$.PlotMultLine("跟跌情况", symbolA + "_" + symbolB, ret[1][0], new Date().getTime(),{layout: 'single', height: '600px'})
$.PlotMultLine("跟跌情况", symbolB + "_" + symbolA, ret[1][2], new Date().getTime(),{layout: 'single', height: '600px'})
LogStatus(ret)
}
Sleep(1000 * 60 * 60 * 24)
}
}
打开策略编辑的页面,这里我们设置两个参数,合约A和合约B,方便观察不同品种之间的跟跌和跟涨的情况。回到代码部分,我们编写一个函数,命名为test,设置参数为pRa和pRb,分别代表两个品种的k线。
函数内第一部分我们来检查两个k线的对齐情况。第一步检查k线的数量是否大于200,并且最新k线的时间戳是否一致,如果不一致,直接进行返回。第二步,检查获取到的k线中,时间戳是否一一对应,没有k线的遗失或者错误,直到拥有较少k线的品种检查完毕。通过这两步检查,我们可以确定在下面阳线和阴线统计的时候,两个品种的k线的时间段都是一致的。
第二部分我们来进行数据的截取,因为两个品种返回k线的数目可能不一致,我们以拥有最小k线的品种为获取的最大数量,建立两个列表,分别收集两个品种的k线数据。这样我们就可以获取到,拥有一致数量和一致时间戳的两个品种的k线数据。
第三部分统计阳线和阴线的数量,定义六个初始变量,分别统计两个品种的阳线,阴线和同时为阳线或者阴线的数量。这里使用轮询的方式,对每个品种的每条k线进行统计,如果收盘价大于开盘价,定义为阳线,如果收盘价小于开盘价,定义为阴线。
最后一部分我们来计算对应的贝叶斯概率了。首先我们来计算两个品种的跟涨情况,这里我们计算品种A,品种B的阳线概率,然后计算在品种A为阳线的情况下,品种B为阳线的概率,使用sumPlusAB,品种A和品种B都为阳线的数量,处于品种A的数量,也就是这个图里的交叉部分除以图A。相对应的在品种B为阳线的情况下,品种为A为阳线的概率,除了直接使用sumPlusAB除以sumPlusB,也可以使用使用上面这三个概率进行推导,这样我们就可以获得两个品种对应的跟涨情况了。
与此对应的跟跌情况,只需要这里改为阴线就可以。最后我们将这些概率进行返回。
定义好函数以后,我们在主函数之中进行检验,分别订阅两个合约,获取对应的k线,这里我们设置获取k线的周期是1天,作为参数放入检验函数当中。为了更直观的显示两个品种的跟涨和跟跌情况,我们使用画图的方法,将这四个值进行动态的呈现。
接下来我们就可以进行检验了。第一组我们设置螺纹钢和热卷,这一组同时使用铁矿石作为原料,价格走势呈现高度的一致性,我们来看下具体的跟涨和跟跌情况。从策略图表里可以发现,对于跟涨的情况,螺纹钢的跟涨情况是要大于热卷的,差距在3%作用,而热卷的跟跌情况大于螺纹钢的,差异大概是2%,但是总体来说,两者之间的价格具有高度的相关性。
第二组我们来设置明星组合品种--纯碱和玻璃,作为最有名的妖孽品种,经常可以看到对这两个品种的热门新闻,而抱怨玻璃对于纯碱的跟跌不跟涨的帖子,在期货论坛里也经常看到。本次我们从数据可视化的角度进行一下呈现。结果发现,对于跟涨情况,黑色线玻璃的跟涨概率是比较稳定的,在68%左右;而纯碱的跟涨情况,蓝色线由年初的73%开始下滑到5月份的61%,然后有了一定的提升,到达65%左右。
至于跟跌情况,纯碱蓝色线是比较稳定的,在70%左右;玻璃黑色线在今年具有比较大的趋势变化,从年初的78%下降到比较稳定的70%左右。但是,可以发现的是,玻璃并不是显著的“跟跌不跟涨”,两者之间的关系相对螺纹钢和热卷来说,更加的独立。
最后我们来看一组,风马牛不相及的组合,螺纹钢和玉米,看看两者的跟跌和跟涨的概率可视化。可以看到,两者之间的跟涨和跟跌的概率,都维持在50%,也就是一半一半,证明两者之间的走势确实不存在显著的关系。
以上呢,就是我们尝试使用贝叶斯理论探讨品种之间的跟涨和跟跌的情况,确实很有趣。在我们的主观交易中,确实会受到一些非理性偏见的影响进而做出一些非理性的决策,所以我们可以专业的量化方法,验证这些常见的经验之谈,让认知重回理性,从而做出正确的交易决策。如果大家有想要验证的交易经验,也可以留言评论区,我们在力所能及的范围内,也会为大家进行验证和解答。
视频参考链接:
37.浅谈量化因子的升级和改造
大家在量化交易的过程中,是否经常苦恼于某些传统量化因子的死板和不灵活,造成某些假性的交易信号,从而导致错误的交易操作。所以想手动的优化一些量化指标,但是又苦于没有合理的思路和想法。本节课呢,我们就来参考优宽论坛的一位量化交易的大拿,来学习一下怎样升级和改造一个因子。
在这篇文章里,这位大拿准备改造的目标因子是PSY(心理线)。PSY因子是一种技术分析指标,它能够衡量市场参与者的情绪对价格走势的影响。简单来说,它就是研究投资者在市场涨跌时心理波动的情绪指标。作为能量类和涨跌类指标,它对市场短期走势的判断有一定的参考意义。
PSY原始因子
PSY因子是从时间的角度上计算N根K线内的多空总力量,来判断市场现在是处于强势还是弱势,是否处于超买或超卖状态。它的计算方式是通过统计N根K线内上涨K线的数量,来衡量投资者的心理承受能力。这样,我们就可以根据这个指标来进行买卖操作了。
PSY因子的计算公式很简单:PSY=(N根K线内上涨天数/N)*100。这里的N周期代表我们选定的计算周期,可以是几天、几周或几个月等。而上涨天数则是指在N周期内出现上涨价格的交易日数。大家可能怀疑这个因子这么简单,能有用吗?其实这个因子的发明时间是1991年,在市场节奏相对比较平稳的年代,这些简单的因子还是能起到一定作用的。
这个因子的使用原理也比较直接,当因子大于设置上限的时候,证明多头强势,我们进行平空开多;相反情况,当因子小于设置下限,证明空头强势,我们进行平多开空。这个因子原本使用在股票市场的,这里我们编写一下,看能否适用于商品期货市场。
在代码编写之前,我们设置一下策略的参数,这里我们设置四个。第一个是目标合约symbol;第二个是psy周期;第三个是psy上限,当psy因子值大于这个值的时候,我们进行平空开多;第四个是psy下限,当psy因子值小于这个值的时候,我们进行平多开空。
回到代码部分,首先我们编写计算psy因子的函数,这里我们设置两个参数,data是k线数据,n是psy周期。在函数体内,定义count变量,用来统计在psy周期内上涨的天数。使用for循环,对psy周期内的收盘价进行判断,如果最新的收盘价大于上一个收盘价,定义为上涨,count递增1。最后使用count除以psy周期就是乘以100就是获得的psy因子值。
这个值适用于判断单向持仓,所以使用CTA函数可以简化代码的编写。还记得CTA函数吗,交易类库中的模版交易函数。固定框架是这样的。在函数体内,编写我们的交易逻辑。因为psy值计算是需要一定数量要求的,所以首先判断k线数量。然后使用刚才编写好的函数,填入k线数据和psyPeriod参数,这样我们的psy值就获取到了。
接下来,就可以定义我们的交易操作,还是固定的开平仓的模版代码。只是这里的开平仓的信号改一下,平空开多改为psy值大于上限,平多开空改为psy值小于下限。这样交易部分就设置完成。
为了更清楚的展示psy值,我们还进行画图的展示,使用多图表画线类库,这里勾选上,第一步画上k线,第二步进行多头信号和空头信号的标注,分别是当psy因子值大于上限,和小于下限的时候,使用PlotMultFlag在k线图上进行标注。
我们使用回测系统测试一下,选择回测时间为最近一年,回测品种为螺纹钢,可以看到最后的收益为-1500多元。怎样优化回测的结果呢,我们可以调整一下策略的参数,获取更良好的策略表现。但是呢,我们也可以选择升级改造因子的计算方法,查看策略的收益能否有显著改变。
PSY+PRICES因子
PSY因子本质是一个动量因子,方法衡量过去一段时间内涨跌力量根数的比较,目的是寻找过去一段时间力量更大的一方。但是仔细观察可以发现,PSY因子仅考虑BAR线是上涨还是下跌,缺乏对BAR本身的描述无法对行情的强度进行判断。有时候上涨的天数虽然很多,但是其中的一根大阴线就可能吞没所有的涨幅,所以psy值的统计是过于简单的,忽略了涨跌幅的影响因素。所以提出解决思路是分别统计上涨k线,和下跌k线的价格变化幅度,取收盘价减去开盘价的绝对值,进行累加,然后使用psy周期内上涨的幅度除以总体的变化幅度。
使用代码编写,分别定义sum_up和sum_down用来统计psy周期内上涨和下跌的总体变化,然后使用轮询,在判断阳线还是阴线情况下,各自进行累加,最后返回上涨的总体幅度除以总体的变化幅度,就是升级的pricepsy的价格。我们将这个指标带入策略运行一下,可以看到策略的收益变为了4500多元,经过升级的pricepsy因子值确定有了一定的提升。
PSY+PRICES+Volume因子
经过上一步的改造,改造后的PSY因子更能反映出过去一段时间的强弱力量,但如果过去一段时间内上涨与下跌幅度基本一致的情况下就不能有很好的分辨了。这时我们继续加入交易量因子,在动量效应中,放量代表市场更加活跃,放量情况更能确认动量方向。所以在上一步的基础上,每日的变化幅度可以乘以一个成交量的变化权重,如果当日的成交量变化较大,那么给当日的价格变化幅度一个更高的权重。我们来看下效果怎么样。
这次的回测结果是5800多元,确实优于第二个因子的表现。怎么样,经过我们一步步的改造因子,使用更多的量价信息,确实实现了将一个因子变废为宝。但是当我们使用这个因子在实盘的时候,还是要考虑市场的具体变化和具体的品种,做具体情况的具体分析。
本节课我们参考大佬的思路,实现了一个因子的改造升级。其实还有很多传统的因子可以根据大家的交易理念,进行更进一步的优化,大家有好的想法也可以留言评论区,我们呢,也会为大家热心进行实现。
附加代码
javascript
//PSY
function calculatePSY(data, n) {
var count = 0;
for (var i = data.length - n; i < data.length; i++) {
if (data[i].Close > data[i - 1].Close) {
count++;
}
}
return (count / n) * 100;
}
//PSY+PRICES
function calculatePricePSY(data, n) {
var sum_up = 0
var sum_down = 0
for (var i = data.length - n; i < data.length; i++) {
if (data[i].Close > data[i].Open) {
sum_up += Math.abs(data[i].Close - data[i].Open)
} else if (data[i].Close < data[i].Open) {
sum_down += Math.abs(data[i].Close - data[i].Open)
}
}
return sum_up / (sum_up + sum_down) * 100
}
//PSY+PRICES+Volume
function calculatePriVolPSY(data, n) {
var sum_up = 0
var sum_down = 0
for (var i = data.length - n; i < data.length; i++) {
if (data[i].Close > data[i].Open) {
sum_up += Math.abs(data[i].Close - data[i].Open) * data[i].Volume
} else if (data[i].Close < data[i].Open) {
sum_down += Math.abs(data[i].Close - data[i].Open) * data[i].Volume
}
}
return sum_up / (sum_up + sum_down) * 100
}
//主函数设置
function main() {
$.CTA(symbol, function(st) {
if (st.records.length < psyPeriod) {
return
}
// var curPsy = calculatePSY(st.records, psyPeriod)
// var curPsy = calculatePricePSY(st.records, psyPeriod)
var curPsy = calculatePriVolPSY(st.records, psyPeriod)
$.PlotMultRecords(symbol, "k线", st.records, {layout: 'single', col: 12, height: '600px'})
if (curPsy > psyUpLine) {
$.PlotMultFlag(symbol, "多头信号", st.records[st.records.length - 1].Time, "flag test1", "多头趋势开启")
}
if (curPsy < psyDownLine) {
$.PlotMultFlag(symbol, "空头信号", st.records[st.records.length - 1].Time, "flag test2", "空头趋势开启")
}
if (st.position.amount <= 0 && curPsy > psyUpLine) { // 平空开多
Log("多头趋势");
Log("当前持仓: ", st.position);
return st.position.amount < 0 ? 2 : 1
} else if (st.position.amount >= 0 && curPsy < psyDownLine) { // 平多开空
Log("空头趋势");
Log("当前持仓: ", st.position);
return st.position.amount > 0 ? -2 : -1
}
})
}
视频参考链接:
- 1

