| |
| from vizro import Vizro |
| import vizro.plotly.express as px |
| import vizro.models as vm |
| from vizro.models.types import capture |
| from vizro.tables import dash_data_table |
|
|
| |
| import yfinance as yf |
| import pandas as pd |
| import numpy as np |
| import plotly.graph_objects as go |
|
|
| |
| from sklearn.model_selection import train_test_split |
|
|
| from skfolio import Population, RiskMeasure |
| from skfolio.datasets import load_sp500_dataset |
| from skfolio.optimization import EqualWeighted, MaximumDiversification, MeanRisk, ObjectiveFunction |
| from skfolio.preprocessing import prices_to_returns |
|
|
| |
| |
| |
| |
|
|
| |
|
|
| |
| tickers = ['EWA', 'EWH', 'EWJ', 'ENZL', 'EWS'] |
|
|
| |
| mkt_idx = 'IPAC' |
|
|
| |
| data = yf.download(tickers, start="2010-09-30", end="2024-09-30", auto_adjust=False) |
| idx_data = yf.download(mkt_idx, start="2010-09-30", end="2024-09-30", auto_adjust=False) |
|
|
| |
|
|
| prices = pd.DataFrame(data['Adj Close']) |
| idx_prices = pd.DataFrame(idx_data['Adj Close']) |
|
|
| |
| |
| |
|
|
| |
| X = prices_to_returns(prices) |
| IDX = prices_to_returns(idx_prices) |
|
|
| |
| |
| X_train, X_test = train_test_split(X, test_size=0.35, shuffle=False) |
|
|
| |
| IDX_test = IDX.loc[X_test.index[0]:] |
|
|
| |
| |
| |
|
|
| daily_ret = pd.concat([X_test, IDX_test], axis=1) |
| daily_ret.columns = tickers + [mkt_idx] |
| cumulative_growth = (1+daily_ret).cumprod()*10000 |
|
|
| |
|
|
| |
| model1 = MaximumDiversification() |
| model1.fit(X_train) |
|
|
| |
| model2 = MeanRisk( |
| risk_measure=RiskMeasure.CVAR, |
| objective_function=ObjectiveFunction.MINIMIZE_RISK, |
| portfolio_params=dict(name="Min CVaR"), |
| ) |
| model2.fit(X_train) |
|
|
| |
| eq = EqualWeighted() |
| eq.fit(X_train) |
|
|
| |
| |
| data = {'Maximum Diversification': model1.weights_, 'Minimum CVaR': model2.weights_, 'Equal Weighted': eq.weights_} |
|
|
| |
| df_weights = pd.DataFrame(data) |
|
|
| |
| df_weights.insert(0, 'Asset', tickers) |
|
|
| |
| df_weights_melted = df_weights.melt(id_vars='Asset', var_name='Portfolio', value_name='Weight') |
|
|
| |
| @capture("graph") |
| def composition(data_frame, x, y, color=None): |
| fig0 = px.bar(data_frame=data_frame, x=x, y=y, color=color) |
| fig0.update_layout( |
| xaxis_title = "Portfolio", |
| yaxis = { |
| "title": "Asset Weight", |
| "tickformat": ",.0%", |
| }, |
| legend_title_text = "Asset" |
| ) |
| return fig0 |
|
|
| |
| """ |
| ptf_model1_train = model1.predict(X_train) |
| ptf_model2_train = model2.predict(X_train) |
| ptf_eq_train = eq.predict(X_train) |
| """ |
|
|
| |
| ptf_model1_test = model1.predict(X_test) |
| ptf_model2_test = model2.predict(X_test) |
| ptf_eq_test = eq.predict(X_test) |
|
|
| |
| df_returns = pd.concat([ptf_model1_test.returns_df, ptf_model2_test.returns_df, ptf_eq_test.returns_df], axis=1) |
| df_returns.columns = ['Maximum Diversification', 'Minimum CVaR', 'Equal Weighted'] |
|
|
| |
| |
| df_cumul_growth = (1+df_returns).cumprod()*10000 |
|
|
| |
| |
| idx_cumulative_growth = (1 + IDX_test['IPAC']).cumprod()*10000 |
|
|
| |
| df_cumul_ret = pd.concat([df_cumul_growth, idx_cumulative_growth], axis=1) |
|
|
| df_cumul_ret = df_cumul_ret.rename(columns={'IPAC':'iShares Core MSCI Pacific ETF'}) |
|
|
|
|
| |
| @capture("graph") |
| def cumulative(data_frame, x, y): |
| fig1 = px.line(data_frame=data_frame, x=x, y=y) |
| fig1.update_layout( |
| xaxis_title = None, |
| yaxis = { |
| "title": None, |
| |
| }, |
| legend_title_text = "Portfolios" |
| ) |
| return fig1 |
|
|
| |
| population = Population([ptf_model1_test, ptf_model2_test, ptf_eq_test]) |
|
|
| |
| summ_df = population.summary().loc[['Mean','Annualized Mean', 'Standard Deviation', 'Annualized Standard Deviation', 'CVaR at 95%', 'Value at Risk at 95%', 'MAX Drawdown', 'Effective Number of Assets', 'Assets Number']].reset_index() |
| summ_df.rename(columns={'index': 'Measure'}, inplace=True) |
|
|
| |
|
|
| first_page = vm.Page( |
| title="Portfolio Assets and Optimisation Models", |
| layout=vm.Layout(grid=[[0, 1, 2], [3, 3, 3]]), |
| components=[ |
| vm.Card( |
| text=""" |
| # Portfolio Assets |
| |
| Our portfolio holds 5 ETFs that track the MSCI indices of the 5 developed market countries in the MSCI Pacific index. |
| |
| These are: |
| |
| - **EWA** [iShares MSCI Australia ETF](https://www.ishares.com/us/products/239607/) |
| - **EWH** [iShares MSCI Hong Kong ETF](https://www.ishares.com/us/products/239657/) |
| - **EWJ** [iShares MSCI Japan ETF](https://www.ishares.com/us/products/239665/ishares-msci-japan-etf) |
| - **ENZL** [iShares MSCI New Zealand ETF](https://www.ishares.com/us/products/239672/) |
| - **EWS** [iShares MSCI Singapore ETF](https://www.ishares.com/us/products/239678/) |
| |
| Each of these ETFs contains large and mid-cap equities of the respective country |
| |
| |
| We **do not** have an ETF that tracks the MSCI Pacific Index (large and mid-cap equities), hence we will use the following ETF: |
| |
| **IPAC** iShares Core MSCI Pacific ETF |
| |
| This ETF tracks the MSCI Pacific Investable Market Index, which includes large, mid **and** small cap equities in the five countries mentioned above. |
| |
| |
| How do the optimised portfolios compare against the market index in terms of cumulative growth? Check out the second page. |
| |
| **Note**: the results will be influenced by the concentrated nature of the portfolio, as it tends to magnify the impact of specific assets. |
| |
| |
| """, |
| ), |
| vm.Card( |
| text = """ |
| # Python Tools Used |
| * **yfinance** to obtain the time series of daily prices of the ETFs from the period 30/09/2010 to 30/09/2024 |
| |
| * **scikit-learn** to split the data into training and testing data sets with a training-testing split of 0.65-0.35 |
| |
| * **skfolio** to train and test the Maximum Diversification, Minimum CVaR and Equal Weighted optimisation models |
| |
| * **vizro** to build this dashboard |
| """ |
| ), |
| vm.Card( |
| text = """ |
| # Optimisation Models |
| |
| ## Maximum Diversification |
| |
| [Example from skfolio](https://skfolio.org/auto_examples/3_maxiumum_diversification/plot_1_maximum_divesification.html#sphx-glr-auto-examples-3-maxiumum-diversification-plot-1-maximum-divesification-py) |
| |
| * Find the portfolio that maximises the diversification ratio, which is the ratio of the weighted volatilities over the total volatility. |
| |
| * Compare the portfolio composition, cumulative returns and risk statistics with that of the equal weighted portfolio. |
| |
| ## Minimum CVaR |
| |
| [Example from skfolio](https://skfolio.org/auto_examples/1_mean_risk/plot_2_minimum_CVaR.html#sphx-glr-auto-examples-1-mean-risk-plot-2-minimum-cvar-py) |
| |
| * Find the portfolio that minimises CVaR (Conditional Value at Risk) with a default confidence level of 95%. |
| |
| * Compare the portfolio composition, cumulative returns and risk statistics with that of the equal weighted portfolio. |
| |
| """ |
| ), |
| vm.Graph(title="Cumulative Growth of $10,000 of Portfolio Assets and the iShares Core MSCI Pacific ETF over the testing dataset period", |
| id="growth", |
| figure=px.line(data_frame=cumulative_growth, x=cumulative_growth.index, y=cumulative_growth.columns.to_list(), labels={ |
| 'X': "", |
| 'value': "", |
| 'variable': 'Ticker' |
| } |
| ) |
| ), |
| |
| ], |
| ) |
|
|
| second_page = vm.Page( |
| title="Portfolio Composition, Returns and Stats", |
| layout=vm.Layout(grid=[[0, 1], [2,2]]), |
| components=[ |
| vm.Graph(title="Portfolio Composition", |
| id="portfolio_comparison", |
| figure=composition(x='Portfolio', y='Weight', color='Asset', data_frame = df_weights_melted) |
| ), |
| vm.Table( |
| title="Summary Stats", |
| figure=dash_data_table(data_frame=summ_df) |
| ), |
| vm.Graph(title="Cumulative Growth of $10,000 invested in the portfolios and iShares Core MSCI Pacific ETF over the testing dataset period", |
| id="cumulative_returns", |
| figure=cumulative(x=df_cumul_ret.index, y=df_cumul_ret.columns.to_list(), data_frame = df_cumul_ret) |
| ), |
| ], |
| ) |
|
|
| dashboard = vm.Dashboard(pages=[first_page, second_page]) |
| app = Vizro().build(dashboard) |
| server = app.dash.server |
|
|
| if __name__ == "__main__": |
| app.run() |
|
|