Skip to content

Commit 8e545a6

Browse files
committed
option helper
1 parent 6144df4 commit 8e545a6

File tree

2 files changed

+297
-0
lines changed

2 files changed

+297
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# @Date : 2022/4/15
4+
# @Author : sukai
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# -*- coding: utf-8 -*-
2+
# Helpers for calculating the Greeks of options
3+
# @Date : 2022/4/15
4+
# @Author : sukai
5+
import argparse
6+
import sys
7+
from typing import List
8+
9+
import QuantLib as ql
10+
11+
12+
class FDDividendOptionHelper(ql.DividendVanillaOption):
13+
def __init__(self,
14+
engine_class,
15+
option_type: ql.Option,
16+
underlying: float,
17+
strike: float,
18+
risk_free_rate: float,
19+
dividend_rate: float,
20+
volatility: float,
21+
reference_date: ql.Date,
22+
exercise: ql.Exercise,
23+
dates: List[ql.Date] = None,
24+
dividends: List[float] = None,
25+
calendar: ql.Calendar = ql.NullCalendar(),
26+
day_counter: ql.DayCounter = ql.Actual365Fixed()
27+
):
28+
self.dates = dates if dates is not None else list()
29+
self.dividends = dividends if dividends is not None else list()
30+
super(FDDividendOptionHelper, self).__init__(ql.PlainVanillaPayoff(option_type, strike), exercise, self.dates,
31+
self.dividends)
32+
33+
self.option_type = option_type
34+
self.underlying = underlying
35+
self.strike = strike
36+
self.risk_free_rate = risk_free_rate
37+
self.dividend_rate = dividend_rate
38+
self.volatility = volatility
39+
self.reference_date = reference_date
40+
self.exercise = exercise
41+
self.calendar = calendar
42+
self.day_counter = day_counter
43+
self.engine_class = engine_class
44+
self.bsm_process: ql.GeneralizedBlackScholesProcess
45+
self.underlying_quote = ql.SimpleQuote(underlying)
46+
self.risk_free_rate_quote = ql.SimpleQuote(risk_free_rate)
47+
self.dividend_rate_quote = ql.SimpleQuote(dividend_rate)
48+
self.volatility_quote = ql.SimpleQuote(volatility)
49+
50+
self.setPricingEngine(self.get_engine(reference_date))
51+
52+
def get_engine(self, date: ql.Date) -> ql.PricingEngine:
53+
spot_handle = ql.QuoteHandle(self.underlying_quote)
54+
risk_free_rate_handle = ql.QuoteHandle(self.risk_free_rate_quote)
55+
volatility_handle = ql.QuoteHandle(self.volatility_quote)
56+
57+
r_ts = ql.YieldTermStructureHandle(ql.FlatForward(date, risk_free_rate_handle, self.day_counter))
58+
vol_ts = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(date, self.calendar, volatility_handle,
59+
self.day_counter))
60+
61+
if self.dividend_rate is not None:
62+
dividend_handle = ql.QuoteHandle(self.dividend_rate_quote)
63+
d_ts = ql.YieldTermStructureHandle(ql.FlatForward(date, dividend_handle, self.day_counter))
64+
self.bsm_process = ql.BlackScholesMertonProcess(spot_handle,
65+
d_ts,
66+
r_ts,
67+
vol_ts)
68+
else:
69+
self.bsm_process = ql.BlackScholesProcess(spot_handle,
70+
r_ts,
71+
vol_ts)
72+
73+
return self.engine_class(self.bsm_process)
74+
75+
def theta(self) -> float:
76+
yesterday = self.reference_date - ql.Period(1, ql.Days)
77+
tomorrow = self.reference_date + ql.Period(1, ql.Days)
78+
if self.exercise.lastDate() == tomorrow:
79+
dt = self.day_counter.yearFraction(yesterday, self.reference_date)
80+
else:
81+
dt = self.day_counter.yearFraction(yesterday, tomorrow)
82+
self.setPricingEngine(self.get_engine(tomorrow))
83+
value_p = self.NPV()
84+
self.setPricingEngine(self.get_engine(yesterday))
85+
value_m = self.NPV()
86+
87+
theta = (value_p - value_m) / dt
88+
self.setPricingEngine(self.get_engine(self.reference_date))
89+
return theta / ql.Daily
90+
91+
def vega(self) -> float:
92+
return self.numeric_first_order(self.volatility_quote)
93+
94+
def rho(self) -> float:
95+
return self.numeric_first_order(self.risk_free_rate_quote)
96+
97+
def implied_volatility(self, price: float, accuracy: float = 1.0e-4, max_evaluations: int = 100,
98+
min_vol: float = 1.0e-7, max_vol: float = 4.0, try_times: int = 4) -> float:
99+
"""
100+
101+
:param price: expected option NPV
102+
:param accuracy:
103+
:param max_evaluations:
104+
:param min_vol:
105+
:param max_vol:
106+
:param try_times:
107+
:return:
108+
"""
109+
try:
110+
return self.impliedVolatility(price, self.bsm_process, accuracy, max_evaluations, min_vol, max_vol)
111+
except RuntimeError as e:
112+
if try_times == 0:
113+
return 0
114+
elif 'root not bracketed' in str(e):
115+
return self.implied_volatility(price, accuracy, max_evaluations, min_vol, max_vol * 2, try_times-1)
116+
else:
117+
raise e
118+
119+
def update_implied_volatility(self, implied_volatility: float):
120+
self.volatility_quote.setValue(implied_volatility)
121+
122+
def numeric_first_order(self, quote: ql.SimpleQuote) -> float:
123+
"""
124+
reference: https://github.com/lballabio/QuantLib/issues/779
125+
https://github.com/frgomes/jquantlib/blob/master/jquantlib-helpers/src/main/java/org/jquantlib/helpers/FDDividendOptionHelper.java
126+
:param self:
127+
:param quote:
128+
:return:
129+
"""
130+
sigma0 = quote.value()
131+
h = sigma0 * 1E-4
132+
quote.setValue(sigma0 - h)
133+
p_minus = self.NPV()
134+
quote.setValue(sigma0 + h)
135+
p_plus = self.NPV()
136+
quote.setValue(sigma0)
137+
return (p_plus - p_minus) / (2 * h) / 100
138+
139+
140+
class FDAmericanDividendOptionHelper(FDDividendOptionHelper):
141+
"""American option"""
142+
143+
def __init__(self,
144+
option_type: ql.Option,
145+
underlying: float,
146+
strike: float,
147+
risk_free_rate: float,
148+
dividend_rate: float,
149+
volatility: float,
150+
settlement_date: ql.Date,
151+
expiration_date: ql.Date,
152+
dates: List[ql.Date] = None,
153+
dividends: List[float] = None,
154+
calendar: ql.Calendar = ql.NullCalendar(),
155+
day_counter: ql.DayCounter = ql.Actual365Fixed(),
156+
engine_class: ql.PricingEngine.__class__ = ql.FdBlackScholesVanillaEngine
157+
):
158+
"""
159+
160+
:param option_type: ql.Option.Call or ql.Option.Put
161+
:param underlying:
162+
:param strike:
163+
:param risk_free_rate:
164+
:param dividend_rate:
165+
:param volatility:
166+
:param settlement_date:
167+
:param expiration_date:
168+
:param dates:
169+
:param dividends:
170+
:param calendar:
171+
:param day_counter:
172+
:param engine_class:
173+
174+
>>> helper = FDAmericanDividendOptionHelper(ql.Option.Call, 985, 990, 0.017, 0, 0.6153, ql.Date(14, 4, 2022), ql.Date(22, 4, 2022))
175+
>>> print(f'value:{helper.NPV()}')
176+
>>> print(f'delta:{helper.delta()}')
177+
>>> print(f'gamma:{helper.gamma()}')
178+
>>> print(f'theta:{helper.theta()}')
179+
>>> print(f'vega:{helper.vega()}')
180+
>>> print(f'rho:{helper.rho()}')
181+
"""
182+
super(FDAmericanDividendOptionHelper, self).__init__(engine_class=engine_class,
183+
option_type=option_type, underlying=underlying,
184+
strike=strike, risk_free_rate=risk_free_rate,
185+
dividend_rate=dividend_rate, volatility=volatility,
186+
reference_date=settlement_date,
187+
exercise=ql.AmericanExercise(settlement_date,
188+
expiration_date),
189+
dates=dates, dividends=dividends, calendar=calendar,
190+
day_counter=day_counter)
191+
192+
193+
class FDEuropeanDividendOptionHelper(FDDividendOptionHelper):
194+
"""European option"""
195+
196+
def __init__(self,
197+
option_type: ql.Option,
198+
underlying: float,
199+
strike: float,
200+
risk_free_rate: float,
201+
dividend_rate: float,
202+
volatility: float,
203+
settlement_date: ql.Date,
204+
expiration_date: ql.Date,
205+
dates: List[ql.Date] = None,
206+
dividends: List[float] = None,
207+
calendar: ql.Calendar = ql.NullCalendar(),
208+
day_counter: ql.DayCounter = ql.Actual365Fixed(),
209+
engine_class: ql.PricingEngine.__class__ = ql.FdBlackScholesVanillaEngine
210+
):
211+
super(FDEuropeanDividendOptionHelper, self).__init__(engine_class=engine_class,
212+
option_type=option_type, underlying=underlying,
213+
strike=strike, risk_free_rate=risk_free_rate,
214+
dividend_rate=dividend_rate, volatility=volatility,
215+
reference_date=settlement_date,
216+
exercise=ql.EuropeanExercise(expiration_date),
217+
dates=dates, dividends=dividends, calendar=calendar,
218+
day_counter=day_counter)
219+
220+
221+
if __name__ == '__main__':
222+
parser = argparse.ArgumentParser(description='Helpers for calculating the Greeks of options')
223+
224+
parser.add_argument('-t', '--type', type=str,
225+
help='Option type, CALL/PUT (C/P)', choices=('C', 'P', 'CALL', 'PUT'))
226+
parser.add_argument('-u', '--underlying', type=float,
227+
help='The price of the underlying asset')
228+
parser.add_argument('-p', '--strike', type=float,
229+
help='The strike price at expiration')
230+
parser.add_argument('-d', '--dividend', type=float, nargs='?', default=0,
231+
help='The dividend rate')
232+
parser.add_argument('-r', '--rfrate', type=float, nargs='?', default=0.01,
233+
help='The risk free rate')
234+
parser.add_argument('-v', '--volatility', type=float, nargs='?', default=0,
235+
help='The implied volatility')
236+
parser.add_argument('-s', '--settlement', type=str,
237+
help='The settlement date, like "2022-04-22"')
238+
parser.add_argument('-e', '--expiration', type=str,
239+
help='The expiration date, like "2022-04-29"')
240+
parser.add_argument('-n', '--npv', type=float, nargs='?',
241+
help='The expected NPV of option')
242+
parser.add_argument('-a', '--ask', type=float, nargs='?',
243+
help='The ask price of option')
244+
parser.add_argument('-b', '--bid', type=float, nargs='?',
245+
help='The bid price of option')
246+
parser.add_argument('-o', '--european', action='store_true',
247+
help='Is european option')
248+
249+
args = parser.parse_args()
250+
251+
if args.type.upper() in ('C', 'CALL'):
252+
ql_option_type = ql.Option.Call
253+
elif args.type.upper() in ('P', 'PUT'):
254+
ql_option_type = ql.Option.Put
255+
else:
256+
parser.error('Wrong option type')
257+
if args.volatility is None and args.npv is None and args.ask is None and args.bid is None:
258+
parser.error('Must specify the volatility or option npv(or option\'s ask, bid)')
259+
260+
print(args.__dict__)
261+
settlement_date = ql.DateParser.parseFormatted(str(args.settlement).replace('/', '-'), '%Y-%m-%d')
262+
expiration_date = ql.DateParser.parseFormatted(str(args.expiration).replace('/', '-'), '%Y-%m-%d')
263+
264+
if args.european:
265+
helper_class = FDEuropeanDividendOptionHelper
266+
else:
267+
helper_class = FDAmericanDividendOptionHelper
268+
269+
helper = helper_class(option_type=ql_option_type,
270+
underlying=args.underlying,
271+
strike=args.strike,
272+
risk_free_rate=args.rfrate,
273+
dividend_rate=args.dividend,
274+
volatility=args.volatility,
275+
settlement_date=settlement_date,
276+
expiration_date=expiration_date)
277+
278+
if args.volatility is None or args.volatility == 0:
279+
expected_npv = None
280+
npv_msg = ''
281+
if args.ask and args.bid:
282+
expected_npv = (args.ask + args.bid) / 2
283+
npv_msg = f'with ask {args.ask}, bid {args.bid}, expected_npv {expected_npv}'
284+
else:
285+
expected_npv = args.npv
286+
npv_msg = f'with expected npv: {expected_npv}'
287+
288+
volatility = helper.implied_volatility(expected_npv)
289+
helper.update_implied_volatility(volatility)
290+
print(f'The implied volatility {npv_msg} is: {volatility}')
291+
292+
print(f'npv: {helper.NPV()}, delta:{helper.delta()}, gamma:{helper.gamma()}, theta:{helper.theta()}, '
293+
f'vega:{helper.vega()}, rho:{helper.rho()}')

0 commit comments

Comments
 (0)