Ejemplo n.º 1
0
    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_pricer = FXForwardsPricer()
        self._premium_output = premium_output
        self._delta_output = delta_output
Ejemplo n.º 2
0
    cross = 'AUDUSD'
    fx_forwards_tenors = ['1W', '2W', '3W', '1M']

    fx_forwards_to_print = ['1W', '2W']

    # Get AUDUSD data for spot, forwards + depos
    md_request = MarketDataRequest(start_date='01 Jan 2020', finish_date='01 Feb 2020',
                                   data_source='bloomberg', cut='NYC', category='fx-forwards-market',
                                   tickers=cross,
                                   cache_algo='cache_algo_return', fx_forwards_tenor=fx_forwards_tenors,
                                   base_depos_currencies=[cross[0:3], cross[3:6]])

    market_df = market.fetch_market(md_request=md_request)

    fx_forwards_price = FXForwardsPricer()

    delivery_dates = Calendar().get_delivery_date_from_horizon_date(market_df.index, "8D", cal=cross)
    interpolated_forwards_df = fx_forwards_price.price_instrument(cross, market_df.index, delivery_dates,
        market_df=market_df, fx_forwards_tenor_for_interpolation =['1W', '2W'])

    interpolated_forwards_df[cross + ".delivery"] = delivery_dates.values

    print(interpolated_forwards_df)

    market_cols = [cross + ".close"]

    for f in fx_forwards_to_print:
        market_cols.append(cross + f + '.close')

    print(market_df[market_cols])
Ejemplo n.º 3
0
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_pricer = 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

                    # Delta neutral strike/or whatever strike is quoted as ATM
                    # usually this is ATM delta neutral strike, but can sometimes be ATMF for some Latam
                    # Take the vol directly quoted, rather than getting it from building vol surface
                    if strike[i] == 'atm':
                        strike[i] = fx_vol_surface.get_atm_strike(tenor)
                        vol[i] = fx_vol_surface.get_atm_quoted_vol(
                            tenor) / 100.0
                        # vol[i] = fx_vol_surface.get_atm_vol(tenor) / 100.0 # interpolated
                    elif strike[i] == 'atms':
                        strike[i] = fx_vol_surface.get_spot(
                        )  # Interpolate vol later
                    elif strike[i] == 'atmf':
                        # Quoted tenor, no need to interpolate
                        strike[i] = float(fx_vol_surface.get_all_market_data()[cross + ".close"][horizon_date[i]]) \
                                          + (float(fx_vol_surface.get_all_market_data()[cross + tenor + ".close"][horizon_date[i]]) \
                                    / self._fx_forwards_pricer.get_forwards_divisor(cross[3:6]))

                        # Interpolate vol later
                    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_vol(
                                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_vol(
                                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_vol(
                                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_vol(
                                tenor) / 100.0

                if not (built_vol_surface):
                    try:
                        fx_vol_surface.build_vol_surface(horizon_date[i])
                    except:
                        logger.warn("Failed to build vol surface for " +
                                    str(horizon_date) +
                                    ", won't be able to interpolate vol")
                    # 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)
Ejemplo n.º 4
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)
        non_usd = 'BRL'

        # Download the whole all market data for USDBRL for pricing options (vol surface)
        md_request = MarketDataRequest(start_date=horizon_date,
                                       finish_date=horizon_date,
                                       data_source='bloomberg',
                                       cut='NYC',
                                       category='fx-vol-market',
                                       tickers=cross,
                                       base_depos_currencies=[cross[0:3]],
                                       cache_algo='cache_algo_return')

        df = market.fetch_market(md_request)

        # Compute implied deposit BRL 1M from USDBRL forwards (and USD 1M depo)
        fx_forwards_price = FXForwardsPricer()

        implied_depo_df = fx_forwards_price.calculate_implied_depo(
            cross,
            non_usd,
            market_df=df,
            fx_forwards_tenor=['1W', '1M'],
            depo_tenor=['1W', '1M'])

        implied_depo_df.columns = [
            x.replace('-implied-depo', '') for x in implied_depo_df.columns
        ]
        df = df.join(implied_depo_df, how='left')

        # USDBRL quoted ATMF implied vol (as opposed to delta neutral) hence 'fwd' parameter
        fx_op = FXOptionsPricer(fx_vol_surface=FXVolSurface(