<p><div><div><div><div data-show-faces="false" data-height="21" data-width="100" data-layout="button_count" data-send="true" data-href="https://www.r-bloggers.com/stock-trading-analytics-and-optimization-in-python-with-pyfolio-rs-performanceanalytics-and-backtrader/"></div></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="https://ntguardian.wordpress.com/2017/06/28/stock-trading-analytics-and-optimization-in-python-with-pyfolio-rs-performanceanalytics-and-backtrader/"> R – Curtis Miller's Personal Website</a></strong>, and kindly contributed to <a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers)</a> </div>
<h2>Introduction</h2>
<p>Having figured out <a rel="nofollow" href="https://ntguardian.wordpress.com/2017/06/19/walk-forward-analysis-demonstration-backtrader/" target="_blank">how to perform walk-forward analysis in Python with <strong>backtrader</strong></a>, I want to have a look at evaluating a strategy’s performance. So far, I have cared about only one metric: the final value of the account at the end of a backtest relative. This should not be the only metric considered. Most people care not only about how much money was made but how much risk was taken on. People are risk-averse; one of finance’s leading principles is that higher risk should be compensated by higher returns. Thus many metrics exist that adjust returns for how much risk was taken on. Perhaps when optimizing only with respect to the final return of the strategy we end up choosing highly volatile strategies that lead to huge losses in out-of-sample data. Adjusting for risk may lead to better strategies being chosen.</p>
<p><span></span></p>
<p>In this article I will be looking more at <strong>backtrader</strong>‘s <code>Analyzer</code>s. These compute metrics for strategies after a backtest that users can then review. We can easily add an <code>Analyzer</code> to a <code>Cerebro</code> instance, <a rel="nofollow" href="https://www.backtrader.com/docu/analyzers-reference.html" target="_blank"><strong>backtrader</strong> already comes with many useful <code>Analyzers</code></a> computing common statistics, and creating a new <code>Analyzer</code> for a new statistic is easy to do. Additionally, <a rel="nofollow" href="https://www.backtrader.com/docu/analyzers/pyfolio-integration/pyfolio-integration.html" target="_blank"><strong>backtrader</strong> allows for <strong>PyFolio</strong> integration</a>, if <strong>PyFolio</strong> is more to your style.</p>
<p>I review these methods here.</p>
<h2>backtrader Analyzers</h2>
<p>Let’s start by picking up where we left off. Most of this code resembles my code from <a rel="nofollow" href="https://ntguardian.wordpress.com/2017/06/12/getting-started-with-backtrader/" target="_blank">previous</a> <a rel="nofollow" href="https://ntguardian.wordpress.com/2017/06/19/walk-forward-analysis-demonstration-backtrader/" target="_blank">articles</a>, though I made some (mostly cosmetic) improvements, such as to the strategy <code>SMAC</code>. I also import everything I will be needing in this article at the very beginning.</p>
<pre>
from sklearn.model_selection import TimeSeriesSplit
from sklearn.utils import indexable
from sklearn.utils.validation import _num_samples
import numpy as np
import backtrader as bt
import backtrader.indicators as btind
import backtrader.analyzers as btanal
import datetime as dt
import pandas as pd
import pandas_datareader as web
from pandas import Series, DataFrame
import random
import pyfolio as pf
from copy import deepcopy

from rpy2.robjects.packages import importr

pa = importr(&quot;PerformanceAnalytics&quot;)    # The R package PerformanceAnalytics, containing the R function VaR

from rpy2.robjects import numpy2ri, pandas2ri
numpy2ri.activate()
pandas2ri.activate()
</pre>
<pre>
class SMAC(bt.Strategy):
    &quot;&quot;&quot;A simple moving average crossover strategy; crossing of a fast and slow moving average generates buy/sell
       signals&quot;&quot;&quot;
    params = {&quot;fast&quot;: 20, &quot;slow&quot;: 50,                  # The windows for both fast and slow moving averages
              &quot;optim&quot;: False, &quot;optim_fs&quot;: (20, 50)}    # Used for optimization; equivalent of fast and slow, but a tuple
                                                       # The first number in the tuple is the fast MA's window, the
                                                       # second the slow MA's window

    def __init__(self):
        &quot;&quot;&quot;Initialize the strategy&quot;&quot;&quot;

        self.fastma = dict()
        self.slowma = dict()
        self.cross  = dict()

        if self.params.optim:    # Use a tuple during optimization
            self.params.fast, self.params.slow = self.params.optim_fs    # fast and slow replaced by tuple's contents

        if self.params.fast > self.params.slow:
            raise ValueError(
                &quot;A SMAC strategy cannot have the fast moving average's window be &quot; + \
                 &quot;greater than the slow moving average window.&quot;)

        for d in self.getdatanames():

            # The moving averages
            self.fastma[d] = btind.SimpleMovingAverage(self.getdatabyname(d),      # The symbol for the moving average
                                                       period=self.params.fast,    # Fast moving average
                                                       plotname=&quot;FastMA: &quot; + d)
            self.slowma[d] = btind.SimpleMovingAverage(self.getdatabyname(d),      # The symbol for the moving average
                                                       period=self.params.slow,    # Slow moving average
                                                       plotname=&quot;SlowMA: &quot; + d)

            # This is different; this is 1 when fast crosses above slow, -1 when fast crosses below slow, 0 o.w.
            self.cross[d] = btind.CrossOver(self.fastma[d], self.slowma[d], plot=False)

    def next(self):
        &quot;&quot;&quot;Define what will be done in a single step, including creating and closing trades&quot;&quot;&quot;
        for d in self.getdatanames():    # Looping through all symbols
            pos = self.getpositionbyname(d).size or 0
            if pos == 0:    # Are we out of the market?
                # Consider the possibility of entrance
                if self.cross[d][0] > 0:    # A buy signal
                    self.buy(data=self.getdatabyname(d))

            else:    # We have an open position
                if self.cross[d][0]  cash:
                return 0    # Not enough money for this trade
            else:
                return shares

        else:    # Selling
            return self.broker.getposition(data).size    # Clear the position


class AcctValue(bt.Observer):
    alias = ('Value',)
    lines = ('value',)

    plotinfo = {&quot;plot&quot;: True, &quot;subplot&quot;: True}

    def next(self):
        self.lines.value[0] = self._owner.broker.getvalue()    # Get today's account value (cash + stocks)


class AcctStats(bt.Analyzer):
    &quot;&quot;&quot;A simple analyzer that gets the gain in the value of the account; should be self-explanatory&quot;&quot;&quot;

    def __init__(self):
        self.start_val = self.strategy.broker.get_value()
        self.end_val = None

    def stop(self):
        self.end_val = self.strategy.broker.get_value()

    def get_analysis(self):
        return {&quot;start&quot;: self.start_val, &quot;end&quot;: self.end_val,
                &quot;growth&quot;: self.end_val - self.start_val, &quot;return&quot;: self.end_val / self.start_val}


class TimeSeriesSplitImproved(TimeSeriesSplit):
    &quot;&quot;&quot;Time Series cross-validator
    Provides train/test indices to split time series data samples
    that are observed at fixed time intervals, in train/test sets.
    In each split, test indices must be higher than before, and thus shuffling
    in cross validator is inappropriate.
    This cross-validation object is a variation of :class:`KFold`.
    In the kth split, it returns first k folds as train set and the
    (k+1)th fold as test set.
    Note that unlike standard cross-validation methods, successive
    training sets are supersets of those that come before them.
    Read more in the :ref:`User Guide `.
    Parameters
    ----------
    n_splits : int, default=3
        Number of splits. Must be at least 1.
    Examples
    --------
    >>> from sklearn.model_selection import TimeSeriesSplit
    >>> X = np.array([[1, 2], [3, 4], [1, 2], [3, 4]])
    >>> y = np.array([1, 2, 3, 4])
    >>> tscv = TimeSeriesSplit(n_splits=3)
    >>> print(tscv)  # doctest: +NORMALIZE_WHITESPACE
    TimeSeriesSplit(n_splits=3)
    >>> for train_index, test_index in tscv.split(X):
    ...    print(&quot;TRAIN:&quot;, train_index, &quot;TEST:&quot;, test_index)
    ...    X_train, X_test = X[train_index], X[test_index]
    ...    y_train, y_test = y[train_index], y[test_index]
    TRAIN: [0] TEST: [1]
    TRAIN: [0 1] TEST: [2]
    TRAIN: [0 1 2] TEST: [3]
    >>> for train_index, test_index in tscv.split(X, fixed_length=True):
    ...     print(&quot;TRAIN:&quot;, train_index, &quot;TEST:&quot;, test_index)
    ...     X_train, X_test = X[train_index], X[test_index]
    ...     y_train, y_test = y[train_index], y[test_index]
    TRAIN: [0] TEST: [1]
    TRAIN: [1] TEST: [2]
    TRAIN: [2] TEST: [3]
    >>> for train_index, test_index in tscv.split(X, fixed_length=True,
    ...     train_splits=2):
    ...     print(&quot;TRAIN:&quot;, train_index, &quot;TEST:&quot;, test_index)
    ...     X_train, X_test = X[train_index], X[test_index]
    ...     y_train, y_test = y[train_index], y[test_index]
    TRAIN: [0 1] TEST: [2]
    TRAIN: [1 2] TEST: [3]

    Notes
    -----
    When ``fixed_length`` is ``False``, the training set has size
    ``i * train_splits * n_samples // (n_splits + 1) + n_samples %
    (n_splits + 1)`` in the ``i``th split, with a test set of size
    ``n_samples//(n_splits + 1) * test_splits``, where ``n_samples``
    is the number of samples. If fixed_length is True, replace ``i``
    in the above formulation with 1, and ignore ``n_samples %
    (n_splits + 1)`` except for the first training set. The number
    of test sets is ``n_splits + 2 - train_splits - test_splits``.
    &quot;&quot;&quot;

    def split(self, X, y=None, groups=None, fixed_length=False,
              train_splits=1, test_splits=1):
        &quot;&quot;&quot;Generate indices to split data into training and test set.
        Parameters
        ----------
        X : array-like, shape (n_samples, n_features)
            Training data, where n_samples is the number of samples
            and n_features is the number of features.
        y : array-like, shape (n_samples,)
            Always ignored, exists for compatibility.
        groups : array-like, with shape (n_samples,), optional
            Always ignored, exists for compatibility.
        fixed_length : bool, hether training sets should always have
            common length
        train_splits : positive int, for the minimum number of
            splits to include in training sets
        test_splits : positive int, for the number of splits to
            include in the test set
        Returns
        -------
        train : ndarray
            The training set indices for that split.
        test : ndarray
            The testing set indices for that split.
        &quot;&quot;&quot;
        X, y, groups = indexable(X, y, groups)
        n_samples = _num_samples(X)
        n_splits = self.n_splits
        n_folds = n_splits + 1
        train_splits, test_splits = int(train_splits), int(test_splits)
        if n_folds > n_samples:
            raise ValueError(
                (&quot;Cannot have number of folds ={0} greater&quot;
                 &quot; than the number of samples: {1}.&quot;).format(n_folds,
                                                             n_samples))
        if (n_folds - train_splits - test_splits)  0 and test_splits > 0):
            raise ValueError(
                (&quot;Both train_splits and test_splits must be positive&quot;
                 &quot; integers.&quot;))
        indices = np.arange(n_samples)
        split_size = (n_samples // n_folds)
        test_size = split_size * test_splits
        train_size = split_size * train_splits
        test_starts = range(train_size + n_samples % n_folds,
                            n_samples - (test_size - split_size),
                            split_size)
        if fixed_length:
            for i, test_start in zip(range(len(test_starts)),
                                     test_starts):
                rem = 0
                if i == 0:
                    rem = n_samples % n_folds
                yield (indices[(test_start - train_size - rem):test_start],
                       indices[test_start:test_start + test_size])
        else:
            for test_start in test_starts:
                yield (indices[:test_start],
                    indices[test_start:test_start + test_size])
</pre>
<p>Let’s create a <code>Cerebro</code> instance with everything added save for the analyzers.</p>
<pre>
start = dt.datetime(2010, 1, 1)
end = dt.datetime(2016, 10, 31)
symbols = [&quot;AAPL&quot;, &quot;GOOG&quot;, &quot;MSFT&quot;, &quot;AMZN&quot;, &quot;YHOO&quot;, &quot;SNY&quot;, &quot;VZ&quot;, &quot;IBM&quot;, &quot;HPQ&quot;, &quot;QCOM&quot;, &quot;NVDA&quot;]
datafeeds = {s: web.DataReader(s, &quot;google&quot;, start, end) for s in symbols}
for df in datafeeds.values():
    df[&quot;OpenInterest&quot;] = 0    # PandasData reader expects an OpenInterest column;
                              # not provided by Google and we don't use it so set to 0

cerebro = bt.Cerebro(stdstats=False)

plot_symbols = [&quot;AAPL&quot;, &quot;GOOG&quot;, &quot;NVDA&quot;]
is_first = True
#plot_symbols = []
for s, df in datafeeds.items():
    data = bt.feeds.PandasData(dataname=df, name=s)
    if s in plot_symbols:
        if is_first:
            data_main_plot = data
            is_first = False
        else:
            data.plotinfo.plotmaster = data_main_plot
    else:
        data.plotinfo.plot = False
    cerebro.adddata(data)

cerebro.broker.setcash(1000000)
cerebro.broker.setcommission(0.02)
cerebro.addstrategy(SMAC)
cerebro.addobserver(AcctValue)
cerebro.addobservermulti(bt.observers.BuySell)
cerebro.addsizer(PropSizer)
</pre>
<p>We add <code>Analyzer</code> objects to <code>Cerebro</code> instances with the <code>addanalyzer()</code> method. The objects collect data while the strategy is running then produce final statistics that users can then review by calling the <code>Analyzer</code>‘s <code>get_analysis()</code> method. These objects are intended to be light-weight, containing only the statistics requested and not the data used to generate the statistics. Usually they return ordered dictionaries containing the statics computed for the strategy.</p>
<p>Some of the built-in analyzers I use include:</p>
<ul>
<li><code>TradeAnalyzer</code>: This computes trade statistics, such as the number of open and closed trades, total and average profit/loss from trades, how long trades are in the market, the longest streaks (successive winning or losing trades), and other similar statistics.</li>
<li><code>Returns</code>: This computes total, average, compound, and annualized (normalized to represent returns over a year) returns, which is the growth of the portfolio.</li>
<li><code>DrawDown</code>: This computes drawdown statistics. Drawdown is how much the value of an investment or account decreases after reaching a new high, from “peak” (the high point) to “trough” (the low point). Investors and traders dislike high drawdown, and thus are interested in metrics such as the average drawdown, the maximum draw down (the biggest gap between peak and trough), and the average and maximum length of a drawdown period (which is the period between a new high and the previous high), which this analyzer computes. Traders would like average drawdowns to be small and don’t want to wait long for the next high to be reached. (Traders often cite maximum drawdown, but beware; as the timeframe increases, <em>maximum</em> drawdown, by definition, can only increase, and thus should not be interpreted separately from the duration of time considered.)</li>
</ul>
<p><img alt="Max Drawdown Illustration" src="https://ntguardian.files.wordpress.com/2017/06/spymaxdrawdownillustration.png?w=456" data-recalc-dims="1"><br>
<em><a rel="nofollow" href="https://www.google.com/finance?q=NYSEARCA:SPY" target="_blank">SPY</a> max drawdown from January 2010 to June 2017, with maximum drawdown and a new peak illustrated. I created this chart using Google Finance data obtained in R via <strong>quantmod</strong> and using <strong>PerformanceAnalytics</strong>.</em></p>
<ul>
<li><code>SharpeRatio_A</code>: This computes the annualized Sharpe ratio. The <a rel="nofollow" href="http://www.investopedia.com/terms/s/sharperatio.asp" target="_blank">Sharpe ratio</a> is the most common risk-adjusted performance measure of an investment used. It is excess returns (which is return on investment minus the <a rel="nofollow" href="http://www.investopedia.com/terms/r/risk-freerate.asp" target="_blank">“risk-free” rate of return</a>, the latter often being the return of some US government debt instruments such as <a rel="nofollow" href="http://www.investopedia.com/terms/t/treasurybill.asp" target="_blank">T-bills</a>) divided by the standard deviation of the returns of the investment. The higher the Sharpe ratio, the better the return of the investment relative to its volatility (which is the type of risk considered). The annualized variant of the Sharpe ratio simply annualizes the metrics used in the computation.</li>
</ul>
<p>I attempted to use another analyzer, <code>AnnualReturn</code>, which computes annualized return metrics, but unfortunately it does not seem to work.</p>
<p>These analyzers are all useful, but we can easily create custom ones for new performance metrics not already implemented by subclassing from the <code>Analyzer</code> class. I did this in <a rel="nofollow" href="https://ntguardian.wordpress.com/2017/06/19/walk-forward-analysis-demonstration-backtrader/" target="_blank">my blog post on walk-forward analysis</a>. Usually creating a new analyzer involves:</p>
<ol>
<li>Creating modifiable parameters with a <code>params</code> dictionary declared at the beginning of the definition of the new analyzer</li>
<li>Defining an <code>__init__()</code> method setting up any variables needed when the analyzer is created or the backtest starts</li>
<li>Defining a <code>next()</code> method that gets any information needed during the backtest</li>
<li>Defining a <code>stop()</code> method for final computations</li>
<li>Defining a <code>get_analysis()</code> method that a user can call after a backtest that returns, say, a dictionary with all the statistics the analyzer computed</li>
</ol>
<p>In truth, only the <code>__init__()</code>, <code>stop()</code>, and <code>get_analysis()</code> methods are truly needed for an analyzer. Furthermore, analyzers should be light-weight; they do not need to include the entire backtest’s data to work after the backtest is completed.</p>
<h2>Using R’s PerformanceAnalytics Package in Python</h2>
<p>I have <a rel="nofollow" href="https://ntguardian.wordpress.com/2017/05/22/the-end-of-the-honeymoon-falling-out-of-love-with-quantstrat/" target="_blank">complaints about <strong>quantstrat</strong></a>, the most notable backtesting package for R, but R has many other packages useful for finance, such as <strong>quantmod</strong> or <strong>PerformanceAnalytics</strong>. In particular, <strong>PerformanceAnalytics</strong> has <a rel="nofollow" href="https://cran.r-project.org/web/packages/PerformanceAnalytics/PerformanceAnalytics.pdf" target="_blank">a lot of analytics functions</a> not provided by <strong>backtrader</strong> and likely without a Python equivalent.</p>
<p>Fortunately we can easily create a <strong>backtrader</strong> <code>Analyzer</code> that uses <strong>PerformanceAnalytics</strong> functions. I used the following template:</p>
<ol>
<li>Interface with R using <a rel="nofollow" href="https://rpy2.readthedocs.io/en/version_2.8.x/overview.html" target="_blank"><strong>rpy2</strong></a>, an excellent R interface for Python that gives Python users access to R functions</li>
<li>Use <code>importr()</code> to import the R package needed (in this case, <strong>PerformanceAnalytics</strong>), and use <code>numpy2ri</code> and <code>pandas2ri</code> so that <strong>NumPy</strong> <code>ndarray</code>s and <strong>pandas</strong> <code>Series</code> and <code>DataFrame</code>s can be translated into their R equivalents</li>
<li>In the <code>Analyzer</code>, record the data of interest in the <code>next()</code> method so that it can be transformed into a <strong>pandas</strong> <code>Series</code> containing a time series, with a <code>dict</code></li>
<li>In the <code>stop()</code> method, transform this <code>dict</code> into a <code>Series</code>, then use the R function from the imported library to compute the desired metric, passing the function the <code>Series</code> of data (this will be interpreted as an R vector, which can also be interpreted as an R <code>ts</code> time series thanks to the date index)</li>
<li>Save the results and delete the <code>dict</code> of data since it is no longer needed, and in <code>get_analysis()</code>, just return a <code>dict</code> containing the resulting calculation.</li>
</ol>
<p><strong>rpy2</strong> integrates R objects quite well, using a familiar Python-like interface. <strong>pandas</strong>, being initially inspired by R, also integrates well. In fact, the <strong>pandas</strong> objects <code>Series</code> and <code>DataFrame</code> were designed initially for storing and manipulating time series data, so do as well a job as, say, R’s <strong>xts</strong> package.</p>
<p>Here, I use <strong>PerformanceAnalytics</strong> to create analyzers calculating two more metrics not provided by <strong>backtrader</strong>. They are:</p>
<ul>
<li><code>VaR</code>: This computes value at risk (abbreviated VaR, and the capitalization here matters, distinguishing value at risk from variance and vector autoregression, which are abbreviated with var and VAR respectively). <a rel="nofollow" href="http://www.investopedia.com/terms/v/var.asp" target="_blank">Value at risk</a> measures the potential loss a portfolio could see. Different ways to precisely define and calculate VaR exist, but in spirit they all give how much money could be lost at a certain likelihood. For example, if the daily 95% VaR for a portfolio is -1%, then 95% of the time the portfolio’s daily return will exceed -1% (a loss of 1%), or 5% of the time losses will exceed 1% of the portfolio’s value. This could be useful information for, say, position sizing. This <code>Analyzer</code> is based on <strong>PerformanceAnalytics</strong> <code>VaR</code> function.</li>
<li><code>SortinoRatio</code>: This computes the account’s Sortino ratio. The <a rel="nofollow" href="http://www.investopedia.com/terms/s/sortinoratio.asp" target="_blank">Sortino ratio</a> is another risk-adjusted performance metric often presented as an alternative to the Sharpe ratio. It’s like the Sharpe ratio, but instead of dividing by the standard deviation of returns–which could punish volitility in the <em>positive</em> direction, which earns money–excess returns are divided by the standard deviation of losses alone. This is based on the <strong>PerformanceAnalytics</strong> function <code>SortinoRatio</code>.</li>
</ul>
<p>My analyzers work with log returns, or <img alt="log(x_t) - log(x_{t - 1})" title="log(x_t) - log(x_{t - 1})" src="https://s0.wp.com/latex.php?latex=%5Clog%28x_t%29+-+%5Clog%28x_%7Bt+-+1%7D%29&bg=ffffff&%23038;fg=444444&%23038;s=0" class="latex">, which are my preferred way of computing returns. The definition of these analyzers is below. (Steps 1 and 2 of the process I outlined above were done when I loaded in all the packages I would be using in this article.) Notice that I included parameters that can be passed on to the respective <strong>PerformanceAnalytics</strong> functions (not all of the parameters, but the ones I most likely would want to change).</p>
<pre>
class VaR(bt.Analyzer):
    &quot;&quot;&quot;
    Computes the value at risk metric for the whole account using the strategy, based on the R package
    PerformanceAnalytics VaR function
    &quot;&quot;&quot;
    params = {
        &quot;p&quot;: 0.95,                # Confidence level of calculation
        &quot;method&quot;: &quot;historical&quot;    # Method used; can be &quot;historical&quot;, &quot;gaussian&quot;, &quot;modified&quot;, &quot;kernel&quot;
    }

    def __init__(self):
        self.acct_return = dict()
        self.acct_last = self.strategy.broker.get_value()
        self.vardict = dict()

    def next(self):
        if len(self.data) > 1:
            curdate = self.strategy.datetime.date(0)
            # I use log returns
            self.acct_return[curdate] = np.log(self.strategy.broker.get_value()) - np.log(self.acct_last)
            self.acct_last = self.strategy.broker.get_value()

    def stop(self):
        srs = Series(self.acct_return)    # Need to pass a time-series-like object to VaR
        srs.sort_index(inplace=True)
        self.vardict[&quot;VaR&quot;] = pa.VaR(srs, p=self.params.p, method=self.params.method)[0]    # Get VaR
        del self.acct_return              # This dict is of no use to us anymore

    def get_analysis(self):
        return self.vardict


class SortinoRatio(bt.Analyzer):
    &quot;&quot;&quot;
    Computes the Sortino ratio metric for the whole account using the strategy, based on the R package
    PerformanceAnalytics SortinoRatio function
    &quot;&quot;&quot;
    params = {&quot;MAR&quot;: 0}    # Minimum Acceptable Return (perhaps the risk-free rate?); must be in same periodicity
                           # as data

    def __init__(self):
        self.acct_return = dict()
        self.acct_last = self.strategy.broker.get_value()
        self.sortinodict = dict()

    def next(self):
        if len(self.data) > 1:
            # I use log returns
            curdate = self.strategy.datetime.date(0)
            self.acct_return[curdate] = np.log(self.strategy.broker.get_value()) - np.log(self.acct_last)
            self.acct_last = self.strategy.broker.get_value()

    def stop(self):
        srs = Series(self.acct_return)    # Need to pass a time-series-like object to SortinoRatio
        srs.sort_index(inplace=True)
        self.sortinodict['sortinoratio'] = pa.SortinoRatio(srs, MAR = self.params.MAR)[0]    # Get Sortino Ratio
        del self.acct_return              # This dict is of no use to us anymore

    def get_analysis(self):
        return self.sortinodict
</pre>
<h2>Seeing Analyzer Results</h2>
<p>I attach all my analyzers, along with the <code>PyFolio</code> <code>Analyzer</code>, which is needed for <strong>PyFolio</strong> integration (which I discuss later).</p>
<pre>
cerebro.addanalyzer(btanal.PyFolio)                # Needed to use PyFolio
cerebro.addanalyzer(btanal.TradeAnalyzer)          # Analyzes individual trades
cerebro.addanalyzer(btanal.SharpeRatio_A)          # Gets the annualized Sharpe ratio
#cerebro.addanalyzer(btanal.AnnualReturn)          # Annualized returns (does not work?)
cerebro.addanalyzer(btanal.Returns)                # Returns
cerebro.addanalyzer(btanal.DrawDown)               # Drawdown statistics
cerebro.addanalyzer(VaR)                           # Value at risk
cerebro.addanalyzer(SortinoRatio, MAR=0.00004)     # Sortino ratio with risk-free rate of 0.004% daily (~1% annually)
</pre>
<p>Then I run the strategy. Notice that I save the results in <code>res</code>. This must be done in order to see the <code>Analyzer</code>s results.</p>
<pre>
res = cerebro.run()
</pre>
<p>Getting the statistics I want is now easy. That said, notice their presentation.</p>
<pre>
res[0].analyzers.tradeanalyzer.get_analysis()
</pre>
<pre><code>AutoOrderedDict([('total',
                  AutoOrderedDict([('total', 197),
                                   ('open', 5),
                                   ('closed', 192)])),
                 ('streak',
                  AutoOrderedDict([('won',
                                    AutoOrderedDict([('current', 2),
                                                     ('longest', 5)])),
                                   ('lost',
                                    AutoOrderedDict([('current', 0),
                                                     ('longest', 18)]))])),
                 ('pnl',
                  AutoOrderedDict([('gross',
                                    AutoOrderedDict([('total',
                                                      222396.9999999999),
                                                     ('average',
                                                      1158.3177083333328)])),
                                   ('net',
                                    AutoOrderedDict([('total',
                                                      -373899.18000000005),
                                                     ('average',
                                                      -1947.3915625000002)]))])),
                 ('won',
                  AutoOrderedDict([('total', 55),
                                   ('pnl',
                                    AutoOrderedDict([('total',
                                                      506889.75999999995),
                                                     ('average',
                                                      9216.177454545454),
                                                     ('max',
                                                      59021.960000000014)]))])),
                 ('lost',
                  AutoOrderedDict([('total', 137),
                                   ('pnl',
                                    AutoOrderedDict([('total',
                                                      -880788.9400000004),
                                                     ('average',
                                                      -6429.116350364967),
                                                     ('max',
                                                      -17297.76000000001)]))])),
                 ('long',
                  AutoOrderedDict([('total', 192),
                                   ('pnl',
                                    AutoOrderedDict([('total',
                                                      -373899.18000000005),
                                                     ('average',
                                                      -1947.3915625000002),
                                                     ('won',
                                                      AutoOrderedDict([('total',
                                                                        506889.75999999995),
                                                                       ('average',
                                                                        9216.177454545454),
                                                                       ('max',
                                                                        59021.960000000014)])),
                                                     ('lost',
                                                      AutoOrderedDict([('total',
                                                                        -880788.9400000004),
                                                                       ('average',
                                                                        -6429.116350364967),
                                                                       ('max',
                                                                        -17297.76000000001)]))])),
                                   ('won', 55),
                                   ('lost', 137)])),
                 ('short',
                  AutoOrderedDict([('total', 0),
                                   ('pnl',
                                    AutoOrderedDict([('total', 0.0),
                                                     ('average', 0.0),
                                                     ('won',
                                                      AutoOrderedDict([('total',
                                                                        0.0),
                                                                       ('average',
                                                                        0.0),
                                                                       ('max',
                                                                        0.0)])),
                                                     ('lost',
                                                      AutoOrderedDict([('total',
                                                                        0.0),
                                                                       ('average',
                                                                        0.0),
                                                                       ('max',
                                                                        0.0)]))])),
                                   ('won', 0),
                                   ('lost', 0)])),
                 ('len',
                  AutoOrderedDict([('total', 9604),
                                   ('average', 50.020833333333336),
                                   ('max', 236),
                                   ('min', 1),
                                   ('won',
                                    AutoOrderedDict([('total', 5141),
                                                     ('average',
                                                      93.47272727272727),
                                                     ('max', 236),
                                                     ('min', 45)])),
                                   ('lost',
                                    AutoOrderedDict([('total', 4463),
                                                     ('average',
                                                      32.57664233576642),
                                                     ('max', 95),
                                                     ('min', 1)])),
                                   ('long',
                                    AutoOrderedDict([('total', 9604),
                                                     ('average',
                                                      50.020833333333336),
                                                     ('max', 236),
                                                     ('min', 1),
                                                     ('won',
                                                      AutoOrderedDict([('total',
                                                                        5141),
                                                                       ('average',
                                                                        93.47272727272727),
                                                                       ('max',
                                                                        236),
                                                                       ('min',
                                                                        45)])),
                                                     ('lost',
                                                      AutoOrderedDict([('total',
                                                                        4463),
                                                                       ('average',
                                                                        32.57664233576642),
                                                                       ('max',
                                                                        95),
                                                                       ('min',
                                                                        1)]))])),
                                   ('short',
                                    AutoOrderedDict([('total', 0),
                                                     ('average', 0.0),
                                                     ('max', 0),
                                                     ('min', 2147483647),
                                                     ('won',
                                                      AutoOrderedDict([('total',
                                                                        0),
                                                                       ('average',
                                                                        0.0),
                                                                       ('max',
                                                                        0),
                                                                       ('min',
                                                                        2147483647)])),
                                                     ('lost',
                                                      AutoOrderedDict([('total',
                                                                        0),
                                                                       ('average',
                                                                        0.0),
                                                                       ('max',
                                                                        0),
                                                                       ('min',
                                                                        2147483647)]))]))]))])
</code></pre>
<pre>
res[0].analyzers.sharperatio_a.get_analysis()
</pre>
<pre><code>OrderedDict([('sharperatio', -0.5028399674826421)])
</code></pre>
<pre>
res[0].analyzers.returns.get_analysis()
</pre>
<pre><code>OrderedDict([('rtot', -0.3075714139075255),
             ('ravg', -0.0001788205894811195),
             ('rnorm', -0.04406254197743979),
             ('rnorm100', -4.406254197743979)])
</code></pre>
<pre>
res[0].analyzers.drawdown.get_analysis()
</pre>
<pre><code>AutoOrderedDict([('len', 1435),
                 ('drawdown', 32.8390619071308),
                 ('moneydown', 359498.7800000005),
                 ('max',
                  AutoOrderedDict([('len', 1435),
                                   ('drawdown', 41.29525957443689),
                                   ('moneydown', 452071.2400000006)]))])
</code></pre>
<pre>
res[0].analyzers.var.get_analysis()
</pre>
<pre><code>{'VaR': -0.011894285243688957}
</code></pre>
<pre>
res[0].analyzers.sortinoratio.get_analysis()
</pre>
<pre><code>{'sortinoratio': -0.04122331025349434}
</code></pre>
<p>This is, technically, all the data I want. This works fine if you don’t care about presentation and if the results are going to be processed and presented by something else. That being said, if you’re working in an interactive environment such as a Jupyter notebook (as I did when writing this article) and are used to pretty printing of tables and charts, you may be disappointed to see the results in long dictionaries. This presentation is barely useful.</p>
<p>Thus, in a notebook environment, you may want to see this data in a more consumable format. This would be where you want to use <strong>PyFolio</strong>.</p>
<h2>PyFolio and backtrader</h2>
<p><a rel="nofollow" href="https://quantopian.github.io/pyfolio/" target="_blank"><strong>PyFolio</strong></a> is a Python library for portfolio analytics. <strong>PyFolio</strong> creates “tear sheets” or tables and graphics of portfolio analytics that give most desired metrics describing the performance of a strategy in a clean, consumable format. It works nicely in an interactive notebook setting.</p>
<p><strong>PyFolio</strong> was designed mainly to work with the <a rel="nofollow" href="http://www.zipline.io/" target="_blank"><strong>zipline</strong></a> backtesting platform (perhaps the more common Python backtesting platform, and one that, on reviewing its documentation, I’m not that impressed with) and with <a rel="nofollow" href="https://www.quantopian.com/" target="_blank">Quantopian</a>, which aspires to be the <a rel="nofollow" href="https://www.kaggle.com/" target="_blank">Kaggle</a> of algorithmic trading and a crowd-sourced hedge fund, providing a platform for algorithm development and putting real money behind strong performers (and paying their creators). While <strong>zipline</strong> is <strong>PyFolio</strong>‘s target, <strong>backtrader</strong> can work with <strong>PyFolio</strong> as well. <strong>PyFolio</strong> needs only four datasets to create a tear sheet: the returns, positions, transactions, and gross leverage of a strategy as it proceeds. The <code>PyFolio</code> <code>Analyzer</code> creates these metrics from a backtest, which can then be passed on to a <strong>PyFolio</strong> function like so:</p>
<pre>
returns, positions, transactions, gross_lev = res[0].analyzers.pyfolio.get_pf_items()
pf.create_round_trip_tear_sheet(returns, positions, transactions)
</pre>
<div>
<table border="1">
<thead>
<tr>
<th>Summary stats</th>
<th>All trades</th>
<th>Long trades</th>
</tr>
</thead>
<tbody>
<tr>
<th>Total number of round_trips</th>
<td>193.00</td>
<td>193.00</td>
</tr>
<tr>
<th>Percent profitable</th>
<td>0.43</td>
<td>0.43</td>
</tr>
<tr>
<th>Winning round_trips</th>
<td>83.00</td>
<td>83.00</td>
</tr>
<tr>
<th>Losing round_trips</th>
<td>110.00</td>
<td>110.00</td>
</tr>
<tr>
<th>Even round_trips</th>
<td>0.00</td>
<td>0.00</td>
</tr>
</tbody>
</table>
</div>
<div>
<table border="1">
<thead>
<tr>
<th>PnL stats</th>
<th>All trades</th>
<th>Long trades</th>
</tr>
</thead>
<tbody>
<tr>
<th>Total profit</th>
<td>$300657.00</td>
<td>$300657.00</td>
</tr>
<tr>
<th>Gross profit</th>
<td>$794165.00</td>
<td>$794165.00</td>
</tr>
<tr>
<th>Gross loss</th>
<td>$-493508.00</td>
<td>$-493508.00</td>
</tr>
<tr>
<th>Profit factor</th>
<td>$1.61</td>
<td>$1.61</td>
</tr>
<tr>
<th>Avg. trade net profit</th>
<td>$1557.81</td>
<td>$1557.81</td>
</tr>
<tr>
<th>Avg. winning trade</th>
<td>$9568.25</td>
<td>$9568.25</td>
</tr>
<tr>
<th>Avg. losing trade</th>
<td>$-4486.44</td>
<td>$-4486.44</td>
</tr>
<tr>
<th>Ratio Avg. Win:Avg. Loss</th>
<td>$2.13</td>
<td>$2.13</td>
</tr>
<tr>
<th>Largest winning trade</th>
<td>$78260.00</td>
<td>$78260.00</td>
</tr>
<tr>
<th>Largest losing trade</th>
<td>$-14152.00</td>
<td>$-14152.00</td>
</tr>
</tbody>
</table>
</div>
<div>
<table border="1">
<thead>
<tr>
<th>Duration stats</th>
<th>All trades</th>
<th>Long trades</th>
</tr>
</thead>
<tbody>
<tr>
<th>Avg duration</th>
<td>73 days 05:28:17.414507</td>
<td>73 days 05:28:17.414507</td>
</tr>
<tr>
<th>Median duration</th>
<td>58 days 00:00:00</td>
<td>58 days 00:00:00</td>
</tr>
</tbody>
</table>
</div>
<div>
<table border="1">
<thead>
<tr>
<th>Return stats</th>
<th>All trades</th>
<th>Long trades</th>
</tr>
</thead>
<tbody>
<tr>
<th>Avg returns all round_trips</th>
<td>0.32%</td>
<td>0.32%</td>
</tr>
<tr>
<th>Avg returns winning</th>
<td>2.40%</td>
<td>2.40%</td>
</tr>
<tr>
<th>Avg returns losing</th>
<td>-1.25%</td>
<td>-1.25%</td>
</tr>
<tr>
<th>Median returns all round_trips</th>
<td>-0.17%</td>
<td>-0.17%</td>
</tr>
<tr>
<th>Median returns winning</th>
<td>1.32%</td>
<td>1.32%</td>
</tr>
<tr>
<th>Median returns losing</th>
<td>-1.03%</td>
<td>-1.03%</td>
</tr>
<tr>
<th>Largest winning trade</th>
<td>18.29%</td>
<td>18.29%</td>
</tr>
<tr>
<th>Largest losing trade</th>
<td>-5.57%</td>
<td>-5.57%</td>
</tr>
</tbody>
</table>
</div>
<div>
<table border="1">
<thead>
<tr>
<th>Symbol stats</th>
<th>AAPL</th>
<th>AMZN</th>
<th>GOOG</th>
<th>HPQ</th>
<th>IBM</th>
<th>MSFT</th>
<th>NVDA</th>
<th>QCOM</th>
<th>SNY</th>
<th>VZ</th>
<th>YHOO</th>
</tr>
</thead>
<tbody>
<tr>
<th>Avg returns all round_trips</th>
<td>0.88%</td>
<td>0.30%</td>
<td>0.37%</td>
<td>0.67%</td>
<td>-0.13%</td>
<td>-0.10%</td>
<td>1.53%</td>
<td>-0.02%</td>
<td>-0.50%</td>
<td>0.16%</td>
<td>0.68%</td>
</tr>
<tr>
<th>Avg returns winning</th>
<td>2.33%</td>
<td>1.92%</td>
<td>1.92%</td>
<td>4.39%</td>
<td>0.84%</td>
<td>1.73%</td>
<td>5.75%</td>
<td>2.02%</td>
<td>0.85%</td>
<td>2.24%</td>
<td>3.69%</td>
</tr>
<tr>
<th>Avg returns losing</th>
<td>-0.99%</td>
<td>-2.40%</td>
<td>-0.56%</td>
<td>-1.98%</td>
<td>-0.57%</td>
<td>-1.26%</td>
<td>-1.43%</td>
<td>-1.65%</td>
<td>-2.00%</td>
<td>-1.12%</td>
<td>-0.82%</td>
</tr>
<tr>
<th>Median returns all round_trips</th>
<td>0.26%</td>
<td>0.43%</td>
<td>-0.16%</td>
<td>-0.73%</td>
<td>-0.12%</td>
<td>-0.55%</td>
<td>-0.40%</td>
<td>-0.28%</td>
<td>0.09%</td>
<td>-0.59%</td>
<td>-0.30%</td>
</tr>
<tr>
<th>Median returns winning</th>
<td>1.70%</td>
<td>1.46%</td>
<td>1.65%</td>
<td>0.31%</td>
<td>0.48%</td>
<td>1.26%</td>
<td>1.64%</td>
<td>1.57%</td>
<td>0.57%</td>
<td>1.38%</td>
<td>3.40%</td>
</tr>
<tr>
<th>Median returns losing</th>
<td>-0.83%</td>
<td>-2.25%</td>
<td>-0.36%</td>
<td>-1.68%</td>
<td>-0.34%</td>
<td>-1.35%</td>
<td>-1.22%</td>
<td>-1.14%</td>
<td>-1.61%</td>
<td>-0.97%</td>
<td>-0.99%</td>
</tr>
<tr>
<th>Largest winning trade</th>
<td>6.91%</td>
<td>6.55%</td>
<td>4.25%</td>
<td>13.84%</td>
<td>2.83%</td>
<td>3.46%</td>
<td>18.29%</td>
<td>5.06%</td>
<td>2.10%</td>
<td>7.23%</td>
<td>8.62%</td>
</tr>
<tr>
<th>Largest losing trade</th>
<td>-1.94%</td>
<td>-4.46%</td>
<td>-1.33%</td>
<td>-3.85%</td>
<td>-1.72%</td>
<td>-3.12%</td>
<td>-2.83%</td>
<td>-4.83%</td>
<td>-5.57%</td>
<td>-2.09%</td>
<td>-1.44%</td>
</tr>
</tbody>
</table>
</div>
<div>
<table border="1">
<thead>
<tr>
<th>Profitability (PnL / PnL total) per name</th>
<th>pnl</th>
</tr>
</thead>
<tbody>
<tr>
<th>NVDA</th>
<td>0.41%</td>
</tr>
<tr>
<th>YHOO</th>
<td>0.27%</td>
</tr>
<tr>
<th>AAPL</th>
<td>0.20%</td>
</tr>
<tr>
<th>AMZN</th>
<td>0.16%</td>
</tr>
<tr>
<th>HPQ</th>
<td>0.08%</td>
</tr>
<tr>
<th>GOOG</th>
<td>0.04%</td>
</tr>
<tr>
<th>QCOM</th>
<td>0.01%</td>
</tr>
<tr>
<th>MSFT</th>
<td>-0.00%</td>
</tr>
<tr>
<th>IBM</th>
<td>-0.03%</td>
</tr>
<tr>
<th>VZ</th>
<td>-0.04%</td>
</tr>
<tr>
<th>SNY</th>
<td>-0.10%</td>
</tr>
</tbody>
</table>
</div>
<p><img alt="png" src="https://ntguardian.files.wordpress.com/2017/06/output_19_7.png?w=456" data-recalc-dims="1"></p>
<p>Above I found the round-trip statistics for my strategy during the backtest (that is, how trades tended to perform). We can see how many trades were conducted, how long the strategy tended to be in the market, how profitable trades tended to be, extreme statistic (largest winning trade, largest losing trade, max duration of a trade, etc.), how profitable certain symbols were, and many other useful metrics, along with graphics. This is much more digestable than the <strong>backtrader</strong> <code>Analyzer</code>s default outputs.</p>
<p>Unfortunately, with Yahoo! Finance’s recent changes breaking <strong>pandas-datareader</strong>, <strong>PyFolio</strong> is not very stable right now and won’t be until <strong>pandas-datareader</strong> is updated. This is unfortunate, and reveals a flaw in <strong>PyFolio</strong>‘s design; using alternative data sources (both online and offline) for the data provided in their tear sheet functions should be better supported. While I was able to get the code below to work, this require some hacks and rolling back of packages. I cannot guarantee the code will work for you at this time. Additionally, some of the graphics <strong>PyFolio</strong> wants to create (such as comparing the performance of the strategy to SPY) don’t work since <strong>PyFolio</strong> can’t fetch the data.</p>
<p>Anyway, what does <strong>PyFolio</strong> include in its “full” tear sheet? We have some of the usual performance metrics, along with some more exotic ones, such as:</p>
<ul>
<li>The <a rel="nofollow" href="http://www.investopedia.com/terms/c/calmarratio.asp" target="_blank">Calmar ratio</a>, a risk-adjusted performance metric comparing compound rate of returns to the maximum drawdown.</li>
<li>The <a rel="nofollow" href="https://en.wikipedia.org/wiki/Omega_ratio" target="_blank">Omega ratio</a>, a newer and more exotic performance metric that effectively compares the gains of an investment to its losses, relative to some target.</li>
<li><a rel="nofollow" href="https://en.wikipedia.org/wiki/Skewness" target="_blank">Skew</a>, which describes the shape of the distribution of returns; a positive skew indicates a longer tail for gains (that is, more “extreme” values for gains), a negative value a longer tail for losses, and a skew of 0 indicates a symmetric distribution of returns around their mean.<br>
<img alt="Skewness Wikipedia illustration" src="https://ntguardian.files.wordpress.com/2017/06/skewness.png?w=456" data-recalc-dims="1"><br>
<em>An illustration of positive and negative skewness provided by <a rel="nofollow" href="https://en.wikipedia.org/wiki/Skewness" target="_blank">Wikipedia</a>.</em></li>
<li><a rel="nofollow" href="https://en.wikipedia.org/wiki/Kurtosis" target="_blank">Kurtosis</a>, another descriptor of the shape of the distribution of returns; a kurtosis larger than 3 indicates that the distribution is more “spread out” than the <a rel="nofollow" href="https://en.wikipedia.org/wiki/Normal_distributiibutedon" target="_blank">Normal distribution</a> and indicates that extreme values in returns are more common, while a kurtosis smaller than 3 indicates that returns are more concentrated than if they were Normally distributed. Sometimes people describe “excess kurtosis”, where the Normal distribution has an excess kurtosis of zero and other distributions have an excess kurtosis above or below zero (so take kurtosis and subtract 3).<br>
<img alt="Kurtosis Wikipedia illustration" src="https://ntguardian.files.wordpress.com/2017/06/640px-standard_symmetric_pdfs.png?w=456" data-recalc-dims="1"><br>
<em>Visualization of distributions with different kurtosis, provided by <a rel="nofollow" href="https://en.wikipedia.org/wiki/Kurtosis" target="_blank">Wikipedia</a>. Notice that the legend displays excess kurtosis, or kurtosis minus three. All of these distributions have mean zero and variance one, yet still can be more or less spread out around the center than the Normal distribution.</em></li>
<li>Tail ratio, which is the ratio between the 95th percentile of returns and the absolute value of the 5th percentile of returns (so a tail ratio of 0.5 would imply that losses were twice as bad as winnings); the larger this number is, the better extreme winnings are compared to extreme losses.</li>
<li>If the profit ratio is total profits divided by total losses, then the common sense ratio is the profit ratio multiplied with the tail ratio.</li>
<li><a rel="nofollow" href="http://www.investopedia.com/terms/i/informationratio.asp" target="_blank">Information ratio</a>, which is the excess return of the strategy relative to a benchmark index divided by the standard deviation of the difference in returns between the strategy and the benchmark. (Here, since I had to use a constant benchmark return representing the risk-free rate, this number is not working as it should; again, you can send your gratitude to Verizon and tell them you approve of what they’ve done with Yahoo! Finance.)</li>
<li><a rel="nofollow" href="https://en.wikipedia.org/wiki/Alpha_(finance)" target="_blank">Alpha</a>, which is performance relative to risk or some benchmark. Positive alpha is good, negative alpha is bad. (Again, not working right now.)</li>
<li><a rel="nofollow" href="https://en.wikipedia.org/wiki/Beta_(finance)" target="_blank">Beta</a>, the volatility of an asset relative to its benchmark. A beta less than one is good (less volatile than the market), while a beta greater than one is bad (more volatile than the market). (Not currently working.)</li>
<li><a rel="nofollow" href="http://www.investopedia.com/terms/t/turnover.asp" target="_blank">Turnover</a>, or how much of the account is sold in trades; a high turnover rate means higher commissions.</li>
<li>Reaction to stress events, such as the <a rel="nofollow" href="https://en.wikipedia.org/wiki/Fukushima_Daiichi_nuclear_disaster" target="_blank">Fukushima disaster</a>, the <a rel="nofollow" href="https://en.wikipedia.org/wiki/2010_Flash_Crash" target="_blank">2010 Flash Crash</a>, and others.</li>
</ul>
<p>For some reason, <code>create_full_tear_sheet()</code> does not seem to be aware of other symbols being traded than Nvidia (NVDA). I have no idea why.</p>
<p>I create the full tear sheet below.</p>
<pre>
len(returns.index)
</pre>
<pre><code>1720
</code></pre>
<pre>
benchmark_rets = pd.Series([0.00004] * len(returns.index), index=returns.index)    # Risk-free rate, we need to
                                                                                   # set this ourselves otherwise
                                                                                   # PyFolio will try to fetch from
                                                                                   # Yahoo! Finance which is now garbage

# NOTE: Thanks to Yahoo! Finance giving the finger to their users (thanks Verizon, we love you too),
# PyFolio is unstable and will be until updates are made to pandas-datareader, so there may be issues
# using it
pf.create_full_tear_sheet(returns, positions, transactions, benchmark_rets=benchmark_rets)
</pre>
<pre><code>Entire data start date: 2010-01-04
Entire data end date: 2016-10-31


Backtest Months: 81
</code></pre>
<div>
<table border="1">
<thead>
<tr>
<th>Performance statistics</th>
<th>Backtest</th>
</tr>
</thead>
<tbody>
<tr>
<th>annual_return</th>
<td>-0.04</td>
</tr>
<tr>
<th>cum_returns_final</th>
<td>-0.26</td>
</tr>
<tr>
<th>annual_volatility</th>
<td>0.11</td>
</tr>
<tr>
<th>sharpe_ratio</th>
<td>-0.34</td>
</tr>
<tr>
<th>calmar_ratio</th>
<td>-0.11</td>
</tr>
<tr>
<th>stability_of_timeseries</th>
<td>0.72</td>
</tr>
<tr>
<th>max_drawdown</th>
<td>-0.41</td>
</tr>
<tr>
<th>omega_ratio</th>
<td>0.94</td>
</tr>
<tr>
<th>sortino_ratio</th>
<td>-0.46</td>
</tr>
<tr>
<th>skew</th>
<td>-0.27</td>
</tr>
<tr>
<th>kurtosis</th>
<td>3.14</td>
</tr>
<tr>
<th>tail_ratio</th>
<td>0.95</td>
</tr>
<tr>
<th>common_sense_ratio</th>
<td>0.90</td>
</tr>
<tr>
<th>gross_leverage</th>
<td>0.20</td>
</tr>
<tr>
<th>information_ratio</th>
<td>-0.03</td>
</tr>
<tr>
<th>alpha</th>
<td>nan</td>
</tr>
<tr>
<th>beta</th>
<td>nan</td>
</tr>
</tbody>
</table>
</div>
<div>
<table border="1">
<thead>
<tr>
<th>Worst drawdown periods</th>
<th>net drawdown in %</th>
<th>peak date</th>
<th>valley date</th>
<th>recovery date</th>
<th>duration</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>41.30</td>
<td>2011-02-17</td>
<td>2016-01-20</td>
<td>NaT</td>
<td>NaN</td>
</tr>
<tr>
<th>1</th>
<td>17.11</td>
<td>2010-04-15</td>
<td>2010-08-30</td>
<td>2011-01-06</td>
<td>191</td>
</tr>
<tr>
<th>2</th>
<td>2.77</td>
<td>2011-01-14</td>
<td>2011-01-21</td>
<td>2011-01-27</td>
<td>10</td>
</tr>
<tr>
<th>3</th>
<td>2.74</td>
<td>2011-01-27</td>
<td>2011-01-28</td>
<td>2011-02-04</td>
<td>7</td>
</tr>
<tr>
<th>4</th>
<td>1.39</td>
<td>2011-02-04</td>
<td>2011-02-10</td>
<td>2011-02-17</td>
<td>10</td>
</tr>
</tbody>
</table>
</div>
<pre><code>[-0.014 -0.032]
</code></pre>
<p><img alt="png" src="https://ntguardian.files.wordpress.com/2017/06/output_22_4.png?w=456" data-recalc-dims="1"></p>
<div>
<table border="1">
<thead>
<tr>
<th>Stress Events</th>
<th>mean</th>
<th>min</th>
<th>max</th>
</tr>
</thead>
<tbody>
<tr>
<th>US downgrade/European Debt Crisis</th>
<td>-0.25%</td>
<td>-3.53%</td>
<td>2.24%</td>
</tr>
<tr>
<th>Fukushima</th>
<td>-0.05%</td>
<td>-1.83%</td>
<td>1.15%</td>
</tr>
<tr>
<th>EZB IR Event</th>
<td>-0.08%</td>
<td>-1.52%</td>
<td>1.22%</td>
</tr>
<tr>
<th>Flash Crash</th>
<td>-0.19%</td>
<td>-1.16%</td>
<td>1.28%</td>
</tr>
<tr>
<th>Apr14</th>
<td>0.02%</td>
<td>-1.39%</td>
<td>1.49%</td>
</tr>
<tr>
<th>Oct14</th>
<td>-0.13%</td>
<td>-1.40%</td>
<td>0.81%</td>
</tr>
<tr>
<th>Fall2015</th>
<td>-0.08%</td>
<td>-1.85%</td>
<td>2.62%</td>
</tr>
<tr>
<th>Recovery</th>
<td>-0.03%</td>
<td>-3.78%</td>
<td>2.91%</td>
</tr>
<tr>
<th>New Normal</th>
<td>-0.00%</td>
<td>-3.23%</td>
<td>2.62%</td>
</tr>
</tbody>
</table>
</div>
<p><img alt="png" src="https://ntguardian.files.wordpress.com/2017/06/output_22_6.png?w=456" data-recalc-dims="1"></p>
<div>
<table border="1">
<thead>
<tr>
<th>Top 10 long positions of all time</th>
<th>max</th>
</tr>
</thead>
<tbody>
<tr>
<th>NVDA</th>
<td>86.99%</td>
</tr>
</tbody>
</table>
</div>
<div>
<table border="1">
<thead>
<tr>
<th>Top 10 short positions of all time</th>
<th>max</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div>
<table border="1">
<thead>
<tr>
<th>Top 10 positions of all time</th>
<th>max</th>
</tr>
</thead>
<tbody>
<tr>
<th>NVDA</th>
<td>86.99%</td>
</tr>
</tbody>
</table>
</div>
<div>
<table border="1">
<thead>
<tr>
<th>All positions ever held</th>
<th>max</th>
</tr>
</thead>
<tbody>
<tr>
<th>NVDA</th>
<td>86.99%</td>
</tr>
</tbody>
</table>
</div>
<p><img alt="png" src="https://ntguardian.files.wordpress.com/2017/06/output_22_11.png?w=456" data-recalc-dims="1"></p>
<pre><code>/home/curtis/anaconda3/lib/python3.6/site-packages/statsmodels/nonparametric/kdetools.py:20: VisibleDeprecationWarning: using a non-integer number instead of an integer will result in an error in the future
  y = X[:m/2+1] + np.r_[0,X[m/2+1:],0]*1j
</code></pre>
<p><img alt="png" src="https://ntguardian.files.wordpress.com/2017/06/output_22_13.png?w=456" data-recalc-dims="1"></p>
<h2>Optimizing with Different Metrics</h2>
<p>Instead of optimizing with respect to returns, we may want to optimize with respect to a risk-adjusted metric, such as the Sharpe ratio. We can investigate the results of using different metrics for optimization with walk-forward analysis.</p>
<p>In walk-forward analysis we might add an additional metric to our arsenal, the walk-forward efficiency ratio (WFER). This ratio gives a sens of our system’s out-of-sample performance. If we were optimizing with respect to returns, we’d divide out-of-sample returns by the returns obtained by the optimized strategy. If this is greater than 1, our out-of-sample returns were higher than optimized returns, while if this is less than 1, out-of-sample returns are less than optimized returns. This can be applied to any metric we use for optimization, and gives a sense for how much our strategy tends to overfit. A low WFER suggests a high propensity to overfit. (Unfortunately, my code for WFER doesn’t seem to be correct right now.)</p>
<p>Below I create a function to automate the walk-forward analysis process. You pass the function a Cerebro object with everything added to it except the strategy and the data; these are passed as arguments to the function and will be handled by it. (You can only work with one strategy at a time.) All analyzers are combined together into one dictionary at the end of one iteration, and all of these dictionaries are returned in a list. If the dictionaries are all flat, this list can easily be converted to a <strong>pandas</strong> <code>DataFrame</code>.</p>
<pre>
def wfa(cerebro, strategy, opt_param, split, datafeeds, analyzer_max, var_maximize,
        opt_p_vals, opt_p_vals_args={}, params={}, minimize=False):
    &quot;&quot;&quot;Perform a walk-forward analysis

    args:
        cerebro: A Cerebro object, with everything added except the strategy and data feeds; that is, all
            analyzers, sizers, starting balance, etc. needed have been added
        strategy: A Strategy object for optimizing
        params: A dict that contains all parameters to be set for the Strategy except the parameter to be
            optimized
        split: Defines the splitting of the data into training and test sets, perhaps created by
            TimeSeriesSplit.split()
        datafeeds: A dict containing pandas DataFrames that are the data feeds to be added to Cerebro
            objects and used by the strategy; keys are the names of the strategies
        analyzer_max: A string denoting the name of the analyzer with the statistic to maximize
        var_maximize: A string denoting the variable to maximize (perhaps the key of a dict)
        opt_param: A string: the parameter of the strategy being optimized
        opt_p_vals: This can be either a list or function; if a function, it must return a list, and the
            list will contain possible values that opt_param will take in optimization; if a list, it already
            contains these values
        opt_p_vals_args: A dict of parameters to pass to opt_p_vals; ignored if opt_p_vals is not a function
        minimize: A boolean that if True will minimize the parameter rather than maximize

    return:
        A list of dicts that contain data for the walk-forward analysis, including the relevant
        analyzer's data, the value of the optimized statistic on the training set, start and end dates for
        the test set, and the walk forward efficiency ratio (WFER)
    &quot;&quot;&quot;

    usr_opt_p_vals = opt_p_vals
    walk_forward_results = list()
    for train, test in split:
        trainer, tester = deepcopy(cerebro), deepcopy(cerebro)

        if callable(usr_opt_p_vals):
            opt_p_vals = usr_opt_p_vals(**opt_p_vals_args)

        # TRAINING
        trainer.optstrategy(strategy, **params, **{opt_param: opt_p_vals})
        for s, df in datafeeds.items():
            data = bt.feeds.PandasData(dataname=df.iloc[train], name=s)
            trainer.adddata(data)
        res = trainer.run()
        res_df = DataFrame({getattr(r[0].params, opt_param): dict(getattr(r[0].analyzers,
                                                                          analyzer_max).get_analysis()) for r in res}
                       ).T.loc[:, var_maximize].sort_values(ascending=minimize)
        opt_res, opt_val = res_df.index[0], res_df[0]

        # TESTING
        tester.addstrategy(strategy, **params, **{opt_param: opt_res})
        for s, df in datafeeds.items():
            data = bt.feeds.PandasData(dataname=df.iloc[test], name=s)
            tester.adddata(data)
        res = tester.run()
        res_dict = dict(getattr(res[0].analyzers, analyzer_max).get_analysis())
        res_dict[&quot;train_&quot; + var_maximize] = opt_val
        res_dict[opt_param] = opt_res
        s0 = [*datafeeds.keys()][0]
        res_dict[&quot;start_date&quot;] = datafeeds[s0].iloc[test[0]].name
        res_dict[&quot;end_date&quot;] = datafeeds[s0].iloc[test[-1]].name
        test_val = getattr(res[0].analyzers, analyzer_max).get_analysis()[var_maximize]
        try:
            res_dict[&quot;WFER&quot;] = test_val / opt_val
        except:
            res_dict[&quot;WFER&quot;] = np.nan
        for anlz in res[0].analyzers:
            res_dict.update(dict(anlz.get_analysis()))

        walk_forward_results.append(res_dict)

    return walk_forward_results
</pre>
<p>The function can be given a list of possible choices of parameters to optimize over, but can also accept a function that generates this list (perhaps you are choosing possible parameter values randomly). I create a generator function below, that will be used in the walk-forward analysis.</p>
<pre>
def random_fs_list():
    &quot;&quot;&quot;Generate random combinations of fast and slow window lengths to test&quot;&quot;&quot;
    windowset = set()    # Use a set to avoid duplicates
    while len(windowset) &lt; 40:
        f = random.randint(1, 10) * 5
        s = random.randint(1, 10) * 10
        if f > s:    # Cannot have the fast moving average have a longer window than the slow, so swap
            f, s = s, f
        elif f == s:    # Cannot be equal, so do nothing, discarding results
            continue
        windowset.add((f, s))

    return list(windowset)
</pre>
<p>Now let’s get the walk-forward analysis results when optimizing with respect to the Sharpe ratio.</p>
<pre>
walkorebro = bt.Cerebro(stdstats=False, maxcpus=1)

walkorebro.broker.setcash(1000000)
walkorebro.broker.setcommission(0.02)
walkorebro.addanalyzer(btanal.SharpeRatio_A)
walkorebro.addanalyzer(btanal.Returns)
walkorebro.addanalyzer(AcctStats)
walkorebro.addsizer(PropSizer)

tscv = TimeSeriesSplitImproved(10)
split = tscv.split(datafeeds[&quot;AAPL&quot;], fixed_length=True, train_splits=2)
</pre>
<pre>
%time wfa_sharpe_res = wfa(walkorebro, SMAC, opt_param=&quot;optim_fs&quot;, params={&quot;optim&quot;: True}, \
                           split=split, datafeeds=datafeeds, analyzer_max=&quot;sharperatio_a&quot;, \
                           var_maximize=&quot;sharperatio&quot;, opt_p_vals=random_fs_list)
</pre>
<pre><code>CPU times: user 2h 7min 13s, sys: 2.55 s, total: 2h 7min 15s
Wall time: 2h 8min 1s
</code></pre>
<pre>
DataFrame(wfa_sharpe_res)
</pre>
<div>
<table border="1">
<thead>
<tr>
<th></th>
<th>WFER</th>
<th>end</th>
<th>end_date</th>
<th>growth</th>
<th>optim_fs</th>
<th>ravg</th>
<th>return</th>
<th>rnorm</th>
<th>rnorm100</th>
<th>rtot</th>
<th>sharperatio</th>
<th>start</th>
<th>start_date</th>
<th>train_sharperatio</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>NaN</td>
<td>953091.50</td>
<td>2011-11-14</td>
<td>-46908.50</td>
<td>(45, 70)</td>
<td>-0.000306</td>
<td>0.953091</td>
<td>-0.074217</td>
<td>-7.421736</td>
<td>-0.048044</td>
<td>NaN</td>
<td>1000000</td>
<td>2011-04-05</td>
<td>3.377805</td>
</tr>
<tr>
<th>1</th>
<td>1.274973</td>
<td>892315.80</td>
<td>2012-06-28</td>
<td>-107684.20</td>
<td>(10, 30)</td>
<td>-0.000726</td>
<td>0.892316</td>
<td>-0.167129</td>
<td>-16.712927</td>
<td>-0.113935</td>
<td>-1.237580</td>
<td>1000000</td>
<td>2011-11-15</td>
<td>-0.970672</td>
</tr>
<tr>
<th>2</th>
<td>1.195509</td>
<td>1007443.84</td>
<td>2013-02-13</td>
<td>7443.84</td>
<td>(15, 90)</td>
<td>0.000047</td>
<td>1.007444</td>
<td>0.011975</td>
<td>1.197496</td>
<td>0.007416</td>
<td>-0.527568</td>
<td>1000000</td>
<td>2012-06-29</td>
<td>-0.441292</td>
</tr>
<tr>
<th>3</th>
<td>NaN</td>
<td>1008344.50</td>
<td>2013-09-26</td>
<td>8344.50</td>
<td>(15, 70)</td>
<td>0.000053</td>
<td>1.008345</td>
<td>0.013427</td>
<td>1.342750</td>
<td>0.008310</td>
<td>NaN</td>
<td>1000000</td>
<td>2013-02-14</td>
<td>0.480783</td>
</tr>
<tr>
<th>4</th>
<td>-3.030306</td>
<td>991597.32</td>
<td>2014-05-12</td>
<td>-8402.68</td>
<td>(40, 70)</td>
<td>-0.000054</td>
<td>0.991597</td>
<td>-0.013453</td>
<td>-1.345278</td>
<td>-0.008438</td>
<td>-3.380193</td>
<td>1000000</td>
<td>2013-09-27</td>
<td>1.115463</td>
</tr>
<tr>
<th>5</th>
<td>NaN</td>
<td>960946.16</td>
<td>2014-12-22</td>
<td>-39053.84</td>
<td>(20, 70)</td>
<td>-0.000254</td>
<td>0.960946</td>
<td>-0.061941</td>
<td>-6.194062</td>
<td>-0.039837</td>
<td>NaN</td>
<td>1000000</td>
<td>2014-05-13</td>
<td>0.636099</td>
</tr>
<tr>
<th>6</th>
<td>-4.286899</td>
<td>966705.62</td>
<td>2015-08-06</td>
<td>-33294.38</td>
<td>(20, 80)</td>
<td>-0.000216</td>
<td>0.966706</td>
<td>-0.052900</td>
<td>-5.289996</td>
<td>-0.033861</td>
<td>-1.600702</td>
<td>1000000</td>
<td>2014-12-23</td>
<td>0.373394</td>
</tr>
<tr>
<th>7</th>
<td>0.293385</td>
<td>1011624.06</td>
<td>2016-03-21</td>
<td>11624.06</td>
<td>(25, 90)</td>
<td>0.000074</td>
<td>1.011624</td>
<td>0.018723</td>
<td>1.872324</td>
<td>0.011557</td>
<td>-0.623374</td>
<td>1000000</td>
<td>2015-08-07</td>
<td>-2.124761</td>
</tr>
<tr>
<th>8</th>
<td>NaN</td>
<td>994889.98</td>
<td>2016-10-31</td>
<td>-5110.02</td>
<td>(40, 100)</td>
<td>-0.000033</td>
<td>0.994890</td>
<td>-0.008189</td>
<td>-0.818938</td>
<td>-0.005123</td>
<td>NaN</td>
<td>1000000</td>
<td>2016-03-22</td>
<td>-0.462381</td>
</tr>
</tbody>
</table>
</div>
<p>Returns are arguable better here than when I optimized with respect to just returns since a catastrophically bad strategy was avoided, but I’m not convinced we’re doing any better.</p>
<p>Let’s try with the Sortino ratio now. I have a few reasons for favoring the Sortino ratio as a risk measure:</p>
<ol>
<li>The Sortino ratio uses the standard deviation of losses, which we might call “bad risk”.</li>
<li>Measures reliant on “standard deviaton” implicitly assume Normally distributed returns or that returns are well-behaved in some sense, but if returns follow, say, a <a rel="nofollow" href="https://en.wikipedia.org/wiki/Power_law" target="_blank">power law</a>, where “extreme events are common”, the standard deviation is not a good measure for risk. Theoretically, the standard deviation may not be well-defined. Since losses are truncated, though (meaning that there is a maximum loss that can be attained), standard deviation for losses should exist theoretically, so we can avoid some these issues. Furthermore, extreme events should make this standard deviation larger rather than smaller (since standard deviation is sensitive to outliers), which makes the overall ratio smaller and thus more conservative in the presence of outlier losses.</li>
</ol>
<p>Let’s see the results when using the Sortino ratio in place of the Sharpe ratio.</p>
<pre>
walkorebro.addanalyzer(SortinoRatio)

split = tscv.split(datafeeds[&quot;AAPL&quot;], fixed_length=True, train_splits=2)
</pre>
<pre>
%time wfa_sortino_res = wfa(walkorebro, SMAC, opt_param=&quot;optim_fs&quot;, params={&quot;optim&quot;: True}, \
                            split=split, datafeeds=datafeeds, analyzer_max=&quot;sortinoratio&quot;, \
                            var_maximize=&quot;sortinoratio&quot;, opt_p_vals=random_fs_list)
</pre>
<pre><code>/home/curtis/anaconda3/lib/python3.6/site-packages/ipykernel/__main__.py:50: RuntimeWarning: invalid value encountered in log


CPU times: user 2h 7min 33s, sys: 9.39 s, total: 2h 7min 43s
Wall time: 2h 8min 38s
</code></pre>
<pre>
DataFrame(wfa_sortino_res)
</pre>
<div>
<table border="1">
<thead>
<tr>
<th></th>
<th>WFER</th>
<th>end</th>
<th>end_date</th>
<th>growth</th>
<th>optim_fs</th>
<th>ravg</th>
<th>return</th>
<th>rnorm</th>
<th>rnorm100</th>
<th>rtot</th>
<th>sharperatio</th>
<th>sortinoratio</th>
<th>start</th>
<th>start_date</th>
<th>train_sortinoratio</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>-0.285042</td>
<td>988750.90</td>
<td>2011-11-14</td>
<td>-11249.10</td>
<td>(45, 90)</td>
<td>-0.000072</td>
<td>0.988751</td>
<td>-0.017994</td>
<td>-1.799434</td>
<td>-0.011313</td>
<td>NaN</td>
<td>-0.033131</td>
<td>1000000</td>
<td>2011-04-05</td>
<td>0.116231</td>
</tr>
<tr>
<th>1</th>
<td>-6.620045</td>
<td>1007201.00</td>
<td>2012-06-28</td>
<td>7201.00</td>
<td>(50, 100)</td>
<td>0.000046</td>
<td>1.007201</td>
<td>0.011583</td>
<td>1.158345</td>
<td>0.007175</td>
<td>-1.777392</td>
<td>0.097353</td>
<td>1000000</td>
<td>2011-11-15</td>
<td>-0.014706</td>
</tr>
<tr>
<th>2</th>
<td>3.543051</td>
<td>1007443.84</td>
<td>2013-02-13</td>
<td>7443.84</td>
<td>(15, 90)</td>
<td>0.000047</td>
<td>1.007444</td>
<td>0.011975</td>
<td>1.197496</td>
<td>0.007416</td>
<td>-0.527568</td>
<td>0.038794</td>
<td>1000000</td>
<td>2012-06-29</td>
<td>0.010949</td>
</tr>
<tr>
<th>3</th>
<td>0.861968</td>
<td>1008344.50</td>
<td>2013-09-26</td>
<td>8344.50</td>
<td>(15, 70)</td>
<td>0.000053</td>
<td>1.008345</td>
<td>0.013427</td>
<td>1.342750</td>
<td>0.008310</td>
<td>NaN</td>
<td>0.042481</td>
<td>1000000</td>
<td>2013-02-14</td>
<td>0.049283</td>
</tr>
<tr>
<th>4</th>
<td>-0.051777</td>
<td>998959.50</td>
<td>2014-05-12</td>
<td>-1040.50</td>
<td>(35, 70)</td>
<td>-0.000007</td>
<td>0.998960</td>
<td>-0.001670</td>
<td>-0.166958</td>
<td>-0.001041</td>
<td>-20.221528</td>
<td>-0.006508</td>
<td>1000000</td>
<td>2013-09-27</td>
<td>0.125685</td>
</tr>
<tr>
<th>5</th>
<td>-2.029468</td>
<td>972178.20</td>
<td>2014-12-22</td>
<td>-27821.80</td>
<td>(30, 80)</td>
<td>-0.000180</td>
<td>0.972178</td>
<td>-0.044279</td>
<td>-4.427936</td>
<td>-0.028216</td>
<td>NaN</td>
<td>-0.157309</td>
<td>1000000</td>
<td>2014-05-13</td>
<td>0.077512</td>
</tr>
<tr>
<th>6</th>
<td>-4.718336</td>
<td>976411.72</td>
<td>2015-08-06</td>
<td>-23588.28</td>
<td>(45, 80)</td>
<td>-0.000152</td>
<td>0.976412</td>
<td>-0.037590</td>
<td>-3.759040</td>
<td>-0.023871</td>
<td>-1.847879</td>
<td>-0.130495</td>
<td>1000000</td>
<td>2014-12-23</td>
<td>0.027657</td>
</tr>
<tr>
<th>7</th>
<td>-8.761974</td>
<td>1012292.72</td>
<td>2016-03-21</td>
<td>12292.72</td>
<td>(30, 90)</td>
<td>0.000078</td>
<td>1.012293</td>
<td>0.019804</td>
<td>1.980425</td>
<td>0.012218</td>
<td>-0.441576</td>
<td>0.176737</td>
<td>1000000</td>
<td>2015-08-07</td>
<td>-0.020171</td>
</tr>
<tr>
<th>8</th>
<td>NaN</td>
<td>1000000.00</td>
<td>2016-10-31</td>
<td>0.00</td>
<td>(20, 100)</td>
<td>0.000000</td>
<td>1.000000</td>
<td>0.000000</td>
<td>0.000000</td>
<td>0.000000</td>
<td>NaN</td>
<td>NaN</td>
<td>1000000</td>
<td>2016-03-22</td>
<td>-0.017480</td>
</tr>
</tbody>
</table>
</div>
<p>This is better, but far from impressive. These analyses reinforce my opinion that the SMAC strategy does not work for the symbols I’ve considered. I would be better off abandoning this line of investigatioin and looking elsewhere.</p>
<h2>Conclusion, and Thoughts On Blogging</h2>
<p>Up to this point, I have been interested mostly in investigating backtesting technology. I wanted to see which packages would allow me to do what I feel I should be able to do with a backtesting system. If I were to move on from this point, I would be looking at identifying different trading systems, not just “how to optimize”, “how to do a walk-forward analysis”, or “how to get the metrics I want from a backtest.”</p>
<p>Now to drift off-topic.</p>
<p>I’ve enjoyed blogging on these topics, but I’ve run into a problem. See, I’m still a graduate student, hoping to get a PhD. I started this blog as a hobby, but these days I have been investing a lot of time into writing these blog posts, which have been technically challenging for me. I spend days researching, writing, and debugging code. I write new blog posts, investing a lot of time in them. I personally grow a lot. Then I look at my website’s stats, and see that despite all the new content I write, perhaps 90% of all the traffic to this blog goes to <a rel="nofollow" href="https://ntguardian.wordpress.com/2016/09/19/introduction-stock-market-data-python-1/" target="_blank">three</a> <a rel="nofollow" href="https://ntguardian.wordpress.com/2017/03/27/introduction-stock-market-data-r-1/" target="_blank">blog</a> <a rel="nofollow" href="https://ntguardian.wordpress.com/2016/09/26/introduction-stock-market-data-python-2/" target="_blank">posts</a>. Despite the fact that I believe I’ve written content that is more interesting than these posts, that content gets hardly any traffic.</p>
<p>I recently read a <a rel="nofollow" href="https://www.quora.com/I-enjoy-reading-scientific-papers-and-gaining-knowledge-in-many-areas-different-fields-Would-getting-a-Ph-D-with-the-aim-of-going-into-the-industry-afterwards-be-a-good-decision/answer/Ben-Y-Zhao?srid=hZycf" target="_blank">response to a Quora question, written by Prof. Ben Y. Zhao at the University of Chicago</a> that rattled me. In his answer, Prof. Zhao says:</p>
<blockquote><p>
I might also add that “gaining knowledge in many areas/fields” is likely to be detrimental to getting a PhD. Enjoying many different fields generally translates into a lack of focus in the context of a PhD program, and you’re likely to spend more time marveling over papers from a wide range of topics rather than focusing and digging deep to produce your own research contributions.
</p>
</blockquote>
<p>As a person who has a lot of interests, I’m bothered by this comment. A lot.</p>
<p>This blog has lead to some book contracts for me. I have one <a rel="nofollow" href="https://www.packtpub.com/big-data-and-business-intelligence/unpacking-numpy-and-pandas-video" target="_blank">video course introducing <strong>NumPy</strong> and <strong>pandas</strong></a> coming out in a few days, published by Packt Publishing (I will write a post about it not long after it’s out). This was a massive time commitment on my part, more than I thought it would be (I am always significantly underestimating how much time projects will take). When I initially told one of my former professors at the University of Utah that I had been offered a contract to create some video courses, he became upset, almost yelling at me, criticizing me for not “guarding your time.” (My professor directed me to another member of the deparment who had written books in hope that he would disuade me by telling me what the process was like; instead he recommended that I leverage the book deal I got into other book deals, which is the opposite of what my professor intended.)</p>
<p>I couldn’t resist the allure of being published so I agreed to the deal and signed the contracts, but since then, I’ve begun wishing I listened to my professor’s advice. I have two qualifying exams coming up in August, and I don’t feel like I have done nearly enough to prepare for them, partly due to the book deal, and partly due to the time I spend writing blog posts (and I also read too much). Furthermore, I have committed to three more volumes and I don’t want to break a contract; I agreed to do something and I intend to see it to the end. Meanwhile, other publishers have offered me publishing deals and I’m greatly pained whenever I say “no”; I like creating content like this, I like the idea of having a portfolio of published works providing a source of prestige and passive income (and with my rent having increased by over \$100 in two years when I could barely afford it in the first place, I could really use that extra income right now).</p>
<p>Long story short, time is an even scarcer commodity now than it used to be for me. I’m worried about my academic success if I keep burning time like this. It also doesn’t seem like any of my new content “sticks” (it doesn’t bump up what you might call my “latent” viewership rate).</p>
<p>Thus, I’m planning on this being my last “long-form” article for some time. I’m slightly heartbroken to say this. I would love to write more articles about agent-based modelling (particularly the Santa Fe Artificial Stock Market or developing a model for the evolution of party systems in republics), finance and algorithmic trading, statistical models for voting in legislatures (like DW-NOMINATE), and many others. I have notebooks filled with article ideas. I cannot afford the time, though.</p>
<p>I may post occasionally, perhaps with a few code snippets or to announce my video courses. I won’t try to keep a regular schedule, though, and I won’t be writing specifically for the site (that is, something in my studies prompted the topic).</p>
<p>For those reading this article, I would still love to hear your comments on algorithmic trading and if there are areas you are curious about. I’d likely put those into my notebook of ideas to investigate and perhaps I will look into them. I’ve learned a lot maintaining this website and I hope eventually to return to writing weekly long-form articles on topics I’m interested in again someday. Algorithmic trading still intreagues me and I want to play with it from time to time.</p>
<p>Thank you all for reading. When I started blogging, I was hoping to get maybe 70 views in a week. I never imagined I’d be getting at least 400 every day (usually around 500 or 600, sometimes 1,000 on the day of a new post). This took me a lot farther than I thought I would go, and it’s been a great trip in the meantime.</p>
<hr>
<p>I have created a video course that <a rel="nofollow" href="http://packtpub.com/" target="_blank">Packt Publishing</a> will be publishing later this month, entitled <a rel="nofollow" href="https://ntguardian.wordpress.com/video-courses/unpacking-numpy-and-pandas/" target="_blank"><em>Unpacking NumPy and Pandas</em></a>, the first volume in a four-volume set of video courses entitled, <em>Taming Data with Python; Excelling as a Data Analyst</em>. This course covers the basics of setting up a Python environment for data analysis with <a rel="nofollow" href="http://www.continuum.io/" target="_blank">Anaconda</a>, using Jupyter notebooks, and using <strong>NumPy</strong> and <strong>pandas</strong>. If you are starting out using Python for data analysis or know someone who is, please consider buying my course or at least spreading the word about it. You can <a rel="nofollow" href="https://www.packtpub.com/big-data-and-business-intelligence/unpacking-numpy-and-pandas-video" target="_blank">buy the course directly</a> or <a rel="nofollow" href="https://www.packtpub.com/mapt/" target="_blank">purchase a subscription to Mapt</a> and watch it there (when it becomes available).</p>
<p>If you like my blog and would like to support it, spread the word (if not get a copy yourself)! Also, stay tuned for future courses I publish with Packt at the <a rel="nofollow" href="https://ntguardian.wordpress.com/video-courses/" target="_blank">Video Courses</a> section of my site.</p>
<p> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/ntguardian.wordpress.com/3081/" target="_blank"><img border="0" alt="" src="http://feeds.wordpress.com/1.0/comments/ntguardian.wordpress.com/3081/"></a> <img border="0" alt="" src="https://i1.wp.com/pixel.wp.com/b.gif?resize=1%2C1&ssl=1" data-recalc-dims="1"></p>
	<p>
<div>
<div>To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://ntguardian.wordpress.com/2017/06/28/stock-trading-analytics-and-optimization-in-python-with-pyfolio-rs-performanceanalytics-and-backtrader/"> R – Curtis Miller's Personal Website</a></strong>.</div>
<hr>
<a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers.com</a> offers <strong><a rel="nofollow" href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers">daily e-mail updates</a></strong> about <a rel="nofollow" href="https://www.r-project.org/" title="The R Project for Statistical Computing">R</a> news and <a rel="nofollow" href="https://www.r-bloggers.com/search/tutorial" title="R tutorials">tutorials</a> on topics such as: <a rel="nofollow" href="https://www.r-bloggers.com/search/data%20science" title="Data science">Data science</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Big%20Data" title="Big Data">Big Data, <a rel="nofollow" href="https://www.r-users.com/" title="R jobs">R jobs</a>, visualization (<a rel="nofollow" href="https://www.r-bloggers.com/search/ggplot2" title="ggplot and ggplot2 tutorials">ggplot2</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/boxplot" title="Boxplots using lattice and ggplot2 tutorials">Boxplots</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/map" title="Maps and gis">maps</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/animation" title="Animation in R">animation</a>), programming (<a rel="nofollow" href="https://www.r-bloggers.com/search/RStudio" title="RStudio IDE for R">RStudio</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/sweave" title="Sweave and literate programming">Sweave</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/LaTeX" title="LaTeX in R">LaTeX</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/SQL" title="SQL and databases">SQL</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/eclipse" title="Eclipse IDE for R">Eclipse</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/git" title="git and github, Version Control System">git</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/hadoop" title="Large data in R using Hadoop">hadoop</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Web+Scraping" title="Web Scraping of google, facebook, yahoo, twitter and more using R">Web Scraping</a>) statistics (<a rel="nofollow" href="https://www.r-bloggers.com/search/regression" title="Regressions and ANOVA analysis tutorials">regression</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/PCA" title="principal component analysis tutorial">PCA</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/time+series" title="Time series">time series</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/trading" title="finance trading">trading</a>) and more...
<img width="1" alt="" src="http://feeds.feedburner.com/~r/RBloggers/~4/ARyZpar1GSE" height="1"></a></div>