Skip to content

Commit

Permalink
VolStats functions and total returns for spot
Browse files Browse the repository at this point in the history
  • Loading branch information
saeedamen committed Dec 19, 2020
1 parent 6a487d7 commit 5f29f1d
Show file tree
Hide file tree
Showing 30 changed files with 1,171 additions and 86 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ In finmarketpy/examples you will find several examples, including some simple tr

# finmarketpy log

* 19 Dec 2020
* Added VolStats and examples to calculate realized vol, vol risk premium and implied vol addons
* Added FX total return calculations for FX spot positions
* Begun to add FX total return calculations for FX forwards (incomplete)
* Adapted FX vol code to latest version of FinancePy (note, may need to download via GitHub instead of PyPI)
* Also calculated implied PDF for FX vol surface using FinancePy underneath
* 04 Dec 2020
* Fix imports in FX vol interpolation
* 02 Dec 2020
* Added FX vol surface interpolation (using FinancePy library underneath) + animated example
* 12 Nov 2020
Expand Down
10 changes: 5 additions & 5 deletions finmarketpy/backtest/backtestengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied_vol.
#
# See the License for the specific language governing permissions and limitations under the License.
#
Expand Down Expand Up @@ -193,7 +193,7 @@ def calculate_trading_PnL(self, br, asset_a_df, signal_df, contract_value_df, ru
# signal_df_copy.columns = [x + '_final_signal' for x in signal_df_copy.columns]
# trade_rets_df_copy.columns = [x + '_cum_trade' for x in trade_rets_df_copy.columns]
#
# to_plot = calculations.pandas_outer_join([asset_df_copy, pre_signal_df, signal_df_copy, trade_rets_df_copy, temp_strategy_rets_df])
# to_plot = _calculations.pandas_outer_join([asset_df_copy, pre_signal_df, signal_df_copy, trade_rets_df_copy, temp_strategy_rets_df])
# to_plot.to_csv('test.csv')

# do we have a vol target for individual signals?
Expand Down Expand Up @@ -404,7 +404,7 @@ def calculate_trading_PnL(self, br, asset_a_df, signal_df, contract_value_df, ru
self._components_pnl.columns = pnl_cols

# TODO FIX very slow - hence only calculate on demand
# _pnl_trades = calculations.calculate_individual_trade_gains(signal_df, _pnl)
# _pnl_trades = _calculations.calculate_individual_trade_gains(signal_df, _pnl)
self._pnl_trades = None
self._components_pnl_trades = None

Expand All @@ -419,7 +419,7 @@ def calculate_trading_PnL(self, br, asset_a_df, signal_df, contract_value_df, ru

# TODO parallel version still work in progress!

logger.info("Cumulative index calculations")
logger.info("Cumulative index _calculations")

if False: # market_constants.backtest_thread_no[market_constants.generic_plat] > 1 and run_in_parallel:
swim_pool = SwimPool(multiprocessing_library=market_constants.multiprocessing_library)
Expand Down Expand Up @@ -474,7 +474,7 @@ def calculate_trading_PnL(self, br, asset_a_df, signal_df, contract_value_df, ru

self._portfolio_cum = calculations.create_add_index(self._portfolio) # portfolio cumulative P&L

logger.info("Completed cumulative index calculations")
logger.info("Completed cumulative index _calculations")

self._pnl_cum.columns = pnl_cols
self._components_pnl_cum.columns = pnl_cols
Expand Down
4 changes: 2 additions & 2 deletions finmarketpy/backtest/tradeanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied_vol.
#
# See the License for the specific language governing permissions and limitations under the License.
#
Expand Down Expand Up @@ -79,7 +79,7 @@ def run_strategy_returns_stats(self, trading_model, index=None, engine='finmarke
if engine == 'pyfolio':
# PyFolio assumes UTC time based DataFrames (so force this localisation)
try:
pnl = tz.localise_index_as_UTC(pnl)
pnl = tz.localize_index_as_UTC(pnl)
except:
pass

Expand Down
3 changes: 3 additions & 0 deletions finmarketpy/curve/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from finmarketpy.curve.abstractcurve import AbstractCurve
from finmarketpy.curve.fxforwardscurve import FXForwardsCurve
from finmarketpy.curve.fxspotcurve import FXSpotCurve
29 changes: 29 additions & 0 deletions finmarketpy/curve/abstractcurve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
__author__ = 'saeedamen' # Saeed Amen / saeed@cuemacro.com

#
# Copyright 2020 Cuemacro Ltd. - http//www.cuemacro.com / @cuemacro
#
# See the License for the specific language governing permissions and limitations under the License.
#
# This may not be distributed without the permission of Cuemacro.
#

import abc

class AbstractCurve(object):
"""Abstract class for creating indices and curves, which is for example implemented by FXSpotCurve and could be implemented
by other asset classes.
"""

@abc.abstractmethod
def generate_key(self):
return

@abc.abstractmethod
def fetch_continuous_time_series(self, md_request, market_data_generator):
return

@abc.abstractmethod
def construct_total_returns_index(self):
return
179 changes: 179 additions & 0 deletions finmarketpy/curve/fxforwardscurve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
__author__ = 'saeedamen' # Saeed Amen / saeed@cuemacro.com

#
# Copyright 2020 Cuemacro Ltd. - http//www.cuemacro.com / @cuemacro
#
# See the License for the specific language governing permissions and limitations under the License.
#
# This may not be distributed without the permission of Cuemacro.
#

import pandas as pd

from findatapy.market import Market, MarketDataRequest
from findatapy.timeseries import Calculations, Calendar
from findatapy.util.dataconstants import DataConstants

constants = DataConstants()

class FXForwardsCurve(object):
"""Constructs continuous forwards time series total return indices from underlying forwards contracts. Incomplete!
"""

def __init__(self, market_data_generator=None, fx_forwards_trading_tenor='1M', roll_date=0, construct_via_currency='no',
fx_forwards_tenor=constants.fx_forwards_tenor, base_depos_tenor=constants.base_depos_tenor):

self._market_data_generator = market_data_generator
self._calculations = Calculations()
self._calendar = Calendar()

self._fx_forwards_trading_tenor = fx_forwards_trading_tenor
self._roll_date = roll_date
self._construct_via_currency = construct_via_currency
self._fx_forwards_tenor = fx_forwards_tenor
self._base_depos_tenor = base_depos_tenor

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'])

def fetch_continuous_time_series(self, md_request, market_data_generator, fx_forwards_trading_tenor=None,
roll_date=None, construct_via_currency=None, fx_forwards_tenor=None, base_depos_tenor=None):

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_date is None: roll_date = self._roll_date
if construct_via_currency is None: construct_via_currency = self._construct_via_currency
if fx_forwards_tenor is None: fx_forwards_tenor = self._fx_forwards_tenor
if base_depos_tenor is None: base_depos_tenor = self._base_depos_tenor

# 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
market = Market(market_data_generator=market_data_generator)

md_request_download = MarketDataRequest(md_request=md_request)

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
md_request_download.base_depos_tenor = base_depos_tenor

forwards_market_df = market.fetch_market(md_request_download)

return self.construct_total_return_index(md_request.tickers, fx_forwards_trading_tenor, roll_date, forwards_market_df,
fx_forwards_tenor=fx_forwards_tenor, base_depos_tenor=base_depos_tenor)
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

base_vals = self.fetch_continuous_time_series(md_request_base, market_data_generator,
construct_via_currency='no')
terms_vals = self.fetch_continuous_time_series(md_request_terms, market_data_generator,
construct_via_currency='no')

# Special case for USDUSD case (and if base or terms USD are USDUSD
if base + terms == 'USDUSD':
base_rets = self._calculations.calculate_returns(base_vals)
cross_rets = pd.DataFrame(0, index=base_rets.index, columns=base_rets.columns)
elif base + 'USD' == 'USDUSD':
cross_rets = -self._calculations.calculate_returns(terms_vals)
elif terms + 'USD' == 'USDUSD':
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 + '-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 ['AUD', 'CAD', 'GBP', 'NZD']:
return 365.0

return 360.0

def construct_total_return_index(self, cross_fx, fx_forwards_trading_tenor, roll_date, forwards_market_df,
fx_forwards_tenor=constants.fx_forwards_tenor, base_depos_tenor=constants.base_depos_tenor):

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

total_return_index_agg = []

# Remove columns where there is no data (because these points typically aren't quoted)
forwards_market_df = forwards_market_df.dropna(axis=1)

for cross in cross_fx:

# Eg. if we specify USDUSD
if cross[0:3] == cross[3:6]:
total_return_index_agg.append(
pd.DataFrame(100, index=forwards_market_df.index, columns=[cross + "-tot.close"]))
else:
spot = forwards_market_df[cross + ".close"].to_frame()

fx_forwards_tenor_pickout = []

for f in fx_forwards_tenor:
if f + ".close" in fx_forwards_tenor:
fx_forwards_tenor_pickout.append(f)

if f == fx_forwards_trading_tenor:
break

divisor = 10000.0

if cross[3:6] == 'JPY':
divisor = 100.0

forward_pts = forwards_market_df[[cross + x + ".close" for x in fx_forwards_tenor_pickout]].to_frame() \
/ divisor

outright = spot + forward_pts

# Calculate the time difference between each data point
spot['index_col'] = spot.index
time = spot['index_col'].diff()
spot = spot.drop('index_col', 1)

total_return_index = pd.DataFrame(index=spot.index, columns=[cross + "-tot.close"])
total_return_index.iloc[0] = 100

time_diff = time.values.astype(float) / 86400000000000.0 # get time difference in days

# TODO incomplete forwards calculations
total_return_index_agg.append(total_return_index)

return self._calculations.pandas_outer_join(total_return_index_agg)

0 comments on commit 5f29f1d

Please sign in to comment.