diff options
| -rw-r--r-- | python/analytics/curve_trades.py (renamed from python/exploration/curve_trades.py) | 138 | ||||
| -rw-r--r-- | python/analytics/scenarios.py | 30 | ||||
| -rw-r--r-- | python/notebooks/Curve Trades.ipynb | 120 |
3 files changed, 231 insertions, 57 deletions
diff --git a/python/exploration/curve_trades.py b/python/analytics/curve_trades.py index d17b35ed..b6c17436 100644 --- a/python/exploration/curve_trades.py +++ b/python/analytics/curve_trades.py @@ -3,16 +3,18 @@ from db import dbengine from analytics import Index, Portfolio from analytics.utils import roll_date from dateutil.relativedelta import relativedelta +from analytics.basket_index import MarkitBasketIndex +from statsmodels.sandbox.regression.predstd import wls_prediction_std +from scipy.interpolate import interp1d +from itertools import chain +from copy import deepcopy + import pandas as pd import math import statsmodels.formula.api as smf -from analytics.basket_index import MarkitBasketIndex import numpy as np import matplotlib.pyplot as plt -from statsmodels.sandbox.regression.predstd import wls_prediction_std -from scipy.interpolate import interp1d - serenitasdb = dbengine('serenitasdb') dawndb = dbengine('dawndb') @@ -65,22 +67,6 @@ def ratio_within_series(index='IG', rolling=6, param='duration'): df = df.reset_index(level=['index', 'version'], drop=True) return df -def curve_3_5_10(df): - """ - Parameters - ---------- - df: duration ratio within series""" - #buy 3y, sell 5y, buy 10y - s = - df.theta2['3yr'] / df.duration_ratio_to_5yr['3yr'] \ - + 2 * df.theta2['5yr'] \ - - df.theta2['10yr'] / df.duration_ratio_to_5yr['10yr'] - s.dropna().unstack(-1).plot() - -def curve_5_10(df): - #buy sell 5y, buy 10y - s = df.theta2['5yr'] - df.theta2['10yr'] / df.duration_ratio_to_5yr['10yr'] - s.dropna().unstack(-1).plot() - def on_the_run_theta(index='IG', rolling=6): otr = on_the_run(index) df = get_index_quotes(index, list(range(otr - rolling, otr + 1)), @@ -206,22 +192,25 @@ def spread_fin_crisis(index='IG'): plt.show() -def forward_spread(index='IG', series=None, tenors=['3yr', '5yr', '7yr', '10yr']): +def forward_spread(report_date, index='IG', series=None, tenors=['3yr', '5yr', '7yr', '10yr']): if series is None: series = on_the_run(index = index) - b_index = MarkitBasketIndex(index, series, tenors) + b_index = MarkitBasketIndex(index, series, tenors, value_date=report_date) b_index.tweak() f_spread = [] date_range = pd.bdate_range(pd.datetime.today(), max(b_index.maturities), freq='M') for d in date_range.date: - b_index.trade_date = d + b_index.value_date = d f_spread.append(b_index.spread()) return pd.concat(f_spread, keys=date_range).unstack(-1) def spot_forward(index='IG', series=None, tenors=['3yr', '5yr', '7yr', '10yr']): + ''' + Calculates the 1-year forward spot rate ''' + if series is None: series = on_the_run(index) b_index = MarkitBasketIndex(index, series, tenors) @@ -239,13 +228,13 @@ def spot_forward(index='IG', series=None, tenors=['3yr', '5yr', '7yr', '10yr']): '1yr': [0., 0.]}, index=['0yr', '1yr']) df_0.index.name = 'tenor' df = df_0.append(df) - df['maturity'] = [b_index.trade_date, maturity_1yr] + b_index.maturities + df['maturity'] = [b_index.value_date, maturity_1yr] + b_index.maturities return df.reset_index().set_index('maturity') -def curve_pos(trade_date, index='IG'): +def curve_pos(value_date, index='IG'): ''' - trade_date : :class:`datetime.date` + value_date : :class:`datetime.date` index : string one of 'IG', 'HY' or 'ITRX' @@ -253,7 +242,7 @@ def curve_pos(trade_date, index='IG'): sql_string = "SELECT * FROM cds where trade_date < %s" df = pd.read_sql_query(sql_string, dawndb, parse_dates=['trade_date', 'maturity'], - params=[trade_date]) + params=[value_date]) df = df[df['folder'] == f'SER_{index}CURVE'] df.notional = df.notional.where(df.protection == 'Seller', -df.notional) df = df.groupby(['security_id', 'maturity'])['notional'].sum() @@ -268,18 +257,18 @@ def curve_pos(trade_date, index='IG'): sql_string = "SELECT closespread FROM index_quotes where index = %s and series = %s and tenor = %s and date = %s" for i, row in df[['index', 'tenor', 'series', 'notional']].iterrows(): temp = Index.from_name(row['index'], row.series, row.tenor) - temp.value_date = trade_date + temp.value_date = value_date.date() if row.notional > 0: temp.direction = 'Seller' temp.notional = abs(row.notional) spread_df = pd.read_sql_query(sql_string, serenitasdb, - params=[row['index'], row.series, row.tenor, trade_date]) + params=[row['index'], row.series, row.tenor, value_date.date()]) temp.spread = spread_df.iloc[0][0] indices.append(temp) return Portfolio(indices) -def curve_shape(trade_date, index='IG', percentile=.95): +def curve_shape(value_date, index='IG', percentile=.95, spread=None): ''' Returns a function to linearly interpolate between the curve based on maturity (in years)''' @@ -288,15 +277,94 @@ def curve_shape(trade_date, index='IG', percentile=.95): steepness = (curve_shape['10yr']/curve_shape['3yr']) series = on_the_run(index) - sql_string = "SELECT closespread FROM index_quotes where index = %s and series = %s and tenor = %s and date = %s" - spread_df = pd.read_sql_query(sql_string, serenitasdb, - params=[index, series, '5yr', trade_date.date()]) + if spread is None: + sql_string = "SELECT closespread FROM index_quotes where index = %s and series = %s and tenor = %s and date = %s" + spread_df = pd.read_sql_query(sql_string, serenitasdb, + params=[index, series, '5yr', value_date.date()]) + spread = spread_df.iloc[0][0] sql_string = "SELECT tenor, maturity FROM index_maturity where index = %s and series = %s" lookup_table = pd.read_sql_query(sql_string, serenitasdb, parse_dates=['maturity'], params=[index, series]) df = curve_shape[steepness == steepness.quantile(percentile, 'nearest')] - df = df * spread_df.iloc[0][0]/df['5yr'][0] + df = df * spread/df['5yr'][0] df = df.stack().rename('spread') df = df.reset_index().merge(lookup_table, on=['tenor']) - df['year_frac'] = (df.maturity - pd.to_datetime(trade_date)).dt.days/365 + df['year_frac'] = (df.maturity - pd.to_datetime(value_date)).dt.days/365 return interp1d(np.hstack([0, df.year_frac]), np.hstack([0, df.spread])) + +def pos_pnl_abs(portf, value_date, index='IG', rolling=6, years=3): + + ''' + Runs PNL analysis on portf using historical on-the-run spread levels - off-the-runs spreads are duration linearly interpolated''' + + series = on_the_run(index) + df = get_index_quotes(index, list(range(series - rolling, series + 1)), + tenor=['3yr', '5yr', '7yr', '10yr'], years=years) + df = df.groupby(level=['date', 'tenor']).nth(-1)['closespread'].unstack(-1) + + sql_string = "SELECT tenor, maturity FROM index_maturity where index = %s and series = %s" + lookup_table = pd.read_sql_query(sql_string, serenitasdb, parse_dates=['maturity'], params=[index, series]) + lookup_table['year_frac'] = (lookup_table.maturity - pd.to_datetime(value_date)).dt.days/365 + + indices = [] + for i, ind in enumerate(portf.indices): + indices.append(deepcopy(ind)) + portf_copy = Portfolio(indices) + portf_copy.reset_pv() + + portf_copy.reset_pv() + + r = [] + for date, row in df.iterrows(): + f = interp1d(np.hstack([0, lookup_table['year_frac']]), np.hstack([row[0]/2, row])) + for ind in portf_copy.indices: + ind.spread = f((pd.to_datetime(ind.end_date) - value_date).days/365) + r.append([[date, f(5)] + [portf_copy.pnl]]) + df = pd.DataFrame.from_records(chain(*r), columns=['date', 'five_yr_spread', 'pnl']) + return df.set_index('date') + +def curve_scen_table(portf): + + ''' + Runs PNL scenario on portf by shocking different points on the curve - off-the-runs shocks are duration linearly interpolated''' + + value_date = portf.value_date + + indices = [] + for i, ind in enumerate(portf.indices): + indices.append(deepcopy(ind)) + portf_copy = Portfolio(indices) + portf_copy.reset_pv() + + index = portf_copy.indices[0].index_type + series = on_the_run(index) + sql_string = "SELECT tenor, maturity FROM index_maturity where index = %s and series = %s" + lookup_table = pd.read_sql_query(sql_string, serenitasdb, parse_dates=['maturity'], params=[index, series]) + lookup_table['year_frac'] = (lookup_table.maturity - pd.to_datetime(value_date)).dt.days/365 + + lookup_table = lookup_table.iloc[[0,1,3]] + year_frac = lookup_table.year_frac.tolist() + tenors = lookup_table.tenor.tolist() + shock = 10 #shocks in bps + r = [] + + tenor_shock = pd.DataFrame(0, index=lookup_table['year_frac'], columns=['shock']) + for t_frac, t_ten in zip(year_frac, tenors): + tenor_shock.loc[t_frac] = -shock + for w_frac, w_ten in zip(year_frac, tenors): + tenor_shock.loc[w_frac] = 0 if t_ten == w_ten else shock + #f is the shock amount interpolated based on tenor + f = interp1d(np.hstack([0, year_frac]), np.hstack([tenor_shock.shock.iloc[0], tenor_shock.shock])) + for i, ind in enumerate(portf_copy.indices): + ind.spread = max(0, portf.indices[i].spread + f((ind.end_date - value_date).days/365)) + r.append([t_ten, w_ten] + [portf_copy.pv - portf.pv]) + tenor_shock.loc[w_frac] = 0 + tenor_shock.loc[t_frac] = 0 + return pd.DataFrame.from_records(r, columns=['tighter', 'wider', 'pnl']) + + + + + + + diff --git a/python/analytics/scenarios.py b/python/analytics/scenarios.py index 1438349c..dfd0be05 100644 --- a/python/analytics/scenarios.py +++ b/python/analytics/scenarios.py @@ -8,6 +8,7 @@ from itertools import chain from functools import partial from multiprocessing import Pool from .index_data import _get_singlenames_curves +from .curve_trades import curve_shape def run_swaption_scenarios(swaption, date_range, spread_shock, vol_shock, vol_surface, params=["pv"], vol_time_roll=True): @@ -119,10 +120,12 @@ def run_tranche_scenarios(tranche, spread_range, date_range, corr_map=False): _get_singlenames_curves.cache_clear() orig_tranche_pvs = tranche.tranche_pvs().bond_price results = [] + print(tranche.tranche_pvs().bond_price) for d in date_range: temp_tranche.value_date = d.date() for i, spread in enumerate(spread_range): temp_tranche.tweak(spread) + print(tranche.tranche_pvs().bond_price) if corr_map: temp_tranche.rho = tranche.map_skew(temp_tranche, 'TLP') index_pv[i] = temp_tranche._snacpv(spread * 1e-4, @@ -149,3 +152,30 @@ def run_tranche_scenarios(tranche, spread_range, date_range, corr_map=False): results = pd.concat(results, keys=date_range) results.index.names = ['date', 'spread_range'] return results + +def run_curve_scenarios(portf, spread_range, date_range, curve_per): + + """computes the pnl of a portfolio of indices for a range of spread/curve scenarios + + Parameters + ---------- + portf : Portfolio + spread_range : `np.array` + date_range : `pandas.Datetime.Index` + """ + + portf.reset_pv() + portf = deepcopy(portf) + index = portf.indices[0].index_type + + r = [] + for p in curve_per: + new_curve = curve_shape(date_range[0], index, p, 100) + for date in date_range: + portf.value_date = date.date() + for s in spread_range: + for ind in portf.indices: + ind.spread = new_curve((pd.to_datetime(ind.end_date) - date).days/365) * s/100 + r.append([[date, s, p] + [portf.pnl]]) + df = pd.DataFrame.from_records(chain(*r), columns=['date', 'spread', 'curve_per', 'pnl']) + return df.set_index('date') diff --git a/python/notebooks/Curve Trades.ipynb b/python/notebooks/Curve Trades.ipynb index 5637beaf..01669aae 100644 --- a/python/notebooks/Curve Trades.ipynb +++ b/python/notebooks/Curve Trades.ipynb @@ -6,11 +6,17 @@ "metadata": {}, "outputs": [], "source": [ - "import curve_trades as ct\n", + "import analytics.curve_trades as ct\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", + "import pandas as pd\n", + "import numpy as np\n", + "import graphics as g\n", + "import globeop_reports as go\n", + "\n", "from ipywidgets import widgets\n", - "import pandas as pd" + "from analytics.scenarios import run_curve_scenarios\n", + "from db import dbengine" ] }, { @@ -34,7 +40,8 @@ "metadata": {}, "outputs": [], "source": [ - "index = w.value" + "index = w.value\n", + "report_date = (pd.datetime.today() - pd.offsets.BDay(2)).normalize()" ] }, { @@ -105,7 +112,10 @@ "source": [ "#Theta with 3-5-10 Strategy\n", "df = ct.ratio_within_series(param='duration')\n", - "ct.curve_3_5_10(df)" + "s = - df.theta2['3yr'] / df.duration_ratio_to_5yr['3yr'] \\\n", + " + 2 * df.theta2['5yr'] \\\n", + " - df.theta2['10yr'] / df.duration_ratio_to_5yr['10yr']\n", + "s.dropna().unstack(-1).plot()" ] }, { @@ -114,9 +124,9 @@ "metadata": {}, "outputs": [], "source": [ - "#Theta with 5-10 Strategy\n", - "df = ct.ratio_within_series(param='duration')\n", - "ct.curve_5_10(df)" + "#Theta with 5-10 Strategy: buy sell 5y, buy 10y\n", + "s = df.theta2['5yr'] - df.theta2['10yr'] / df.duration_ratio_to_5yr['10yr']\n", + "s.dropna().unstack(-1).plot()" ] }, { @@ -146,10 +156,44 @@ "metadata": {}, "outputs": [], "source": [ - "#Scenario Anslysis on current position\n", - "report_date = (pd.datetime.today() - pd.offsets.BDay(1)).normalize()\n", - "curve_pos = ct.curve_pos(report_date)\n", - "origpv = curve_pos.pv" + "df = ct.forward_spread(report_date, index)\n", + "df.plot()\n", + "plt.ylabel('spread')\n", + "plt.xlabel('forward spread start date')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = ct.spot_forward(index)\n", + "df = df.rename(columns={'1yr': 'Spot Spread - 1 Year Forward', 'current': 'Spot Spread - Today'})\n", + "ax = df.plot(title = 'Credit Curve Roll Down')\n", + "plt.ylabel('spread (bps)')\n", + "ax.figure.savefig(\"/home/serenitas/edwin/PythonGraphs/curve_trades_roll_down.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "portf = ct.curve_pos(report_date, index)\n", + "shock_min = -.5\n", + "shock_max = .8\n", + "spread_shock = np.arange(shock_min, shock_max, 0.05)\n", + "sql_string = \"SELECT closespread FROM index_quotes where index = %s and series = %s and tenor = %s and date = %s\"\n", + "spread_df = pd.read_sql_query(sql_string, dbengine('serenitasdb'),\n", + " params=[index, ct.on_the_run(index), '5yr', report_date])\n", + "spread_range = (1+ spread_shock) * spread_df.iloc[0][0]\n", + "#need to max it at the closest maturity date\n", + "date_range = pd.bdate_range(report_date, report_date + 180* pd.offsets.DateOffset(), freq='5B')\n", + "curve_per = np.arange(.01, .99, .1)\n", + "\n", + "df = run_curve_scenarios(portf, spread_range, date_range, curve_per)" ] }, { @@ -158,9 +202,8 @@ "metadata": {}, "outputs": [], "source": [ - "flat_curve = ct.curve_shape(report_date, percentile = .05)\n", - "for ind in curve_pos.indices:\n", - " ind.spread = flat_curve((pd.to_datetime(ind.end_date) - trade_date).days/365)" + "df_plot = df[df.curve_per == curve_per[5]]\n", + "g.plot_time_color_map(df_plot, spread_range, attr='pnl')" ] }, { @@ -169,8 +212,14 @@ "metadata": {}, "outputs": [], "source": [ + "#Scenario Anslysis on current position\n", + "#curve_positions = ct.curve_pos(report_date, index)\n", + "#origpv = curve_positions.pv\n", + "#flat_curve = ct.curve_shape(report_date, index, percentile = .05)\n", + "#for ind in curve_positions.indices:\n", + "# ind.spread = flat_curve((pd.to_datetime(ind.end_date) - report_date).days/365)\n", "#PNL in flattening to a 5% case\n", - "curve_pos.pv - origpv" + "#curve_positions.pv - origpv" ] }, { @@ -179,10 +228,10 @@ "metadata": {}, "outputs": [], "source": [ - "df = ct.forward_spread(index)\n", - "df.plot()\n", - "plt.ylabel('spread')\n", - "plt.xlabel('forward spread start date')" + "curve_positions = ct.curve_pos(report_date, index)\n", + "df = ct.pos_pnl_abs(curve_positions, report_date)\n", + "navs = go.get_net_navs()\n", + "df_plot = df.pnl/navs.loc['2018-03-31'].endbooknav" ] }, { @@ -191,9 +240,36 @@ "metadata": {}, "outputs": [], "source": [ - "df = ct.spot_forward(index)\n", - "df.plot()\n", - "plt.ylabel('spread')" + "fig, ax = plt.subplots()\n", + "ax.plot(df_plot.index, df_plot.values)\n", + "ax.set(xlabel='date', ylabel='% of NAV',\n", + " title='PNL impact from spread curve scenario')\n", + "plt.xticks(rotation=90)\n", + "y_ticks = ax.get_yticks()\n", + "ax.set_yticklabels(['{:.2f}%'.format(y*100) for y in y_ticks])\n", + "plt.tight_layout()\n", + "#ax.figure.savefig(\"/home/serenitas/edwin/PythonGraphs/curve_trades.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#Historical PNL in a 5% case\n", + "df.pnl.quantile(.05)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scen_table = ct.curve_scen_table(curve_positions)\n", + "scen_table.pnl = scen_table.pnl/navs.loc['2018-03-31'].endbooknav *100\n", + "scen_table.pivot(index='tighter', columns='wider')" ] }, { |
