{
 "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
}
