与QuantLib配合使用

曾有一段时间,在Stack Overflow上有一个有趣的问题:

为什么使用QuantLib进行期权定价如此繁琐,就算一个简单的香草期权,也要使用很多行代码来实现?

确实如此,当你使用QuantLib定价香草期权,你可能会这样做::

# Set Evaluation Date
today = ql.Date(18, 3, 2021)
ql.Settings.instance().evaluationDate = today

# Build An Option
# Set Payoff
payoff=ql.PlainVanillaPayoff(ql.Option.Call, 100.0)

# Set Exercise Date
europeanExercise=ql.EuropeanExercise(ql.Date(18, 11, 2021))
option = ql.EuropeanOption(payoff, europeanExercise)

# Set Initial Stock Price, Risk-Free Rate, Volatility
u = ql.SimpleQuote(100.0)      # Initial Stock Price
r = ql.SimpleQuote(0.05)       # Risk-Free Rate
sigma = ql.SimpleQuote(0.20)    # Volatility

# Set Term Structure Of Curve
riskFreeCurve = ql.FlatForward(0, ql.TARGET(), ql.QuoteHandle(r), ql.Actual360())
volatility = ql.BlackConstantVol(0, ql.TARGET(), ql.QuoteHandle(sigma), ql.Actual360())

# Initialize Stochastic Process And Pricing Enging
process = ql.BlackScholesProcess(ql.QuoteHandle(u),
                              ql.YieldTermStructureHandle(riskFreeCurve),
                              ql.BlackVolTermStructureHandle(volatility))
engine = ql.AnalyticEuropeanEngine(process)

# Set Enging To Option
option.setPricingEngine(engine)

# Print Result
print(f'Option Value:{option.NPV():.4f}')
print("%-12s: %4.4f" %("Delta", option.delta() ))
print("%-12s: %4.4f" %("Gamma", option.gamma() ))
print("%-12s: %4.4f" %("Theta", option.vega()))

但如果你有十个期权,想对他们同时进行定价,或者你只是想探索一下,期权的价格是如何随着市场情况改变的,要如何方便地做到呢?无论你是学生还是衍生品交易员,你有时可能会有一个数据表,它看起来像这样:

OptionCode

PayOff

ExerciseType

ExerciseDate

StockPrice

RiskFreeRate

Sigma

A

PlainVanilla

European

2021-11-18

100.0

0.05

0.20

B

PlainVanilla

European

2022-03-20

97.6

0.032

0.17

在RiskQuantLib中进行期权定价,我们需要创建我们自己的欧式期权定价模板。首先我们使用终端命令来新建一个工程::

newRQL yourProjectPath

之后,我们打开 config.py,编辑并使得它看起来像这样:

#-|instrument: myEuropeanOption
#-|instrument-ParentQuantLibClassName: myEuropeanOption@EuropeanOption
#-|instrument-DefaultInstrumentType: myEuropeanOption@myEuropeanOption

之后我们继续向 config.py 文件添加内容:

#-|attribute: myEuropeanOption.myPayOff@qlPayOff, myEuropeanOption.myExercise@qlExercise, myEuropeanOption.underlyingStockPrice@qlQuote, myEuropeanOption.riskFreeRate@qlQuote, myEuropeanOption.sigma@qlQuote

然后我们使用终端命令来编译工程::

cd yourProjectPath
python build.py

现在我们有了一个全新的RiskQuantLib来便于进行欧式期权定价,RiskQuantLib已经自动创建了模板类,数据类型类。我们要更改这些类文件,使得他们符合我们的数据处理项目。我们打开并编辑 RiskQuantLib/Property/QlExercise/qlExercise.py,使得它看起来像这样::

#!/usr/bin/python
# coding = utf-8

import QuantLib as ql
import pandas as pd
from RiskQuantLib.Property.property import property

class qlExercise(property):

    def __nullFunction__(self):
        pass

    def __init__(self, value : pd.Timestamp):
        value = ql.EuropeanExercise(ql.Date(value.day, value.month, value.year))
        super(qlExercise,self).__init__(value)

    def setValue(self,value : pd.Timestamp):
        self.value = ql.EuropeanExercise(ql.Date(value.day, value.month, value.year))

在这一步,我们打包了QuantLib中关于行权的代码,以便于我们在仅仅传入一个到期日期的情况下使用它。当这一步完成后,我们就不需要每定价一个欧式期权都定义一次行权。

接着,我们打开并编辑 RiskQuantLib/Property/QlPayOff/qlPayOff.py,使得它看起来像这样::

#!/usr/bin/python
# coding = utf-8

import QuantLib as ql
from RiskQuantLib.Property.property import property

class qlPayOff(property):

    def __nullFunction__(self):
        pass

    def __init__(self, value : float):
        value = ql.PlainVanillaPayoff(ql.Option.Call, value)
        super(qlPayOff,self).__init__(value)

    def setValue(self, value : float):
        self.value = ql.PlainVanillaPayoff(ql.Option.Call, value)

在这一步,我们打包了QuantLib中关于回报的代码,以便于我们可以在仅传入回报值的情况下使用。这一步完成后,我们就不需要每定价一个欧式期权都定义一次回报。

接着,我们打开并编辑 RiskQuantLib/Property/QlQuote/qlQuote.py,使得它看起来像这样::

#!/usr/bin/python
# coding = utf-8

import QuantLib as ql
from RiskQuantLib.Property.property import property

class qlQuote(property):

    def __nullFunction__(self):
        pass

    def __init__(self, value : float):
        value = ql.SimpleQuote(value)
        super(qlQuote,self).__init__(value)

    def setValue(self,value):
        self.value.setValue(value)

最后的准备工作是编辑 RiskQuantLib/Instrument/Security/MyEuropeanOption/myEuropeanOption.py,使得它看起来像这样::

#!/usr/bin/python
# coding = utf-8

import QuantLib as ql
from RiskQuantLib.Instrument.Security.security import security
from QuantLib import EuropeanOption
from RiskQuantLib.Auto.Instrument.Security.MyEuropeanOption.myEuropeanOption import setMyEuropeanOption

class myEuropeanOption(security,EuropeanOption,setMyEuropeanOption):

    def __nullFunction__(self):
        pass

    def __init__(self, codeString,nameString,securityTypeString = 'myEuropeanOption'):
        security.__init__(self,codeString,nameString,securityTypeString)

    def iniPricingModule(self, *args):
        EuropeanOption.__init__(self,*args)

    def pricing(self):
        self.iniPricingModule(self.myPayOff,self.myExercise)
        riskFreeCurve = ql.FlatForward(0, ql.TARGET(), ql.QuoteHandle(self.riskFreeRate), ql.Actual360())
        volatility = ql.BlackConstantVol(0, ql.TARGET(), ql.QuoteHandle(self.sigma), ql.Actual360())
        process = ql.BlackScholesProcess(ql.QuoteHandle(self.underlyingStockPrice),
                                         ql.YieldTermStructureHandle(riskFreeCurve),
                                         ql.BlackVolTermStructureHandle(volatility))
        engine = ql.AnalyticEuropeanEngine(process)
        self.setPricingEngine(engine)

        # Calculate Result
        self.npvValue = self.NPV()
        self.deltaValue = self.delta()
        self.gammaValue = self.gamma()
        self.vegaValue = self.vega()

现在一切都准备好了,我们换到项目根目录下的 main.py 文件,定价刚才提到的期权,我们只需要::

# With RiskQuantLib

# Don't Forget to Set Evaluation Date
today = ql.Date(18, 3, 2021)
ql.Settings.instance().evaluationDate = today

from RiskQuantLib.module import *
vanillaOption = myEuropeanOption("A","A")
vanillaOption.setMyPayOff(100)
vanillaOption.setMyExercise(pd.Timestamp("20211118"))
vanillaOption.setUnderlyingStockPrice(100)
vanillaOption.setRiskFreeRate(0.05)
vanillaOption.setSigma(0.20)
vanillaOption.pricing()

代码变得更加可读了不是么。更重要的是,你可以更改参数的值,来方便地进行重定价。如果需要对一个新的期权进行定价,只需要初始化另一个实例::

# You already set valuation date, don't need to do it again.

from RiskQuantLib.module import *
vanillaOption = myEuropeanOption("B","B")
vanillaOption.setMyPayOff(100)
vanillaOption.setMyExercise(pd.Timestamp("20220320"))
vanillaOption.setUnderlyingStockPrice(97.6)
vanillaOption.setRiskFreeRate(0.032)
vanillaOption.setSigma(0.17)
vanillaOption.pricing()

或者你可以更优雅地做到这一点,那就是使用RiskQuantLib模板列表。记得我们有一个这样的数据表:

OptionCode

PayOff

ExerciseType

ExerciseDate

StockPrice

RiskFreeRate

Sigma

A

PlainVanilla

European

2021-11-18

100.0

0.05

0.20

B

PlainVanilla

European

2022-03-20

97.6

0.032

0.17

我们在项目根目录下将它保存为excel文件,命名为 European_Option.xlsx,接着在 main.py 我们这样编写代码:

# With RiskQuantLib List
# Set Evaluation Date
today = ql.Date(18, 3, 2021)
ql.Settings.instance().evaluationDate = today

from RiskQuantLib.module import *
df = pd.read_excel(path+os.sep+'European_Option.xlsx')

vanillaOptionList = myEuropeanOptionList()
vanillaOptionList.addMyEuropeanOptionSeries(df['OptionCode'],df['OptionCode'])
vanillaOptionList.setMyPayOff(df['OptionCode'],[100 for payoff in df['OptionCode']])
vanillaOptionList.setMyExercise(df['OptionCode'],[pd.Timestamp(date) for date in df['ExerciseDate']])
vanillaOptionList.setUnderlyingStockPrice(df['OptionCode'],df['StockPrice'])
vanillaOptionList.setRiskFreeRate(df['OptionCode'],df['RiskFreeRate'])
vanillaOptionList.setSigma(df['OptionCode'],df['Sigma'])
vanillaOptionList.execFunc('pricing')

如果你想保存结果,你只需要将它转换为dataframe::

result = pd.DataFrame(vanillaOptionList[['code','npvValue','deltaValue','gammaValue','vegaValue']])

别忘了将你的工程保存为工程模板,这样的期权定价模板可以被反复使用,这是RiskQuantLib很重要的一点。

为了保存为模板,你应该首先删除 European_Option.xlsx,并且清空 main.py 中的代码,因为这些数据和操作是不可以被反复使用的,模板工程只需要保留数据处理的逻辑。当做完了这些后,你可以打开终端运行命令::

saveRQL yourProjectPath europeanOption

当下次你遇见欧式期权定价任务时,你可以使用终端命令来恢复模板工程::

tplRQL europeanOption yourNewProjectPath