Skip to content

Commit

Permalink
Speed up pricing & more flexible option backtesting
Browse files Browse the repository at this point in the history
  • Loading branch information
saeedamen committed Jan 16, 2021
1 parent b808762 commit 4fec292
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 128 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ pip install chartpy
pip install findatapy
```

Note that if you use the option pricing/total returns you might need to get the latest FinancePy version from GitHub
https://github.com/domokane/FinancePy/ as opposed to PyPI

```
pip install git+https://github.com/domokane/FinancePy/FinancePy.git
```

# Binder and Jupyter - Run finmarketpy in your browser

Expand Down Expand Up @@ -159,6 +166,11 @@ In finmarketpy/examples you will find several examples, including some simple tr

# finmarketpy log

* 16 Jan 2021
* Additional work on FXOptionPricer and total returns (FXOptionCurve)
* Speed up and deal with non-convergence of solver
* Allow options entry on user specified dates
* Added more option examples
* 11 Jan 2021
* Fixed issue with OTM strikes in FXOptionPricer
* 10 Jan 2021
Expand Down
2 changes: 1 addition & 1 deletion finmarketpy/backtest/backtestengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1870,7 +1870,7 @@ def plot_strategy_net_exposures_notional(self, strip=None, silent_plot=False, re
#### grab signals for specific days
def _grab_signals(self, strategy_signal, date=None, strip=None):
if date is None:
last_day = strategy_signal.loc[-1].transpose().to_frame()
last_day = strategy_signal.iloc[-1].transpose().to_frame()
else:
if not (isinstance(date, list)):
date = [date]
Expand Down
278 changes: 194 additions & 84 deletions finmarketpy/curve/fxoptionscurve.py

Large diffs are not rendered by default.

24 changes: 18 additions & 6 deletions finmarketpy/curve/rates/fxforwardspricer.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ def __init__(self, market_df=None, quoted_delivery_df=None):
self._quoted_delivery_df = quoted_delivery_df

def price_instrument(self, cross, horizon_date, delivery_date, option_expiry_date=None, market_df=None, quoted_delivery_df=None,
fx_forwards_tenor_for_interpolation=market_constants.fx_forwards_tenor_for_interpolation):
fx_forwards_tenor_for_interpolation=market_constants.fx_forwards_tenor_for_interpolation,
return_as_df=True):
"""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.
Expand Down Expand Up @@ -209,13 +210,16 @@ def price_instrument(self, cross, horizon_date, delivery_date, option_expiry_dat
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"])
if return_as_df:
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
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
return interpolated_df

return interpolated_outright_forwards_arr

def get_day_count_conv(self, currency):
if currency in market_constants.currencies_with_365_basis:
Expand Down Expand Up @@ -261,13 +265,18 @@ def _setup_forwards_calculation(self, cross, spot_date, market_df, quoted_delive
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):

if not(isinstance(fx_forwards_tenor, list)):
fx_forwards_tenor = [fx_forwards_tenor]

# 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)
Expand Down Expand Up @@ -308,6 +317,9 @@ def calculate_implied_depo(self, cross, implied_currency, market_df=None, quoted
DataFrame
"""

if not(isinstance(fx_forwards_tenor, list)):
fx_forwards_tenor = [fx_forwards_tenor]

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
Expand Down
25 changes: 17 additions & 8 deletions finmarketpy/curve/volatility/fxoptionspricer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from financepy.models.FinModelBlackScholes import FinModelBlackScholes
from financepy.products.fx.FinFXVanillaOption import FinFXVanillaOption
from financepy.finutils.FinGlobalTypes import FinOptionTypes
from financepy.products.fx.FinFXMktConventions import *

market_constants = MarketConstants()

Expand All @@ -39,7 +40,7 @@ def __init__(self, fx_vol_surface=None, premium_output=market_constants.fx_optio

self._calendar = Calendar()
self._fx_vol_surface = fx_vol_surface
self._fx_forwards_price = FXForwardsPricer()
self._fx_forwards_pricer = FXForwardsPricer()
self._premium_output = premium_output
self._delta_output = delta_output

Expand Down Expand Up @@ -131,17 +132,22 @@ def _price_option(contract_type_, contract_type_fin_):

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_vol(tenor) / 100.0
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()
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]))

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())
# Interpolate vol later
elif strike[i] == '25d-otm':
if 'call' in contract_type_:
strike[i] = fx_vol_surface.get_25d_call_strike(tenor)
Expand All @@ -158,7 +164,10 @@ def _price_option(contract_type_, contract_type_fin_):
vol[i] = fx_vol_surface.get_10d_put_vol(tenor) / 100.0

if not(built_vol_surface):
fx_vol_surface.build_vol_surface(horizon_date[i])
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
Expand Down
20 changes: 20 additions & 0 deletions finmarketpy/curve/volatility/fxvolsurface.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@ def get_vol_from_quoted_tenor(self, K, tenor, gaps=None):

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

Expand Down Expand Up @@ -446,6 +452,20 @@ def get_10d_call_ms_strike(self, expiry_date=None, tenor=None):
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']

Expand Down
22 changes: 15 additions & 7 deletions finmarketpy/util/marketconstants.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class MarketConstants(object):
fx_forwards_cum_index = 'mult'

# What is the point at which we roll?
fx_forwards_roll_event = 'month-end' # 'month-end', 'quarter-end', 'year-end', 'expiry'
fx_forwards_roll_event = 'month-end' # 'month-end', 'quarter-end', 'year-end', 'delivery-date'

# How many days before that point should we roll?
fx_forwards_roll_days_before = 5
Expand Down Expand Up @@ -102,7 +102,7 @@ class MarketConstants(object):
fx_options_freeze_implied_vol = False

# What is the point at which we roll?
fx_options_roll_event = 'month-end' # 'month-end', 'quarter-end', 'year-end', 'expiry'
fx_options_roll_event = 'expiry-date' # 'month-end', 'expiry-date', 'no-roll'

# How many days before that point should we roll?
fx_options_roll_days_before = 5
Expand All @@ -111,17 +111,25 @@ class MarketConstants(object):
fx_options_roll_months = 1

# For fitting vol surface
fx_options_vol_function_type = 'CLARK5' # 'CLARK5', 'CLARK', 'BBG', 'SABR' and 'SABR3'

# 'CLARK5', 'CLARK', 'BBG', 'SABR' and 'SABR3'
fx_options_vol_function_type = 'CLARK5'
fx_options_depo_tenor = '1M'
fx_options_atm_method = 'fwd-delta-neutral-premium-adj' # 'fwd-delta-neutral' or 'fwd-delta-neutral-premium-adj'
fx_options_delta_method = 'fwd-delta'# 'fwd-delta-prem-adj'

# 'fwd-delta-neutral' or 'fwd-delta-neutral-premium-adj' or 'spot' or 'fwd'
fx_options_atm_method = 'fwd-delta-neutral-premium-adj'

# 'fwd-delta' or 'fwd-delta-prem-adj' or 'spot-delta-prem-adj' or 'spot-delta'
fx_options_delta_method = 'spot-delta-prem-adj'
fx_options_alpha = 0.5

# 'pct-for' (in base currency pct) or 'pct-dom' (in terms currency pct)
fx_options_premium_output = 'pct-for'
fx_options_delta_output = 'pct-fwd-delta-prem-adj'

fx_options_solver = 'nelmer-mead-numba' # 'nelmer-mead' or 'nelmer-mead-numba' or 'cg'
fx_options_pricing_engine = 'financepy' # 'finmarketpy' or 'financepy'
# 'nelmer-mead' or 'nelmer-mead-numba' (faster but less accurate) or 'cg' (conjugate gradient tends to be slower, but more accurate)
fx_options_solver = 'nelmer-mead-numba'
fx_options_pricing_engine = 'financepy' # 'financepy' or 'finmarketpy'

# overwrite field variables with those listed in MarketCred
def __init__(self):
Expand Down
60 changes: 40 additions & 20 deletions finmarketpy_examples/fx_options_indices_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
# For loading market data
from findatapy.market import Market, MarketDataGenerator, MarketDataRequest

from findatapy.timeseries import Filter, Calculations
from findatapy.timeseries import Filter, Calculations, RetStats

from findatapy.util.loggermanager import LoggerManager

Expand All @@ -39,26 +39,28 @@
logger = LoggerManager().getLogger(__name__)

chart = Chart(engine='plotly')

market = Market(market_data_generator=MarketDataGenerator())

# Choose run_example = 0 for everything
# run_example = 1 - create total return index AUDUSD 1M long calls (and separately long puts) over 2008 financial crisis and further
# run_example = 2 - create total return index USDJPY 1W short straddles over a long sample
# run_example = 3 - create total return index USDJPY 1W short straddles (only selling on the last day of every month)

run_example = 1
run_example = 2

def prepare_indices(df_tot=None, df_tc=None, df_spot_tot=None):
def prepare_indices(cross, df_option_tot=None, df_option_tc=None, df_spot_tot=None):
df_list = []

if df_tot is not None:
df_list.append(pd.DataFrame(df_tot[cross + '-option-tot.close']))
df_list.append(pd.DataFrame(df_tot[cross + '-option-delta-tot.close']))
df_list.append(pd.DataFrame(df_tot[cross + '-delta-pnl-index.close']))
if df_option_tot is not None:
df_list.append(pd.DataFrame(df_option_tot[cross + '-option-tot.close']))
df_list.append(pd.DataFrame(df_option_tot[cross + '-option-delta-tot.close']))
df_list.append(pd.DataFrame(df_option_tot[cross + '-delta-pnl-index.close']))

if df_tc is not None:
df_list.append(pd.DataFrame(df_tc[cross + '-option-tot-with-tc.close']))
df_list.append(pd.DataFrame(df_tc[cross + '-option-delta-tot-with-tc.close']))
df_list.append(pd.DataFrame(df_tc[cross + '-delta-pnl-index-with-tc.close']))
if df_option_tc is not None:
df_list.append(pd.DataFrame(df_option_tc[cross + '-option-tot-with-tc.close']))
df_list.append(pd.DataFrame(df_option_tc[cross + '-option-delta-tot-with-tc.close']))
df_list.append(pd.DataFrame(df_option_tc[cross + '-delta-pnl-index-with-tc.close']))

if df_spot_tot is not None:
df_list.append(df_spot_tot)
Expand Down Expand Up @@ -112,14 +114,15 @@ def prepare_indices(df_tot=None, df_tc=None, df_spot_tot=None):
df_cuemacro_option_call_tc = fx_options_curve.apply_tc_to_total_return_index(cross, df_cuemacro_option_call_tot,
option_tc_bp=5, spot_tc_bp=2)

# Let's trade a short 1M put, and we roll at expiry
# Let's trade a long 1M OTM put, and we roll at expiry
df_cuemacro_option_put_tot = fx_options_curve.construct_total_return_index(
cross, df, contract_type='european-put', strike='10d-otm', position_multiplier=1.0)

# Add transaction costs to the option index (bid/ask bp for the option premium and spot FX)
df_cuemacro_option_put_tc = fx_options_curve.apply_tc_to_total_return_index(cross, df_cuemacro_option_put_tot,
option_tc_bp=5, spot_tc_bp=2)


# Get total returns for spot
md_request.abstract_curve = None

Expand All @@ -130,16 +133,32 @@ def prepare_indices(df_tot=None, df_tc=None, df_spot_tot=None):
df_bbg_tot = market.fetch_market(md_request)
df_bbg_tot.columns = [x + '-bbg' for x in df_bbg_tot.columns]

# Calculate a hedged portfolio of spot + 2*options (can we reduce drawdowns?)
calculations = Calculations()

ret_stats = RetStats()

df_hedged = calculations.pandas_outer_join([df_bbg_tot[cross + '-tot.close-bbg'].to_frame(), df_cuemacro_option_put_tc[cross + '-option-tot-with-tc.close'].to_frame()])
df_hedged = df_hedged.fillna(method='ffill')
df_hedged = df_hedged.pct_change()

df_hedged['Spot + 2*option hedge'] = df_hedged[cross + '-tot.close-bbg'] + df_hedged[cross + '-option-tot-with-tc.close']

df_hedged.columns = RetStats(returns_df=df_hedged, ann_factor=252).summary()

# Plot everything
chart.plot(calculations.create_mult_index_from_prices(
prepare_indices(df_tot=df_cuemacro_option_call_tot, df_tc=df_cuemacro_option_call_tc, df_spot_tot=df_bbg_tot)))
prepare_indices(cross=cross, df_option_tot=df_cuemacro_option_call_tot,
df_option_tc=df_cuemacro_option_call_tc, df_spot_tot=df_bbg_tot)))

chart.plot(calculations.create_mult_index_from_prices(
prepare_indices(df_tot=df_cuemacro_option_put_tot, df_tc=df_cuemacro_option_put_tc, df_spot_tot=df_bbg_tot)))
prepare_indices(cross=cross,df_option_tot=df_cuemacro_option_put_tot,
df_option_tc=df_cuemacro_option_put_tc, df_spot_tot=df_bbg_tot)))

chart.plot(calculations.create_mult_index_from_prices(
prepare_indices(df_tc=df_cuemacro_option_put_tc, df_spot_tot=df_bbg_tot)))
prepare_indices(cross=cross,df_option_tc=df_cuemacro_option_put_tc, df_spot_tot=df_bbg_tot)))

chart.plot(calculations.create_mult_index(df_hedged))


###### Fetch market data for pricing EURUSD options from 2006-2020 (ie. FX spot, FX forwards, FX deposits and FX vol quotes)
Expand All @@ -155,20 +174,20 @@ def prepare_indices(df_tot=None, df_tc=None, df_spot_tot=None):
# start_date = '01 Jan 2007'; finish_date = '31 Dec 2007' # Use smaller window for quicker execution

cross = 'USDJPY'
fx_options_trading_tenor = '1W' # Try changing between 1W, 1M or 3M!
fx_options_trading_tenor = '1W' # Try changing between 1W and 1M!

# Download the whole all market data for USDJPY for pricing options (FX vol surface + spot + FX forwards + depos)
md_request = MarketDataRequest(start_date=start_date, finish_date=finish_date,
data_source='bloomberg', cut='10AM', category='fx-vol-market',
tickers=cross, fx_vol_tenor=['1W', '1M', '2M', '3M'],
tickers=cross, fx_vol_tenor=['1W', '1M'], base_depos_tenor=['1W', '1M'],
cache_algo='cache_algo_return', base_depos_currencies=[cross[0:3], cross[3:6]])

df = market.fetch_market(md_request)

# Fill data for every workday and use weekend calendar (note: this is a bit of a fudge, filling down)
# CHECK DATA isn't missing at start of series
df = df.resample('B').last().fillna(method='ffill')
# df = df[df.index >= '09 Mar 2007'] # Try starting on a different day of the week & see how it impact P&L
df = df[df.index >= '09 Mar 2007'] # Try starting on a different day of the week & see how it impact P&L?
cal = 'WKD'

# Remove New Year's Day and Christmas
Expand All @@ -181,11 +200,12 @@ def prepare_indices(df_tot=None, df_tc=None, df_spot_tot=None):
roll_days_before=0,
roll_event='expiry-date',
roll_months=1, # This is ignored if we roll on expiry date
fx_options_tenor_for_interpolation=['1W', '1M', '2M', '3M'],
fx_options_tenor_for_interpolation=['1W', '1M'],
strike='atm',
contract_type='european-straddle',
position_multiplier=-1.0, # +1.0 for long options, -1.0 for short options
output_calculation_fields=True,
freeze_implied_vol=True,
cal=cal,
cum_index='mult')

Expand All @@ -211,7 +231,7 @@ def prepare_indices(df_tot=None, df_tc=None, df_spot_tot=None):
calculations = Calculations()

df_index = calculations.create_mult_index_from_prices(
prepare_indices(df_tc=df_cuemacro_option_straddle_tc, df_spot_tot=df_bbg_tot))
prepare_indices(cross=cross, df_option_tc=df_cuemacro_option_straddle_tc, df_spot_tot=df_bbg_tot))

from finmarketpy.economics.quickchart import QuickChart

Expand Down

0 comments on commit 4fec292

Please sign in to comment.