def test_add_transaction_dict(self): pos = Position() # # Do sanity checks # self.assertEqual(EXO_POS_DICT_TEST['transactions'][-1]['date'], pd.Timestamp("2016-09-14T12:45:00")) self.assertEqual(EXO_POS_DICT_TEST['transactions'][0]['date'], pd.Timestamp("2011-06-01T12:45:00")) # Reconstruct position from initial transactions for trans_dict in EXO_POS_DICT_TEST['transactions']: pos.add_transaction_dict(trans_dict) self.assertEqual(len(EXO_POS_DICT_TEST['position']['positions']), len(pos.netpositions)) for asset_hash, pos_dict in EXO_POS_DICT_TEST['position'][ 'positions'].items(): self.assertEqual(pos_dict['qty'], pos.netpositions[int(asset_hash)]['qty']) self.assertEqual(pos_dict['value'], pos.netpositions[int(asset_hash)]['value'])
class PayoffAnalyzer: def __init__(self, datasource): self.datasource = datasource self.position = None self.position_type = None self.position_name = None self.analysis_date = None def load_transactions(self, transactions_list, analysis_date, position_name=''): """ Create payoff diagram analysis from transactions list :param transactions_list: :return: """ self.position = Position() for trans in transactions_list: self.position.add(trans) self.position_type = 'TransList' self.position_name = position_name self.analysis_date = analysis_date def load_exo(self, exo_name, date=None): """ Load EXO positions for further analysis :param exo_name: Name of EXO to analyze :param date: calculate EXO position on particular date (if None - return current position) :return: None """ # Load EXO dict from EXO engine exo_data = self.datasource.exostorage.load_exo(exo_name) if exo_data is None: raise NameError("EXO data for {0} not found.".format(exo_name)) # Calculate net position on particular date # Reconstruct position passing transactions from early days to current day pos_date = datetime.now() if date is None else date self.position = Position() for trans in exo_data['transactions']: if trans['date'] <= pos_date: self.position.add_transaction_dict(trans) else: break if len(self.position.netpositions) == 0: if len(exo_data['transactions']) == 0: warnings.warn("EXO doesn't contain any transactions") return else: # Can't find any transactions on specific date warnings.warn( "Can't find any transactions on specific date. First EXO transaction occured on {0}" .format(exo_data['transactions'][0]['date'])) return # Convert position to normal state # We will load all assets information from DB # And this will allow us to use position pricing as well self.position.convert(self.datasource, pos_date) self.position_type = 'EXO' self.position_name = exo_name self.analysis_date = pos_date def load_campaign(self, campaign_name, date=None): """ Load campaign net positions for further analysis :param campaign_name: :param date: :return: """ # Load campaign positions campaign_dict = self.datasource.exostorage.campaign_load(campaign_name) if campaign_dict is None: warnings.warn("Campaign not found: " + campaign_name) return cmp = Campaign(campaign_dict, self.datasource) # Calculate campaign's net exo position on particular date pos_date = datetime.now() if date is None else date exo_exposure = cmp.exo_positions(date) transactions = [] for exo_name, exp_dict in exo_exposure.items(): print("Loading: {0} Exposure: {1}".format(exo_name, exp_dict['exposure'])) # Skip zero-positions if exp_dict['exposure'] == 0: continue # Calculate position based on EXO transactions exo_data = self.datasource.exostorage.load_exo(exo_name) if exo_data is None: raise NameError("EXO data for {0} not found.".format(exo_name)) for trans in exo_data['transactions']: if trans['date'] <= pos_date: trans['qty'] *= exp_dict['exposure'] trans['usdvalue'] *= exp_dict['exposure'] transactions.append(trans) else: break # Sort transactions by date transactions = sorted(transactions, key=lambda k: k['date']) # Construct position self.position = Position() for t in transactions: self.position.add_transaction_dict(t) # Convert position to normal state # We will load all assets information from DB # And this will allow us to use position pricing as well self.position.convert(self.datasource, pos_date) # Store positions values for analysis self.position_type = 'Campaign' self.position_name = campaign_name self.analysis_date = pos_date def calc_payoff(self, strikes_to_analyze=10, iv_change=0.0, days_to_expiration=None): """ Calculates options positions payoff data for graphs (incl. PnL on expiration, current PnL, greeks) :param strikes_to_analyze: number of strikes to show on Payoff graph :param iv_change: IV change in WhatIF scenario :param days_to_expiration: Days to expiration in WhatIF scenario :return: """ if self.position is None: raise Exception( "You should run load_exo() or load_campaign() first") # Get actual price for underlying future contract current_price = self.position.underlying_price # Calculate ATM strike for current price instrument = self.position.underlying atm_strike = instrument.get_atm_strike(current_price) strike_inc = instrument.optionstrikeincrement # Store position's opened value (used for PnL calculation) pos_value = self.position.usdvalue payoffs = [] # Building option payoff diagram for soffset in range(-strikes_to_analyze, strikes_to_analyze, 1): price_to_analyze = atm_strike + soffset * strike_inc # Calculate current payoff of options position whatif_data_current = self.position.price_whatif( underlying_price=price_to_analyze) # Calculate options position value at expiration whatif_data_exp = self.position.price_whatif( underlying_price=price_to_analyze, days_to_expiration=0) # Calculate options position with WhatIF scenario included whatif_data_scenario = self.position.price_whatif( underlying_price=price_to_analyze, iv_change=iv_change, days_to_expiration=days_to_expiration) # Calculate payoff strike_payoff = { 'strike': price_to_analyze, 'current_payoff': whatif_data_current['usdvalue'] - pos_value, 'current_delta': whatif_data_current['delta'], 'expiration_payoff': whatif_data_exp['usdvalue'] - pos_value, 'expiration_delta': whatif_data_exp['delta'], 'scenario_payoff': whatif_data_scenario['usdvalue'] - pos_value, 'scenario_delta': whatif_data_scenario['delta'], } payoffs.append(strike_payoff) dfresult = pd.DataFrame(payoffs) dfresult = dfresult.set_index('strike') return dfresult def position_info(self, iv_change=0.0, days_to_expiration=None): """ Returns net positions values (Qty, Greeks, Prices) :param iv_change: IV change in WhatIF scenario :param days_to_expiration: Days to expiration in WhatIF scenario :return: """ if self.position is None: raise Exception( "You should run load_exo() or load_campaign() first") pos_info = self.position.price_whatif( iv_change=iv_change, days_to_expiration=days_to_expiration) pos_info['opened_value'] = self.position.usdvalue pos_info['current_pnl'] = pos_info['usdvalue'] - self.position.usdvalue pos_info['current_ulprice'] = self.position.underlying_price return pos_info def plot(self, strikes_on_graph, iv_change, days_to_expiration): """ Plot payoff diagram with WhatIF scenario :param strikes_to_analyze: number of strikes to show on Payoff graph :param iv_change: IV change in WhatIF scenario :param days_to_expiration: Days to expiration in WhatIF scenario :return: """ if len(self.position.netpositions) == 0: warnings.warn("Can't calculate payoff diagram for empty position") return dfpayoff = self.calc_payoff(strikes_to_analyze=strikes_on_graph, iv_change=iv_change, days_to_expiration=days_to_expiration) pos_info = self.position_info() f, (ax1, ax2) = plt.subplots(2, gridspec_kw={'height_ratios': [3, 1]}) ax1.set_title('{0}: {1}'.format(self.position_type, self.position_name)) dfpayoff['expiration_payoff'].plot(ax=ax1, label='At expiration', lw=2, c='blue') dfpayoff['current_payoff'].plot(ax=ax1, label='Current', c='green') dfpayoff['scenario_payoff'].plot(ax=ax1, label='WhatIf', style='--', c='red') ax1.axvline(pos_info['current_ulprice'], linestyle='--', c='grey', label='Current price') ax1.axhline(0, c='grey') ax1.legend() ax2.axvline(pos_info['current_ulprice'], linestyle='--', c='grey') dfpayoff['expiration_delta'].plot(ax=ax2, c='blue', lw=2) dfpayoff['current_delta'].plot(ax=ax2, c='green') dfpayoff['scenario_delta'].plot(ax=ax2, style='--', c='red') ax2.set_title('Delta') ax2.axhline(0, c='grey') delta = dfpayoff['expiration_delta'] ax2.set_ylim(delta.min() - 0.2, delta.max() + 0.2) def show_report(self, iv_change, days_to_expiration): if len(self.position.netpositions) == 0: warnings.warn("Can't calculate position report for empty position") return pos_info = self.position_info() print('Position analysis for {0}: {1}\n'.format( self.position_type, self.position_name)) print('Analysis date: {0}'.format(self.analysis_date)) print("PnL: {0:>10}".format(pos_info['current_pnl'])) print("Delta: {0:>10}".format(pos_info['delta'])) df = pd.DataFrame(pos_info['whatif_positions']) display(HTML(self._format_position_table(df))) whatif_pos_info = self.position_info( iv_change=iv_change, days_to_expiration=days_to_expiration) whatifdf = pd.DataFrame(whatif_pos_info['whatif_positions']) display( HTML( self._format_whatif_position_table(whatifdf, iv_change, days_to_expiration))) def _format_position_table(self, posdf): rows = "" table_template = """ <div style="font-family:'Courier New', Courier, monospace;"> <h4>Positions at {1}</h4> <table border="0" cellpadding="10" width="100%"> <thead> <tr> <th style="text-align: center;">Asset</th> <th style="text-align: center;">OpenPrice</th> <th style="text-align: center;">CurrentPrice</th> <th style="text-align: center;">Qty</th> <th style="text-align: center;">PnL</th> <th style="text-align: center;">IV</th> <th style="text-align: center;">Delta</th> <th style="text-align: center;">ToExpiration</th> <th style="text-align: center;">RFR</th> </tr> </thead> {0} </table> </div> """ row_template = ''' <tr> <td>{asset}</td> <td style="text-align: right;">{open_price}</td> <td style="text-align: right;">{price}</td> <td style="text-align: right;">{qty}</td> <td style="text-align: right; {pnl_style}">${pnl:0.0f}</td> <td style="text-align: right;">{iv}</td> <td style="text-align: right;">{delta:0.2f}</td> <td style="text-align: right;">{days_to_expiration} days</td> <td style="text-align: right;">{riskfreerate}</td> </tr> ''' def pnl_color(pnl): if pnl < 0: return 'color: #CC3327;' if pnl > 0: return 'color: #28CC52;' return '' instrument = self.position.underlying def format_price(asset, instrument, price): if asset.startswith('F.'): return round(price, len(str(instrument.ticksize)) - 2) elif asset.startswith('C.') or asset.startswith('P.'): return round(price, len(str(instrument.optionticksize)) - 2) return price def format_iv(iv): if np.isnan(iv): return '' return '{0:0.2f}%'.format(iv * 100) for k, v in posdf.iterrows(): values_dict = v.to_dict() values_dict['open_price'] = format_price(v['asset'], instrument, v['open_price']) values_dict['price'] = format_price(v['asset'], instrument, v['price']) values_dict['pnl_style'] = pnl_color(v['pnl']) values_dict['iv'] = format_iv(v['iv']) values_dict['riskfreerate'] = format_iv(v['riskfreerate']) rows += row_template.format(**values_dict) return table_template.format(rows, self.analysis_date) def _format_whatif_position_table(self, posdf, iv_change, days_to_expiration): rows = "" table_template = """ <div style="font-family:'Courier New', Courier, monospace;"> <h4>WhatIf scenario</h4> <p> IV change: {iv_change} </p> <p> Days to expiration: {days_to_expiration} </p> <table border="0" cellpadding="10" width="100%"> <thead> <tr> <th style="text-align: center;">Asset</th> <th style="text-align: center;">OpenPrice</th> <th style="text-align: center;">CurrentPrice</th> <th style="text-align: center;">Qty</th> <th style="text-align: center;">PnL</th> <th style="text-align: center;">IV</th> <th style="text-align: center;">Delta</th> <th style="text-align: center;">ToExpiration</th> <th style="text-align: center;">RFR</th> </tr> </thead> {rows} </table> </div> """ row_template = ''' <tr> <td>{asset}</td> <td style="text-align: right;">{open_price}</td> <td style="text-align: right;">{price}</td> <td style="text-align: right;">{qty}</td> <td style="text-align: right; {pnl_style}">${pnl:0.0f}</td> <td style="text-align: right;">{iv}</td> <td style="text-align: right;">{delta:0.2f}</td> <td style="text-align: right;">{days_to_expiration} days</td> <td style="text-align: right;">{riskfreerate}</td> </tr> ''' def pnl_color(pnl): if pnl < 0: return 'color: #CC3327;' if pnl > 0: return 'color: #28CC52;' return '' instrument = self.position.underlying def format_price(asset, instrument, price): if asset.startswith('F.'): return round(price, len(str(instrument.ticksize)) - 2) elif asset.startswith('C.') or asset.startswith('P.'): return round(price, len(str(instrument.optionticksize)) - 2) return price def format_iv(iv): if np.isnan(iv): return '' return '{0:0.2f}%'.format(iv * 100) for k, v in posdf.iterrows(): values_dict = v.to_dict() values_dict['open_price'] = format_price(v['asset'], instrument, v['open_price']) values_dict['price'] = format_price(v['asset'], instrument, v['price']) values_dict['pnl_style'] = pnl_color(v['pnl']) values_dict['iv'] = format_iv(v['iv']) values_dict['riskfreerate'] = format_iv(v['riskfreerate']) rows += row_template.format(**values_dict) table_context = { 'rows': rows, 'iv_change': iv_change, 'days_to_expiration': days_to_expiration, } return table_template.format(**table_context)