Skip to content

Commit 7c0069b

Browse files
committed
Merge branch 'feature_optionlib' into 'dev'
Feature optionlib See merge request server/openapi/openapi-python-sdk!106
2 parents 16aef7c + d5fc40c commit 7c0069b

File tree

2 files changed

+299
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)