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

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

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

工程处理

前面的课程,我们介绍了多因子模型中的展期收益率和高阶中心矩类因子。这些因子都是来源于期货的价格数据,属于技术指标。但是多因子模型的因子来源不是仅来自于量价指标,基本面因子,作为外部因子,在多因子模型中也发挥着重要的作用。

在期货交易市场,无论是私募机构还是普通散户,争论技术分析和基本面分析谁好谁坏,从来就没有停止过。技术分析者认为价格已经包含了一切,相信未来价格还以趋势方式演变,只关心图表上价格行为本身的变化,判断它能卖多少钱。基本面分析者认为真正价值最终将会反映在价格上,并不需要关心短期价格走势,更多的是分析影响价格背后的因素,判断它值多少钱。

作为期货交易的老手,不管技术指标使用的多么熟悉,但是总体的趋势判断还是要基于基本面分析的基础上。如果总体趋势判断出现问题,那么技术指标的应用缺乏实际的根基。

首先我们来看下影响期货基本面分析的因素,我们知道影响商品期货的三大因素中包括:宏观、品种、其他,每个因素下面大致可以分为以下的几类:

  • 宏观 宏观政策 产业政策 政治因素 外汇汇率 经济周期 货币政策

  • 其他 季节因素 天气因素 新闻事件 市场情绪

  • 品种 升水贴水 供需关系 商品库存 产业利润

宏观经济数据包括宏观政策,产业政策,政治因素,外汇汇率,经济周期和货币政策,这些数据复杂多变,每天每时每刻,地球上有太多的经济数据公布,各国政界、央行、投行,官方的和非官方的。但是,使用宏观指标预测所有品种的变化是比较困难的,比如螺纹钢,热卷,玻璃等建材对应房地产的变化,玉米,豆类对应农业需要的变化,所以在多因子模型当中,我们暂且不使用宏观因子。

第二类包含的其他事件,更多的时候是一些比较突发的因素而引起的市场趋势的转变。但是对于散户来说,是消息接受的滞后一方,所以我们大多数时候只能被动的应对市场的突然转换。并且这类消息也比较难以量化,如果大家感兴趣,可以使用自然语言处理的技术抓取关键的新闻进行市场情绪指数的量化。

但是对于散户来说,基本面分析就真的没有办法了吗,也并不是。我们可以关注品种信息。在商品经济中,影响价格最关键的因素,就是供需关系。我们可以从第二类数据,升水贴水,供需关系,商品库存和产业利润中,找到目标期货品种对应的现货市场中库存和价格的实时变化。这些数据在官方的交易所每日都会公布,我们从这些公开的数据可以预判出相对的供需关系,从而判断期货未来的大概价值。

本节课呢,我们要尝试一下使用品种信息中的仓单和基差数据,验证一下基本面因子的有效性。

第一种我们来介绍仓单因子。影响商品现货价格的因素虽然有很多,但最终大都体现在供需关系上。如果买者多于卖者,价格就会上涨;如果卖者多于买者,价格就会下跌。国内的商品期货大致上可以分为:农产品和工业品。期货圈子中流传着这样一句话:“农产品看供给,工业品看需求。”农产品是刚需,需求是相对稳定的,决定价格主要看供给;工业品是下游需求带动的,再者国内基本都产能过剩,决定价格主要看需求。虽然,在实际操作中我们很难获取工业品的需求数据,也很难计算出农产品的供给数据。但是价格波动依存于供给与需求的相互作用,这种相互作用的结果就是库存。如果库存处于低位,说明市场供不应求,需求的力量大于供给的力量,未来价格看涨;如果库存处于高位,说明市场供大于求,供给的力量大于需求的力量,未来价格看跌。

仓单是库存的一个实时的指标,具体代表的是交易所的交割仓库入库现货后开具的标准仓单,它反映的是交易所公布的库存数量。仓单既可以注册也可以注销,当期货主力想要价格上涨时,会把持有的注册仓单注销掉,改变交易所公布的库存数量,来达到交割货物不足的假象,进而影响期货价格上涨的预期。当期货主力想要价格下跌时,会注册仓单,造成交割货物增多的假象,使得被动影响期货价格下跌。所以根据这个原理我们可以反推出在期货中的交易方向。

  • 期货多头:如果仓单大量减少,说明期货价格低于现货价格,应该做多。
  • 期货空头:如果仓单大量增加,说明期货价格高于现货价格,应该做空。

第二种我们使用基差因子。同样一个商品品种,在现货市场与期货市场的价格差,叫做基差。如果期货价格大于现货价格,我们称之为期货升水;如果期货价格小于现货价格,我们称之为期货贴水。无论是升水还是贴水,随着交割日期的临近,现货价格与期货价格都会趋于一致。所以我们可以做多贴水最多的合约,做空升水最高的合约,来实现现货价格和期货价格的一致。

前面的课程呢,当我们计算出来因子,是直接拿过来使用的,对于因子的处理是比较简单的,实际上一个实时动态的因子可能由于突发的情况或者趋势的短暂回调,出现小额的波动,而这种小额的波动可能造成交易趋势判断的假性信号,所以我们对不同类型的因子,需要更精细的思考和处理。

本节课,我们就以两种基本面因子作为示例,讲解一下对不同因子的处理方法,并使用前面两节课讲过的因子有效性检验方法,验证下不同因子处理方法的有效性。

仓单因子处理

第一个我们要处理的是仓单因子。这是我们从公共数据源获取的,包括8个比较活跃品种,从2020年11月到今年11月三年的仓单数据。因为不同品种存在季节效应,比如玻璃期货品种的金九银十的补库,会造成仓单短暂的大量减少,我们需要考虑这些季节性的变化。相对于直接使用仓单的每日变化作为因子直接放入模型中,我们尝试去对这个仓单数据进行一下特征的挖掘和构建。

这里我们参考专业的研报,使用这个公式进行仓单年同比增长率因子的构建。

image

$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路径要设置对应哈,不然会访问不到对应文件里的数据。

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

除去不同的仓单因子之外,这里还有一个可调的的参数,是持仓的天数,这样就可以使用不同的仓单因子和持仓天数的组合,挑选出来目标品种最优的参数配置。大家可以尝试一下。

基差因子处理

接下来我们进行基差因子的处理。相对于以往我们直接使用基差的变化率作为因子直接导入模型。这里我们将因子转换为动量因子,通过比较基差在不同时间点的变化来衡量期货市场中基差的趋势。

其实动量因子在多因子模型中使用的方面有很多。动量因子是量化投资中常用的一种技术指标,用于衡量因子指标在一定时间内的趋势或变动方向。动量因子的核心思想是过去的指标趋势可能在未来一段时间内继续,即走势有一定的延续性。通常,动量因子可通过计算一定时间窗口内的因子数值变动来衡量。

动量因子的基本计算公式为:

image

在这个公式中,"值"可以是价格、成交量、收益率等。动量因子的值为正时,表示因子的趋势向上;为负时,表示因子的趋势向下。动量因子的大小可用于衡量趋势的强度。

基差动量因子是动量因子的一种变体,专门用于期货市场。基差是指现货价格与期货价格之间的差异,而基差动量因子通过比较基差在不同时间点的变化来衡量期货市场中基差的趋势。基差动量因子同样具有正负和大小的特征,可以帮助投资者和交易者识别期货市场中基差趋势的方向和强度。这种因子的使用也受到市场供需、季节性因素和其他宏观经济因素的影响。这里我们也设置一个参数k,表示不同周期的动量因子值,并使用原始的基差,和不同参数k下的基差动量因子,检验不同因子的有效性。

'''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个因子的初始数据。我们的特征,就是这六个因子,我们的目标变量就是这个收益率。接下来,我们就要对这六个因子进行相应的处理。

import pandas as pd
factor_df = pd.read_csv('准因子.csv', index_col=0).reset_index(drop=True)
factor_df

在因子处理之前,我们首先看下各个因子的统计特征。这里我们定义获取各个因子的均值,标准差,最大值和最小值。可以看到这里有一些极值,我们后续需要处理一下。

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)

第一步是缺失值的处理,我们检查下各列的缺失值数量。这里的缺失值数据我们在因子计算中处理过,所以不存在缺失值。这一步骤我们可以省略。

factor_df.isnull().sum()

第二步,来去除极值。通常使用的办法是3倍绝对中位差法。3倍绝对中位差法是一种确定数据分布离散程度的方法,用于检测异常值。其基本思想是:如果数据集中有一个异常值,那么它将会使得整个数据集的波动性(即标准差)变大。因此,我们可以通过计算中位数与每个数据点之间的差的绝对值,来衡量数据的波动性,然后找到那些超过3倍中位绝对差的点,这些点可能就是异常值,我们将这些异常值进行替换。

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函数,可以方便的进行数据的正态标准化。

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。如果数字不太敏感的话,我们可以画一下相关热力图。

# 计算相关矩阵
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 将正交化矩阵应用于标准化后的因子。

当然,这里的具体数学知识大家不用过于深究,我们可以定义成为函数的形式,在模型中直接使用。

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进行相关分析,可以看到各个因子的相关性降到了最低。

怎样检验因子的信息是否丢失呢,我们可以使用一种比较粗糙的办法。使用各因子作为自变量对因变量收益率进行回归分析,查看回归模型的显著水平和各因子的显著水平。如果正交后因子的显著性水平和整体回归模型的显著性水平与正交前相似,说明正交后因子保留了与因变量相关的信息。

## 正交前因子回归

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等无关。从理论上来讲,它的优势在于,如果因子值矩阵的性质在不同截面期比较稳定,合成后因子稳定性比较好;它的劣势在于,由于合成过程不涉及因子收益等信息,合成后因子不具备明显的经济学含义,其在未来表现也不一定会优于待合成的几个单因子。

需要注意的是,在合成过程中,选用不同历史窗口期及不同因子合成数目,对合成效果都会有影响。

历史因子收益率加权法

首先我们介绍历史因子收益率加权法,这种方法通过线性回归结合历史因子收益率进行加权,从而预测未来期货品种收益率。具体步骤如下:

  1. 回归分析获取因子收益率:
  • 在每个交易日$t$,使用 period 个交易日前的因子值(因子暴露)作为自变量,期货品种收益率作为因变量进行带截距项的 OLS 回归。 $R_t = \lambda_{t}C_{t} + \beta_{t-1}\lambda_{t-1} + \alpha_{t}$
  • 获取当天的各个因子收益率$\lambda_{t}$。
  1. 累加因子收益率取平均:
  • 通过滑动窗口周期的,获得不同时间段的因子收益率,然后累加各个因子的收益率取平均。
  • 但是累加收益率的时候,我们可以考虑因子收益率有效性的约束条件。使用回归判断如果因子对应的系数p值小于0.1,证明因子对因变量有着较为显著的作用,那么累加这个因子的收益率,反之,如果因子对应的p值大于0.1,证明该因子在该阶段是无效的,那么将该因子的预期收益率定义为0,防止无效信息的干扰。
  1. 估计未来期望因子收益率:
  • 使用得到的各个因子平均收益率 $\lambda_{t}$,和今天的因子数值进行相乘,获取因子组合数值。

这种方法利用历史因子收益率对未来收益率进行预测,并结合了因子收益率有效性的约束条件,用来构建多空组合策略。

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检验是显著的,合成因子对收益率具有显著的正向预测作用。这就是使用历史因子收益率加权法进行因子合成的演示,大家可以更换不同的参数和品种试验一下。另外,对于函数中因子的收益率系数,我们也可以使用半筛的方法进行不同权重的合成。

动态因子收益率加权法

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在一般的编程软件上都有内置函数可以直接完成计算,我们不需要自己编写代码。我们来看下代码实现。

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用来获取基本面数据基差和仓差,参数为具体的日期和合约。这上面的几个函数都是用来获取因子的基本数据


更多内容