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

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

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

操作
看涨期权(认购期权)
看跌期权(认沽期权)

我们简单说明一下各种期权类型的风险和收益,也就是关于盈利和亏损的上下限,又来献出这幅经典的图像。

image

对于买入看涨期权来说,我们的亏损最多就是权利金全部,而如果伴随标的资产的价格增高,盈利是无限的;对于卖出看涨期权的盈利和收益和买入看涨期权刚刚相反,它的盈利最多就是权利金,亏损是无限的;

对于买入看跌期权,盈利最多是标的资产价格变为0,亏损最多就是保证金;对于卖出看跌期权,恰恰相反。

作为一个买方,开仓的权利金亏损完是不需要补仓的,但是对于卖方来说,当亏损出现,还需要及时的进行补足保证金。从一方面来看,卖出期权的风险更大,为什么要当卖方呢?当然并不能仅仅从盈利大小来看,盈利期望的大小还需要乘以概率。巴菲特老爷子,就曾经是股指期货看跌期权的卖方,结果伴随美国股票指数的上升,赚得盆满锛满。

期权价值

下面来了解一下期权的计价方式,期权是有「虚实」之分的,具体指的是实值期权和虚值期权。实值期权,也称价内期权,是指期权的行权价格低于标的资产价格的看涨期权,或是行权价格高于标的资产价格的看跌期权;虚值期权,也称价外期权,是指期权的行权价格高于标的资产价格的看涨期权,或是行权价格低于标的资产价格的看跌期权。

期权类型 实值期权 虚值期权
看涨期权 行权价格低于标的资产 行权价格高于标的资产
看跌期权 行权价格高于标的资产 行权价格低于标的资产

我们举例说明一下,对于行权价为3600的螺纹钢看涨期权,当价格点位为3700点时,该期权为「实值期权」,意味着合约交割日如果维持价格不变的话可以以行权价3600便宜买入3700点位的指数;而如果价格点位低于行权价3600点,该期权为「虚值期权」,此时行权就会直接亏损。

但是没有实值的期权就不值钱了吗?当然不是的,期权的价值包括内在价值,也就是实值部分,还包括时间价值,可以等待价格变化达到盈利的点位。

期权价值 = 内在价值 + 时间价值

image

我们看这张图,这就是标准的T型报价图。中间是行权价格,从低到高排列,左边是认购期权,右边是认沽期权。期权的价值,也就是这里的最新价格,是内在价值和时间价值的和。最新标的资产价格是909,对于认购期权来说,当最新的价格大于行权价格,内在价值是正数,是实值期权;然后随着行权价格升高,内在价值降低直至为0,开始变为虚值期权,这时候的期权的价值开始由时间价值占主导的位置;对于右边的认沽期权,内在价值和时间价值走向刚好相反。

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。

以上呢,就是期权价值各部分的计算方式。

期权的了结方式和盈利计算。

期权的了结方式包括平仓和行权。期权在开仓以后,是随时可以平仓的。

image

比如我们进行期权开仓以后,对已持有的期权仓位进行反向操作叫作平仓。比如我们卖出一手看涨合约,开仓均价是15.5,当最新价格跌到7.5的时候,不考虑手续费,盈利就是(15.5-7.5)*结算单位,比如是10,就是盈利为80块钱。这是我们最常见的了结方式。

还有了结方式是行权了结,就是期权买方行使权利,看涨期权买方以约定的价格买入约定数量的标的期货,看跌期权买方以约定的价格卖出约定数量的标的期货,期权卖方不能行权。期权是有行权日期的,但是对于我们普通的散户来说,我们需要拿一个期权直到行权日吗?当然不是必须的。期权分为美式期权和欧式期权,欧式期权必须要等到行权日才能了结,国内属于欧式期权的品种有:铜期权、黄金期权、上证50ETF期权和沪深300股指期权,其他商品类的期权都是美式期权。

最后的了结方式是放弃,期权买方仅能在到期日当天提交放弃申请。对期权认购方来说,如果亏损大于权利金,期权买方通常选择放弃行权。期权的了结方式比期货多一个方式,相对来说更加灵活。

了解完期权的整体概念以后,我们来在优宽平台实现期权的买卖的操作,因为期权包括认购期权和认沽期权,买卖的方向分为买入合约和卖出合约,并且同时具有开仓和平仓。有不少的同学容易搞混,我们使用代码实现一下。

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),它是将市场价格中蕴含的对未来波动率的预期提取出的产物。

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的动态平衡。这对于套期保值的领域很是友好,在降低风险的情况下,实现自己初始的建仓目的。我们来看下具体的策略设计。


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的变化,发送消息可以选择手动的进行对冲期权的加仓或者加仓。大家可以根据自己想法进行进一步的优化。

视频参考链接:

《Deribit期权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区域动量策略。这个策略包括两个指标:

  1. RSI多头范围:RSI过去N天内在40到100之间波动。
  2. RSI多头动量:RSI的极值高点在N天内大于70

这个策略相当于一个双重认证,当RSI必须在过去的N天内在40到100之间波动的时候,并且在这期间出现了一个大于动量阈值70的高点。所以具体的交易逻辑是:

  1. 当RSI多头范围和多头动量条件都成立时,开仓。
  2. 当RSI多头范围和多头动量条件都不再成立时,平仓。

原始的策略中多头范围设置的区间变量是100,但是对于目前的交易环境多空头转换的很快,所以我们将多头范围区间设置为30。

我们来看下具体代码的编写,首先设置策略参数变量,rsiPeriod,rsi计算周期,rsiWindow,rsi多头范围,rsi_lower和rsi_upper是回看周期内最低和最高阈值,rsi_highest是rsi动量阈值。接下来进入主函数,设置合约,获取k线,计算rsi值,周期设置为rsiPeriod参数,然后取回看范围rsiList,可以使用数组的slice函数,参数填写-rsiWindow,取到回看周期内的rsi值。

接着进行多头范围和多头动量的判断。在原始的python代码里,由于没有使用封装,所以这部分的设置显得较为复杂一点,我们看下能否编写的更加简单一点。多头范围是判断在回看周期内rsi的值都需要在一定范围,可以使用数组的every函数,判断每一个值是否大于最低值并且小于最高值;接着多头动量,判断回看周期内是否有一个值大于


更多内容