class FXForwardsPricer(AbstractPricer): """Prices forwards for odd dates which are not quoted using linear interpolation, eg. if we have forward points for 1W and 1M, and spot date but we want to price a 3W forward, or any arbitrary horizon date that lies in that interval Also calculates the implied deposit rate from FX spot, FX forward points and deposit rate. """ def __init__(self, market_df=None, quoted_delivery_df=None): self._calendar = Calendar() self._market_df = market_df self._quoted_delivery_df = quoted_delivery_df def price_instrument(self, cross, horizon_date, delivery_date, market_df=None, quoted_delivery_df=None, fx_forwards_tenor_for_interpolation=market_constants. fx_forwards_tenor_for_interpolation): """Creates an interpolated outright FX forward (and the associated points), for horizon dates/delivery dates given by the user from FX spot rates and FX forward points. This can be useful when we have an odd/broken date which isn't quoted. Uses linear interpolation between quoted dates to calculate the appropriate interpolated forward. Eg. if we ask for a delivery date in between 1W and 1M, we will interpolate between those. Parameters ---------- cross : str Currency pair horizon_date : DateTimeIndex Horizon dates for forward contracts delivery_date : DateTimeIndex Delivery dates for forward contracts market_df : DataFrame Contains FX spot and FX forward points data quoted_delivery_df : DataFrame (DateTimeIndex) Delivery dates for every quoted forward point fx_forwards_tenor_for_interpolation : str(list) Which forwards to use for interpolation Returns ------- DataFrame """ if market_df is None: market_df = self._market_df if quoted_delivery_df is None: quoted_delivery_df = self._quoted_delivery_df if quoted_delivery_df is None: quoted_delivery_df = self.generate_quoted_delivery( cross, market_df, quoted_delivery_df, fx_forwards_tenor_for_interpolation, cross) # Make horizon date and delivery date pandas DatetimeIndex if isinstance(horizon_date, pd.Timestamp): horizon_date = pd.DatetimeIndex([horizon_date]) delivery_date = pd.DatetimeIndex([delivery_date]) else: horizon_date = pd.DatetimeIndex(horizon_date) delivery_date = pd.DatetimeIndex(delivery_date) # Truncate the market data so covers only the dates where we want to price the forwards market_df = market_df[market_df.index.isin(horizon_date)] quoted_delivery_df = quoted_delivery_df[quoted_delivery_df.index.isin( horizon_date)] cal = cross # Get the spot date (different currency pairs have different conventions for this!) spot_date = self._calendar.get_spot_date_from_horizon_date( horizon_date, cal) # How many days between spot date (typically T+1 or T+2) and delivery date spot_delivery_days_arr = (delivery_date - spot_date).days spot_arr = market_df[cross + '.close'].values quoted_delivery_df, quoted_delivery_days_arr, forwards_points_arr, divisor = \ self._setup_forward_calculation(cross, spot_date, market_df, quoted_delivery_df, fx_forwards_tenor_for_interpolation) interpolated_outright_forwards_arr = _forwards_interpolate_numba( spot_arr, spot_delivery_days_arr, quoted_delivery_days_arr, forwards_points_arr, len(fx_forwards_tenor_for_interpolation)) interpolated_df = pd.DataFrame( index=market_df.index, columns=[ cross + '-interpolated-outright-forward.close', cross + "-interpolated-forward-points.close" ]) interpolated_df[ cross + '-interpolated-outright-forward.close'] = interpolated_outright_forwards_arr interpolated_df[cross + "-interpolated-forward-points.close"] = ( interpolated_outright_forwards_arr - spot_arr) * divisor return interpolated_df def get_day_count_conv(self, currency): if currency in market_constants.currencies_with_365_basis: return 365.0 return 360.0 def get_forwards_divisor(self, currency): # Typically most divisors of forward points are 10000.0 divisor = 10000.0 if currency in market_constants.fx_forwards_points_divisor_100: divisor = 100.0 if currency in market_constants.fx_forwards_points_divisor_1000: divisor = 1000.0 return divisor def _setup_forward_calculation(self, cross, spot_date, market_df, quoted_delivery_df, fx_forwards_tenor): cal = cross # Get the quoted delivery dates for every quoted tenor in our forwards market data # Eg. what's the delivery date for EURUSD SN, 1W etc. quoted_delivery_df = self.generate_quoted_delivery( cross, market_df, quoted_delivery_df, fx_forwards_tenor, cal) # Typically most divisors of forward points are 10000.0 (but JPY is an exception) divisor = self.get_forwards_divisor(cross[3:6]) forwards_points_arr = market_df[[ cross + x + ".close" for x in fx_forwards_tenor ]].values / divisor quoted_delivery_days_arr = np.zeros( (len(quoted_delivery_df.index), len(fx_forwards_tenor))) # Get difference between each quoted point and the spot date (NOT horizon date) # eg. for 1W this is generally 7 days, for 2W it's 14 days etc. for i, tenor in enumerate(fx_forwards_tenor): quoted_delivery_days_arr[:, i] = (pd.DatetimeIndex( quoted_delivery_df[cross + tenor + ".delivery"]) - spot_date).days return quoted_delivery_df, quoted_delivery_days_arr, forwards_points_arr, divisor def generate_quoted_delivery(self, cross, market_df, quoted_delivery_df, fx_forwards_tenor, cal): # Get the quoted delivery dates for every quoted tenor in our forwards market data # Eg. what's the delivery date for EURUSD SN, 1W etc. if quoted_delivery_df is None: quoted_delivery_df = pd.DataFrame( index=market_df.index, columns=[ cross + tenor + ".delivery" for tenor in fx_forwards_tenor ]) for tenor in fx_forwards_tenor: quoted_delivery_df[cross + tenor + ".delivery"] = \ self._calendar.get_delivery_date_from_horizon_date(quoted_delivery_df.index, tenor, cal=cal) return quoted_delivery_df def calculate_implied_depo( self, cross, implied_currency, market_df=None, quoted_delivery_df=None, fx_forwards_tenor=market_constants.fx_forwards_trading_tenor, depo_tenor=None): """Calculates implied deposit rates for a particular currency from spot, forward points and deposit rate for the other currency. Uses the theory of covered interest rate parity. See BIS publication from 2016, Covered interest parity lost: understanding the cross-currency basis downloadable from https://www.bis.org/publ/qtrpdf/r_qt1609e.pdf for an explanation Eg. using EURUSD spot, EURUSD 1W forward points and USDON deposit rate, we can calculate the implied deposit for EUR 1W Parameters ---------- cross : str Currency pair implied_currency : str Currency for which we want to imply deposit market_df : DataFrame With FX spot rate, FX forward points and deposit rates fx_forwards_tenor : str (list) Tenors of forwards where we want to imply deposit depo_tenor : str Deposit rate to use (default - ON) Returns ------- DataFrame """ if market_df is None: market_df = self._market_df if quoted_delivery_df is None: quoted_delivery_df = self._quoted_delivery_df if depo_tenor is None: depo_tenor = fx_forwards_tenor if not (isinstance(depo_tenor, list)): depo_tenor = [depo_tenor] * len(fx_forwards_tenor) cal = cross # Get the spot date (different currency pairs have different conventions for this!) spot_date = self._calendar.get_spot_date_from_horizon_date( market_df.index, cal) quoted_delivery_df, quoted_delivery_days_arr, forwards_points_arr, divisor = \ self._setup_forward_calculation(cross, spot_date, market_df, quoted_delivery_df, fx_forwards_tenor) spot_arr = market_df[cross + '.close'].values outright_forwards_arr = np.vstack( [spot_arr] * len(fx_forwards_tenor)).T + forwards_points_arr base_conv = self.get_day_count_conv(cross[0:3]) terms_conv = self.get_day_count_conv(cross[3:6]) # Infer base currency if implied_currency == cross[0:3]: original_currency = cross[3:6] depo_arr = market_df[[ original_currency + d + '.close' for d in depo_tenor ]].values / 100.0 implied_depo_arr = _infer_base_currency_depo_numba( spot_arr, outright_forwards_arr, depo_arr, quoted_delivery_days_arr, base_conv, terms_conv, len(fx_forwards_tenor)) # for i in range(len(implied_depo_arr)): # for j, tenor in enumerate(fx_forwards_tenor): # implied_depo_arr[i, j] = (((1 + depo_arr[i,j] * (quoted_delivery_days_arr[i, j] / terms_conv)) # / (outright_forwards_arr[i,j] / spot_arr[i])) - 1) \ # / (quoted_delivery_days_arr[i, j] / base_conv) # Infer terms currency if implied_currency == cross[3:6]: original_currency = cross[0:3] depo_arr = market_df[[ original_currency + d + '.close' for d in depo_tenor ]].values / 100.0 implied_depo_arr = _infer_terms_currency_depo_numba( spot_arr, outright_forwards_arr, depo_arr, quoted_delivery_days_arr, base_conv, terms_conv, len(fx_forwards_tenor)) # for i in range(len(implied_depo_arr)): # for j, tenor in enumerate(fx_forwards_tenor): # implied_depo_arr[i,j] = ((outright_forwards_arr[i,j] / spot_arr[i]) * # (1 + depo_arr[i,j] * (quoted_delivery_days_arr[i,j] / base_conv)) - 1) \ # / (quoted_delivery_days_arr[i,j] / terms_conv) return pd.DataFrame(index=market_df.index, columns=[ implied_currency + x + "-implied-depo.close" for x in fx_forwards_tenor ], data=implied_depo_arr * 100.0)
calendar.get_holidays(start_date='01 Jan 2000 00:10', end_date='31 Dec 2000', cal='EURUSD')) # Get the holidays (which are weekends) print( calendar.get_holidays(start_date='01 Jan 1999 00:50', end_date='31 Dec 1999', cal='WKY')) if run_example == 2 or run_example == 0: # Get delivery dates for these horizon dates - typically would use to get forward maturities print( calendar.get_delivery_date_from_horizon_date(pd.to_datetime( [pd.Timestamp('02 Nov 2020')]), 'ON', cal='EURUSD')) print( calendar.get_delivery_date_from_horizon_date(pd.to_datetime( [pd.Timestamp('02 Nov 2020')]), '1W', cal='EURUSD')) print( calendar.get_delivery_date_from_horizon_date(pd.to_datetime( [pd.Timestamp('27 Nov 2020')]), '1W', cal='EURUSD')) # Get 1M expires for these horizon dates - typically would use to get option expiries
class FXOptionsPricer(AbstractPricer): """Prices various vanilla FX options, using FinancePy underneath. """ def __init__(self, fx_vol_surface=None, premium_output=market_constants.fx_options_premium_output, delta_output=market_constants.fx_options_delta_output): self._calendar = Calendar() self._fx_vol_surface = fx_vol_surface self._fx_forwards_price = FXForwardsPricer() self._premium_output = premium_output self._delta_output = delta_output def price_instrument(self, cross, horizon_date, strike, expiry_date=None, vol=None, notional=1000000, contract_type='european-call', tenor=None, fx_vol_surface=None, premium_output=None, delta_output=None, depo_tenor=None, return_as_df=True): """Prices FX options for horizon dates/expiry dates given by the user from FX spot rates, FX volatility surface and deposit rates. Parameters ---------- cross : str Currency pair horizon_date : DateTimeIndex Horizon dates for options expiry_date : DateTimeIndex expiry dates for options market_df : DataFrame Contains FX spot, FX vol surface quotes, FX forwards and base depos Returns ------- DataFrame """ # if market_df is None: market_df = self._market_df if fx_vol_surface is None: fx_vol_surface = self._fx_vol_surface if premium_output is None: premium_output = self._premium_output if delta_output is None: delta_output = self._delta_output logger = LoggerManager().getLogger(__name__) field = fx_vol_surface._field # Make horizon date and expiry date pandas DatetimeIndex if isinstance(horizon_date, pd.Timestamp): horizon_date = pd.DatetimeIndex([horizon_date]) else: horizon_date = pd.DatetimeIndex(horizon_date) if expiry_date is not None: if isinstance(expiry_date, pd.Timestamp): expiry_date = pd.DatetimeIndex([expiry_date]) else: expiry_date = pd.DatetimeIndex(expiry_date) else: expiry_date = self._calendar.get_expiry_date_from_horizon_date( horizon_date, tenor, cal=cross) # If the strike hasn't been supplied need to work this out if not (isinstance(strike, np.ndarray)): old_strike = strike if isinstance(strike, str): strike = np.empty(len(horizon_date), dtype=object) else: strike = np.empty(len(horizon_date)) strike.fill(old_strike) # If the vol hasn't been supplied need to work this out if not (isinstance(vol, np.ndarray)): if vol is None: vol = np.nan old_vol = vol vol = np.empty(len(horizon_date)) vol.fill(old_vol) option_values = np.zeros(len(horizon_date)) spot = np.zeros(len(horizon_date)) delta = np.zeros(len(horizon_date)) intrinsic_values = np.zeros(len(horizon_date)) def _price_option(contract_type_, contract_type_fin_): for i in range(len(expiry_date)): built_vol_surface = False # If we have a "key strike" need to fit the vol surface if isinstance(strike[i], str): if not (built_vol_surface): fx_vol_surface.build_vol_surface(horizon_date[i]) fx_vol_surface.extract_vol_surface( num_strike_intervals=None) built_vol_surface = True if strike[i] == 'atm': strike[i] = fx_vol_surface.get_atm_strike(tenor) vol[i] = fx_vol_surface.get_atm_vol(tenor) / 100.0 elif strike[i] == 'atms': strike[i] = fx_vol_surface.get_spot() elif strike[i] == 'atmf': delivery_date = self._calendar.get_delivery_date_from_horizon_date( horizon_date[i], cal=cross) strike[i] = self._fx_forwards_price.price_instrument( cross, delivery_date, market_df=fx_vol_surface.get_all_market_data()) elif strike[i] == '25d-otm': if 'call' in contract_type_: strike[i] = fx_vol_surface.get_25d_call_strike( tenor) vol[i] = fx_vol_surface.get_25d_call_strike( tenor) / 100.0 elif 'put' in contract_type_: strike[i] = fx_vol_surface.get_25d_put_strike( tenor) vol[i] = fx_vol_surface.get_25d_put_strike( tenor) / 100.0 elif strike[i] == '10d-otm': if 'call' in contract_type_: strike[i] = fx_vol_surface.get_10d_call_strike( tenor) vol[i] = fx_vol_surface.get_10d_call_strike( tenor) / 100.0 elif 'put' in contract_type_: strike[i] = fx_vol_surface.get_10d_put_strike( tenor) vol[i] = fx_vol_surface.get_10d_put_strike( tenor) / 100.0 if not (built_vol_surface): fx_vol_surface.build_vol_surface(horizon_date[i]) # fx_vol_surface.extract_vol_surface(num_strike_intervals=None) # If an implied vol hasn't been provided, interpolate that one, fit the vol surface (if hasn't already been # done) if np.isnan(vol[i]): if tenor is None: vol[i] = fx_vol_surface.calculate_vol_for_strike_expiry( strike[i], expiry_date=expiry_date[i], tenor=None) else: vol[i] = fx_vol_surface.calculate_vol_for_strike_expiry( strike[i], expiry_date=None, tenor=tenor) model = FinModelBlackScholes(float(vol[i])) logger.info("Pricing " + contract_type_ + " option, horizon date = " + str(horizon_date[i]) + ", expiry date = " + str(expiry_date[i])) option = FinFXVanillaOption(self._findate(expiry_date[i]), strike[i], cross, contract_type_fin_, notional, cross[0:3]) spot[i] = fx_vol_surface.get_spot() """ FinancePy will return the value in the following dictionary for values {'v': vdf, "cash_dom": cash_dom, "cash_for": cash_for, "pips_dom": pips_dom, "pips_for": pips_for, "pct_dom": pct_dom, "pct_for": pct_for, "not_dom": notional_dom, "not_for": notional_for, "ccy_dom": self._domName, "ccy_for": self._forName} """ option_values[i] = option_values[i] + option.value( self._findate(horizon_date[i]), spot[i], fx_vol_surface.get_dom_discount_curve(), fx_vol_surface.get_for_discount_curve(), model)[premium_output.replace('-', '_')] intrinsic_values[i] = intrinsic_values[i] + option.value( self._findate(expiry_date[i]), spot[i], fx_vol_surface.get_dom_discount_curve(), fx_vol_surface.get_for_discount_curve(), model)[premium_output.replace('-', '_')] """FinancePy returns this dictionary for deltas {"pips_spot_delta": pips_spot_delta, "pips_fwd_delta": pips_fwd_delta, "pct_spot_delta_prem_adj": pct_spot_delta_prem_adj, "pct_fwd_delta_prem_adj": pct_fwd_delta_prem_adj} """ delta[i] = delta[i] + option.delta( self._findate(horizon_date[i]), spot[i], fx_vol_surface.get_dom_discount_curve(), fx_vol_surface.get_for_discount_curve(), model)[delta_output.replace('-', '_')] if contract_type == 'european-call': contract_type_fin = FinOptionTypes.EUROPEAN_CALL _price_option(contract_type, contract_type_fin) elif contract_type == 'european-put': contract_type_fin = FinOptionTypes.EUROPEAN_PUT _price_option(contract_type, contract_type_fin) elif contract_type == 'european-straddle' or contract_type == 'european-strangle': contract_type = 'european-call' contract_type_fin = FinOptionTypes.EUROPEAN_CALL _price_option(contract_type, contract_type_fin) contract_type = 'european-put' contract_type_fin = FinOptionTypes.EUROPEAN_PUT _price_option(contract_type, contract_type_fin) if return_as_df: option_prices_df = pd.DataFrame(index=horizon_date) option_prices_df[cross + '-option-price.' + field] = option_values option_prices_df[cross + '.' + field] = spot option_prices_df[cross + '-strike.' + field] = strike option_prices_df[cross + '-vol.' + field] = vol option_prices_df[cross + '-delta.' + field] = delta option_prices_df[cross + '.expiry-date'] = expiry_date option_prices_df[cross + '-intrinsic-value.' + field] = intrinsic_values return option_prices_df return option_values, spot, strike, vol, delta, expiry_date, intrinsic_values def get_day_count_conv(self, currency): if currency in market_constants.currencies_with_365_basis: return 365.0 return 360.0 def _findate(self, timestamp): return FinDate(timestamp.day, timestamp.month, timestamp.year, hh=timestamp.hour, mm=timestamp.minute, ss=timestamp.second)
class FXForwardsCurve(object): """Constructs continuous forwards time series total return indices from underlying forwards contracts. """ def __init__(self, market_data_generator=None, fx_forwards_trading_tenor=market_constants.fx_forwards_trading_tenor, roll_days_before=market_constants.fx_forwards_roll_days_before, roll_event=market_constants.fx_forwards_roll_event, construct_via_currency='no', fx_forwards_tenor_for_interpolation=market_constants.fx_forwards_tenor_for_interpolation, base_depos_tenor=data_constants.base_depos_tenor, roll_months=market_constants.fx_forwards_roll_months, cum_index=market_constants.fx_forwards_cum_index, output_calculation_fields=market_constants.output_calculation_fields): """Initializes FXForwardsCurve Parameters ---------- market_data_generator : MarketDataGenerator Used for downloading market data fx_forwards_trading_tenor : str What is primary forward contract being used to trade (default - '1M') roll_days_before : int Number of days before roll event to enter into a new forwards contract roll_event : str What constitutes a roll event? ('month-end', 'quarter-end', 'year-end', 'expiry') construct_via_currency : str What currency should we construct the forward via? Eg. if we asked for AUDJPY we can construct it via AUDUSD & JPYUSD forwards, as opposed to AUDJPY forwards (default - 'no') fx_forwards_tenor_for_interpolation : str(list) Which forwards should we use for interpolation base_depos_tenor : str(list) Which base deposits tenors do we need (this is only necessary if we want to start inferring depos) roll_months : int After how many months should we initiate a roll. Typically for trading 1M this should 1, 3M this should be 3 etc. cum_index : str In total return index, do we compute in additive or multiplicative way ('add' or 'mult') output_calculation_fields : bool Also output additional data should forward expiries etc. alongside total returns indices """ self._market_data_generator = market_data_generator self._calculations = Calculations() self._calendar = Calendar() self._filter = Filter() self._fx_forwards_trading_tenor = fx_forwards_trading_tenor self._roll_days_before = roll_days_before self._roll_event = roll_event self._construct_via_currency = construct_via_currency self._fx_forwards_tenor_for_interpolation = fx_forwards_tenor_for_interpolation self._base_depos_tenor = base_depos_tenor self._roll_months = roll_months self._cum_index = cum_index self._output_calcultion_fields = output_calculation_fields def generate_key(self): from findatapy.market.ioengine import SpeedCache # Don't include any "large" objects in the key return SpeedCache().generate_key(self, ['_market_data_generator', '_calculations', '_calendar', '_filter']) def fetch_continuous_time_series(self, md_request, market_data_generator, fx_forwards_trading_tenor=None, roll_days_before=None, roll_event=None, construct_via_currency=None, fx_forwards_tenor_for_interpolation=None, base_depos_tenor=None, roll_months=None, cum_index=None, output_calculation_fields=False): if market_data_generator is None: market_data_generator = self._market_data_generator if fx_forwards_trading_tenor is None: fx_forwards_trading_tenor = self._fx_forwards_trading_tenor if roll_days_before is None: roll_days_before = self._roll_days_before if roll_event is None: roll_event = self._roll_event if construct_via_currency is None: construct_via_currency = self._construct_via_currency if fx_forwards_tenor_for_interpolation is None: fx_forwards_tenor_for_interpolation = self._fx_forwards_tenor_for_interpolation if base_depos_tenor is None: base_depos_tenor = self._base_depos_tenor if roll_months is None: roll_months = self._roll_months if cum_index is None: cum_index = self._cum_index if output_calculation_fields is None: output_calculation_fields # Eg. we construct EURJPY via EURJPY directly (note: would need to have sufficient forward data for this) if construct_via_currency == 'no': # Download FX spot, FX forwards points and base depos etc. market = Market(market_data_generator=market_data_generator) md_request_download = MarketDataRequest(md_request=md_request) fx_conv = FXConv() # CAREFUL: convert the tickers to correct notation, eg. USDEUR => EURUSD, because our data # should be fetched in correct convention md_request_download.tickers = [fx_conv.correct_notation(x) for x in md_request.tickers] md_request_download.category = 'fx-forwards-market' md_request_download.fields = 'close' md_request_download.abstract_curve = None md_request_download.fx_forwards_tenor = fx_forwards_tenor_for_interpolation md_request_download.base_depos_tenor = base_depos_tenor forwards_market_df = market.fetch_market(md_request_download) # Now use the original tickers return self.construct_total_return_index(md_request.tickers, forwards_market_df, fx_forwards_trading_tenor=fx_forwards_trading_tenor, roll_days_before=roll_days_before, roll_event=roll_event, fx_forwards_tenor_for_interpolation=fx_forwards_tenor_for_interpolation, roll_months=roll_months, cum_index=cum_index, output_calculation_fields=output_calculation_fields) else: # eg. we calculate via your domestic currency such as USD, so returns will be in your domestic currency # Hence AUDJPY would be calculated via AUDUSD and JPYUSD (subtracting the difference in returns) total_return_indices = [] for tick in md_request.tickers: base = tick[0:3] terms = tick[3:6] md_request_base = MarketDataRequest(md_request=md_request) md_request_base.tickers = base + construct_via_currency md_request_terms = MarketDataRequest(md_request=md_request) md_request_terms.tickers = terms + construct_via_currency # Construct the base and terms separately (ie. AUDJPY => AUDUSD & JPYUSD) base_vals = self.fetch_continuous_time_series(md_request_base, market_data_generator, fx_forwards_trading_tenor=fx_forwards_trading_tenor, roll_days_before=roll_days_before, roll_event=roll_event, fx_forwards_tenor_for_interpolation=fx_forwards_tenor_for_interpolation, base_depos_tenor=base_depos_tenor, roll_months=roll_months, output_calculation_fields=False, cum_index=cum_index, construct_via_currency='no') terms_vals = self.fetch_continuous_time_series(md_request_terms, market_data_generator, fx_forwards_trading_tenor=fx_forwards_trading_tenor, roll_days_before=roll_days_before, roll_event=roll_event, fx_forwards_tenor_for_interpolation=fx_forwards_tenor_for_interpolation, base_depos_tenor=base_depos_tenor, roll_months=roll_months, cum_index=cum_index, output_calculation_fields=False, construct_via_currency='no') # Special case for USDUSD case (and if base or terms USD are USDUSD if base + terms == construct_via_currency + construct_via_currency: base_rets = self._calculations.calculate_returns(base_vals) cross_rets = pd.DataFrame(0, index=base_rets.index, columns=base_rets.columns) elif base + construct_via_currency == construct_via_currency + construct_via_currency: cross_rets = -self._calculations.calculate_returns(terms_vals) elif terms + construct_via_currency == construct_via_currency + construct_via_currency: cross_rets = self._calculations.calculate_returns(base_vals) else: base_rets = self._calculations.calculate_returns(base_vals) terms_rets = self._calculations.calculate_returns(terms_vals) cross_rets = base_rets.sub(terms_rets.iloc[:, 0], axis=0) # First returns of a time series will by NaN, given we don't know previous point cross_rets.iloc[0] = 0 cross_vals = self._calculations.create_mult_index(cross_rets) cross_vals.columns = [tick + '-forward-tot.close'] total_return_indices.append(cross_vals) return self._calculations.pandas_outer_join(total_return_indices) def unhedged_asset_fx(self, assets_df, asset_currency, home_curr, start_date, finish_date, spot_df=None): pass def hedged_asset_fx(self, assets_df, asset_currency, home_curr, start_date, finish_date, spot_df=None, total_return_indices_df=None): pass def get_day_count_conv(self, currency): if currency in market_constants.currencies_with_365_basis: return 365.0 return 360.0 def construct_total_return_index(self, cross_fx, forwards_market_df, fx_forwards_trading_tenor=None, roll_days_before=None, roll_event=None, roll_months=None, fx_forwards_tenor_for_interpolation=None, cum_index=None, output_calculation_fields=False): if not (isinstance(cross_fx, list)): cross_fx = [cross_fx] if fx_forwards_trading_tenor is None: fx_forwards_trading_tenor = self._fx_forwards_trading_tenor if roll_days_before is None: roll_days_before = self._roll_days_before if roll_event is None: roll_event = self._roll_event if roll_months is None: roll_months = self._roll_months if fx_forwards_tenor_for_interpolation is None: fx_forwards_tenor_for_interpolation = self._fx_forwards_tenor_for_interpolation if cum_index is None: cum_index = self._cum_index total_return_index_df_agg = [] # Remove columns where there is no data (because these points typically aren't quoted) forwards_market_df = forwards_market_df.dropna(how='all', axis=1) fx_forwards_pricer = FXForwardsPricer() def get_roll_date(horizon_d, delivery_d, asset_hols, month_adj=1): if roll_event == 'month-end': roll_d = horizon_d + CustomBusinessMonthEnd(roll_months + month_adj, holidays=asset_hols) elif roll_event == 'delivery-date': roll_d = delivery_d return (roll_d - CustomBusinessDay(n=roll_days_before, holidays=asset_hols)) for cross in cross_fx: # Eg. if we specify USDUSD if cross[0:3] == cross[3:6]: total_return_index_df_agg.append( pd.DataFrame(100, index=forwards_market_df.index, columns=[cross + "-forward-tot.close"])) else: # Is the FX cross in the correct convention old_cross = cross cross = FXConv().correct_notation(cross) horizon_date = forwards_market_df.index delivery_date = [] roll_date = [] new_trade = np.full(len(horizon_date), False, dtype=bool) asset_holidays = self._calendar.get_holidays(cal=cross) # Get first delivery date delivery_date.append( self._calendar.get_delivery_date_from_horizon_date(horizon_date[0], fx_forwards_trading_tenor, cal=cross, asset_class='fx')[0]) # For first month want it to expire within that month (for consistency), hence month_adj=0 ONLY here roll_date.append(get_roll_date(horizon_date[0], delivery_date[0], asset_holidays, month_adj=0)) # New trade => entry at beginning AND on every roll new_trade[0] = True # Get all the delivery dates and roll dates # At each "roll/trade" day we need to reset them for the new contract for i in range(1, len(horizon_date)): # If the horizon date has reached the roll date (from yesterday), we're done, and we have a # new roll/trade if (horizon_date[i] - roll_date[i-1]).days == 0: new_trade[i] = True # else: # new_trade[i] = False # If we're entering a new trade/contract, we need to get new delivery and roll dates if new_trade[i]: delivery_date.append(self._calendar.get_delivery_date_from_horizon_date(horizon_date[i], fx_forwards_trading_tenor, cal=cross, asset_class='fx')[0]) roll_date.append(get_roll_date(horizon_date[i], delivery_date[i], asset_holidays)) else: # Otherwise use previous delivery and roll dates, because we're still holding same contract delivery_date.append(delivery_date[i-1]) roll_date.append(roll_date[i-1]) interpolated_forward = fx_forwards_pricer.price_instrument(cross, horizon_date, delivery_date, market_df=forwards_market_df, fx_forwards_tenor_for_interpolation=fx_forwards_tenor_for_interpolation)[cross + '-interpolated-outright-forward.close'].values # To record MTM prices mtm = np.copy(interpolated_forward) # Note: may need to add discount factor when marking to market forwards? # Special case: for very first trading day # mtm[0] = interpolated_forward[0] # On rolling dates, MTM will be the previous forward contract (interpolated) # otherwise it will be the current forward contract for i in range(1, len(horizon_date)): if new_trade[i]: mtm[i] = fx_forwards_pricer.price_instrument(cross, horizon_date[i], delivery_date[i-1], market_df=forwards_market_df, fx_forwards_tenor_for_interpolation=fx_forwards_tenor_for_interpolation) \ [cross + '-interpolated-outright-forward.close'].values # else: # mtm[i] = interpolated_forward[i] # Eg. if we asked for USDEUR, we first constructed spot/forwards for EURUSD # and then need to invert it if old_cross != cross: mtm = 1.0 / mtm interpolated_forward = 1.0 / interpolated_forward forward_rets = mtm / np.roll(interpolated_forward, 1) - 1.0 forward_rets[0] = 0 if cum_index == 'mult': cum_rets = 100 * np.cumprod(1.0 + forward_rets) elif cum_index == 'add': cum_rets = 100 + 100 * np.cumsum(forward_rets) total_return_index_df = pd.DataFrame(index=horizon_date, columns=[cross + "-forward-tot.close"]) total_return_index_df[cross + "-forward-tot.close"] = cum_rets if output_calculation_fields: total_return_index_df[cross + '-interpolated-outright-forward.close'] = interpolated_forward total_return_index_df[cross + '-mtm.close'] = mtm total_return_index_df[cross + '-roll.close'] = new_trade total_return_index_df[cross + '.roll-date'] = roll_date total_return_index_df[cross + '.delivery-date'] = delivery_date total_return_index_df[cross + '-forward-return.close'] = forward_rets total_return_index_df_agg.append(total_return_index_df) return self._calculations.pandas_outer_join(total_return_index_df_agg)
# Get the holidays (which aren"t weekends) print(calendar.get_holidays(start_date="01 Jan 1999 00:50", end_date="31 Dec 1999", cal="FX")) print(calendar.get_holidays(start_date="01 Jan 2000 00:10", end_date="31 Dec 2000", cal="EUR")) print(calendar.get_holidays(start_date="01 Jan 2000 00:10", end_date="31 Dec 2000", cal="EURUSD")) # Get the holidays (which are weekends) print(calendar.get_holidays(start_date="01 Jan 1999 00:50", end_date="31 Dec 1999", cal="WKD")) if run_example == 2 or run_example == 0: # Get delivery dates for these horizon dates - typically would use # to get forward maturities print(calendar.get_delivery_date_from_horizon_date( pd.to_datetime([pd.Timestamp("02 Nov 2020")]), "ON", cal="EURUSD")) print(calendar.get_delivery_date_from_horizon_date( pd.to_datetime([pd.Timestamp("02 Nov 2020")]), "1W", cal="EURUSD")) print(calendar.get_delivery_date_from_horizon_date( pd.to_datetime([pd.Timestamp("27 Nov 2020")]), "1W", cal="EURUSD")) # Get 1M expires for these horizon dates - typically would use to get # option expiries print(calendar.get_expiry_date_from_horizon_date( pd.to_datetime([pd.Timestamp("26 Oct 2020")]), "1M", cal="EURUSD")) if run_example == 3 or run_example == 0: # Create a list of business days and one which is + 1 day