{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-12-08T04:05:46.144680Z",
     "start_time": "2018-12-08T04:05:44.066654Z"
    },
    "run_control": {
     "frozen": false,
     "read_only": false
    }
   },
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "from functools import reduce\n",
    "import datetime\n",
    "\n",
    "from pyqstrat.pq_utils import *\n",
    "from pyqstrat.evaluator import compute_return_metrics, display_return_metrics, plot_return_metrics\n",
    "from pyqstrat.strategy import Strategy\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2018-12-08T04:05:46.195238Z",
     "start_time": "2018-12-08T04:05:46.146950Z"
    },
    "run_control": {
     "frozen": false,
     "read_only": false
    }
   },
   "outputs": [],
   "source": [
    "class Portfolio:\n",
    "    '''A portfolio contains one or more strategies that run concurrently so you can test running strategies that are uncorrelated together.'''\n",
    "    def __init__(self, name = 'main'):\n",
    "        '''Args:\n",
    "            name: String used for displaying this portfolio\n",
    "        '''\n",
    "        self.name = name\n",
    "        self.strategies = {}\n",
    "        \n",
    "    def add_strategy(self, name, strategy):\n",
    "        '''\n",
    "        Args:\n",
    "            name: Name of the strategy\n",
    "            strategy: Strategy object\n",
    "        '''\n",
    "        self.strategies[name] = strategy\n",
    "        strategy.portfolio = self\n",
    "        strategy.name = name\n",
    "        \n",
    "    def run_indicators(self, strategy_names = None):\n",
    "        '''Compute indicators for the strategies specified\n",
    "        \n",
    "        Args:\n",
    "            strategy_names: A list of strategy names.  By default this is set to None and we use all strategies.\n",
    "        '''\n",
    "        if strategy_names is None: strategy_names = list(self.strategies.keys())\n",
    "        if len(strategy_names) == 0: raise Exception('a portofolio must have at least one strategy')\n",
    "        for name in strategy_names: self.strategies[name].run_indicators()\n",
    "                \n",
    "    def run_signals(self, strategy_names = None):\n",
    "        '''Compute signals for the strategies specified.  Must be called after run_indicators\n",
    "        \n",
    "        Args:\n",
    "            strategy_names: A list of strategy names.  By default this is set to None and we use all strategies.\n",
    "        '''\n",
    "        if strategy_names is None: strategy_names = list(self.strategies.keys())\n",
    "        if len(strategy_names) == 0: raise Exception('a portofolio must have at least one strategy')\n",
    "        for name in strategy_names: self.strategies[name].run_signals()\n",
    "            \n",
    "    def _get_iterations(self, strategies, start_date, end_date):\n",
    "        '''\n",
    "        >>> class Strategy:\n",
    "        ...    def __init__(self, num): \n",
    "        ...        self.num = num\n",
    "        ...        self.dates = [\n",
    "        ...            np.array(['2018-01-01', '2018-01-02', '2018-01-03'], dtype = 'M8[D]'),\n",
    "        ...            np.array(['2018-01-02', '2018-01-03', '2018-01-04'], dtype = 'M8[D]')]\n",
    "        ...    def _check_for_orders(self, args): pass\n",
    "        ...    def _check_for_trades(self, args): pass\n",
    "        ...    def _get_iteration_indices(self, start_date, end_date):\n",
    "        ...        i = self.num\n",
    "        ...        return self.dates[self.num - 1], [f'oarg_1_{1}', f'oarg_2_{i}', f'oarg_3_{i}'], [f'targ_1_{i}', f'targ_2_{i}', f'targ_3_{i}']\n",
    "        ...    def __repr__(self):\n",
    "        ...        return f'{self.num}'\n",
    "\n",
    "        >>> orders_iter, trades_iter = Portfolio._get_iterations(None, [Strategy(1), Strategy(2)], None, None)\n",
    "        >>> assert(len(orders_iter) == 4)\n",
    "        >>> assert(len(trades_iter) == 4)\n",
    "        >>> print(orders_iter[2]) #doctest: +ELLIPSIS\n",
    "        [(<function Strategy._check_for_orders at ...>, (1, 2, 'oarg_3_1')), (<function Strategy._check_for_orders at ...>, (2, 1, 'oarg_2_2'))]\n",
    "        >>> print(trades_iter[3]) #doctest: +ELLIPSIS\n",
    "        [(<function Strategy._check_for_trades at ...>, (2, 2, 'targ_3_2'))]\n",
    "        '''\n",
    "        orders_iter_list = []\n",
    "        trades_iter_list = []\n",
    "\n",
    "        for strategy in strategies:\n",
    "            dates, orders_iter, trades_iter = strategy._get_iteration_indices(start_date = start_date, end_date = end_date)\n",
    "            orders_iter_list.append((strategy, dates, orders_iter))\n",
    "            trades_iter_list.append((strategy, dates, trades_iter))\n",
    "\n",
    "        dates_list = [tup[1] for tup in orders_iter_list] + [tup[1] for tup in trades_iter_list]\n",
    "        all_dates = np.array(reduce(np.union1d, dates_list))\n",
    "        order_iterations = [[] for x in range(len(all_dates))]\n",
    "\n",
    "        for tup in orders_iter_list: # per strategy\n",
    "            strategy = tup[0]\n",
    "            dates = tup[1]\n",
    "            orders_iter = tup[2] # vector with list of (rule, symbol, iter_params dict)\n",
    "\n",
    "            for i, date in enumerate(dates):\n",
    "                idx = np.searchsorted(all_dates, date)\n",
    "                args = (strategy, i, orders_iter[i])\n",
    "                order_iterations[idx].append((Strategy._check_for_orders, args))\n",
    "\n",
    "        trade_iterations = [[] for x in range(len(all_dates))]\n",
    "\n",
    "        for tup in trades_iter_list: # per strategy\n",
    "            strategy = tup[0]\n",
    "            dates = tup[1]\n",
    "            trades_iter = tup[2] # vector with list of (rule, symbol, iter_params dict)\n",
    "\n",
    "            for i, date in enumerate(dates):\n",
    "                idx = np.searchsorted(all_dates, date)\n",
    "                args = (strategy, i, trades_iter[i])\n",
    "                trade_iterations[idx].append((Strategy._check_for_trades, args))\n",
    "\n",
    "        return order_iterations, trade_iterations\n",
    "                \n",
    "    def run_rules(self, strategy_names = None, start_date = None, end_date = None):\n",
    "        '''Run rules for the strategies specified.  Must be called after run_indicators and run_signals.  \n",
    "          See run function for argument descriptions\n",
    "        '''\n",
    "        start_date, end_date = str2date(start_date), str2date(end_date)\n",
    "        if strategy_names is None: strategy_names = list(self.strategies.keys())\n",
    "        if len(strategy_names) == 0: raise Exception('a portofolio must have at least one strategy')\n",
    "\n",
    "        strategies = [self.strategies[key] for key in strategy_names]\n",
    "        \n",
    "        min_date = min([strategy.dates[0] for strategy in strategies])\n",
    "        if start_date: min_date = max(min_date, start_date)\n",
    "        max_date = max([strategy.dates[-1] for strategy in strategies])\n",
    "        if end_date: max_date = min(max_date, end_date)\n",
    "            \n",
    "        order_iterations, trade_iterations = self._get_iterations(strategies, start_date, end_date)\n",
    "                \n",
    "        for i, orders_iter in enumerate(order_iterations):\n",
    "            trades_iter = trade_iterations[i] # Per date\n",
    "            for tup in trades_iter: # Per strategy\n",
    "                func = tup[0]\n",
    "                args = tup[1]\n",
    "                func(*args)\n",
    "            \n",
    "            for tup in orders_iter: # Per strategy\n",
    "                func = tup[0]\n",
    "                args = tup[1]\n",
    "                func(*args)\n",
    "                \n",
    "    def run(self, strategy_names = None, start_date = None, end_date = None):\n",
    "        '''\n",
    "        Run indicators, signals and rules.\n",
    "        \n",
    "        Args:\n",
    "            strategy_names: A list of strategy names.  By default this is set to None and we use all strategies.\n",
    "            start_date: Run rules starting from this date.  \n",
    "              Sometimes we have a few strategies in a portfolio that need different lead times before they are ready to trade\n",
    "              so you can set this so they are all ready by this date.  Default None\n",
    "            end_date: Don't run rules after this date.  Default None\n",
    "         '''\n",
    "        start_date, end_date = str2date(start_date), str2date(end_date)\n",
    "        self.run_indicators()\n",
    "        self.run_signals()\n",
    "        return self.run_rules(strategy_names, start_date, end_date)\n",
    "        \n",
    "    def df_returns(self, sampling_frequency = 'D', strategy_names = None):\n",
    "        '''\n",
    "        Return dataframe containing equity and returns with a date index.  Equity and returns are combined from all strategies passed in.\n",
    "        \n",
    "        Args:\n",
    "            sampling_frequency: Date frequency for rows.  Default 'D' for daily so we will have one row per day\n",
    "            strategy_names: A list of strategy names.  By default this is set to None and we use all strategies.\n",
    "        '''\n",
    "        if strategy_names is None: strategy_names = list(self.strategies.keys())\n",
    "        if len(strategy_names) == 0: raise Exception('portfolio must have at least one strategy')\n",
    "        equity_list = []\n",
    "        for name in strategy_names:\n",
    "            equity = self.strategies[name].df_returns(sampling_frequency = sampling_frequency)[['equity']]\n",
    "            equity.columns = [name]\n",
    "            equity_list.append(equity)\n",
    "        df = pd.concat(equity_list, axis = 1)\n",
    "        df['equity'] = df.sum(axis = 1)\n",
    "        df['ret'] = df.equity.pct_change()\n",
    "        return df\n",
    "        \n",
    "    def evaluate_returns(self, sampling_frequency = 'D', strategy_names = None, plot = True, float_precision = 4):\n",
    "        '''Returns a dictionary of common return metrics.\n",
    "        \n",
    "        Args:\n",
    "            sampling_frequency: Date frequency.  Default 'D' for daily so we downsample to daily returns before computing metrics\n",
    "            strategy_names: A list of strategy names.  By default this is set to None and we use all strategies.\n",
    "            plot: If set to True, display plots of equity, drawdowns and returns.  Default False\n",
    "            float_precision: Number of significant figures to show in returns.  Default 4\n",
    "        '''\n",
    "        returns = self.df_returns(sampling_freq, strategy_names)\n",
    "        ev = compute_return_metrics(returns.index.values, returns.ret.values, returns.equity.values[0])\n",
    "        display_return_metrics(ev.metrics(), float_precision = float_precision)\n",
    "        if plot: plot_return_metrics(ev.metrics())\n",
    "        return ev.metrics()\n",
    "    \n",
    "    def plot(self, sampling_frequency = 'D', strategy_names = None):\n",
    "        '''Display plots of equity, drawdowns and returns\n",
    "        \n",
    "        Args:\n",
    "            sampling_frequency: Date frequency.  Default 'D' for daily so we downsample to daily returns before computing metrics\n",
    "            strategy_names: A list of strategy names.  By default this is set to None and we use all strategies.\n",
    "        '''\n",
    "        returns = self.df_returns(sampling_frequency, strategy_names)\n",
    "        ev = compute_return_metrics(returns.index.values, returns.ret.values, returns.equity.values[0])\n",
    "        plot_return_metrics(ev.metrics())\n",
    "        \n",
    "    def __repr__(self):\n",
    "        return f'{self.name} {self.strategies.keys()}'"
   ]
  },
  {
   "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.7.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
