{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-08-15T21:28:25.786191Z",
     "start_time": "2018-08-15T21:28:25.030035Z"
    },
    "run_control": {
     "frozen": false,
     "read_only": false
    }
   },
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\", message=\"numpy.dtype size changed\") # another bogus warning, see https://github.com/numpy/numpy/pull/432\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "from pyqstrat.pq_utils import *\n",
    "from pyqstrat.plot import *\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-08-15T21:28:26.598486Z",
     "start_time": "2018-08-15T21:28:25.789721Z"
    },
    "run_control": {
     "frozen": false,
     "read_only": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>gmean</th>\n",
       "      <th>amean</th>\n",
       "      <th>std</th>\n",
       "      <th>shrp</th>\n",
       "      <th>srt</th>\n",
       "      <th>calmar</th>\n",
       "      <th>mar</th>\n",
       "      <th>mdd_pct</th>\n",
       "      <th>mdd_dates</th>\n",
       "      <th>dd_3y_pct</th>\n",
       "      <th>dd_3y_dates</th>\n",
       "      <th>up_dwn</th>\n",
       "      <th>2018</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th></th>\n",
       "      <td>0.0426</td>\n",
       "      <td>0.000166</td>\n",
       "      <td>0.000899</td>\n",
       "      <td>2.93</td>\n",
       "      <td>5.69</td>\n",
       "      <td>-8.73</td>\n",
       "      <td>-8.73</td>\n",
       "      <td>-0.00479</td>\n",
       "      <td>2018-01-19/2018-01-22</td>\n",
       "      <td>-0.00479</td>\n",
       "      <td>2018-01-19/2018-01-22</td>\n",
       "      <td>34/25/0.576</td>\n",
       "      <td>0.0426</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   gmean     amean       std  shrp   srt calmar    mar   mdd_pct  \\\n",
       "  0.0426  0.000166  0.000899  2.93  5.69  -8.73  -8.73  -0.00479   \n",
       "\n",
       "              mdd_dates dd_3y_pct            dd_3y_dates       up_dwn    2018  \n",
       "  2018-01-19/2018-01-22  -0.00479  2018-01-19/2018-01-22  34/25/0.576  0.0426  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 432x288 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 76428 characters]\n",
      "text/plain": [
       "<Figure size 1080x576 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "_VERBOSE = False\n",
    "\n",
    "def compute_amean(returns):\n",
    "    \"\"\"Computes arithmetic mean of a return array, ignoring NaNs\n",
    "    \n",
    "    Args:\n",
    "        returns: a numpy array of floats representing returns at any frequency\n",
    "        \n",
    "    Returns:\n",
    "        a float\n",
    "        \n",
    "    >>> compute_amean(np.array([3, 4, np.nan]))\n",
    "    3.5\n",
    "    \"\"\"\n",
    "    if not len(returns): return np.nan\n",
    "    return np.nanmean(returns)\n",
    "\n",
    "def compute_periods_per_year(dates):\n",
    "    \"\"\"Computes trading periods per year for an array of numpy datetime64's.\n",
    "      E.g. if most of the dates are separated by 1 day, will return 252.\n",
    "      \n",
    "    Args:\n",
    "        dates: a numpy array of datetime64's\n",
    "        \n",
    "    Returns:\n",
    "        a float\n",
    "        \n",
    "    >>> compute_periods_per_year(np.array(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-09'], dtype = 'M8[D]'))\n",
    "    252.0\n",
    "    \"\"\"\n",
    "    if not len(dates): return np.nan\n",
    "    freq = infer_frequency(dates)\n",
    "    return 252. * freq\n",
    "\n",
    "def compute_gmean(returns, periods_per_year):\n",
    "    \"\"\" Computes geometric mean of an array of returns\n",
    "    \n",
    "    Args:\n",
    "        returns: a numpy array of returns\n",
    "        periods_per_year: number of trading periods per year\n",
    "        \n",
    "    Returns:\n",
    "        a float\n",
    "        \n",
    "    >>> round(compute_gmean(np.array([0.001, 0.002, 0.003]), 252.), 6)\n",
    "    0.654358\n",
    "    \"\"\"\n",
    "    assert(periods_per_year > 0)\n",
    "    if not len(returns): return np.nan\n",
    "    g_mean = ((1.0 + returns).prod())**(1.0/len(returns))\n",
    "    g_mean = np.power(g_mean, periods_per_year) - 1.0\n",
    "    return g_mean\n",
    "\n",
    "def compute_std(returns):\n",
    "    \"\"\" Computes standard deviation of an array of returns, ignoring nans \"\"\"\n",
    "    if not len(returns): return np.nan\n",
    "    return np.nanstd(returns)\n",
    "\n",
    "def compute_sortino(returns, amean, periods_per_year):\n",
    "    '''\n",
    "    Note that this assumes target return is 0.\n",
    "    \n",
    "    Args:\n",
    "        returns: a numpy array of returns\n",
    "        amean: arithmetic mean of returns\n",
    "        periods_per_year: number of trading periods per year\n",
    "        \n",
    "    >>> print(round(compute_sortino(np.array([0.001, -0.001, 0.002]), 0.001, 252), 6))\n",
    "    33.674916\n",
    "    '''\n",
    "    if not len(returns) or not np.isfinite(amean) or periods_per_year <= 0: return np.nan\n",
    "    returns = np.where((~np.isfinite(returns)), 0.0, returns)\n",
    "    normalized_rets = np.where(returns > 0.0, 0.0, returns)\n",
    "    sortino_denom = np.std(normalized_rets)\n",
    "    sortino = np.nan if sortino_denom == 0 else amean / sortino_denom * np.sqrt(periods_per_year)\n",
    "    return sortino\n",
    "\n",
    "def compute_sharpe(returns, amean, periods_per_year):\n",
    "    '''\n",
    "    Note that this does not take into risk free returns so it's really a sharpe0, i.e. assumes risk free returns are 0\n",
    "    \n",
    "    Args:\n",
    "        returns: a numpy array of returns\n",
    "        amean: arithmetic mean of returns\n",
    "        periods_per_year: number of trading periods per year\n",
    "        \n",
    "    >>> round(compute_sharpe(np.array([0.001, -0.001, 0.002]), 0.001, 252), 6)\n",
    "    12.727922\n",
    "    '''\n",
    "    if not len(returns) or not np.isfinite(amean) or periods_per_year <= 0: return np.nan\n",
    "    returns = np.where((~np.isfinite(returns)), 0.0, returns)\n",
    "    s = np.std(returns)\n",
    "    sharpe = np.nan if s == 0 else amean / s * np.sqrt(periods_per_year)\n",
    "    return sharpe\n",
    "\n",
    "def compute_equity(dates, starting_equity, returns):\n",
    "    ''' Given starting equity, dates and returns, create a numpy array of equity at each date'''\n",
    "    return starting_equity * np.cumprod(1. + returns)\n",
    "\n",
    "def compute_rolling_dd(dates, equity):\n",
    "    '''\n",
    "    Compute numpy array of rolling drawdown percentage\n",
    "    \n",
    "    Args:\n",
    "        dates: numpy array of datetime64\n",
    "        equity: numpy array of equity\n",
    "    '''\n",
    "    assert(len(dates) == len(equity))\n",
    "    if not len(dates): return np.array([], dtype = 'M8[ns]'), np.array([], dtype = np.float)\n",
    "    s = pd.Series(equity, index = dates)\n",
    "    rolling_max = s.expanding(min_periods = 1).max()\n",
    "    dd = np.where(s >= rolling_max, 0.0, (s - rolling_max) / rolling_max)\n",
    "    return dates, dd\n",
    "\n",
    "def compute_maxdd_pct(rolling_dd):\n",
    "    '''Compute max drawdown percentage given a numpy array of rolling drawdowns, ignoring NaNs'''\n",
    "    if not len(rolling_dd): return np.nan\n",
    "    return np.nanmin(rolling_dd)\n",
    "\n",
    "def compute_maxdd_date(rolling_dd_dates, rolling_dd):\n",
    "    ''' Compute date of max drawdown given numpy array of dates, and corresponding rolling dd percentages'''\n",
    "    if not len(rolling_dd_dates): return pd.NaT\n",
    "    assert(len(rolling_dd_dates) == len(rolling_dd))\n",
    "    return rolling_dd_dates[np.argmin(rolling_dd)]\n",
    "\n",
    "def compute_maxdd_start(rolling_dd_dates, rolling_dd, mdd_date):\n",
    "    '''Compute date when max drawdown starts, given numpy array of dates, corresponding rolling dd percentages and date that max dd starts'''\n",
    "    if not len(rolling_dd_dates) or pd.isnull(mdd_date): return pd.NaT\n",
    "    assert(len(rolling_dd_dates) == len(rolling_dd))\n",
    "    return rolling_dd_dates[(rolling_dd >= 0) & (rolling_dd_dates < mdd_date)][-1]\n",
    "\n",
    "def compute_mar(returns, periods_per_year, mdd_pct):\n",
    "    '''Compute MAR ratio, which is annualized return divided by biggest drawdown since inception.'''\n",
    "    if not len(returns) or np.isnan(mdd_pct): return np.nan\n",
    "    return np.mean(returns) * periods_per_year / mdd_pct\n",
    "\n",
    "def compute_dates_3yr(dates):\n",
    "    ''' Given an array of numpy datetimes, return those that are within 3 years of the last date in the array'''\n",
    "    if not len(dates): return np.array([], dtype = 'M8[D]')\n",
    "    last_date = dates[-1]\n",
    "    d = pd.to_datetime(last_date)\n",
    "    start_3yr = np.datetime64( d.replace(year = d.year - 3))\n",
    "    return dates[dates > start_3yr]\n",
    "\n",
    "def compute_returns_3yr(dates, returns):\n",
    "    '''Given an array of numpy datetimes and an array of returns, return those that are within 3 years of the last date in the datetime array '''\n",
    "    if not len(dates): return np.array([], dtype = np.float)\n",
    "    assert(len(dates) == len(returns))\n",
    "    dates_3yr = compute_dates_3yr(dates)\n",
    "    return returns[dates >= dates_3yr[0]]\n",
    "\n",
    "def compute_rolling_dd_3yr(dates, equity):\n",
    "    '''Compute rolling drawdowns over the last 3 years'''\n",
    "    if not len(dates): return np.array([], dtype = 'M8[D]')\n",
    "    last_date = dates[-1]\n",
    "    d = pd.to_datetime(last_date)\n",
    "    start_3yr = np.datetime64( d.replace(year = d.year - 3))\n",
    "    equity = equity[dates >= start_3yr]\n",
    "    dates = dates[dates >= start_3yr]\n",
    "    return compute_rolling_dd(dates, equity)\n",
    "\n",
    "def compute_maxdd_pct_3yr(rolling_dd_3yr):\n",
    "    '''Compute max drawdown percentage over the last 3 years'''\n",
    "    return compute_maxdd_pct(rolling_dd_3yr)\n",
    "\n",
    "def compute_maxdd_date_3yr(rolling_dd_3yr_dates, rolling_dd_3yr):\n",
    "    '''Compute max drawdown date over the last 3 years'''\n",
    "    return compute_maxdd_date(rolling_dd_3yr_dates, rolling_dd_3yr)\n",
    "\n",
    "def compute_maxdd_start_3yr(rolling_dd_3yr_dates, rolling_dd_3yr, mdd_date_3yr):\n",
    "    '''Comput max drawdown start date over the last 3 years'''\n",
    "    return compute_maxdd_start(rolling_dd_3yr_dates, rolling_dd_3yr, mdd_date_3yr)\n",
    "\n",
    "def compute_calmar(returns_3yr, periods_per_year, mdd_pct_3yr):\n",
    "    '''Compute Calmar ratio, which is the annualized return divided by max drawdown over the last 3 years'''\n",
    "    return compute_mar(returns_3yr, periods_per_year, mdd_pct_3yr)\n",
    "\n",
    "def compute_bucketed_returns(dates, returns):\n",
    "    '''\n",
    "    Bucket returns by year\n",
    "    \n",
    "    Returns:\n",
    "        A tuple with the first element being a list of years and the second a list of numpy arrays containing returns for each corresponding year\n",
    "    '''\n",
    "    assert(len(dates) == len(returns))\n",
    "    if not len(dates): return np.array([], dtype = np.str), np.array([], dtype = np.float)\n",
    "    s = pd.Series(returns, index = dates)\n",
    "    years_list = []\n",
    "    rets_list = []\n",
    "    for year, rets in s.groupby(s.index.map(lambda x : x.year)):\n",
    "        years_list.append(year)\n",
    "        rets_list.append(rets.values)\n",
    "    \n",
    "    return years_list, rets_list\n",
    "\n",
    "def compute_annual_returns(dates, returns, periods_per_year):\n",
    "    '''Takes the output of compute_bucketed_returns and returns geometric mean of returns by year\n",
    "    \n",
    "    Returns:\n",
    "        A tuple with the first element being an array of years (integer) and the second element \n",
    "        an array of annualized returns for those years\n",
    "        \n",
    "    '''\n",
    "    assert(len(dates) == len(returns) and periods_per_year > 0)\n",
    "    if not len(dates): return np.array([], dtype = np.str), np.array([], dtype = np.float)\n",
    "    s = pd.Series(returns, index = dates)\n",
    "    ret_by_year = s.groupby(s.index.map(lambda x: x.year)).agg(\n",
    "        lambda y: compute_gmean(y, periods_per_year))\n",
    "    return ret_by_year.index.values, ret_by_year.values\n",
    "\n",
    "class Evaluator:\n",
    "    \"\"\"You add functions to the evaluator that are dependent on the outputs of other functions.  The evaluator will call these functions in the right order\n",
    "    so dependencies are computed first before the functions that need their output.  You can retrieve the output of a metric using the metric member function\n",
    "    \n",
    "    >>> evaluator = Evaluator(initial_metrics={'x' : np.array([1, 2, 3]), 'y' : np.array([3, 4, 5])})\n",
    "    >>> evaluator.add_metric('z', lambda x, y: sum(x, y), dependencies=['x', 'y'])\n",
    "    >>> evaluator.compute()\n",
    "    >>> evaluator.metric('z')\n",
    "    array([ 9, 10, 11])\n",
    "    \"\"\"\n",
    "    def __init__(self, initial_metrics):\n",
    "        \"\"\"Inits Evaluator with a dictionary of initial metrics that are used to compute subsequent metrics\n",
    "        \n",
    "        Args:\n",
    "            initial_metrics: a dictionary of string name -> metric.  metric can be any object including a scalar, an array or a tuple\n",
    "        \"\"\"\n",
    "        assert(type(initial_metrics) == dict)\n",
    "        self.metric_values = initial_metrics\n",
    "        self._metrics = {}\n",
    "        \n",
    "    def add_metric(self, name, func, dependencies):\n",
    "        self._metrics[name] = (func, dependencies)\n",
    "    \n",
    "    def compute(self, metric_names = None):\n",
    "        '''Compute metrics using the internal dependency graph\n",
    "        \n",
    "        Args:\n",
    "            metric_names: an array of metric names.  If not passed in, evaluator will compute and store all metrics\n",
    "        '''\n",
    "\n",
    "        if metric_names is None: metric_names = list(self._metrics.keys())\n",
    "        for metric_name in metric_names:\n",
    "            if _VERBOSE: print(f'computing: {metric_name}')\n",
    "            self.compute_metric(metric_name)\n",
    "            \n",
    "    def compute_metric(self, metric_name):\n",
    "        '''\n",
    "        Compute and store a single metric:\n",
    "        \n",
    "        Args:\n",
    "            metric_name: string representing the metric to compute\n",
    "        '''\n",
    "        func, dependencies = self._metrics[metric_name]\n",
    "        for dependency in dependencies:\n",
    "            if dependency not in self.metric_values:\n",
    "                self.compute_metric(dependency)\n",
    "        dependency_values = {k: self.metric_values[k] for k in dependencies}\n",
    "        \n",
    "        values = func(**dependency_values)\n",
    "            \n",
    "        self.metric_values[metric_name] = values\n",
    "                \n",
    "    def metric(self, metric_name):\n",
    "        '''Return the value of a single metric given its name'''\n",
    "        return self.metric_values[metric_name]\n",
    "    \n",
    "    def metrics(self):\n",
    "        '''Return a dictionary of metric name -> metric value'''\n",
    "        return self.metric_values\n",
    "    \n",
    "    \n",
    "def compute_return_metrics(dates, rets, starting_equity):\n",
    "    '''\n",
    "    Compute a set of common metrics using returns (for example, of an instrument or a portfolio)\n",
    "    \n",
    "    Args:\n",
    "        dates: a numpy datetime array with one date per return\n",
    "        rets: a numpy float array of returns\n",
    "        starting_equity: starting equity value in your portfolio\n",
    "        \n",
    "    Returns:\n",
    "        An Evaluator object containing computed metrics off the returns passed in.  \n",
    "        If needed, you can add your own metrics to this object based on the values of existing metrics and recompute the Evaluator.\n",
    "        Otherwise, you can just use the output of the evaluator using the metrics function.\n",
    "    '''\n",
    "    \n",
    "    assert(starting_equity > 0.)\n",
    "    assert(type(rets) == np.ndarray and rets.dtype == np.float64)\n",
    "    assert(type(dates) == np.ndarray and np.issubdtype(dates.dtype, np.datetime64) and monotonically_increasing(dates))\n",
    "\n",
    "    rets = nan_to_zero(rets)\n",
    "\n",
    "    ev = Evaluator({'dates' : dates, 'returns' : rets, 'starting_equity' : starting_equity})\n",
    "    ev.add_metric('periods_per_year', compute_periods_per_year, dependencies = ['dates'])\n",
    "    ev.add_metric('amean', compute_amean, dependencies = ['returns'])\n",
    "    ev.add_metric('std', compute_std, dependencies = ['returns'])\n",
    "    ev.add_metric('up_periods', lambda returns : len(returns[returns > 0]), dependencies = ['returns'])\n",
    "    ev.add_metric('down_periods', lambda returns : len(returns[returns < 0]), dependencies = ['returns'])\n",
    "    ev.add_metric('up_pct', lambda up_periods, down_periods : up_periods * 1.0 / (up_periods + down_periods), dependencies=['up_periods', 'down_periods'])\n",
    "    ev.add_metric('gmean', compute_gmean, dependencies=['returns', 'periods_per_year'])\n",
    "    ev.add_metric('sharpe', compute_sharpe, dependencies = ['returns', 'periods_per_year', 'amean'])\n",
    "    ev.add_metric('sortino', compute_sortino, dependencies = ['returns', 'periods_per_year', 'amean'])\n",
    "    ev.add_metric('equity', compute_equity, dependencies = ['dates', 'starting_equity', 'returns'])\n",
    "    \n",
    "    # Drawdowns\n",
    "    ev.add_metric('rolling_dd', compute_rolling_dd, dependencies = ['dates', 'equity'])\n",
    "    ev.add_metric('mdd_pct', lambda rolling_dd : compute_maxdd_pct(rolling_dd[1]), dependencies = ['rolling_dd'])\n",
    "    ev.add_metric('mdd_date', lambda rolling_dd : compute_maxdd_date(rolling_dd[0], rolling_dd[1]), dependencies = ['rolling_dd'])\n",
    "    ev.add_metric('mdd_start', lambda rolling_dd, mdd_date : compute_maxdd_start(rolling_dd[0], rolling_dd[1], mdd_date), dependencies = ['rolling_dd', 'mdd_date'])\n",
    "    ev.add_metric('mar', compute_mar, dependencies = ['returns', 'periods_per_year', 'mdd_pct'])\n",
    "    \n",
    "    ev.add_metric('dates_3yr', compute_dates_3yr, dependencies = ['dates'])\n",
    "    ev.add_metric('returns_3yr', compute_returns_3yr, dependencies = ['dates', 'returns'])\n",
    "\n",
    "    ev.add_metric('rolling_dd_3yr', compute_rolling_dd_3yr, dependencies = ['dates', 'equity'])\n",
    "    ev.add_metric('mdd_pct_3yr', lambda rolling_dd_3yr : compute_maxdd_pct_3yr(rolling_dd_3yr[1]), dependencies = ['rolling_dd_3yr'])\n",
    "    ev.add_metric('mdd_date_3yr', lambda rolling_dd_3yr : compute_maxdd_date_3yr(rolling_dd_3yr[0], rolling_dd_3yr[1]) , dependencies = ['rolling_dd_3yr'])\n",
    "    ev.add_metric('mdd_start_3yr', lambda rolling_dd_3yr, mdd_date_3yr : compute_maxdd_start_3yr(rolling_dd_3yr[0], rolling_dd_3yr[1], mdd_date_3yr), \n",
    "                  dependencies = ['rolling_dd_3yr', 'mdd_date_3yr'])\n",
    "    ev.add_metric('calmar', compute_calmar, dependencies = ['returns_3yr', 'periods_per_year', 'mdd_pct_3yr'])\n",
    "\n",
    "    ev.add_metric('annual_returns', compute_annual_returns, dependencies=['dates', 'returns', 'periods_per_year'])\n",
    "    ev.add_metric('bucketed_returns', compute_bucketed_returns, dependencies=['dates', 'returns'])\n",
    "\n",
    "    ev.compute()\n",
    "    return ev\n",
    "\n",
    "def display_return_metrics(metrics, float_precision = 3):\n",
    "    '''\n",
    "    Creates a dataframe making it convenient to view the output of the metrics obtained using the compute_return_metrics function.\n",
    "    \n",
    "    Args:\n",
    "        float_precision: Change if you want to display floats with more or less significant figures than the default, 3 significant figures.\n",
    "        \n",
    "    Returns:\n",
    "        A one row dataframe with formatted metrics.\n",
    "    '''\n",
    "    \n",
    "    \n",
    "    from IPython.core.display import display\n",
    "    \n",
    "    _metrics = {}\n",
    "    cols = ['gmean', 'amean', 'std', 'shrp', 'srt', 'calmar', 'mar', 'mdd_pct', 'mdd_start', 'mdd_date', 'dd_3y_pct', 'up_periods', 'down_periods', 'up_pct',\n",
    "           'mdd_start_3yr', 'mdd_date_3yr']\n",
    "    \n",
    "    translate = {'shrp' : 'sharpe', 'srt' : 'sortino', 'dd_3y_pct' : 'mdd_pct_3yr'}\n",
    "    for col in cols:\n",
    "        key = col\n",
    "        if col in translate: key = translate[col]\n",
    "        _metrics[col] = metrics[key]\n",
    "            \n",
    "    _metrics['mdd_dates'] = f'{str(metrics[\"mdd_start\"])[:10]}/{str(metrics[\"mdd_date\"])[:10]}'\n",
    "    _metrics['up_dwn'] = f'{metrics[\"up_periods\"]}/{metrics[\"down_periods\"]}/{metrics[\"up_pct\"]:.3g}'\n",
    "    _metrics['dd_3y_dates'] = f'{str(metrics[\"mdd_start_3yr\"])[:10]}/{str(metrics[\"mdd_date_3yr\"])[:10]}'\n",
    "    \n",
    "    years = metrics['annual_returns'][0]\n",
    "    ann_rets = metrics['annual_returns'][1]\n",
    "    for i, year in enumerate(years):\n",
    "        _metrics[str(year)] = ann_rets[i]\n",
    "        \n",
    "    format_str = '{:.' + str(float_precision) + 'g}'\n",
    "        \n",
    "    for k, v in _metrics.items():\n",
    "        if isinstance(v, np.float) or isinstance(v, float):\n",
    "            _metrics[k] = format_str.format(v)\n",
    "       \n",
    "    cols = ['gmean', 'amean', 'std', 'shrp', 'srt', 'calmar', 'mar', 'mdd_pct', 'mdd_dates', 'dd_3y_pct', 'dd_3y_dates', 'up_dwn'\n",
    "           ] + [str(year) for year in sorted(years)]\n",
    "    \n",
    "    df = pd.DataFrame(index = [''])\n",
    "    for metric_name, metric_value in _metrics.items():\n",
    "        df.insert(0, metric_name, metric_value)\n",
    "    df = df[cols]\n",
    "        \n",
    "    display(df)\n",
    "    return df\n",
    "\n",
    "def plot_return_metrics(metrics, title = None):\n",
    "    '''\n",
    "    Plot equity, rolling drawdowns and and a boxplot of annual returns given the output of compute_return_metrics.\n",
    "    '''\n",
    "    returns = metrics['returns']\n",
    "    dates = metrics['dates']\n",
    "    equity =  metrics['equity']\n",
    "    equity = TimeSeries('equity', dates = dates, values = equity)\n",
    "    mdd_date, mdd_start = metrics['mdd_start'], metrics['mdd_date']\n",
    "    mdd_date_3yr, mdd_start_3yr = metrics['mdd_start_3yr'], metrics['mdd_date_3yr']\n",
    "    drawdown_lines = [DateLine(name = 'max dd', date = mdd_start, color = 'red'),\n",
    "                      DateLine(date = mdd_date, color = 'red'),\n",
    "                      DateLine(name = '3y dd', date = mdd_start_3yr, color = 'orange'),\n",
    "                      DateLine(date = mdd_date_3yr, color = 'orange')]\n",
    "    equity_subplot = Subplot(equity, ylabel = 'Equity', height_ratio = 0.6, log_y = True, y_tick_format = '${x:,.0f}', \n",
    "                             date_lines = drawdown_lines, horizontal_lines=[HorizontalLine(metrics['starting_equity'], color = 'black')]) \n",
    "    \n",
    "\n",
    "    rolling_dd = TimeSeries('drawdowns', dates = metrics['rolling_dd'][0], values = metrics['rolling_dd'][1])\n",
    "    zero_line = HorizontalLine(y = 0, color = 'black')\n",
    "    dd_subplot = Subplot(rolling_dd, ylabel = 'Drawdowns', height_ratio = 0.2, date_lines = drawdown_lines, horizontal_lines = [zero_line])\n",
    "    \n",
    "    years = metrics['bucketed_returns'][0]\n",
    "    ann_rets = metrics['bucketed_returns'][1]\n",
    "    ann_ret = BucketedValues('annual returns', bucket_names = years, bucket_values = ann_rets)\n",
    "    ann_ret_subplot = Subplot(ann_ret, ylabel = 'Annual Returns', height_ratio = 0.2, horizontal_lines=[zero_line])\n",
    "    \n",
    "    \n",
    "    plt = Plot([equity_subplot, dd_subplot, ann_ret_subplot], title = title)\n",
    "    plt.draw()\n",
    "    \n",
    "def test_evaluator():\n",
    "    from datetime import datetime, timedelta\n",
    "    np.random.seed(10)\n",
    "    dates = np.arange(datetime(2018, 1, 1), datetime(2018, 3, 1), timedelta(days = 1))\n",
    "    rets = np.random.normal(size = len(dates)) / 1000\n",
    "    starting_equity = 1.e6\n",
    "    \n",
    "    ev = compute_return_metrics(dates, rets, starting_equity)\n",
    "    metrics = display_return_metrics(ev.metrics());\n",
    "    plot_return_metrics(ev.metrics())\n",
    "    \n",
    "    assert(round(ev.metric('sharpe'), 6) == 2.932954)\n",
    "    assert(round(ev.metric('sortino'), 6) == 5.690878)\n",
    "    assert(ev.metric('annual_returns')[0] == [2018])\n",
    "    assert(round(ev.metric('annual_returns')[1][0], 6) == [0.042643])\n",
    "    assert(ev.metric('mdd_start') == np.datetime64('2018-01-19'))\n",
    "    assert(ev.metric('mdd_date') == np.datetime64('2018-01-22'))\n",
    "    \n",
    "if __name__ == \"__main__\":\n",
    "    test_evaluator()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "run_control": {
     "frozen": false,
     "read_only": false
    }
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
