资源加载中... loading...

商品期货量化交易实践系列课程

Author: 雨幕(youquant), Created: 2023-09-12 09:54:22, Updated: 2023-12-18 16:43:42

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!](https://www.youtube.com/watch?v=zhEukjCzXwM)

# 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)

视频参考链接:

《协整关系(cointegration)和配对交易》

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多元,证明本地回测的结果是可信的。当然这只是一个试验的策略,大家可以在本地继续优化它,直到做出来一个满意的策略。

怎么样,有没有很心动,一行命令安装一个模块,就可以搭建一个属于自己的专属量化交易系统,省去了大量的环境配置的操作。通过这样的方式,等到调试完成一个成熟的策略,我们就可以搭建实盘进行运行,成为真正的量化交易者,大家快动手尝试起来吧!

视频参考链接:

《经典MACD交易策略》

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.展期收益率单因子模型

在商品期货市场,多因子模型是用于解释价格波动和预测市场走势的重要工具。多因子模型的一种简单模式就是单因子模型。本节课程将深入介绍单因子模型,并以展期收益作为单因子来探讨其概念、计算方法以及如何搭建模型筛选多空品种。作为多因子模型的基础,理解单因子模型是掌握多因子模型的关键步骤。

  1. 单因子模型:多因子模型的基础

上节课我们介绍了多因子模型的具体概念。它假设资产价格的变动是由多个因子引起的,如市场因子、价值因子、动量因子等。对于我们初始来说,一开始就搭建这么复杂的模型肯定是不适合的。所以我们决定从单因子模型入手。单因子模型则是多因子模型的最基本形式,只考虑一个因子对资产价格的影响。在这节课程中,我们将介绍展期收益作为单因子,探讨如何使用它来分析期货市场。

  1. 展期收益是什么?

这节课我们要使用的单因子是展期收益。展期收益(Roll Yield)是期货市场中的一种单因子,表示期货合同的价格与现货市场价格之间的差异。它通常反映了市场对未来价格变动的预期。当对某品种未来价格比较悲观的时候,近期期货价格高于远期期货价格时,展期收益为正,称为远月合约贴水,或现货升水;相反比较乐观的时候,近期期货价格低于远期期货价格,展期收益为负,称为远月合约升水,或现货贴水。

展期收益的计算通常针对不同交割日期的期货合同,以分析市场对不同交割日期的价格预期。展期收益可以通过以下简单的公式来计算:

展期收益(年度) = Log(近期合约期货价格/ 远期合约期货价格)/间隔月份 * 12

展期收益率有一个特性——单调性,强者恒强,弱者恒弱。按照专业机构提供的交易逻辑:不同合约的展期收益率排名可在一定程度上体现“多强空弱”,这里的“强”、“弱”概念可以认为是目前该品种趋势的判断,所以我们的交易逻辑就是从展期收益率的角度做到“顺势而为”。按照“多强空弱”中的理念,我们做多展期收益率最高的品种,做空展期收益率最低的品种,从而根据展期收益率排名构建相应的交易策略。

以上呢,就是展期收益率的介绍。当然我们的重点还是通过这个策略是实现单因子模型,我们来看怎样使用代码进行实现。

这个单因子策略是作为多因子模型准备的,因此各个模块函数需要保持复用性,就是在多因子模型中也可以稳定使用。所以呢,这次我们决定使用模板类库的方式,构建各个模块函数,这样呢,即使我们使用单因子或者多个因子去尝试因子的有效性,都是可以的。

第一个模块我们来补充getTarList函数,但是需要额外注意的一点是,这里我们不仅要获取主力合约,还要获取次主力合约,所以这里面定义mainList代表主力合约列表,nextList定义次主力合约列表。然后我们使用轮询在代码和具体主力月份的字典里,找到当前的主力合约代码和次主力合约代码,首先获取当前时间,使用“_D()”,然后获取当前的年份和月份,接着就要处理不同的情况了。当月份是12月的时候,我们更换主力合约,因为下一年的第一个主力合约即将到期,所以我们更换主力合约为下一年的第二个主力,次主力呢,更换为下一年的第三个。这里面需要对大小写进行不同的处理,如果是大写,年份保留2位,大写保留1位。然后针对于第一个主力到第二个主力,第二个到第三个,到三个到12月份,我们进行轮换的处理,这种方法确实比较复杂一点,但是理解起来更加简单。有的同学可能会询问为什么不使用代码加上888,然后获取代码的instrumentid呢,因为系统的主力合约是根据交易量和持仓量决定的,所以有时候非主力合约会成为不在列表里的主力合约,造成次主力合约判断出现错误。

# 获取主力/次主力合约代码
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函数后,第一个索引就是主力合约的列表,第二个索引是次主力合约的列


更多内容