├── LICENSE.TXT ├── README.md ├── construction_execution.py ├── dynamic_allocation_performance_analysis.py ├── estimation.py ├── evaluation_attribution.py ├── my_python_setup.md ├── projection_pricing_aggregation.py ├── quest_for_invariance.py └── rnr_meucci_functions.py /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Peter Chan (peter-at-return-and-risk-dot-com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Code for Meucci Related Blog Posts 2 | Including my original code and some ports of Attilio Meucci's Matlab code. 3 | 4 | ## Instructions 5 | Save the files to a folder on your PYTHONPATH 6 | 7 | ## The MIT License (MIT) 8 | Copyright (c) 2016 Peter Chan (peter-at-return-and-risk-dot-com) 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /construction_execution.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python code for blog post "mini-Meucci : Applying The Checklist - Steps 8-9" 3 | http://www.returnandrisk.com/2016/06/mini-meucci-applying-checklist-steps-8-9.html 4 | Copyright (c) 2016 Peter Chan (peter-at-return-and-risk-dot-com) 5 | """ 6 | #%matplotlib inline 7 | from pandas_datareader import data 8 | import numpy as np 9 | import pandas as pd 10 | import datetime 11 | import math 12 | import matplotlib.pyplot as plt 13 | import seaborn 14 | 15 | # Get Yahoo data on 30 DJIA stocks and a few ETFs 16 | tickers = ['MMM','AXP','AAPL','BA','CAT','CVX','CSCO','KO','DD','XOM','GE','GS', 17 | 'HD','INTC','IBM','JNJ','JPM','MCD','MRK','MSFT','NKE','PFE','PG', 18 | 'TRV','UNH','UTX','VZ','V','WMT','DIS','SPY','DIA','TLT','SHY'] 19 | start = datetime.datetime(2008, 4, 1) 20 | end = datetime.datetime(2016, 5, 31) 21 | rawdata = data.DataReader(tickers, 'yahoo', start, end) 22 | prices = rawdata.to_frame().unstack(level=1)['Adj Close'] 23 | 24 | # Setup 25 | tau = 21 # investment horizon in days 26 | n_scenarios = len(prices) - tau 27 | n_asset = 30 28 | asset_tickers = tickers[0:30] 29 | 30 | ############################################################################### 31 | # Construction - 2 step mean-variance optimization 32 | ############################################################################### 33 | # Take shortcut and bypass some of the checklist steps in this toy example since 34 | # returns are invariants, estimation interval = horizon ie can use linear return 35 | # distribution directly as input into mean-variance optimizer 36 | 37 | # Projected linear returns to the horizon - historical simulation 38 | asset_rets = np.array(prices.pct_change(tau).ix[tau:, asset_tickers]) 39 | 40 | # Mean-variance inputs 41 | # Distribution of asset returns at horizon with flexible probabilities 42 | # Time-conditioned flexible probs with exponential decay 43 | half_life = 252 * 2 # half life of 2 years 44 | es_lambda = math.log(2) / half_life 45 | exp_probs = np.exp(-es_lambda * (np.arange(0, n_scenarios)[::-1])) 46 | exp_probs = exp_probs / sum(exp_probs) 47 | 48 | # Apply flexible probabilities to asset return scenarios 49 | import rnr_meucci_functions as rnr 50 | mu_pc, sigma2_pc = rnr.fp_mean_cov(asset_rets.T, exp_probs) 51 | 52 | # Perform shrinkage to mitigate estimation risk 53 | mu_shrk, cov_shrk = rnr.simple_shrinkage(mu_pc, sigma2_pc) 54 | 55 | # Step 1: m-v quadratic optimization for efficient frontier 56 | n_portfolio = 40 57 | weights_pc, rets_pc, vols_pc = rnr.efficient_frontier_qp_rets(n_portfolio, 58 | cov_shrk, mu_shrk) 59 | 60 | # Step 2: evaluate satisfaction for all allocations on the frontier 61 | satisfaction_pc = -vols_pc 62 | 63 | # Choose the allocation that maximises satisfaction 64 | max_sat_idx = np.asscalar(np.argmax(satisfaction_pc)) 65 | max_sat = satisfaction_pc[max_sat_idx] 66 | max_sat_weights = weights_pc[max_sat_idx, :] 67 | print('Optimal portfolio is minimum volatility portfolio with satisfaction\ 68 | index = {:.2}'.format(max_sat)) 69 | 70 | # Plot charts 71 | import matplotlib.gridspec as gridspec 72 | fig = plt.figure(figsize=(9, 8)) 73 | fig.hold(True) 74 | gs = gridspec.GridSpec(2, 1) 75 | ax = fig.add_subplot(gs[0, 0]) 76 | ax2 = fig.add_subplot(gs[1, 0]) 77 | ax.plot(vols_pc, rets_pc) 78 | ax.set_xlim(vols_pc[0]*0.95, vols_pc[-1]*1.02) 79 | ax.set_ylim(min(rets_pc)*0.9, max(rets_pc)*1.05) 80 | ax.set_xlabel('Standard Deviation') 81 | ax.set_ylabel('Expected Return') 82 | ax.set_title("Efficient Frontier") 83 | ax.plot(vols_pc[0], rets_pc[0], 'g.', markersize=10.0) 84 | ax.text(vols_pc[0]*1.02, rets_pc[0], 'minimum volatility portfolio', 85 | fontsize=10) 86 | 87 | ax2.plot(vols_pc, satisfaction_pc) 88 | ax2.set_xlim(vols_pc[0]*0.95, vols_pc[-1]*1.02) 89 | ax2.set_ylim(min(satisfaction_pc)*1.05, max(satisfaction_pc)*0.9) 90 | ax2.set_xlabel('Standard Deviation') 91 | ax2.set_ylabel('Satisfaction') 92 | ax2.set_title("Satisfaction") 93 | ax2.plot(vols_pc[max_sat_idx], max(satisfaction_pc), 'g.', markersize=10.0) 94 | ax2.text(vols_pc[max_sat_idx]*1.02, max(satisfaction_pc), 'maximum satisfaction', 95 | fontsize=10) 96 | plt.tight_layout() 97 | plt.show() 98 | 99 | # Plot minimum volatility portfolio weights 100 | pd.DataFrame(weights_pc[0,:], index=asset_tickers, columns=['w']).sort_values('w', \ 101 | ascending=False).plot(kind='bar', title='Minimum Volatility Portfolio Weights', \ 102 | legend=None, figsize=(10, 8)) 103 | plt.show() 104 | 105 | ############################################################################### 106 | # Execution 107 | ############################################################################### 108 | # See zipline simulation in dynamic allocation code file -------------------------------------------------------------------------------- /dynamic_allocation_performance_analysis.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python code for blog post "mini-Meucci : Applying The Checklist - Steps 10+" 3 | http://www.returnandrisk.com/2016/07/mini-meucci-applying-checklist-steps-10.html 4 | Copyright (c) 2016 Peter Chan (peter-at-return-and-risk-dot-com) 5 | """ 6 | ############################################################################### 7 | # Dynamic Allocation 8 | ############################################################################### 9 | #%matplotlib inline 10 | import rnr_meucci_functions as rnr 11 | import numpy as np 12 | from zipline.api import (set_slippage, slippage, set_commission, commission, 13 | order_target_percent, record, schedule_function, 14 | date_rules, time_rules, get_datetime, symbol) 15 | 16 | # Set tickers for data loading i.e. DJIA constituents and DIA ETF for benchmark 17 | tickers = ['MMM','AXP','AAPL','BA','CAT','CVX','CSCO','KO','DD','XOM','GE','GS', 18 | 'HD','INTC','IBM','JNJ','JPM','MCD','MRK','MSFT','NKE','PFE','PG', 19 | 'TRV','UNH','UTX','VZ','V','WMT','DIS', 'DIA'] 20 | 21 | # Set investable asset tickers 22 | asset_tickers = ['MMM','AXP','AAPL','BA','CAT','CVX','CSCO','KO','DD','XOM','GE','GS', 23 | 'HD','INTC','IBM','JNJ','JPM','MCD','MRK','MSFT','NKE','PFE','PG', 24 | 'TRV','UNH','UTX','VZ','V','WMT','DIS'] 25 | 26 | def initialize(context): 27 | # Turn off the slippage model 28 | set_slippage(slippage.FixedSlippage(spread=0.0)) 29 | # Set the commission model 30 | set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.0)) 31 | context.day = -1 # using zero-based counter for days 32 | context.set_benchmark(symbol('DIA')) 33 | context.assets = [] 34 | print('Setup investable assets...') 35 | for ticker in asset_tickers: 36 | #print(ticker) 37 | context.assets.append(symbol(ticker)) 38 | context.n_asset = len(context.assets) 39 | context.n_portfolio = 40 # num mean-variance efficient portfolios to compute 40 | context.today = None 41 | context.tau = None 42 | context.min_data_window = 756 # min of 3 yrs data for calculations 43 | context.first_rebal_date = None 44 | context.first_rebal_idx = None 45 | context.weights = None 46 | # Schedule dynamic allocation calcs to occur 1 day before month end - note that 47 | # actual trading will occur on the close on the last trading day of the month 48 | schedule_function(rebalance, 49 | date_rule=date_rules.month_end(days_offset=1), 50 | time_rule=time_rules.market_close()) 51 | # Record some stuff every day 52 | schedule_function(record_vars, 53 | date_rule=date_rules.every_day(), 54 | time_rule=time_rules.market_close()) 55 | 56 | def handle_data(context, data): 57 | context.day += 1 58 | #print(context.day) 59 | 60 | def rebalance(context, data): 61 | # Wait for 756 trading days (3 yrs) of historical prices before trading 62 | if context.day < context.min_data_window - 1: 63 | return 64 | # Get expanding window of past prices and compute returns 65 | context.today = get_datetime().date() 66 | prices = data.history(context.assets, "price", context.day, "1d") 67 | if context.first_rebal_date is None: 68 | context.first_rebal_date = context.today 69 | context.first_rebal_idx = context.day 70 | print('Starting dynamic allocation simulation...') 71 | # Get investment horizon in days ie number of trading days next month 72 | context.tau = rnr.get_num_days_nxt_month(context.today.month, context.today.year) 73 | # Calculate HFP distribution 74 | asset_rets = np.array(prices.pct_change(context.tau).iloc[context.tau:, :]) 75 | num_scenarios = len(asset_rets) 76 | # Set Flexible Probabilities Using Exponential Smoothing 77 | half_life_prjn = 252 * 2 # in days 78 | lambda_prjn = np.log(2) / half_life_prjn 79 | probs_prjn = np.exp(-lambda_prjn * (np.arange(0, num_scenarios)[::-1])) 80 | probs_prjn = probs_prjn / sum(probs_prjn) 81 | mu_pc, sigma2_pc = rnr.fp_mean_cov(asset_rets.T, probs_prjn) 82 | # Perform shrinkage to mitigate estimation risk 83 | mu_shrk, sigma2_shrk = rnr.simple_shrinkage(mu_pc, sigma2_pc) 84 | weights, _, _ = rnr.efficient_frontier_qp_rets(context.n_portfolio, 85 | sigma2_shrk, mu_shrk) 86 | print('Optimal weights calculated 1 day before month end on %s (day=%s)' \ 87 | % (context.today, context.day)) 88 | #print(weights) 89 | min_var_weights = weights[0,:] 90 | # Rebalance portfolio accordingly 91 | for stock, weight in zip(prices.columns, min_var_weights): 92 | order_target_percent(stock, np.asscalar(weight)) 93 | context.weights = min_var_weights 94 | 95 | def record_vars(context, data): 96 | record(weights=context.weights, tau=context.tau) 97 | 98 | def analyze(perf, bm_value, start_idx): 99 | pd.DataFrame({'portfolio':results.portfolio_value,'benchmark':bm_value})\ 100 | .iloc[start_idx:,:].plot(title='Portfolio Performance vs Benchmark') 101 | 102 | if __name__ == '__main__': 103 | from datetime import datetime 104 | import pytz 105 | from zipline.algorithm import TradingAlgorithm 106 | from zipline.utils.factory import load_bars_from_yahoo 107 | import pandas as pd 108 | import matplotlib.pyplot as plt 109 | 110 | # Create and run the algorithm. 111 | algo = TradingAlgorithm(initialize=initialize, handle_data=handle_data) 112 | 113 | start = datetime(2010, 5, 1, 0, 0, 0, 0, pytz.utc) 114 | end = datetime(2016, 5, 31, 0, 0, 0, 0, pytz.utc) 115 | print('Getting Yahoo data for 30 DJIA stocks and DIA ETF as benchmark...') 116 | data = load_bars_from_yahoo(stocks=tickers, start=start, end=end) 117 | # Check price data 118 | data.loc[:, :, 'price'].plot(figsize=(8,7), title='Input Price Data') 119 | plt.ylabel('price in $'); 120 | plt.legend(loc='center left', bbox_to_anchor=(1.0, 0.5)) 121 | plt.show() 122 | 123 | # Run algorithm 124 | results = algo.run(data) 125 | 126 | # Fix possible issue with timezone 127 | results.index = results.index.normalize() 128 | if results.index.tzinfo is None: 129 | results.index = results.index.tz_localize('UTC') 130 | 131 | # Adjust benchmark returns for delayed trading due to 3 year min data window 132 | bm_rets = algo.perf_tracker.all_benchmark_returns 133 | bm_rets[0:algo.first_rebal_idx + 2] = 0 134 | bm_rets.name = 'DIA' 135 | bm_rets.index.freq = None 136 | bm_value = algo.capital_base * np.cumprod(1+bm_rets) 137 | 138 | # Plot portfolio and benchmark values 139 | analyze(results, bm_value, algo.first_rebal_idx + 1) 140 | print('End value portfolio = {:.0f}'.format(results.portfolio_value.ix[-1])) 141 | print('End value benchmark = {:.0f}'.format(bm_value[-1])) 142 | 143 | # Plot end weights 144 | pd.DataFrame(results.weights.ix[-1], index=asset_tickers, columns=['w'])\ 145 | .sort_values('w', ascending=False).plot(kind='bar', \ 146 | title='End Simulation Weights', legend=None); 147 | 148 | ############################################################################### 149 | # Sequel Step - Ex-post performance analysis 150 | ############################################################################### 151 | import pyfolio as pf 152 | 153 | returns, positions, transactions, gross_lev = pf.utils.\ 154 | extract_rets_pos_txn_from_zipline(results) 155 | trade_start = results.index[algo.first_rebal_idx + 1] 156 | trade_end = datetime(2016, 5, 31, 0, 0, 0, 0, pytz.utc) 157 | 158 | print('Annualised volatility of the portfolio = {:.4}'.\ 159 | format(pf.timeseries.annual_volatility(returns[trade_start:trade_end]))) 160 | print('Annualised volatility of the benchmark = {:.4}'.\ 161 | format(pf.timeseries.annual_volatility(bm_rets[trade_start:trade_end]))) 162 | print('') 163 | 164 | pf.create_returns_tear_sheet(returns[trade_start:trade_end], 165 | benchmark_rets=bm_rets[trade_start:trade_end], 166 | return_fig=False) 167 | -------------------------------------------------------------------------------- /estimation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python code for blog post "mini-Meucci : Applying The Checklist - Step 2" 3 | http://www.returnandrisk.com/2016/06/mini-meucci-applying-checklist-step-2.html 4 | Copyright (c) 2016 Peter Chan (peter-at-return-and-risk-dot-com) 5 | """ 6 | #%matplotlib inline 7 | from pandas_datareader import data 8 | import numpy as np 9 | import datetime 10 | import math 11 | import matplotlib.pyplot as plt 12 | import matplotlib.gridspec as gridspec 13 | import seaborn 14 | 15 | # Get Yahoo data on 30 DJIA stocks and a few ETFs 16 | tickers = ['MMM','AXP','AAPL','BA','CAT','CVX','CSCO','KO','DD','XOM','GE','GS', 17 | 'HD','INTC','IBM','JNJ','JPM','MCD','MRK','MSFT','NKE','PFE','PG', 18 | 'TRV','UNH','UTX','VZ','V','WMT','DIS','SPY','DIA','TLT','SHY'] 19 | start = datetime.datetime(2005, 12, 31) 20 | end = datetime.datetime(2016, 5, 30) 21 | rawdata = data.DataReader(tickers, 'yahoo', start, end) 22 | prices = rawdata.to_frame().unstack(level=1)['Adj Close'] 23 | risk_drivers = np.log(prices) 24 | invariants = risk_drivers.diff().drop(risk_drivers.index[0]) 25 | T = len(invariants) 26 | 27 | # Get VIX data 28 | vix = data.DataReader('^VIX', 'yahoo', start, end)['Close'] 29 | vix.drop(vix.index[0], inplace=True) 30 | 31 | # Equal probs 32 | equal_probs = np.ones(len(vix)) / len(vix) 33 | 34 | # Time-conditioned flexible probs with exponential decay 35 | half_life = 252 * 2 # half life of 2 years 36 | es_lambda = math.log(2) / half_life 37 | exp_probs = np.exp(-es_lambda * (np.arange(0, len(vix))[::-1])) 38 | exp_probs = exp_probs / sum(exp_probs) 39 | # effective number of scenarios 40 | ens_exp_probs = math.exp(sum(-exp_probs * np.log(exp_probs))) 41 | 42 | # State-conditioned flexible probs based on VIX > 20 43 | state_probs = np.zeros(len(vix)) / len(vix) 44 | state_cond = np.array(vix > 20) 45 | state_probs[state_cond] = 1 / state_cond.sum() 46 | 47 | # Plot charts 48 | fig = plt.figure(figsize=(9, 8)) 49 | gs = gridspec.GridSpec(2, 2) 50 | ax = fig.add_subplot(gs[0, 0]) 51 | ax2 = fig.add_subplot(gs[0, 1]) 52 | ax3 = fig.add_subplot(gs[1, 0]) 53 | ax4 = fig.add_subplot(gs[1, 1]) 54 | ax.plot(vix.index, equal_probs) 55 | ax.set_title("Equal Probabilities (weights)") 56 | ax2.plot(vix.index, vix) 57 | ax2.set_title("Implied Volatility Index (VIX)") 58 | ax2.axhline(20, color='r') 59 | ax3.plot(vix.index, exp_probs) 60 | ax3.set_title("Time-conditioned Probabilities with Exponential Decay") 61 | ax4.plot(vix.index, state_probs, marker='o', markersize=3, linestyle='None', alpha=0.7) 62 | ax4.set_title("State-conditioned Probabilities (VIX > 20)") 63 | plt.tight_layout() 64 | plt.show() 65 | 66 | # Stress analysis 67 | import rnr_meucci_functions as rnr 68 | 69 | tmp_tickers = ['AAPL', 'JPM', 'WMT', 'SPY', 'TLT'] 70 | 71 | # HFP distribution of invariants using equal probs 72 | mu, sigma2 = rnr.fp_mean_cov(invariants.ix[:,tmp_tickers].T, equal_probs) 73 | 74 | # HFP distribution of invariants using state-conditioned probs (VIX > 20) 75 | mu_s, sigma2_s = rnr.fp_mean_cov(invariants.ix[:,tmp_tickers].T, state_probs) 76 | 77 | # Calculate correlations 78 | from statsmodels.stats.moment_helpers import cov2corr 79 | corr = cov2corr(sigma2) 80 | corr_s = cov2corr(sigma2_s) 81 | 82 | # Plot correlation heatmaps 83 | rnr.plot_2_corr_heatmaps(corr, corr_s, tmp_tickers, 84 | "HFP Correlation Heatmap - equal probs", 85 | "HFP Correlation Heatmap - state probs (VIX > 20)") -------------------------------------------------------------------------------- /evaluation_attribution.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python code for blog post "mini-Meucci : Applying The Checklist - Steps 6-7" 3 | http://www.returnandrisk.com/2016/06/mini-meucci-applying-checklist-steps-6-7.html 4 | Copyright (c) 2016 Peter Chan (peter-at-return-and-risk-dot-com) 5 | """ 6 | #%matplotlib inline 7 | from pandas_datareader import data 8 | import numpy as np 9 | import pandas as pd 10 | import datetime 11 | import math 12 | import matplotlib.pyplot as plt 13 | import seaborn 14 | 15 | # Get Yahoo data on 30 DJIA stocks and a few ETFs 16 | tickers = ['MMM','AXP','AAPL','BA','CAT','CVX','CSCO','KO','DD','XOM','GE','GS', 17 | 'HD','INTC','IBM','JNJ','JPM','MCD','MRK','MSFT','NKE','PFE','PG', 18 | 'TRV','UNH','UTX','VZ','V','WMT','DIS','SPY','DIA','TLT','SHY'] 19 | start = datetime.datetime(2008, 4, 1) 20 | end = datetime.datetime(2016, 5, 31) 21 | rawdata = data.DataReader(tickers, 'yahoo', start, end) 22 | prices = rawdata.to_frame().unstack(level=1)['Adj Close'] 23 | 24 | ############################################################################### 25 | # Quest for Invariance (random walk model) and Estimation (historical approach) 26 | ############################################################################### 27 | risk_drivers = np.log(prices) 28 | 29 | # Set estimation interval = investment horizon (tau) 30 | tau = 21 # investment horizon in days 31 | invariants = risk_drivers.diff(tau).drop(risk_drivers.index[0:tau]) 32 | 33 | ############################################################################### 34 | # Projection to the Investment Horizon 35 | ############################################################################### 36 | # Using the historical simulation approach and setting estimation interval = 37 | # investment horizon, means that projected invariants = invariants 38 | 39 | # Recover the projected scenarios for the risk drivers at the tau-day horizon 40 | risk_drivers_prjn = risk_drivers.loc[end,:] + invariants 41 | 42 | ############################################################################### 43 | # Pricing at the Investment Horizon 44 | ############################################################################### 45 | # Compute the projected $ P&L per unit of each stock for all scenarios 46 | prices_prjn = np.exp(risk_drivers_prjn) 47 | pnl = prices_prjn - prices.loc[end,:] 48 | 49 | ############################################################################### 50 | # Aggregation at the Investment Horizon 51 | ############################################################################### 52 | # Aggregate the individual stock P&Ls into projected portfolio P&L for all scenarios 53 | # Assume equally weighted protfolio at beginning of investment period 54 | capital = 1e6 55 | n_asset = 30 56 | asset_tickers = tickers[0:30] 57 | asset_weights = np.ones(n_asset) / n_asset 58 | # initial holdings ie number of shares 59 | h0 = capital * asset_weights / prices.loc[end, asset_tickers] 60 | pnl_portfolio = np.dot(pnl.loc[:, asset_tickers], h0) 61 | 62 | # Apply flexible probabilities to portfolio P&L scenarios 63 | n_scenarios = len(pnl_portfolio) 64 | 65 | # Time-conditioned flexible probs with exponential decay 66 | half_life = 252 * 2 # half life of 2 years 67 | es_lambda = math.log(2) / half_life 68 | exp_probs = np.exp(-es_lambda * (np.arange(0, n_scenarios)[::-1])) 69 | exp_probs = exp_probs / sum(exp_probs) 70 | # effective number of scenarios 71 | ens_exp_probs = np.exp(sum(-exp_probs * np.log(exp_probs))) 72 | 73 | # Projected Distribution of Portfolio P&L at Horizon with flexible probabilities 74 | import rnr_meucci_functions as rnr 75 | mu_port_e, sigma2_port_e = rnr.fp_mean_cov(pnl_portfolio.T, exp_probs) 76 | 77 | ############################################################################### 78 | # Ex-ante Evaluation 79 | ############################################################################### 80 | # Evaluate the ex-ante portfolio by some satisfaction index 81 | # For example, assume the investor evaluates allocations based only on volatility, 82 | # as measured by standard deviation, and does not take into account expected returns. 83 | # In this case, satisfaction is the opposite of the projected volatility of the portfolio 84 | satisfaction = - np.sqrt(sigma2_port_e) 85 | print('Ex-ante satisfaction index : {:,.0f} (in $ terms)'.format(-np.sqrt(sigma2_port_e))) 86 | print('Ex-ante satisfaction index : {:,.2%} (in % terms)'.format(-np.sqrt(sigma2_port_e)/capital)) 87 | 88 | ############################################################################### 89 | # 7. Ex-ante Attribution 90 | ############################################################################### 91 | # Linearly attribute the portfolio ex-ante PnL to the S&P500, long bond ETF + a residual 92 | # Additively attribute the volatility of the portfolio's PnL to S&P500, long bond ETF + a residual 93 | 94 | # Set factors 95 | factor_tickers = ['SPY', 'TLT', 'Residual'] 96 | n_factor = 2 97 | # Calculate linear returns - historical simulation 98 | asset_rets = np.array(prices.pct_change(tau).ix[tau:, asset_tickers]) 99 | factor_rets = np.array(prices.pct_change(tau).ix[tau:, ['SPY', 'TLT']]) 100 | #port_rets = pnl_portfolio / capital 101 | 102 | # Calculate portfolio standard deviation (in percentage terms) 103 | port_std = np.sqrt(sigma2_port_e) / capital 104 | 105 | # Factor attribution exposures and risk contributions (using flexible probs) 106 | beta, vol_contr_Z = rnr.factor_attribution(asset_rets, factor_rets, asset_weights, exp_probs, n_factor) 107 | 108 | print('Ex-ante factor exposure (beta):') 109 | for i, factor in enumerate(factor_tickers): 110 | print('\t\t{}:\t{:.2f}'.format(factor, beta[i])) 111 | print('') 112 | print('Ex-ante portfolio volatility = {:.2%}'.format(port_std)) 113 | print('\tFactor risk contribution:') 114 | for j, factor in enumerate(factor_tickers): 115 | print('\t\t{}:\t{:.2%}'.format(factor, vol_contr_Z[j])) 116 | 117 | # Plot factor risk contribution chart 118 | rnr.plot_waterfall_chart(pd.Series(vol_contr_Z, index=factor_tickers), 119 | 'Factor Contribution To Portfolio Volatility') 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /my_python_setup.md: -------------------------------------------------------------------------------- 1 | # My Python Setup 2 | Getting setup with Python for the first time can be a bit painful, so here's what worked for me (as always, your mileage may vary). 3 | 4 | ## Installation Instructions 5 | OS: Windows 10 (64-bit) 6 | 7 | Python Distribution: Anaconda by Continuum https://www.continuum.io/downloads 8 | 9 | Install Python 3.5 using the Windows 64-bit graphical installer 10 | 11 | Once the Anaconda installation is complete, open a command prompt window and type in the following commands to create a Python 3.4 environment (you need to use Python 3.4 for Quantopian's zipline package to work as of May 2016): 12 | 13 | conda create --name py34 python=3.4 anaconda 14 | 15 | activate py34 16 | 17 | conda install -c Quantopian zipline 18 | 19 | conda install -c https://conda.anaconda.org/omnia cvxopt 20 | 21 | conda install -c quantopian pyfolio 22 | 23 | conda install quandl 24 | 25 | ## Python IDE 26 | Coming from a Matlab/R background, my favourite is spyder. 27 | 28 | To try it out, open a command prompt window and type in: 29 | 30 | activate py34 31 | 32 | spyder 33 | 34 | ## Misc 35 | For writing blog posts and doing presentations, try using a Jupyter Notebook (you can also use it to develop and test code). 36 | 37 | To try it out, open a command prompt window and type in: 38 | 39 | activate py34 40 | 41 | jupyter notebook 42 | 43 | 44 | -------------------------------------------------------------------------------- /projection_pricing_aggregation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python code for blog post "mini-Meucci : Applying The Checklist - Steps 3-5" 3 | http://www.returnandrisk.com/2016/06/mini-meucci-applying-checklist-steps-3-5.html 4 | Copyright (c) 2016 Peter Chan (peter-at-return-and-risk-dot-com) 5 | """ 6 | #%matplotlib inline 7 | from pandas_datareader import data 8 | import numpy as np 9 | import datetime 10 | import math 11 | import matplotlib.pyplot as plt 12 | import seaborn 13 | 14 | # Get Yahoo data on 30 DJIA stocks and a few ETFs 15 | tickers = ['MMM','AXP','AAPL','BA','CAT','CVX','CSCO','KO','DD','XOM','GE','GS', 16 | 'HD','INTC','IBM','JNJ','JPM','MCD','MRK','MSFT','NKE','PFE','PG', 17 | 'TRV','UNH','UTX','VZ','V','WMT','DIS','SPY','DIA','TLT','SHY'] 18 | start = datetime.datetime(2008, 4, 1) 19 | end = datetime.datetime(2016, 5, 31) 20 | rawdata = data.DataReader(tickers, 'yahoo', start, end) 21 | prices = rawdata.to_frame().unstack(level=1)['Adj Close'] 22 | 23 | ############################################################################### 24 | # Quest for Invariance (random walk model) and Estimation (historical approach) 25 | ############################################################################### 26 | risk_drivers = np.log(prices) 27 | 28 | # Set estimation interval = investment horizon (tau) 29 | tau = 21 # investment horizon in days 30 | invariants = risk_drivers.diff(tau).drop(risk_drivers.index[0:tau]) 31 | 32 | ############################################################################### 33 | # Projection to the Investment Horizon 34 | ############################################################################### 35 | # Using the historical simulation approach and setting estimation interval = 36 | # investment horizon, means that projected invariants = invariants 37 | 38 | # Recover the projected scenarios for the risk drivers at the tau-day horizon 39 | risk_drivers_prjn = risk_drivers.loc[end,:] + invariants 40 | 41 | ############################################################################### 42 | # Pricing at the Investment Horizon 43 | ############################################################################### 44 | # Compute the projected $ P&L per unit of each stock for all scenarios 45 | prices_prjn = np.exp(risk_drivers_prjn) 46 | pnl = prices_prjn - prices.loc[end,:] 47 | 48 | ############################################################################### 49 | # Aggregation at the Investment Horizon 50 | ############################################################################### 51 | # Aggregate the individual stock P&Ls into projected portfolio P&L for all scenarios 52 | # Assume equally weighted protfolio at beginning of investment period 53 | capital = 1e6 54 | n_asset = 30 55 | asset_tickers = tickers[0:30] 56 | asset_weights = np.ones(n_asset) / n_asset 57 | # initial holdings ie number of shares 58 | h0 = capital * asset_weights / prices.loc[end, asset_tickers] 59 | pnl_portfolio = np.dot(pnl.loc[:, asset_tickers], h0) 60 | 61 | # Apply flexible probabilities to portfolio P&L scenarios 62 | n_scenarios = len(pnl_portfolio) 63 | 64 | # Equal probs 65 | equal_probs = np.ones(n_scenarios) / n_scenarios 66 | 67 | # Time-conditioned flexible probs with exponential decay 68 | half_life = 252 * 2 # half life of 2 years 69 | es_lambda = math.log(2) / half_life 70 | exp_probs = np.exp(-es_lambda * (np.arange(0, n_scenarios)[::-1])) 71 | exp_probs = exp_probs / sum(exp_probs) 72 | # effective number of scenarios 73 | ens_exp_probs = np.exp(sum(-exp_probs * np.log(exp_probs))) 74 | 75 | # Projected Distribution of Portfolio P&L at Horizon with flexible probabilities 76 | import rnr_meucci_functions as rnr 77 | mu_port, sigma2_port = rnr.fp_mean_cov(pnl_portfolio.T, equal_probs) 78 | mu_port_e, sigma2_port_e = rnr.fp_mean_cov(pnl_portfolio.T, exp_probs) 79 | 80 | print('Ex-ante portfolio $P&L mean over horizon (equal probs) : {:,.0f}'.format(mu_port)) 81 | print('Ex-ante portfolio $P&L volatility over horizon (equal probs) : {:,.0f}'.format(np.sqrt(sigma2_port))) 82 | print('') 83 | print('Ex-ante portfolio $P&L mean over horizon (flex probs) : {:,.0f}'.format(mu_port_e)) 84 | print('Ex-ante portfolio $P&L volatility over horizon (flex probs) : {:,.0f}'.format(np.sqrt(sigma2_port_e))) 85 | 86 | fig = plt.figure(figsize=(9, 8)) 87 | ax = fig.add_subplot(111) 88 | ax.hist(pnl_portfolio, 50, weights=exp_probs) 89 | ax.set_title('Ex-ante Distribution of Portfolio P&L (flexbile probabilities with exponential decay)') 90 | plt.show() 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /quest_for_invariance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python code for blog post "mini-Meucci : Applying The Checklist - Step 1" 3 | http://www.returnandrisk.com/2016/06/mini-meucci-applying-checklist-step-1.html 4 | Copyright (c) 2016 Peter Chan (peter-at-return-and-risk-dot-com) 5 | """ 6 | #%matplotlib inline 7 | from pandas_datareader import data 8 | import numpy as np 9 | import datetime 10 | import matplotlib.pyplot as plt 11 | import seaborn 12 | 13 | # Get Yahoo data on 30 DJIA stocks and a few ETFs 14 | tickers = ['MMM','AXP','AAPL','BA','CAT','CVX','CSCO','KO','DD','XOM','GE','GS', 15 | 'HD','INTC','IBM','JNJ','JPM','MCD','MRK','MSFT','NKE','PFE','PG', 16 | 'TRV','UNH','UTX','VZ','V','WMT','DIS','SPY','DIA','TLT','SHY'] 17 | start = datetime.datetime(2005, 12, 31) 18 | end = datetime.datetime(2016, 5, 30) 19 | rawdata = data.DataReader(tickers, 'yahoo', start, end) 20 | prices = rawdata.to_frame().unstack(level=1)['Adj Close'] 21 | risk_drivers = np.log(prices) 22 | invariants = risk_drivers.diff().drop(risk_drivers.index[0]) 23 | 24 | # Plots 25 | plt.figure() 26 | prices['AAPL'].plot(figsize=(10, 8), title='AAPL Daily Stock Price (Value)') 27 | plt.show() 28 | plt.figure() 29 | risk_drivers['AAPL'].plot(figsize=(10, 8), 30 | title='AAPL Daily Log of Stock Price (Log Value = Risk Driver)') 31 | plt.show() 32 | plt.figure() 33 | invariants['AAPL'].plot(figsize=(10, 8), 34 | title='AAPL Continuously Compounded Daily Returns (Log Return = Invariant)') 35 | plt.show() 36 | 37 | # Test for invariance using simulated data 38 | import rnr_meucci_functions as rnr 39 | np.random.seed(3) 40 | Data = np.random.randn(1000) 41 | rnr.IIDAnalysis(Data) 42 | 43 | # Test for invariance using real data 44 | rnr.IIDAnalysis(invariants.ix[:,'AAPL']) -------------------------------------------------------------------------------- /rnr_meucci_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python code functions used in blog posts on Attilio Meucci's The Checklist 3 | on www.returnandrisk.com 4 | Copyright (c) 2016 Peter Chan (peter-at-return-and-risk-dot-com) 5 | """ 6 | ############################################################################### 7 | # Quest for Invariance 8 | ############################################################################### 9 | import matplotlib.pyplot as plt 10 | import math 11 | import numpy as np 12 | import pandas as pd 13 | import matplotlib.gridspec as gridspec 14 | 15 | def IIDAnalysis(Data): 16 | """ 17 | Port of Attilio Meucci's Matlab file IIDAnalysis.m 18 | https://www.mathworks.com/matlabcentral/fileexchange/25010-exercises-in-advanced-risk-and-portfolio-management 19 | this function performs simple invariance (i.i.d.) tests on a time series 20 | 1. it checks that the variables are identically distributed by looking at the 21 | histogram of two subsamples 22 | 2. it checks that the variables are independent by looking at the 1-lag scatter plot 23 | under i.i.d. the location-dispersion ellipsoid should be a circle 24 | see "Risk and Asset Allocation"-Springer (2005), by A. Meucci 25 | """ 26 | 27 | # test "identically distributed hypothesis": split observations into two sub-samples and plot histogram 28 | Sample_1 = Data[0:math.floor(Data.size/2)] 29 | Sample_2 = Data[math.floor(Data.size/2):] 30 | num_bins_1 = math.floor(5 * math.log(Sample_1.size)) 31 | num_bins_2 = math.floor(5 * math.log(Sample_2.size)) 32 | X_lim = [Data.min() - .1 * (Data.max() - Data.min()), Data.max() + .1 * (Data.max() - Data.min())] 33 | n1, xout1 = np.histogram(Sample_1, num_bins_1) 34 | n2,xout2 = np.histogram(Sample_2, num_bins_2) 35 | 36 | fig=plt.figure(figsize=(9, 8), dpi= 80, facecolor='w', edgecolor='k') 37 | fig.hold(True) 38 | gs = gridspec.GridSpec(2, 2) 39 | ax = fig.add_subplot(gs[0, 0]) 40 | ax2 = fig.add_subplot(gs[0, 1]) 41 | ax3 = fig.add_subplot(gs[1, 0]) 42 | ax.set_position([0.03, .58, .44, .38]) 43 | ax2.set_position([.53, .58, .44, .38]) 44 | ax3.set_position([.31, .08, .38, .38]) 45 | ax.hist(Sample_1, num_bins_1, color=(0.7, 0.7, 0.7), edgecolor='k') 46 | ax.set_xlim(X_lim) 47 | ax.set_ylim([0, max([max(n1), max(n2)])]) 48 | ax.set_yticks([]) 49 | ax.set_title(" Distribution 1st Half Sample") 50 | ax2.hist(Sample_2, num_bins_2, color=(0.7, 0.7, 0.7), edgecolor='k') 51 | ax2.set_xlim(X_lim) 52 | ax2.set_ylim([0, max([max(n1), max(n2)])]) 53 | ax2.set_yticks([]) 54 | ax2.set_title("Distribution 2nd Half Sample") 55 | 56 | # test "independently distributed hypothesis": scatter plot of observations at lagged times 57 | X = Data[0:-1] 58 | Y = Data[1:] 59 | ax3.grid(True) 60 | ax3.scatter(X, Y, s=5, color='#0C63C7', marker='.') 61 | ax3.set_aspect('equal', 'box') 62 | 63 | tmp = np.column_stack((X, Y)) 64 | m = np.atleast_2d(tmp.mean(0)).transpose() 65 | S = np.cov(tmp, rowvar=0) 66 | TwoDimEllipsoid(m,S,2,0,0) 67 | plt.show() 68 | 69 | def TwoDimEllipsoid(Location, Square_Dispersion, Scale, PlotEigVectors, PlotSquare): 70 | """ 71 | Port of Attilio Meucci's Matlab file TwoDimEllipsoid.m 72 | https://www.mathworks.com/matlabcentral/fileexchange/25010-exercises-in-advanced-risk-and-portfolio-management 73 | this function computes the location-dispersion ellipsoid 74 | see "Risk and Asset Allocation"-Springer (2005), by A. Meucci 75 | """ 76 | 77 | # compute the ellipsoid in the r plane, solution to ((R-Location)' * Dispersion^-1 * (R-Location) ) = Scale^2 78 | EigenValues, EigenVectors = np.linalg.eigh(Square_Dispersion) 79 | 80 | Angle = np.arange(0, 2 * math.pi + math.pi/500, math.pi/500) 81 | Centered_Ellipse = np.zeros((2, np.size(Angle)), dtype=complex) 82 | NumSteps = np.size(Angle) 83 | for i in range(NumSteps): 84 | # normalized variables (parametric representation of the ellipsoid) 85 | y = np.array([[math.cos(Angle[i])], [math.sin(Angle[i])]]) 86 | Centered_Ellipse[:,i] = (np.dot(np.dot(EigenVectors, np.diag(np.sqrt(EigenValues))), y)).reshape(1,2) 87 | 88 | Centered_Ellipse = np.real(Centered_Ellipse) 89 | R = np.dot(Location, np.ones((1, NumSteps))) + Scale * Centered_Ellipse 90 | plt.plot(R[0,:], R[1,:], color='r', linewidth=2) 91 | plt.title("Location-Dispersion Ellipsoid") 92 | plt.xlabel("obs") 93 | plt.ylabel("lagged obs") 94 | 95 | # plot a rectangle centered in Location with semisides of lengths Dispersion(1) and Dispersion(2), respectively 96 | if PlotSquare: 97 | Dispersion = np.sqrt(np.diag(Square_Dispersion)) 98 | Vertex_LowRight_A = Location[0] + Scale * Dispersion[0] 99 | Vertex_LowRight_B = Location[1] - Scale * Dispersion[1] 100 | Vertex_LowLeft_A = Location[0] - Scale * Dispersion[0] 101 | Vertex_LowLeft_B = Location[1] - Scale * Dispersion[1] 102 | Vertex_UpRight_A = Location[0] + Scale * Dispersion[0] 103 | Vertex_UpRight_B = Location[1] + Scale * Dispersion[1] 104 | Vertex_UpLeft_A = Location[0] - Scale * Dispersion[0] 105 | Vertex_UpLeft_B = Location[1] + Scale * Dispersion[1] 106 | 107 | Square = np.array([[Vertex_LowRight_A, Vertex_LowRight_B], 108 | [Vertex_LowLeft_A, Vertex_LowLeft_B], 109 | [Vertex_UpLeft_A, Vertex_UpLeft_B], 110 | [Vertex_UpRight_A, Vertex_UpRight_B], 111 | [Vertex_LowRight_A, Vertex_LowRight_B]]) 112 | 113 | plt.plot(Square[:,0], Square[:,1], color='r', linewidth=2) 114 | 115 | # plot eigenvectors in the r plane (centered in Location) of length the 116 | # square root of the eigenvalues (rescaled) 117 | if PlotEigVectors: 118 | L_1 = Scale * np.sqrt(EigenValues[0]) 119 | L_2 = Scale * np.sqrt(EigenValues[1]) 120 | 121 | # deal with reflection: matlab chooses the wrong one 122 | Sign = np.sign(EigenVectors[0,0]) 123 | Start_A = Location[0] # eigenvector 1 124 | End_A = Location[0] + Sign * (EigenVectors[0,0]) * L_1 125 | Start_B = Location[1] 126 | End_B = Location[1] + Sign * (EigenVectors[0,1]) * L_1 127 | plt.plot([Start_A, End_A], [Start_B, End_B], color='r', linewidth=2) 128 | 129 | Start_A = Location[0] # eigenvector 2 130 | End_A = Location[0] + (EigenVectors[1,0] * L_2) 131 | Start_B = Location[1] 132 | End_B = Location[1] + (EigenVectors[1,1] * L_2) 133 | plt.plot([Start_A, End_A], [Start_B, End_B], color='r', linewidth=2) 134 | 135 | 136 | ############################################################################### 137 | # Estimation 138 | ############################################################################### 139 | def fp_mean_cov(x, p): 140 | """ 141 | Computes the HFP-mean and HFP-covariance of the data in x 142 | """ 143 | # FP mean 144 | if x.ndim == 1: 145 | mu = np.average(x, axis=0, weights=p) 146 | else: 147 | mu = np.average(x, axis=1, weights=p) 148 | # FP covariance 149 | cov = np.cov(x, aweights=p, ddof=0) 150 | return((mu, cov)) 151 | 152 | import seaborn as sns 153 | def plot_corr_heatmap(corr, labels, heading): 154 | 155 | sns.set(style="white") 156 | 157 | # Generate a mask for the upper triangle 158 | mask = np.zeros_like(corr, dtype=np.bool) 159 | mask[np.triu_indices_from(mask)] = True 160 | 161 | # Set up the matplotlib figure 162 | f, ax = plt.subplots(figsize=(8, 8)) 163 | 164 | # Generate a custom diverging colormap 165 | cmap = sns.diverging_palette(220, 10, as_cmap=True) 166 | 167 | # Draw the heatmap with the mask and correct aspect ratio 168 | sns.heatmap(corr, mask=mask, cmap=cmap, vmax=.3, 169 | square=True, xticklabels=labels, yticklabels=labels, 170 | linewidths=.5, ax=ax, cbar_kws={"shrink": .5}, annot=True) 171 | ax.set_title(heading) 172 | plt.show() 173 | 174 | def plot_2_corr_heatmaps(corr1, corr2, labels, title1, title2): 175 | fig=plt.figure(figsize=(9, 8)) 176 | gs = gridspec.GridSpec(1, 2) 177 | ax1 = fig.add_subplot(gs[0, 0]) 178 | ax2 = fig.add_subplot(gs[0, 1]) 179 | 180 | sns.set(style="white") 181 | 182 | # Generate a mask for the upper triangle 183 | mask = np.zeros_like(corr1, dtype=np.bool) 184 | mask[np.triu_indices_from(mask)] = True 185 | 186 | # Generate a custom diverging colormap 187 | cmap = sns.diverging_palette(220, 10, as_cmap=True) 188 | 189 | # Draw the heatmap with the mask and correct aspect ratio 190 | sns.heatmap(corr1, mask=mask, cmap=cmap, vmax=.3, 191 | square=True, xticklabels=labels, yticklabels=labels, 192 | linewidths=.5, ax=ax1, cbar_kws={"shrink": .3}, annot=True) 193 | ax1.set_title(title1) 194 | sns.heatmap(corr2, mask=mask, cmap=cmap, vmax=.3, 195 | square=True, xticklabels=labels, yticklabels=labels, 196 | linewidths=.5, ax=ax2, cbar_kws={"shrink": .3}, annot=True) 197 | ax2.set_title(title2) 198 | fig.tight_layout() 199 | plt.show() 200 | 201 | ############################################################################### 202 | # Attribution 203 | ############################################################################### 204 | def factor_attribution(asset_rets, factor_rets, asset_weights, probs, N_factors): 205 | # Ref: http://www.mathworks.com/matlabcentral/fileexchange/26853-factors-on-demand 206 | # StatisticalVsCrossSectional > S_Main.m 207 | port_rets = np.dot(asset_rets, asset_weights) 208 | port_std = np.sqrt(np.cov(port_rets, aweights=probs, ddof=0)) 209 | # Notation: X = asset, Z = factor, P = portfolio, U = residual 210 | # sigma2 = variance-covariance matrix 211 | # sigma = covariance terms only 212 | mu_PZ, sigma2_PZ = fp_mean_cov(np.concatenate((port_rets[:, None], factor_rets), axis=1).T, probs) 213 | sigma_PZ = sigma2_PZ[0, 1:N_factors+1] 214 | sigma2_Z = sigma2_PZ[1:N_factors+1, 1:N_factors+1] 215 | # Compute OLS loadings for the linear return model 216 | # Compute exposure i.e. beta 217 | beta = np.dot(np.dot(sigma_PZ.T, sigma2_Z.T), np.linalg.inv(np.dot(sigma2_Z, sigma2_Z.T))) 218 | mu_P = mu_PZ[0] 219 | mu_Z = mu_PZ[1:N_factors+1] 220 | alpha = mu_P - np.dot(beta, mu_Z) 221 | # Compute residuals 222 | U = port_rets - alpha - np.dot(factor_rets, beta) 223 | # Compute risk contribution 224 | mu_ZU, sigma2_ZU = fp_mean_cov(np.concatenate((factor_rets, U[:, None]), axis=1).T, probs) 225 | beta_ = np.append(beta, 1) 226 | vol_contr_Z = beta_ * np.dot(sigma2_ZU, beta_) / port_std 227 | return(beta_, vol_contr_Z) 228 | 229 | def plot_waterfall_chart(series, title): 230 | df = pd.DataFrame({'pos':np.maximum(series,0),'neg':np.minimum(series,0)}) 231 | blank = series.cumsum().shift(1).fillna(0) 232 | df.plot(kind='bar', stacked=True, bottom=blank, title=title, figsize=(9, 8)) 233 | 234 | ############################################################################### 235 | # Construction 236 | ############################################################################### 237 | def simple_shrinkage(mu, cov, mu_shrk_wt=0.1, cov_shrk_wt=0.1): 238 | # Reference: Attilio Meucci's Matlab file S_MVHorizon.m 239 | # https://www.mathworks.com/matlabcentral/fileexchange/25010-exercises-in-advanced-risk-and-portfolio-management 240 | n_asset = len(mu) 241 | 242 | # Mean shrinkage 243 | Shrk_Exp = np.zeros(n_asset) 244 | Exp_C_Hat = (1 - mu_shrk_wt) * mu + mu_shrk_wt * Shrk_Exp 245 | 246 | # Covariance shrinkage 247 | Shrk_Cov = np.eye(n_asset) * np.trace(cov) / n_asset 248 | Cov_C_Hat = (1-cov_shrk_wt) * cov + cov_shrk_wt * Shrk_Cov 249 | 250 | return((Exp_C_Hat, Cov_C_Hat)) 251 | 252 | def efficient_frontier_qp_rets(n_portfolio, covariance, expected_values): 253 | """ 254 | Port of Attilio Meucci's Matlab file EfficientFrontierQPRets.m 255 | https://www.mathworks.com/matlabcentral/fileexchange/25010-exercises-in-advanced-risk-and-portfolio-management 256 | This function returns the n_portfolio x 1 vector expected returns, 257 | the n_portfolio x 1 vector of volatilities and 258 | the n_portfolio x n_asset matrix of weights 259 | of n_portfolio efficient portfolios whose expected returns are equally spaced along the whole range of the efficient frontier 260 | """ 261 | import cvxopt as opt 262 | from cvxopt import solvers, blas 263 | 264 | solvers.options['show_progress'] = False 265 | n_asset = covariance.shape[0] 266 | expected_values = opt.matrix(expected_values) 267 | 268 | # determine weights, return and volatility of minimum-risk portfolio 269 | S = opt.matrix(covariance) 270 | pbar = opt.matrix(np.zeros(n_asset)) 271 | # 1. positive weights 272 | G = opt.matrix(0.0, (n_asset, n_asset)) 273 | G[::n_asset+1] = -1.0 274 | h = opt.matrix(0.0, (n_asset, 1)) 275 | # 2. weights sum to one 276 | A = opt.matrix(1.0, (1, n_asset)) 277 | b = opt.matrix(1.0) 278 | x0 = opt.matrix(1 / n_asset * np.ones(n_asset)) 279 | min_x = solvers.qp(S, pbar, G, h, A, b, 'coneqp', x0)['x'] 280 | min_ret = blas.dot(min_x.T, expected_values) 281 | min_vol = np.sqrt(blas.dot(min_x, S * min_x)) 282 | 283 | # determine weights, return and volatility of maximum-risk portfolio 284 | max_idx = np.asscalar(np.argmax(expected_values)) 285 | max_x = np.zeros(n_asset) 286 | max_x[max_idx] = 1 287 | max_ret = expected_values[max_idx] 288 | max_vol = np.sqrt(np.dot(max_x, np.dot(covariance, max_x))) 289 | 290 | # slice efficient frontier returns into n_portfolio segments 291 | target_rets = np.linspace(min_ret, max_ret, n_portfolio).tolist() 292 | 293 | # compute the n_portfolio weights and risk-return coordinates of the optimal allocations for each slice 294 | weights = np.zeros((n_portfolio, n_asset)) 295 | rets = np.zeros(n_portfolio) 296 | vols = np.zeros(n_portfolio) 297 | # start with min vol portfolio 298 | weights[0,:] = np.asarray(min_x).T 299 | rets[0] = min_ret 300 | vols[0] = min_vol 301 | 302 | for i in range(1, n_portfolio-1): 303 | # determine least risky portfolio for given expected return 304 | A = opt.matrix(np.vstack([np.ones(n_asset), expected_values.T])) 305 | b = opt.matrix(np.hstack([1, target_rets[i]])) 306 | x = solvers.qp(S, pbar, G, h, A, b, 'coneqp', x0)['x'] 307 | weights[i,:] = np.asarray(x).T 308 | rets[i] = blas.dot(x.T, expected_values) 309 | vols[i] = np.sqrt(blas.dot(x, S * x)) 310 | 311 | # add max ret portfolio 312 | weights[n_portfolio-1,:] = np.asarray(max_x).T 313 | rets[n_portfolio-1] = max_ret 314 | vols[n_portfolio-1] = max_vol 315 | 316 | return(weights, rets, vols) 317 | 318 | ############################################################################### 319 | # Dynamic Allocation 320 | ############################################################################### 321 | from zipline.utils.tradingcalendar import get_trading_days 322 | from datetime import datetime 323 | import pytz 324 | 325 | def get_num_days_nxt_month(month, year): 326 | """ 327 | Inputs: today's month number and year number 328 | Output: number of trading days in the following month 329 | """ 330 | nxt_month = month + 1 if month < 12 else 1 331 | _year = year if nxt_month != 1 else year + 1 332 | start = datetime(_year, nxt_month, 1, tzinfo=pytz.utc) 333 | end = datetime(_year if nxt_month != 12 else _year + 1, nxt_month + 1 if nxt_month != 12 else 1, 1, tzinfo=pytz.utc) 334 | return(len(get_trading_days(start, end))) 335 | 336 | 337 | --------------------------------------------------------------------------------