{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "9e6ecb20-c3d6-46d7-8ea4-18021afaec72", "metadata": {}, "outputs": [], "source": [ "from io import BytesIO\n", "\n", "import numpy as np\n", "import pandas as pd\n", "import holoviews as hv\n", "import panel as pn\n", "\n", "from scipy.optimize import minimize\n", "\n", "pn.extension('tabulator', design='material', template='material', loading_indicator=True)\n", "import hvplot.pandas" ] }, { "cell_type": "markdown", "id": "a9b07d1c-444a-4148-aa9b-03a547acb799", "metadata": {}, "source": [ "## Load data" ] }, { "cell_type": "code", "execution_count": null, "id": "9d225a2f-db1d-46ad-a742-f8431d006ecc", "metadata": { "tags": [] }, "outputs": [], "source": [ "@pn.cache\n", "def get_stocks(data):\n", " if data is None:\n", " stock_file = 'https://datasets.holoviz.org/stocks/v1/stocks.csv'\n", " else:\n", " stock_file = BytesIO(data)\n", " return pd.read_csv(stock_file, index_col='Date', parse_dates=True)\n", "\n", "file_input = pn.widgets.FileInput(sizing_mode='stretch_width')\n", "\n", "stocks = hvplot.bind(get_stocks, file_input).interactive()\n", "\n", "selector = pn.widgets.MultiSelect(\n", " name='Select stocks', sizing_mode='stretch_width',\n", " options=stocks.columns.to_list()\n", ")\n", "\n", "selected_stocks = stocks.pipe(\n", " lambda df, cols: df[cols] if cols else df, selector\n", ")" ] }, { "cell_type": "markdown", "id": "01630a8d-6683-4555-9271-26839b217faf", "metadata": {}, "source": [ "## Business logic" ] }, { "cell_type": "code", "execution_count": null, "id": "be207249-4478-4eaf-994f-3f8bd927c8c4", "metadata": {}, "outputs": [], "source": [ "def compute_random_allocations(log_return, num_ports=15000):\n", " _, ncols = log_return.shape\n", " \n", " # Compute log and mean return\n", " mean_return = np.nanmean(log_return, axis=0)\n", " \n", " # Allocate normalized weights\n", " weights = np.random.random((num_ports, ncols))\n", " normed_weights = (weights.T / np.sum(weights, axis=1)).T\n", " data = dict(zip(log_return.columns, normed_weights.T))\n", "\n", " # Compute expected return and volatility of random portfolios\n", " data['Return'] = expected_return = np.sum((mean_return * normed_weights) * 252, axis=1)\n", " return_covariance = np.cov(log_return[1:], rowvar=False) * 252\n", " if not return_covariance.shape:\n", " return_covariance = np.array([[252.]])\n", " data['Volatility'] = volatility = np.sqrt((normed_weights * np.tensordot(return_covariance, normed_weights.T, axes=1).T).sum(axis=1))\n", " data['Sharpe'] = sharpe_ratio = expected_return/volatility\n", " \n", " df = pd.DataFrame(data)\n", " df.attrs['mean_return'] = mean_return\n", " df.attrs['log_return'] = log_return\n", " return df\n", "\n", "def check_sum(weights):\n", " return np.sum(weights) - 1\n", "\n", "def get_return(mean_ret, weights):\n", " return np.sum(mean_ret * weights) * 252\n", "\n", "def get_volatility(log_ret, weights):\n", " return np.sqrt(np.dot(weights.T, np.dot(np.cov(log_ret[1:], rowvar=False) * 252, weights)))\n", "\n", "def compute_frontier(df, n=30):\n", " frontier_ret = np.linspace(df.Return.min(), df.Return.max(), n)\n", " frontier_volatility = []\n", "\n", " cols = len(df.columns) - 3\n", " bounds = tuple((0, 1) for i in range(cols))\n", " init_guess = [1./cols for i in range(cols)]\n", " for possible_return in frontier_ret:\n", " cons = (\n", " {'type':'eq', 'fun': check_sum},\n", " {'type':'eq', 'fun': lambda w: get_return(df.attrs['mean_return'], w) - possible_return}\n", " )\n", " result = minimize(lambda w: get_volatility(df.attrs['log_return'], w), init_guess, bounds=bounds, constraints=cons)\n", " frontier_volatility.append(result['fun'])\n", " return pd.DataFrame({'Volatility': frontier_volatility, 'Return': frontier_ret})\n", "\n", "def minimize_difference(weights, des_vol, des_ret, log_ret, mean_ret):\n", " ret = get_return(mean_ret, weights)\n", " vol = get_volatility(log_ret, weights)\n", " return abs(des_ret-ret) + abs(des_vol-vol)\n", "\n", "@pn.cache\n", "def find_best_allocation(log_return, vol, ret):\n", " cols = log_return.shape[1]\n", " vol = vol or 0\n", " ret = ret or 0\n", " mean_return = np.nanmean(log_return, axis=0)\n", " bounds = tuple((0, 1) for i in range(cols))\n", " init_guess = [1./cols for i in range(cols)]\n", " cons = (\n", " {'type':'eq','fun': check_sum},\n", " {'type':'eq','fun': lambda w: get_return(mean_return, w) - ret},\n", " {'type':'eq','fun': lambda w: get_volatility(log_return, w) - vol}\n", " )\n", " opt = minimize(\n", " minimize_difference, init_guess, args=(vol, ret, log_return, mean_return),\n", " bounds=bounds, constraints=cons\n", " )\n", " ret = get_return(mean_return, opt.x)\n", " vol = get_volatility(log_return, opt.x)\n", " return pd.Series(list(opt.x)+[ret, vol], index=list(log_return.columns)+['Return', 'Volatility'], name='Weight')" ] }, { "cell_type": "markdown", "id": "89881e6f-218e-4ec3-923f-633de5b48f82", "metadata": {}, "source": [ "## Declare UI components" ] }, { "cell_type": "code", "execution_count": null, "id": "66abc13d-ba6f-47bf-85d4-9cdd2f804f27", "metadata": {}, "outputs": [], "source": [ "n_samples = pn.widgets.IntSlider(\n", " name='Random samples', value=10_000, start=1000, end=20_000, step=1000, sizing_mode='stretch_width'\n", ")\n", "button = pn.widgets.Button(name='Run Analysis', sizing_mode='stretch_width')\n", "posxy = hv.streams.Tap(x=None, y=None)\n", "\n", "text = \"\"\"\n", "# Portfolio optimization\n", "\n", "This application performs portfolio optimization given a set of stock time series.\n", "\n", "To optimize your portfolio:\n", "\n", "1. Upload a CSV of the daily stock time series for the stocks you are considering\n", "2. Select the stocks to be included.\n", "3. Run the Analysis\n", "4. Click on the Return/Volatility plot to select the desired risk/reward profile\n", "\n", "Upload a CSV containing stock data:\n", "\"\"\"\n", "\n", "explanation = \"\"\"\n", "The code for this app was taken from [this excellent introduction to Python for Finance](https://github.com/PrateekKumarSingh/Python/tree/master/Python%20for%20Finance/Python-for-Finance-Repo-master).\n", "To learn some of the background and theory about portfolio optimization see [this notebook](https://github.com/PrateekKumarSingh/Python/blob/master/Python%20for%20Finance/Python-for-Finance-Repo-master/09-Python-Finance-Fundamentals/02-Portfolio-Optimization.ipynb).\n", "\"\"\"\n", "\n", "sidebar = pn.layout.WidgetBox(\n", " pn.pane.Markdown(text, margin=(0, 10)),\n", " file_input,\n", " selector,\n", " n_samples,\n", " explanation,\n", " max_width=350,\n", " sizing_mode='stretch_width'\n", ").servable(area='sidebar')\n", "\n", "sidebar" ] }, { "cell_type": "markdown", "id": "6f641fd3-d39a-44cf-8784-b681b868ea7b", "metadata": {}, "source": [ "## Plot" ] }, { "cell_type": "markdown", "id": "1df1811f-09b8-41d1-8f22-a6bb0b802c25", "metadata": {}, "source": [ "### Portfolio optimization plot" ] }, { "cell_type": "code", "execution_count": null, "id": "2694f7ef-44a4-40e5-9822-61a259cc7dfe", "metadata": {}, "outputs": [], "source": [ "# Set up data pipelines\n", "log_return = np.log(selected_stocks/selected_stocks.shift(1))\n", "random_allocations = log_return.pipe(compute_random_allocations, n_samples)\n", "closest_allocation = log_return.pipe(find_best_allocation, posxy.param.x, posxy.param.y)\n", "efficient_frontier = random_allocations.pipe(compute_frontier)\n", "max_sharpe = random_allocations.pipe(lambda df: df[df.Sharpe==df.Sharpe.max()])\n", "\n", "# Generate plots\n", "opts = {'x': 'Volatility', 'y': 'Return', 'responsive': True}\n", "\n", "allocations_scatter = random_allocations.hvplot.scatter(\n", " alpha=0.1, color='Sharpe', cmap='plasma', **opts\n", ").dmap().opts(tools=[])\n", "\n", "frontier_curve = efficient_frontier.hvplot(\n", " line_dash='dashed', color='green', **opts\n", ").dmap()\n", "\n", "max_sharpe_point = max_sharpe.hvplot.scatter(\n", " line_color='black', size=50, **opts\n", ").dmap()\n", "\n", "closest_point = closest_allocation.to_frame().T.hvplot.scatter(color='green', line_color='black', size=50, **opts).dmap()\n", "\n", "posxy.source = allocations_scatter\n", "\n", "summary = pn.pane.Markdown(\n", " pn.bind(lambda p: f\"\"\"\n", " The selected portfolio has a volatility of {p.Volatility:.2f}, a return of {p.Return:.2f}\n", " and Sharpe ratio of {p.Return/p.Volatility:.2f}.\"\"\", closest_allocation), width=250\n", ")\n", "\n", "table = pn.widgets.Tabulator(closest_allocation.to_frame().iloc[:-2])\n", "\n", "plot = (allocations_scatter * frontier_curve * max_sharpe_point * closest_point).opts(min_height=400, show_grid=True)\n", "\n", "pn.Row(plot, pn.Column(summary, table), sizing_mode='stretch_both')" ] }, { "cell_type": "markdown", "id": "dfb3a77b-1b00-4aa5-8af3-45ff188388e4", "metadata": {}, "source": [ "### Portfolio Performance plot" ] }, { "cell_type": "code", "execution_count": null, "id": "60a4e495-4151-4b91-a51f-b8d921fe96ce", "metadata": { "tags": [] }, "outputs": [], "source": [ "investment = pn.widgets.Spinner(name='Investment Value in $', value=5000, step=1000, start=1000, end=100000)\n", "year = pn.widgets.DateRangeSlider(name='Year', value=(stocks.index.min().eval(), stocks.index.max().eval()), start=stocks.index.min(), end=stocks.index.max())\n", "\n", "stocks_between_dates = selected_stocks[year.param.value_start:year.param.value_end]\n", "price_on_start_date = selected_stocks[year.param.value_start:].iloc[0]\n", "allocation = (closest_allocation.iloc[:-2] * investment)\n", "\n", "performance_plot = (stocks_between_dates * allocation / price_on_start_date).sum(axis=1).rename().hvplot.line(\n", " ylabel='Total Value ($)', title='Portfolio performance', responsive=True, min_height=400\n", ").dmap()\n", "\n", "performance = pn.Column(\n", " pn.Row(year, investment),\n", " performance_plot,\n", " sizing_mode='stretch_both'\n", ")\n", "\n", "performance" ] }, { "cell_type": "markdown", "id": "0d4f92af-8ab6-441d-837a-120c1c362c50", "metadata": {}, "source": [ "### Plot stock prices" ] }, { "cell_type": "code", "execution_count": null, "id": "40fe832a-45c4-4dc6-a5a7-877d40486257", "metadata": {}, "outputs": [], "source": [ "timeseries = selected_stocks.hvplot.line(\n", " 'Date', group_label='Stock', value_label='Stock Price ($)', title='Daily Stock Price',\n", " min_height=300, responsive=True, grid=True, legend='top_left'\n", ").dmap()\n", "\n", "timeseries" ] }, { "cell_type": "markdown", "id": "c7d3360b-8c31-4433-ab44-ec5a819aa252", "metadata": {}, "source": [ "### Log return plots" ] }, { "cell_type": "code", "execution_count": null, "id": "62793c41-c536-42ac-bd09-4aed9d8759f2", "metadata": {}, "outputs": [], "source": [ "log_ret_hists = log_return.hvplot.hist(min_height=300, min_width=400, responsive=True, bins=100, subplots=True, group_label='Stock').cols(2).opts(sizing_mode='stretch_both').panel()\n", "\n", "log_ret_hists" ] }, { "cell_type": "markdown", "id": "0fd69653-d22d-4e3a-9c21-a843c911399f", "metadata": {}, "source": [ "### Overall layout" ] }, { "cell_type": "code", "execution_count": null, "id": "e746c44f-9aa0-4704-bc9d-6358e9717068", "metadata": {}, "outputs": [], "source": [ "main = pn.Tabs(\n", " ('Analysis', pn.Column(\n", " pn.Row(\n", " plot, pn.Column(summary, table),\n", " sizing_mode='stretch_both'\n", " ),\n", " performance,\n", " sizing_mode='stretch_both'\n", " )\n", " ),\n", " ('Timeseries', timeseries),\n", " ('Log Return', pn.Column(\n", " '## Daily normalized log returns',\n", " 'Width of distribution indicates volatility and center of distribution the mean daily return.',\n", " log_ret_hists,\n", " sizing_mode='stretch_both'\n", " )),\n", " sizing_mode='stretch_both', min_height=1000\n", ").servable(title='Portfolio Optimizer')\n", "\n", "pn.Row(sidebar, main)" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 }