def test_FinFXMktVolSurface1(verboseCalibration): ########################################################################### if 1 == 1: # Example from Book extract by Iain Clark using Tables 3.3 and 3.4 # print("EURUSD EXAMPLE CLARK") valueDate = FinDate(10, 4, 2020) forName = "EUR" domName = "USD" forCCRate = 0.03460 # EUR domCCRate = 0.02940 # USD domDiscountCurve = FinDiscountCurveFlat(valueDate, domCCRate) forDiscountCurve = FinDiscountCurveFlat(valueDate, forCCRate) currencyPair = forName + domName spotFXRate = 1.3465 tenors = ['1M', '2M', '3M', '6M', '1Y', '2Y'] atmVols = [21.00, 21.00, 20.750, 19.400, 18.250, 17.677] marketStrangle25DeltaVols = [0.65, 0.75, 0.85, 0.90, 0.95, 0.85] riskReversal25DeltaVols = [-0.20, -0.25, -0.30, -0.50, -0.60, -0.562] marketStrangle10DeltaVols = [2.433, 2.83, 3.228, 3.485, 3.806, 3.208] riskReversal10DeltaVols = [ -1.258, -1.297, -1.332, -1.408, -1.359, -1.208 ] notionalCurrency = forName atmMethod = FinFXATMMethod.FWD_DELTA_NEUTRAL deltaMethod = FinFXDeltaMethod.SPOT_DELTA volFunctionType = FinVolFunctionTypes.CLARK5 alpha = 0.5 # FIT WINGS AT 10D if ALPHA = 1.0 fxMarketPlus = FinFXVolSurfacePlus( valueDate, spotFXRate, currencyPair, notionalCurrency, domDiscountCurve, forDiscountCurve, tenors, atmVols, marketStrangle25DeltaVols, riskReversal25DeltaVols, marketStrangle10DeltaVols, riskReversal10DeltaVols, alpha, atmMethod, deltaMethod, volFunctionType) fxMarketPlus.checkCalibration(False) if 1 == 0: # PLOT_GRAPHS: fxMarketPlus.plotVolCurves() plt.figure() dbns = fxMarketPlus.impliedDbns(0.5, 2.0, 1000) for i in range(0, len(dbns)): plt.plot(dbns[i]._x, dbns[i]._densitydx) plt.title(volFunctionType) print("SUM:", dbns[i].sum())
def test_FinFXMktVolSurface2(verboseCalibration): #print("==============================================================") # Example from Book extract by Iain Clarke using Tables 3.3 and 3.4 # print("EURJPY EXAMPLE CLARK") valueDate = FinDate(10, 4, 2020) forName = "EUR" domName = "JPY" forCCRate = 0.0294 # EUR domCCRate = 0.0171 # USD domDiscountCurve = FinDiscountCurveFlat(valueDate, domCCRate) forDiscountCurve = FinDiscountCurveFlat(valueDate, forCCRate) currencyPair = forName + domName spotFXRate = 90.72 tenors = ['1M', '2M', '3M', '6M', '1Y', '2Y'] atmVols = [21.50, 20.50, 19.85, 18.00, 15.95, 14.009] marketStrangle25DeltaVols = [0.35, 0.325, 0.300, 0.225, 0.175, 0.100] riskReversal25DeltaVols = [-8.350, -8.650, -8.950, -9.250, -9.550, -9.500] marketStrangle10DeltaVols = [3.704, 4.047, 4.396, 4.932, 5.726, 5.709] riskReversal10DeltaVols = [ -15.855, -16.467, -17.114, -17.882, -18.855, -18.217 ] alpha = 0.50 # Equally fit 10 and 25 Delta notionalCurrency = forName atmMethod = FinFXATMMethod.FWD_DELTA_NEUTRAL_PREM_ADJ deltaMethod = FinFXDeltaMethod.SPOT_DELTA_PREM_ADJ volFunctionType = FinVolFunctionTypes.CLARK5 fxMarketPlus = FinFXVolSurfacePlus( valueDate, spotFXRate, currencyPair, notionalCurrency, domDiscountCurve, forDiscountCurve, tenors, atmVols, marketStrangle25DeltaVols, riskReversal25DeltaVols, marketStrangle10DeltaVols, riskReversal10DeltaVols, alpha, atmMethod, deltaMethod, volFunctionType) # fxMarketPlus.checkCalibration(True) if PLOT_GRAPHS: fxMarketPlus.plotVolCurves() plt.figure() dbns = fxMarketPlus.impliedDbns(30, 120, 1000) for i in range(0, len(dbns)): plt.plot(dbns[i]._x, dbns[i]._densitydx) plt.title(volFunctionType) print("SUM:", dbns[i].sum())
class FXVolSurface(AbstractVolSurface): """Holds data for an FX vol surface and also interpolates vol surface, converts strikes to implied vols etc. """ def __init__( self, market_df=None, asset=None, field='close', tenors=market_constants.fx_options_tenor_for_interpolation, vol_function_type=market_constants.fx_options_vol_function_type, atm_method=market_constants.fx_options_atm_method, delta_method=market_constants.fx_options_delta_method, depo_tenor=market_constants.fx_options_depo_tenor, solver=market_constants.fx_options_solver, alpha=market_constants.fx_options_alpha): """Initialises object, with market data and various market conventions Parameters ---------- market_df : DataFrame Market data with spot, FX volatility surface, FX forwards and base depos asset : str Eg. 'EURUSD' field : str Market data field to use default - 'close' tenors : str(list) Tenors to be used vol_function_type : str What type of interpolation scheme to use default - 'CLARK5' (also 'CLARK', 'BBG' and 'SABR') atm_method : str How is the ATM quoted? Eg. delta neutral, ATMF etc. default - 'fwd-delta-neutral-premium-adj' delta_method : str Spot delta, forward delta etc. default - 'spot-delta' solver : str Which solver to use in FX vol surface calibration? default - 'nelmer-mead' alpha : float Between 0 and 1 (default 0.5) """ self._market_df = market_df self._tenors = tenors self._asset = asset self._field = field self._depo_tenor = depo_tenor self._market_util = MarketUtil() self._dom_discount_curve = None self._for_discount_curve = None self._spot = None self._value_date = None self._fin_fx_vol_surface = None self._df_vol_dict = None for_name_base = asset[0:3] dom_name_terms = asset[3:6] field = '.' + field # CAREFUL: need to divide by 100 for depo rate, ie. 0.0346 = 3.46% self._forCCRate = market_df[for_name_base + depo_tenor + field].values / 100.0 # 0.03460 # EUR self._domCCRate = market_df[dom_name_terms + depo_tenor + field].values / 100.0 # 0.02940 # USD self._spot_history = market_df[asset + field].values self._atm_vols = market_df[[asset + "V" + t + field for t in tenors]].values self._market_strangle25DeltaVols = market_df[[ asset + "25B" + t + field for t in tenors ]].values self._risk_reversal25DeltaVols = market_df[[ asset + "25R" + t + field for t in tenors ]].values self._market_strangle10DeltaVols = market_df[[ asset + "10B" + t + field for t in tenors ]].values self._risk_reversal10DeltaVols = market_df[[ asset + "10R" + t + field for t in tenors ]].values if vol_function_type == 'CLARK': self._vol_function_type = FinVolFunctionTypes.CLARK elif vol_function_type == 'CLARK5': self._vol_function_type = FinVolFunctionTypes.CLARK5 elif vol_function_type == 'BBG': self._vol_function_type = FinVolFunctionTypes.BBG # Note: currently SABR isn't fully implemented in FinancePy elif vol_function_type == 'SABR': self._vol_function_type = FinVolFunctionTypes.SABR elif vol_function_type == 'SABR3': self._vol_function_type = FinVolFunctionTypes.SABR3 # What does ATM mean? (for most if atm_method == 'fwd-delta-neutral': # ie. strike such that a straddle would be delta neutral self._atm_method = FinFXATMMethod.FWD_DELTA_NEUTRAL elif atm_method == 'fwd-delta-neutral-premium-adj': self._atm_method = FinFXATMMethod.FWD_DELTA_NEUTRAL_PREM_ADJ elif atm_method == 'spot': # ATM is spot self._atm_method = FinFXATMMethod.SPOT elif atm_method == 'fwd': # ATM is forward self._atm_method = FinFXATMMethod.FWD # How are the deltas quoted? if delta_method == 'spot-delta': self._delta_method = FinFXDeltaMethod.SPOT_DELTA elif delta_method == 'fwd-delta': self._delta_method = FinFXDeltaMethod.FORWARD_DELTA elif delta_method == 'spot-delta-prem-adj': self._delta_method = FinFXDeltaMethod.SPOT_DELTA_PREM_ADJ elif delta_method == 'fwd-delta-prem-adj': self._delta_method = FinFXDeltaMethod.FORWARD_DELTA_PREM_ADJ # Which solver to use in FX vol surface calibration if solver == 'nelmer-mead': self._solver = FinSolverTypes.NELDER_MEAD elif solver == 'nelmer-mead-numba': self._solver = FinSolverTypes.NELDER_MEAD_NUMBA elif solver == 'cg': self._solver = FinSolverTypes.CONJUGATE_GRADIENT self._alpha = alpha def build_vol_surface(self, value_date): """Builds the implied volatility surface for a particular value date and calculates the benchmark strikes etc. Before we do any sort of interpolation later, we need to build the implied_vol vol surface. Parameters ---------- value_date : str Value date (need to have market data for this date) asset : str Asset name depo_tenor : str Depo tenor to use default - '1M' field : str Market data field to use default - 'close' """ self._value_date = self._market_util.parse_date(value_date) value_fin_date = self._findate(self._value_date) date_index = self._market_df.index == value_date # TODO: add whole rates curve dom_discount_curve = FinDiscountCurveFlat(value_fin_date, self._domCCRate[date_index]) for_discount_curve = FinDiscountCurveFlat(value_fin_date, self._forCCRate[date_index]) self._dom_discount_curve = dom_discount_curve self._for_discount_curve = for_discount_curve self._spot = float(self._spot_history[date_index][0]) # New implementation in FinancePy also uses 10d for interpolation self._fin_fx_vol_surface = FinFXVolSurface( value_fin_date, self._spot, self._asset, self._asset[0:3], dom_discount_curve, for_discount_curve, self._tenors.copy(), self._atm_vols[date_index][0], self._market_strangle25DeltaVols[date_index][0], self._risk_reversal25DeltaVols[date_index][0], self._market_strangle10DeltaVols[date_index][0], self._risk_reversal10DeltaVols[date_index][0], self._alpha, atmMethod=self._atm_method, deltaMethod=self._delta_method, volatilityFunctionType=self._vol_function_type, finSolverType=self._solver) def calculate_vol_for_strike_expiry(self, K, expiry_date=None, tenor='1M'): """Calculates the implied_vol volatility for a given strike and tenor (or expiry date, if specified). The expiry date/broken dates are intepolated linearly in variance space. Parameters ---------- K : float Strike for which to find implied_vol volatility expiry_date : str (optional) Expiry date of option tenor : str (optional) Tenor of option default - '1M' Returns ------- float """ if expiry_date is not None: expiry_date = self._findate( self._market_util.parse_date(expiry_date)) return self._fin_fx_vol_surface.volatilityFromStrikeDate( K, expiry_date) else: try: tenor_index = self._get_tenor_index(tenor) return self.get_vol_from_quoted_tenor(K, tenor_index) except: pass return None def calculate_vol_for_delta_expiry(self, delta_call, expiry_date=None): """Calculates the implied_vol volatility for a given delta call and expiry date. The expiry date/broken dates are intepolated linearly in variance space. Parameters ---------- delta_call : float Delta for the strike for which to find implied volatility expiry_date : str (optional) Expiry date of option Returns ------- float """ if expiry_date is not None: expiry_date = self._findate( self._market_util.parse_date(expiry_date)) return self._fin_fx_vol_surface.volatilityFromDeltaDate( delta_call, expiry_date) return None def extract_vol_surface(self, num_strike_intervals=60): """Creates an interpolated implied vol surface which can be plotted (in strike space), and also in delta space for key strikes (ATM, 25d call and put). Also for key strikes converts from delta to strike space. Parameters ---------- num_strike_intervals : int Number of points to interpolate Returns ------- dict """ ## Modified from FinancePy code for plotting vol curves # columns = tenors df_vol_surface_strike_space = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) df_vol_surface_delta_space = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) # columns = tenors df_vol_surface_implied_pdf = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) # Conversion between main deltas and strikes df_deltas_vs_strikes = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) # ATM, 10d + 25d market strangle and 25d risk reversals df_vol_surface_quoted_points = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) # Note, at present we're not using 10d strikes quoted_strikes_names = [ 'ATM', 'STR_25D_MS', 'RR_25D_P', 'STR_10D_MS', 'RR_10D_P' ] key_strikes_names = [ 'K_10D_P', 'K_10D_P_MS', 'K_25D_P', 'K_25D_P_MS', 'ATM', 'K_25D_C', 'K_25D_C_MS', 'K_10D_C', 'K_10D_C_MS' ] # Get max/min strikes to interpolate (from the longest dated tenor) low_K = self._fin_fx_vol_surface._K_25D_P[-1] * 0.95 high_K = self._fin_fx_vol_surface._K_25D_C[-1] * 1.05 if num_strike_intervals is not None: # In case using old version of FinancePy try: implied_pdf_fin_distribution = self._fin_fx_vol_surface.impliedDbns( low_K, high_K, num_strike_intervals) except: pass for tenor_index in range(0, self._fin_fx_vol_surface._numVolCurves): # Get the quoted vol points tenor_label = self._fin_fx_vol_surface._tenors[tenor_index] atm_vol = self._fin_fx_vol_surface._atmVols[tenor_index] * 100 ms_25d_vol = self._fin_fx_vol_surface._mktStrangle25DeltaVols[ tenor_index] * 100 rr_25d_vol = self._fin_fx_vol_surface._riskReversal25DeltaVols[ tenor_index] * 100 ms_10d_vol = self._fin_fx_vol_surface._mktStrangle10DeltaVols[ tenor_index] * 100 rr_10d_vol = self._fin_fx_vol_surface._riskReversal10DeltaVols[ tenor_index] * 100 df_vol_surface_quoted_points[tenor_label] = pd.Series( index=quoted_strikes_names, data=[atm_vol, ms_25d_vol, rr_25d_vol, ms_10d_vol, rr_10d_vol]) # Do interpolation in strike space for the implied vols (if intervals have been specified) strikes = [] vols = [] if num_strike_intervals is not None: K = low_K dK = (high_K - low_K) / num_strike_intervals for i in range(0, num_strike_intervals): sigma = self.get_vol_from_quoted_tenor(K, tenor_index) * 100.0 strikes.append(K) vols.append(sigma) K = K + dK df_vol_surface_strike_space[tenor_label] = pd.Series( index=strikes, data=vols) try: df_vol_surface_implied_pdf[tenor_label] = pd.Series( index=implied_pdf_fin_distribution[tenor_index]._x, data=implied_pdf_fin_distribution[tenor_index]._densitydx) except: pass # Extract strikes for the quoted points (ie. 10d, 25d and ATM) key_strikes = [] key_strikes.append(self._fin_fx_vol_surface._K_10D_P[tenor_index]) key_strikes.append( self._fin_fx_vol_surface._K_10D_P_MS[tenor_index]) key_strikes.append(self._fin_fx_vol_surface._K_25D_P[tenor_index]) key_strikes.append( self._fin_fx_vol_surface._K_25D_P_MS[tenor_index]) key_strikes.append(self._fin_fx_vol_surface._K_ATM[tenor_index]) key_strikes.append(self._fin_fx_vol_surface._K_25D_C[tenor_index]) key_strikes.append( self._fin_fx_vol_surface._K_25D_C_MS[tenor_index]) key_strikes.append(self._fin_fx_vol_surface._K_10D_C[tenor_index]) key_strikes.append( self._fin_fx_vol_surface._K_10D_C_MS[tenor_index]) df_deltas_vs_strikes[tenor_label] = pd.Series( index=key_strikes_names, data=key_strikes) # Put a conversion between quoted deltas and strikes (eg. which is ATM in strike space, 25d call/put strikes) key_vols = [] for K, name in zip(key_strikes, key_strikes_names): sigma = self.get_vol_from_quoted_tenor(K, tenor_index) * 100.0 key_vols.append(sigma) df_vol_surface_delta_space[tenor_label] = pd.Series( index=key_strikes_names, data=key_vols) df_vol_dict = {} df_vol_dict['vol_surface_implied_pdf'] = df_vol_surface_implied_pdf df_vol_dict['vol_surface_strike_space'] = df_vol_surface_strike_space df_vol_dict['vol_surface_delta_space'] = df_vol_surface_delta_space df_vol_dict[ 'vol_surface_delta_space_exc_ms'] = df_vol_surface_delta_space[ ~df_vol_surface_delta_space.index.str.contains('_MS')] df_vol_dict['vol_surface_quoted_points'] = df_vol_surface_quoted_points df_vol_dict['deltas_vs_strikes'] = df_deltas_vs_strikes self._df_vol_dict = df_vol_dict return df_vol_dict def get_vol_from_quoted_tenor(self, K, tenor, gaps=None): if not (isinstance(tenor, int)): tenor_index = self._get_tenor_index(tenor) else: tenor_index = tenor if gaps is None: gaps = np.array([0.1]) params = self._fin_fx_vol_surface._parameters[tenor_index] t = self._fin_fx_vol_surface._texp[tenor_index] f = self._fin_fx_vol_surface._F0T[tenor_index] return volFunction(self._vol_function_type.value, params, np.array([K]), gaps, f, K, t) def get_atm_method(self): return self._atm_method def get_delta_method(self): return self._delta_method def get_all_market_data(self): return self._market_df def get_spot(self): return self._spot def get_atm_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['ATM'] def get_25d_call_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_25D_C'] def get_25d_put_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_25D_P'] def get_10d_call_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_10D_C'] def get_10d_put_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_10D_P'] def get_25d_call_ms_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_25D_C_MS'] def get_25d_put_ms_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_25D_P_MS'] def get_10d_call_ms_strike(self, expiry_date=None, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_10D_C_MS'] def get_10d_put_ms_strike(self, expiry_date=None, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_10D_P_MS'] def get_atm_quoted_vol(self, tenor): """The quoted ATM vol from the market (ie. which has NOT been obtained from build vol surface) Parameters ---------- tenor : str Tenor Returns ------- float """ return self._atm_vols[self._market_df.index == self._value_date][0][ self._get_tenor_index(tenor)] def get_atm_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor]['ATM'] def get_25d_call_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor]['K_25D_C'] def get_25d_put_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor]['K_25D_P'] def get_25d_call_ms_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor][ 'K_25D_C_MS'] def get_25d_put_ms_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor][ 'K_25D_P_MS'] def get_10d_call_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor]['K_10D_C'] def get_10d_put_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor]['K_10D_P'] def get_10d_call_ms_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor][ 'K_10D_C_MS'] def get_10d_put_ms_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor][ 'K_10D_P_MS'] def get_dom_discount_curve(self): return self._dom_discount_curve def get_for_discount_curve(self): return self._for_discount_curve def plot_vol_curves(self): if self._fin_fx_vol_surface is not None: self._fin_fx_vol_surface.plotVolCurves() def _findate(self, timestamp): return FinDate(timestamp.day, timestamp.month, timestamp.year, hh=timestamp.hour, mm=timestamp.minute, ss=timestamp.second)
class FXVolSurface(AbstractVolSurface): """Holds data for an FX vol surface and also interpolates vol surface, converts strikes to implied_vol vols etc. """ def __init__(self, market_df=None, asset=None, field='close', tenors=data_constants.fx_vol_tenor): self._market_df = market_df self._tenors = tenors self._asset = asset self._field = field self._market_util = MarketUtil() self._value_date = None self._fin_fx_vol_surface = None self._df_vol_dict = None self._vol_function_type = FinVolFunctionTypes.CLARKE.value def build_vol_surface(self, value_date, asset=None, depo_tenor='1M', field=None, atm_method=FinFXATMMethod.FWD_DELTA_NEUTRAL, delta_method=FinFXDeltaMethod.SPOT_DELTA): """Builds the implied_vol volatility for a particular value date and calculates the benchmark strikes etc. Before we do any sort of interpolation later, we need to build the implied_vol vol surface. Parameters ---------- value_date : str Value data (need to have market data for this date) asset : str Asset name depo_tenor : str Depo tenor to use default - '1M' field : str Market data field to use default - 'close' atm_method : FinFXATMMethod How is the ATM quoted? Eg. delta neutral, ATMF etc. default - FinFXATMMethod.FWD_DELTA_NEUTRAL delta_method : FinFXDeltaMethod Spot delta, forward delta etc. default - FinFXDeltaMethod.SPOT_DELTA """ value_date = self._market_util.parse_date(value_date) self._value_date = value_date market_df = self._market_df value_fin_date = self._findate( self._market_util.parse_date(value_date)) tenors = self._tenors # Change ON (overnight) to 1D (convention for financepy) # tenors_financepy = list(map(lambda b: b.replace("ON", "1D"), self._tenors.copy())) tenors_financepy = self._tenors.copy() if field is None: field = self._field field = '.' + field if asset is None: asset = self._asset for_name_base = asset[0:3] dom_name_terms = asset[3:6] notional_currency = for_name_base date_index = market_df.index == value_date # CAREFUL: need to divide by 100 for depo rate, ie. 0.0346 = 3.46% forCCRate = market_df[ for_name_base + depo_tenor + field][date_index].values[0] / 100.0 # 0.03460 # EUR domCCRate = market_df[ dom_name_terms + depo_tenor + field][date_index].values[0] / 100.0 # 0.02940 # USD currency_pair = for_name_base + dom_name_terms spot_fx_rate = float(market_df[currency_pair + field][date_index].values[0]) # For vols we do NOT need to divide by 100 (financepy does that internally) atm_vols = market_df[[currency_pair + "V" + t + field for t in tenors]][date_index].values[0] market_strangle25DeltaVols = market_df[[ currency_pair + "25B" + t + field for t in tenors ]][date_index].values[0] #[0.65, 0.75, 0.85, 0.90, 0.95, 0.85] risk_reversal25DeltaVols = market_df[[ currency_pair + "25R" + t + field for t in tenors ]][date_index].values[0] #[-0.20, -0.25, -0.30, -0.50, -0.60, -0.562] market_strangle10DeltaVols = market_df[[ currency_pair + "10B" + t + field for t in tenors ]][date_index].values[0] risk_reversal10DeltaVols = market_df[[ currency_pair + "10R" + t + field for t in tenors ]][date_index].values[0] dom_discount_curve = FinDiscountCurveFlat(value_fin_date, domCCRate) for_discount_curve = FinDiscountCurveFlat(value_fin_date, forCCRate) use_only_25d = True # Construct financepy vol surface (uses polynomial interpolation for determining vol between strikes) if use_only_25d: self._fin_fx_vol_surface = FinFXVolSurface( value_fin_date, spot_fx_rate, currency_pair, notional_currency, dom_discount_curve, for_discount_curve, tenors_financepy, atm_vols, market_strangle25DeltaVols, risk_reversal25DeltaVols, atm_method, delta_method) else: # New implementation in FinancePy also uses 10d for interpolation from financepy.market.volatility.FinFXVolSurfacePlus import FinFXVolSurfacePlus self._fin_fx_vol_surface = FinFXVolSurfacePlus( value_fin_date, spot_fx_rate, currency_pair, notional_currency, dom_discount_curve, for_discount_curve, tenors_financepy, atm_vols, market_strangle25DeltaVols, risk_reversal25DeltaVols, market_strangle10DeltaVols, risk_reversal10DeltaVols, atm_method, delta_method) def calculate_vol_for_strike_expiry(self, K, expiry_date=None, tenor='1M'): """Calculates the implied_vol volatility for a given strike Parameters ---------- K : float Strike for which to find implied_vol volatility expiry_date : str (optional) Expiry date of option (TODO not implemented) tenor : str (optional) Tenor of option default - '1M' Returns ------- """ # TODO interpolate for broken dates, not just quoted tenors if tenor is not None: try: tenor_index = self._get_tenor_index(tenor) return self.vol_function(K, tenor_index) except: pass return None def extract_vol_surface(self, num_strike_intervals=60): """Creates an interpolated implied vol surface which can be plotted (in strike space), and also in delta space for key strikes (ATM, 25d call and put). Also for key strikes converts from delta to strike space. Parameters ---------- num_strike_intervals : int Number of points to interpolate Returns ------- dict """ ## Modified from FinancePy code for plotting vol curves # columns = tenors df_vol_surface_strike_space = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) df_vol_surface_delta_space = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) # columns = tenors df_vol_surface_implied_pdf = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) # Conversion between main deltas and strikes df_deltas_vs_strikes = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) # ATM, 25d market strangle and 25d risk reversals df_vol_surface_quoted_points = pd.DataFrame( columns=self._fin_fx_vol_surface._tenors) # Note, at present we're not using 10d strikes quoted_strikes_names = ['ATM', 'STR_25D_MS', 'RR_25D_P'] key_strikes_names = [ 'K_25D_P', 'K_25D_P_MS', 'ATM', 'K_25D_C', 'K_25D_C_MS' ] # Get max/min strikes to interpolate (from the longest dated tenor) low_K = self._fin_fx_vol_surface._K_25D_P[-1] * 0.95 high_K = self._fin_fx_vol_surface._K_25D_C[-1] * 1.05 # In case using old version of FinancePy try: implied_pdf_fin_distribution = self._fin_fx_vol_surface.impliedDbns( low_K, high_K, num_strike_intervals) except: pass for tenor_index in range(0, self._fin_fx_vol_surface._numVolCurves): # Get the quoted vol points tenor_label = self._fin_fx_vol_surface._tenors[tenor_index] atm_vol = self._fin_fx_vol_surface._atmVols[tenor_index] * 100 ms_25d_vol = self._fin_fx_vol_surface._mktStrangle25DeltaVols[ tenor_index] * 100 rr_10d_vol = self._fin_fx_vol_surface._riskReversal25DeltaVols[ tenor_index] * 100 df_vol_surface_quoted_points[tenor_label] = pd.Series( index=quoted_strikes_names, data=[atm_vol, ms_25d_vol, rr_10d_vol]) # Do interpolation in strike space for the implied_vol vols strikes = [] vols = [] if num_strike_intervals is not None: K = low_K dK = (high_K - low_K) / num_strike_intervals for i in range(0, num_strike_intervals): sigma = self.vol_function(K, tenor_index) * 100.0 strikes.append(K) vols.append(sigma) K = K + dK df_vol_surface_strike_space[tenor_label] = pd.Series( index=strikes, data=vols) try: df_vol_surface_implied_pdf[tenor_label] = pd.Series( index=implied_pdf_fin_distribution[tenor_index]._x, data=implied_pdf_fin_distribution[tenor_index]._densitydx) except: pass # Extract strikes for the quoted points (ie. 25d and ATM) key_strikes = [] key_strikes.append(self._fin_fx_vol_surface._K_25D_P[tenor_index]) key_strikes.append( self._fin_fx_vol_surface._K_25D_P_MS[tenor_index]) key_strikes.append(self._fin_fx_vol_surface._K_ATM[tenor_index]) key_strikes.append(self._fin_fx_vol_surface._K_25D_C[tenor_index]) key_strikes.append( self._fin_fx_vol_surface._K_25D_C_MS[tenor_index]) df_deltas_vs_strikes[tenor_label] = pd.Series( index=key_strikes_names, data=key_strikes) # Put a conversion between quoted deltas and strikes (eg. which is ATM in strike space, 25d call/put strikes) key_vols = [] for K, name in zip(key_strikes, key_strikes_names): sigma = self.vol_function(K, tenor_index) * 100.0 key_vols.append(sigma) df_vol_surface_delta_space[tenor_label] = pd.Series( index=key_strikes_names, data=key_vols) df_vol_dict = {} df_vol_dict['vol_surface_implied_pdf'] = df_vol_surface_implied_pdf df_vol_dict['vol_surface_strike_space'] = df_vol_surface_strike_space df_vol_dict['vol_surface_delta_space'] = df_vol_surface_delta_space df_vol_dict['vol_surface_quoted_points'] = df_vol_surface_quoted_points df_vol_dict['deltas_vs_strikes'] = df_deltas_vs_strikes self._df_vol_dict = df_vol_dict return df_vol_dict def vol_function(self, K, tenor_index): params = self._fin_fx_vol_surface._parameters[tenor_index] t = self._fin_fx_vol_surface._texp[tenor_index] f = self._fin_fx_vol_surface._F0T[tenor_index] return volFunctionFAST(self._vol_function_type, params, f, K, t) def get_all_market_data(self): return self._market_df def get_atm_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['ATM'] def get_25d_call_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_25D_C'] def get_25d_put_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_25P_C'] def get_10d_call_strike(self, tenor=None): pass def get_10d_put_strike(self, tenor=None): pass def get_25d_call_ms_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_25D_C_MS'] def get_25d_put_ms_strike(self, tenor=None): return self._df_vol_dict['deltas_vs_strikes'][tenor]['K_25D_C_MS'] def get_10d_call_ms_strike(self, expiry_date=None, tenor=None): pass def get_10d_put_ms_strike(self, expiry_date=None, tenor=None): pass def get_atm_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor]['ATM'] def get_25d_call_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor]['K_25D_C'] def get_25d_put_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor]['K_25D_P'] def get_25d_call_ms_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor][ 'K_25D_C_MS'] def get_25d_put_ms_vol(self, tenor=None): return self._df_vol_dict['vol_surface_delta_space'][tenor][ 'K_25D_P_MS'] def get_10d_call_vol(self, tenor=None): pass def get_10d_put_vol(self, tenor=None): pass def get_10d_call_ms_vol(self, tenor=None): pass def get_10d_put_ms_vol(self, tenor=None): pass def plot_vol_curves(self): if self._fin_fx_vol_surface is not None: self._fin_fx_vol_surface.plotVolCurves()
def test_FinFXMktVolSurface3(verboseCalibration): ########################################################################### if 1 == 1: # Example from Book extract by Iain Clark using Tables 4.4 and 4.5 # where we examine the calibration to a full surface in Chapter 4 valueDate = FinDate(10, 4, 2020) forName = "EUR" domName = "USD" forCCRate = 0.03460 # EUR domCCRate = 0.02940 # USD domDiscountCurve = FinDiscountCurveFlat(valueDate, domCCRate) forDiscountCurve = FinDiscountCurveFlat(valueDate, forCCRate) currencyPair = forName + domName spotFXRate = 1.3465 tenors = ['1Y', '2Y'] atmVols = [18.250, 17.677] marketStrangle25DeltaVols = [0.95, 0.85] riskReversal25DeltaVols = [-0.60, -0.562] marketStrangle10DeltaVols = [3.806, 3.208] riskReversal10DeltaVols = [-1.359, -1.208] notionalCurrency = forName # I HAVE NO YET MADE DELTA METHOD A VECTOR FOR EACH TERM AS I WOULD # NEED TO DO AS DESCRIBED IN CLARK PAGE 70 atmMethod = FinFXATMMethod.FWD_DELTA_NEUTRAL deltaMethod = FinFXDeltaMethod.FORWARD_DELTA # THIS IS DIFFERENT volFunctionType = FinVolFunctionTypes.CLARK5 alpha = 0.5 # FIT WINGS AT 10D if ALPHA = 1.0 fxMarketPlus = FinFXVolSurfacePlus(valueDate, spotFXRate, currencyPair, notionalCurrency, domDiscountCurve, forDiscountCurve, tenors, atmVols, marketStrangle25DeltaVols, riskReversal25DeltaVols, marketStrangle10DeltaVols, riskReversal10DeltaVols, alpha, atmMethod, deltaMethod, volFunctionType) fxMarketPlus.checkCalibration(False) if 1==0: # PLOT_GRAPHS: fxMarketPlus.plotVolCurves() plt.figure() dbns = fxMarketPlus.impliedDbns(0.5, 2.0, 1000) for i in range(0, len(dbns)): plt.plot(dbns[i]._x, dbns[i]._densitydx) plt.title(volFunctionType) print("SUM:", dbns[i].sum()) # Test interpolation years = [1.0, 1.5, 2.0] dates = valueDate.addYears(years) strikes = np.linspace(1.0, 2.0, 20) if 1==0: volSurface = [] for k in strikes: volSmile = [] for dt in dates: vol = fxMarketPlus.volatilityFromStrikeDate(k, dt) volSmile.append(vol*100.0) print(k, dt, vol*100.0) volSurface.append(volSmile) fig = plt.figure() ax = fig.add_subplot(111, projection='3d') X, Y = np.meshgrid(years, strikes) zs = np.array(volSurface) Z = zs.reshape(X.shape) ax.plot_surface(X, Y, Z) ax.set_xlabel('Years') ax.set_ylabel('Strikes') ax.set_zlabel('Volatility') plt.show() ####################################################################### deltas = np.linspace(0.10, 0.90, 17) if 1==0: volSurface = [] for delta in deltas: volSmile = [] for dt in dates: (vol, k) = fxMarketPlus.volatilityFromDeltaDate(delta, dt) volSmile.append(vol*100.0) print(delta, k, dt, vol*100.0) volSurface.append(volSmile) fig = plt.figure() ax = fig.add_subplot(111, projection='3d') X, Y = np.meshgrid(years, deltas) zs = np.array(volSurface) Z = zs.reshape(X.shape) ax.plot_surface(X, Y, Z) ax.set_xlabel('Years') ax.set_ylabel('Delta') ax.set_zlabel('Volatility') plt.show()
def test_FinFXMktVolSurface1LONG(verboseCalibration): ########################################################################### if 1 == 1: # Example from Book extract by Iain Clarke using Tables 3.3 and 3.4 # print("EURUSD EXAMPLE CLARKE") valueDate = FinDate(10, 4, 2020) forName = "EUR" domName = "USD" forCCRate = 0.03460 # EUR domCCRate = 0.02940 # USD domDiscountCurve = FinDiscountCurveFlat(valueDate, domCCRate) forDiscountCurve = FinDiscountCurveFlat(valueDate, forCCRate) currencyPair = forName + domName spotFXRate = 1.3465 tenors = ['1M', '2M', '3M', '6M', '1Y', '2Y'] atmVols = [21.00, 21.00, 20.750, 19.400, 18.250, 17.677] marketStrangle25DeltaVols = [0.65, 0.75, 0.85, 0.90, 0.95, 0.85] riskReversal25DeltaVols = [-0.20, -0.25, -0.30, -0.50, -0.60, -0.562] marketStrangle10DeltaVols = [2.433, 2.83, 3.228, 3.485, 3.806, 3.208] riskReversal10DeltaVols = [-1.258, -1.297, -1.332, -1.408, -1.359, -1.208] if 1==1: tenors = ['1Y'] atmVols = [18.250] marketStrangle25DeltaVols = [0.950] riskReversal25DeltaVols = [-0.600] marketStrangle10DeltaVols = [3.806] riskReversal10DeltaVols = [-1.359] notionalCurrency = forName atmMethod = FinFXATMMethod.FWD_DELTA_NEUTRAL deltaMethod = FinFXDeltaMethod.SPOT_DELTA volFunctionType = FinVolFunctionTypes.CLARKE # EXPLORE AND TEST DIFFERENT CATEGORICAL PARAMETERS for atmMethod in FinFXATMMethod: for deltaMethod in FinFXDeltaMethod: for volFunctionType in FinVolFunctionTypes: fxMarket = FinFXVolSurfacePlus(valueDate, spotFXRate, currencyPair, notionalCurrency, domDiscountCurve, forDiscountCurve, tenors, atmVols, marketStrangle25DeltaVols, riskReversal25DeltaVols, marketStrangle10DeltaVols, riskReversal10DeltaVols, atmMethod, deltaMethod, volFunctionType) fxMarket.checkCalibration(verboseCalibration) if PLOT_GRAPHS: fxMarket.plotVolCurves() dbns = fxMarket.impliedDbns(0.00001, 5.0, 10000) for i in range(0, len(dbns)): plt.plot(dbns[i]._x, dbns[i]._densitydx) print("SUM:", dbns[i].sum())