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