Package: optimalportfolios
Optimal Portfolios Backtesteroptimalportfolios package implements analytics for backtesting of optimal portfolios including:computing of inputs (covariance matrices, returns) using roll forward computations (to avoid hindsight bias)implementation of core solvers:Minimum varianceMaximum quadratic utilityEqual risk contributionMaximum diversificationMaximum Sharpe ratioMaximum Cara utility under Gaussian mixture modelcomputing performances of simulated portfoliosreportingOptimalPortfolios package is split into 5 main modules with the
dependecy path increasing sequentially as follows.optimisationis module containing implementation of quadratic and nonlinear solversreportsis module for computing performance statistics and performance attribution including returns, volatilities, etc.examples.crypto_allocationis module for computations and visualisations for
paper "Optimal Allocation to Cryptocurrencies in Diversified Portfolios"https://ssrn.com/abstract=4217841(see paper for description of the rolling-forward methodology and estimation of inputs)Table of contentsAnalyticsInstallationPortfolio OptimisersImplemented optimisersAdding an optimiserDefault parametersPrice time series dataExamplesOptimal Portfolio BacktestCustomised reportingParameters sensitivity backtestMulti optimisers cross backtestOptimal allocation to cryptocurrenciesContributionsUpdatesDisclaimerInstallationinstall usingpipinstalloptimalportfoliosupgrade usingpipinstall--upgradeoptimalportfoliosclose usinggitclonehttps://github.com/ArturSepp/OptimalPortfolios.gitCore dependencies:
python = ">=3.8,<3.11",
numba = ">=0.56.4",
numpy = ">=1.22.4",
scipy = ">=1.9.0",
pandas = ">=1.5.2",
matplotlib = ">=3.2.2",
seaborn = ">=0.12.2",
seaborn = ">=0.12.2",
scikit_learn = ">=1.3.0",
cvxpy = ">=1.3.2",
qis = ">=2.0.6",Optional dependencies:
yfinance ">=0.2.3" (for getting test price data),
pybloqs ">=1.2.13" (for producing html and pdf factsheets)Portfolio optimisers1. Implemented optimisersSubpackageoptimisation.rollingimplements specific optimisers
with lookback rolling windows rebalanced at given rebalancing_freqImplemented optimisers are enumerated inoptimization.config.pyclassPortfolioObjective(Enum):# risk-based:MAX_DIVERSIFICATION=1# maximum diversification measureEQUAL_RISK_CONTRIBUTION=2# implementation in risk_parityMIN_VARIANCE=3# min w^t @ covar @ wRISK_PARITY_ALT=4# alternative implementation of risk_parity# return-risk basedQUADRATIC_UTILITY=5# max means^t*w- 0.5*gamma*w^t*covar*wMAXIMUM_SHARPE_RATIO=6# max means^t*w / sqrt(*w^t*covar*w)# return-skeweness basedMAX_MIXTURE_CARA=7# carra for mixture distributionsEach module inoptimization.rollingimplements specific optimisers and estimators for their inputs.Moduleoptimization.rolling.risk_based.pyimplements following optimisers[PortfolioObjective.EQUAL_RISK_CONTRIBUTION,
PortfolioObjective.MAX_DIVERSIFICATION,
PortfolioObjective.RISK_PARITY_ALT,
PortfolioObjective.MIN_VAR]Moduleoptimization.rolling.max_utility_sharpe.pyimplements following optimisers[PortfolioObjective.QUADRATIC_UTILITY,
PortfolioObjective.MAXIMUM_SHARPE_RATIO]Moduleoptimization.rolling.max_mixure_carra.pyimplements following optimisers[PortfolioObjective.MAX_MIXTURE_CARA]Moduleoptimisation.engine.pywraps different optimisers into one functioncompute_rolling_optimal_weights()for each implementedPortfolioObjectivelisted inoptimisation.config.py2. Adding an optimiserAdd analytics for computing rolling weights using a new estimator in
subpackageoptimization.rolling. Any third-party packages can be usedAdd new optimiser type tooptimisation.config.pyand link implemented
optimiser in wrapper functioncompute_rolling_optimal_weights()inoptimisation.engine.py3. Default parametersKey parameters include the specification of the estimation sample.returns_freqdefines the frequency of returns for covariance matrix estimation. This parameter affects all methods.The default (assuming daily price data) is weekly Wednesday returnsreturns_freq = 'W-WED'.For price data with monthly observations
(such us hedged funds), monthly returns should be usedreturns_freq = 'M'.spandefines the estimation span for ewma covariance matrix. This parameter affects all methods which use
EWMA covariance matrix:PortfolioObjective in [MAX_DIVERSIFICATION, EQUAL_RISK_CONTRIBUTION, MIN_VARIANCE]andPortfolioObjective in [QUADRATIC_UTILITY, MAXIMUM_SHARPE_RATIO]The span is defined as the number of returns
for the half-life of EWMA filter:ewma_lambda = 1 - 2 / (span+1).span=52with weekly returns means that
last 52 weekly returns (one year of data) contribute 50% of weight to estimated covariance matrixThe default (assuming weekly returns) is 52:span=52.For monthly returns, I recommend to usespan=12orspan=24.rebalancing_freqdefines the frequency of weights update. This parameter affects all methods.The default value is quarterly rebalancingrebalancing_freq='Q'.For the following methodsPortfolioObjective in [QUADRATIC_UTILITY, MAXIMUM_SHARPE_RATIO, MAX_MIXTURE_CARA]Rebalancing frequency is also the rolling sample update frequency when mean returns and mixture distributions are estimated.roll_windowdefines the number of past returns applied for estimation of rolling mean returns and mixture distributions.This parameter affects the following optimisersPortfolioObjective in [QUADRATIC_UTILITY, MAXIMUM_SHARPE_RATIO, MAX_MIXTURE_CARA]and it is linked torebalancing_freq.Default value isroll_window=20which means that data for past 20 (quarters) are used in the sample
withrebalancing_freq='Q'For monthly rebalancing, I recomend to useroll_window=60which corresponds to using past 5 years of data4. Price time series dataThe input to all optimisers is dataframe prices which contains dividend and split adjusted prices.The price data can include assets with prices starting an ending at different times.All optimisers will set maximum weight to zero for assets with missing prices in the estimation sample period.Examples1. Optimal Portfolio BacktestSee script inoptimalportfolios.examples.optimal_portfolio_backtest.py# importsimportpandasaspdimportnumpyasnpimportmatplotlib.pyplotaspltimportseabornassnsimportyfinanceasyffromtypingimportTupleimportqisasqisfromoptimalportfolios.optimization.configimportPortfolioObjectivefromoptimalportfolios.optimization.engineimportcompute_rolling_optimal_weights# 1. we define the investment universe and allocation by asset classesdeffetch_universe_data()->Tuple[pd.DataFrame,pd.DataFrame,pd.Series]:"""fetch universe data for the portfolio construction:1. dividend and split adjusted end of day prices: price data may start / end at different dates2. benchmark prices which is used for portfolio reporting and benchmarking3. universe group data for portfolio reporting and risk attribution for large universesthis function is using yfinance to fetch the price data"""universe_data=dict(SPY='Equities',QQQ='Equities',EEM='Equities',TLT='Bonds',IEF='Bonds',SHY='Bonds',LQD='Credit',HYG='HighYield',GLD='Gold')tickers=list(universe_data.keys())group_data=pd.Series(universe_data)prices=yf.download(tickers,start=None,end=None,ignore_tz=True)['Adj Close'].dropna()prices=prices[tickers]# arrange as givenprices=prices.asfreq('B',method='ffill')benchmark_prices=prices[['SPY','TLT']]returnprices,benchmark_prices,group_data# 2. get universe dataprices,benchmark_prices,group_data=fetch_universe_data()# 3.a. define optimisation setupportfolio_objective=PortfolioObjective.MAX_DIVERSIFICATION# define portfolio objectivemin_weights={x:0.0forxinprices.columns}# all weights >= 0max_weights={x:1.0forxinprices.columns}# all weights <= 1rebalancing_freq='Q'# weights rebalancing frequencyreturns_freq=None# use data implied frequency = Bspan=72# span of number of returns for covariance estimation = 3 monthsis_gross_notional_one=True# sum of weights = 1.0is_long_only=True# all weights >= 0# 3.b. compute rolling portfolio weights rebalanced every quarterweights=compute_rolling_optimal_weights(prices=prices,portfolio_objective=portfolio_objective,min_weights=min_weights,max_weights=max_weights,rebalancing_freq=rebalancing_freq,is_long_only=is_long_only,span=span)# 4. given portfolio weights, construct the performance of the portfoliofunding_rate=None# on positive / negative cash balancesrebalancing_costs=0.0010# rebalancing costs per volume = 10bpportfolio_data=qis.backtest_model_portfolio(prices=prices,weights=weights,is_rebalanced_at_first_date=True,ticker='MaxDiversification',funding_rate=funding_rate,rebalancing_costs=rebalancing_costs,is_output_portfolio_data=True)# 5. using portfolio_data run the reporting with strategy factsheet# for group-based reporting set_group_dataportfolio_data.set_group_data(group_data=group_data,group_order=list(group_data.unique()))# set time period for portfolio reportingtime_period=qis.TimePeriod('31Dec2005','17Aug2023')fig=qis.generate_strategy_factsheet(portfolio_data=portfolio_data,benchmark_prices=benchmark_prices,time_period=time_period,**qis.fetch_default_report_kwargs(time_period=time_period))# save report to pdf and pngqis.save_figs_to_pdf(figs=[fig],file_name=f"{portfolio_data.nav.name}_portfolio_factsheet",orientation='landscape',local_path="C://Users//Artur//OneDrive//analytics//outputs")qis.save_fig(fig=fig,file_name=f"example_portfolio_factsheet",local_path=f"figures/")2. Customised reportingPortfolio data classPortfolioDatais implemented inQIS package# 6. can create customised reporting using portfolio_data custom reportingdefrun_customised_reporting(portfolio_data)->plt.Figure:withsns.axes_style("darkgrid"):fig,axs=plt.subplots(3,1,figsize=(12,12),tight_layout=True)kwargs=dict(x_date_freq='A',framealpha=0.8)portfolio_data.plot_nav(ax=axs[0],**kwargs)portfolio_data.plot_weights(ncol=len(prices.columns)//3,legend_stats=qis.LegendStats.AVG_LAST,title='Portfolio weights',bbox_to_anchor=None,ax=axs[1],**kwargs)portfolio_data.plot_returns_scatter(benchmark_price=benchmark_prices.iloc[:,0],ax=axs[2],**kwargs)returnfig# run customised reportfig=run_customised_reporting(portfolio_data)# save pngqis.save_fig(fig=fig,file_name=f"example_customised_report",local_path=f"figures/")3. Parameters sensitivity backtestSee script inoptimalportfolios.examples.parameter_sensitivity_backtest.py4. Multi optimisers cross backtestSee script inoptimalportfolios.examples.multi_optimisers_backtest.py5. Optimal allocation to cryptocurrenciesComputations and visualisations for
paper "Optimal Allocation to Cryptocurrencies in Diversified Portfolios"https://ssrn.com/abstract=4217841are implemented in moduleoptimalportfolios.crypto_allocation,
seeREADME in this moduleUpdates8 July 2023,  Version 1.0.1 releasedImplementation of optimisation methods and data considered in
Sepp A (2023), Optimal Allocation to Cryptocurrencies in Diversified Portfolios,
forthcoming in Risk. Available at SSRN:https://ssrn.com/abstract=42178412 September 2023,  Version 1.0.8 releasedAdded subpackageoptimisation.rollingwith optimisers grouped by the type of inputs and
data thy require.DisclaimerOptimalPortfolios package is distributed FREE & WITHOUT ANY WARRANTY under the GNU GENERAL PUBLIC LICENSE.See theLICENSE.txtin the release for details.Please report any bugs or suggestions by opening anissue.