# -*-mode: python; py-indent-offset: 4; indent-tabs-mode: nil; encoding: utf-8-dos; coding: utf-8 -*-

"""
Backtest recipes with chefs, and serve the results as metrics and plots.

The subcommands are:
* omlette   - An omlette is an HDF5 file that saves all the data from a backtest
* feed      - Create feeds (pandas DataFrames) from CSV OHLCV files
* recipe    - Set the recipe that the chef will use, and make the ingredients from the feeds
* chef      - Set the chef that we will use, and cook from the ingredients and the feeds
* servings  - List the servings the chef has cooked, and dish out the servings
* plot      - Plot the servings the chef has cooked, using matplotlib
"""

SDOC = __doc__

import sys
import os
import traceback
from optparse import make_option
from pprint import pprint, pformat

try:
    from OpenTrader.deps import tabview
except ImportError:
    # depends on curses
    tabview = None

from OpenTrader.OTUtils import sStripCreole, lConfigToList
from OpenTrader.doer import Doer

sCURRENT_OMLETTE_DIR = ""

LOPTIONS = [make_option("-C", "--chef",
                          dest="sChef",
                          # no default here - we want it to come from the ini
                          default="",
                          help="the backtest package (one of: PybacktestChef)"),
              make_option("-R", "--recipe",
                          dest="sRecipe",
                          # no default here - we want it to come from the ini
                          default="",
                          help="recipe to backtest (one of SMARecipe"),
              make_option("-H", "--history_dir",
                          dest="sHistoryDir",
                          # no default here
                          default="",
                          help="directory for creating Create feeds from CSV OHLCV files")
              ]

LCOMMANDS = []

#? feed rename delete

dFEED_CACHE = {}
sFEED_CACHE_KEY = ""

def oEnsureOmlette(ocmd2, _oValues, sNewOmlette=""):
    from OpenTrader.Omlettes import Omlette
    if not sNewOmlette and hasattr(ocmd2, 'oOm') and ocmd2.oOm:
        return ocmd2.oOm
    oOm = Omlette.Omlette(oFd=sys.stdout)
    if sNewOmlette:
        # The default is no HDF file - it's not in ocmd2.oOptions.sOmlette
        oOm.oAddHdfStore(sNewOmlette)

    ocmd2.oOm = oOm
    return oOm

def oEnsureRecipe(ocmd2, oValues, sNewRecipe=""):
    oOm = oEnsureOmlette(ocmd2, oValues)
    if not sNewRecipe and hasattr(oOm, 'oRecipe') and oOm.oRecipe:
        return oOm.oRecipe
    if not sNewRecipe:
        # The are set earlier in the call to do_back
        sNewRecipe = ocmd2.sRecipe
    if hasattr(oOm, 'oRecipe') and oOm.oRecipe:
        sOldName = oOm.oRecipe.sName
    else:
        sOldName = ""
    oRecipe = oOm.oAddRecipe(sNewRecipe)
    if sOldName and sOldName != sNewRecipe:
        #? do we invalidate the current servings if the recipe changed?
        vClearOven(ocmd2, oValues)
    return oRecipe

def oEnsureChef(ocmd2, oValues, sNewChef=""):
    oOm = oEnsureOmlette(ocmd2, oValues)
    if not sNewChef and hasattr(oOm, 'oChefModule') and oOm.oChefModule:
        return oOm.oChefModule
    if not sNewChef:
        # The are set earlier in the call to do_back
        sNewChef = ocmd2.sChef
    if hasattr(oOm, 'oChefModule') and oOm.oChefModule and oOm.oChefModule.sChef:
        sOldName = oOm.oChefModule.sChef
    else:
        sOldName = ""
    oChefModule = oOm.oAddChef(sNewChef)
    if sOldName and sOldName != sNewChef:
        #? do we invalidate the current servings if the chef changed?
        vClearOven(ocmd2, oValues)
    return oOm.oChefModule

def vClearOven(ocmd2, oValues):
    oOm = oEnsureOmlette(ocmd2, oValues)
    oOm.oBt = None

class DoBacktest(Doer):
    __doc__ = SDOC
    # putting this as a module variable backtest it available
    # before an instance has been instantiated.
    global LCOMMANDS

    dhelp = {'': __doc__}

    def __init__(self, ocmd2):
        Doer.__init__(self, ocmd2, 'backtest')
        self._G = self.ocmd2._G

    LCOMMANDS += ['omlette']
    def backtest_omlette(self):
        """backtest omlette

An omlette is an HDF5 file that saves all the information from a backtest,
including the metadata: all of parameter values that were used in the recipe,
the parameters used by the cook, and the servings results.

You should open an omlette before you backtest giving it a filename,
and close it after the 'chef cook' and 'servings'.
{{{
back omlette open FILE           - open an HDF file to save all the backtest parts
back omlette check               - show the current omlette filename
back omlette display             - display the current omlette HDF sections
back omlette close               - close the HDF file saving the omlette
}}}
Real Soon Now you will be able to enjoy them more by reloading previously saved
omlettes, plotting the data or the results, and adding or editing comments.
        """
        sDo = 'omlette'
        #!WTF local variable '__doc__' referenced before assignment
        self.dhelp['omlette'] = __doc__

        # is this a backtest command or should it move up
        # leave it here for now, and move it up later

        global sCURRENT_OMLETTE_DIR
        # ingredients recipe dish
        # plot sSection
        #
        lArgs = self.lArgs
        _lCmds = ['load', 'open', 'check', 'save', 'close', 'display']
        assert len(lArgs) > 1, "ERROR: " +sDo +" " +str(_lCmds)
        sCmd = lArgs[1]
        assert sCmd in _lCmds, "ERROR: " +sDo +" " +str(_lCmds)

        oValues = self.oValues
        oOm = oEnsureOmlette(self.ocmd2, oValues, sNewOmlette="")
        if sCmd == 'check':
            assert hasattr(oOm, 'oHdfStore')
            assert oOm.oHdfStore is not None
            # FixMe: something better than filename
            self.poutput(sDo +" filename: " +oOm.oHdfStore.filename)
            return

        if sCmd == 'display':
            # display gives a complete listing of the contents of the HDF file
            assert hasattr(oOm, 'oHdfStore') and oOm.oHdfStore
            self.poutput(repr(oOm.oHdfStore))
            return

        ## if sCmd == 'save':
        ##     return

        if sCmd == 'close':
            assert oOm.oHdfStore is not None, \
                   "ERROR: " +sDo +" " +sCmd +"; not open: use '" +sDo +" open FILE'"
            oOm.vClose()
            return

        assert len(lArgs) >= 3, \
               "ERROR: " +sDo +" " +sCmd +" " +" FILENAME"
        sFile = lArgs[2]

        if sCmd == 'open':
            o = oOm.oAddHdfStore(sFile)
            assert o is not None and oOm.oHdfStore is not None
            sCURRENT_OMLETTE_DIR = os.path.dirname(sFile)
            return

        if sCmd == 'load':
            assert os.path.isfile(sFile), \
                   "ERROR: " +sDo +" " +sCmd +" file not found " +sFile
            sCURRENT_OMLETTE_DIR = os.path.dirname(sFile)
            raise RuntimeError(NotImplemented)
            return

        self.vError("Unrecognized omlette command: " + str(lArgs) +'\n' +__doc__)
        return

    # Create feeds (pandas DataFrames) from CSV OHLCV files
    LCOMMANDS += ['feed']
    def backtest_feed(self):
        """
==== OTCmd2 backtest feed

Create feeds (pandas DataFrames) from CSV OHLCV files.
{{{
back feed dir                                  - NotImplemented
back feed dir dirname                          - NotImplemented

back feed read_mt4_csv SYMBOL TIMEFRAME [YEAR] - read a CSV file from Mt4 into pandas
back feed read_yahoo_csv SYMBOL [STARTYEAR]    - read a Yahoo internet feed into pandas
back feed list                                 - list the feeds we have read
back feed get                                  - get the key name of the current feed
back feed info                                 - concise summary of the DataFrame
back feed plot                                 - plot the CSV data using OTPpnAmgc
               This plots the feed, with SMA, RSIs and MACDs, using matplotlib.
}}}
        """
        global dFEED_CACHE
        global sFEED_CACHE_KEY
        sDo = 'feed'
        #!WTF local variable '__doc__' referenced before assignment
        self.dhelp['feed'] = __doc__
        lArgs = self.lArgs
        oValues = self.oValues

        #? rename delete
        _lCmds = ['dir', 'list', 'get', 'set',
                  'read_mt4_csv', 'read_yahoo_csv', 'info', 'plot', 'to_hdf']
        assert len(lArgs) > 1, "ERROR: " +sDo +" command required: " +str(_lCmds)
        sCmd = lArgs[1]
        assert lArgs[1] in _lCmds, "ERROR: " +sDo +" " +str(_lCmds)

        if sCmd == 'dir':
            if len(lArgs) == 2:
                if not self.ocmd2.oConfig['feed']['sHistoryDir']:
                    self.poutput("No default history directory set: use \"feed dir dir\"")
                elif not os.path.isdir(self.ocmd2.oConfig['feed']['sHistoryDir']):
                    self.poutput("History directory not found: use \"feed dir dir\": " + \
                                 self.ocmd2.oConfig['feed']['sHistoryDir'])
                else:
                    self.poutput("Default history directory: " + \
                                 self.ocmd2.oConfig['feed']['sHistoryDir'])
                return

            sDir = lArgs[2]
            assert os.path.isdir(sDir)
            self.ocmd2.oConfig['feed']['sHistoryDir'] = sDir
            return

        if sCmd == 'read_mt4_csv':
            from OpenTrader.PandasMt4 import oReadMt4Csv
            assert len(lArgs) >= 3, \
                   "ERROR: " +sDo +" " +sCmd +" FILENAME [SYMBOL TIMEFRAME YEAR]"
            sFile = lArgs[2]
            if not os.path.isabs(sFile):
               sFile = os.path.join(self.ocmd2.sRoot, sFile)
            if False and not os.path.isfile(sFile):
                #? was self.ocmd2.oOptions.sMt4Dir
                sHistoryDir = os.path.join(self.ocmd2.oConfig['OTCmd2']['sMt4Dir'], 'history')
                if os.path.isdir(sHistoryDir):
                    import glob
                    l = glob.glob(sHistoryDir +"/*/" +sFile)
                    if l:
                        sFile = l[0]
                        self.vInfo("Found history file: " + sFile)
            assert os.path.isfile(sFile), \
                   "ERROR: " +sDo +" " +sCmd +" file not found " +sFile
            if len(lArgs) > 5:
                sSymbol = lArgs[3]
                sTimeFrame = lArgs[4]
                sYear = lArgs[5]
            else:
                lCooked = os.path.split(sFile)[-1].split('.')[0].split('-')
                if len(lCooked) > 1:
                    sYear = lCooked[-1]
                else:
                    sYear = ""
                sSymbol = lCooked[0][:6]
                sTimeFrame = lCooked[0][6:]

            self.vDebug(sDo +" " +sCmd +" " + \
                       "sSymbol=" +sSymbol \
                       +", sYear=" +sYear \
                       +", sTimeFrame=" +sTimeFrame)

            mFeedOhlc = oReadMt4Csv(sFile, sTimeFrame, sSymbol, sYear="")
            assert mFeedOhlc is not None, "oReadMt4Csv failed on " + sFile
            mFeedOhlc.info(True, sys.stdout)

            # NaturalNameWarning: object name is not a valid Python identifier: 'Mt4_csv|EURUSD|1440|2014'; it does not match the pattern ``^[a-zA-Z_][a-zA-Z0-9_]*$``;
            sKey = 'Mt4_csv' +'_' +sSymbol +'_' +sTimeFrame +'_' +sYear
            oOm = oEnsureOmlette(self.ocmd2, oValues)
            _dCurrentFeedFrame = oOm.dGetFeedFrame(sFile,
                                                   sTimeFrame,
                                                   sSymbol,
                                                   sYear)
            from OpenTrader.PandasMt4 import oReadMt4Csv, oPreprocessOhlc, vResampleFiles
            mFeedOhlc = oPreprocessOhlc(_dCurrentFeedFrame['mFeedOhlc'])
            sys.stdout.write('INFO:  Data Open length: %d\n' % len(mFeedOhlc))
            _dCurrentFeedFrame['mFeedOhlc'] = mFeedOhlc

            dFEED_CACHE[sKey] = _dCurrentFeedFrame
            sFEED_CACHE_KEY = sKey
            return

        _lFeedCacheKeys = dFEED_CACHE.keys()
        if sCmd == 'list':
            self.poutput("Feed keys: %r" % (self.G(_lFeedCacheKeys,)))
            return

        if sCmd == 'get':
            self.poutput("Current Feed key: %s" % (self.G(sFEED_CACHE_KEY,)))
            return

        if sCmd == 'set':
            assert len(lArgs) >= 3, \
                   "ERROR: " +sDo +" " +sCmd +" " + '|'.join(_lFeedCacheKeys)
            sKey = lArgs[2]
            assert sKey in _lFeedCacheKeys, \
                   "ERROR: " +sDo +" " +sCmd +" " + '|'.join(_lFeedCacheKeys)
            sFEED_CACHE_KEY = sKey
            return

        # The following all require that a feed has been loaded
        if not sFEED_CACHE_KEY or sFEED_CACHE_KEY not in dFEED_CACHE:
            self.vError("Run \"back read_*\" first to read a DataFrame")
            return
        _dCurrentFeedFrame = dFEED_CACHE[sFEED_CACHE_KEY]

        if _dCurrentFeedFrame is None:
            self.vError("Run \"back read_*\" first to read a DataFrame")
            return
        sSymbol = _dCurrentFeedFrame['sSymbol']
        sKey = _dCurrentFeedFrame['sKey']
        sTimeFrame = _dCurrentFeedFrame['sTimeFrame']
        mFeedOhlc = _dCurrentFeedFrame['mFeedOhlc']

        if sCmd in ['to_hdf']:
            """ DataFrame.to_hdf(path_or_buf, key, **kwargs)
            activate the HDFStore
            Parameters :
              path_or_buf : the path (string) or buffer to put the store
              key : string indentifier for the group in the store
              """

            assert len(lArgs) >= 3, \
                   "ERROR: " +sDo +" " +sCmd +" fixed|table filename"
            sType = lArgs[2]
            assert sType in ['fixed', 'table']
            sFile = lArgs[3]
            # FixME: if absolute assert os.path.exists(os.path.dirname(sFile))
            #? lArgs[4:] -> **kw ?
            vRetval = mFeedOhlc.to_hdf(sFile, sKey, format=sType)
            return

        _dPlotParams = self.ocmd2.oConfig['feed.plot.params']

        if sCmd == 'info':
            """Concise summary of a DataFrame.
            Parameters :
            verbose : boolean, default True
                    If False, don’t print column count summary
            buf : writable buffer, defaults to sys.stdout
            """
            import yaml

            mFeedOhlc.info(True, sys.stdout)

            s = '|  %s  |' % ("Plot Params",)
            self.poutput('-' * len(s))
            self.poutput(s)
            self.poutput('-' * len(s))
            yaml.dump(_dPlotParams,
                      allow_unicode=True,
                      default_flow_style=False)
            self.poutput('-' * len(s))
            return

        # 'download_hst_zip'
        if sCmd == 'plot':

            import matplotlib
            if 'matplotlib.rcParams' in self.ocmd2.oConfig:
                for sKey, gVal in self.ocmd2.oConfig['matplotlib.rcParams'].items():
                    matplotlib.rcParams[sKey] = gVal
            import numpy as np
            from OpenTrader.OTPpnAmgc import vGraphData

            from OpenTrader.OTBackTest import oPreprocessOhlc
            mFeedOhlc = oPreprocessOhlc(mFeedOhlc)
            # (Pdb) pandas.tseries.converter._dt_to_float_ordinal(mFeedOhlc.index)[0]
            # 735235.33333333337
            nDates = matplotlib.dates.date2num(mFeedOhlc.index.to_pydatetime())
            nVolume = 1000*np.random.normal(size=len(mFeedOhlc))

            self.vInfo("This may take minutes to display depending on your computer's speed")
            vGraphData(sSymbol, nDates,
                       mFeedOhlc.C.values, mFeedOhlc.H.values, mFeedOhlc.L.values, mFeedOhlc.O.values,
                       nVolume,
                       **_dPlotParams)
            return

        self.vError("Unrecognized feed command: " + str(lArgs) +'\n' +__doc__)
        return

    # Set the recipe that the chef will use, and make the ingredients from the feeds
    LCOMMANDS += ['recipe']
    def backtest_recipe(self):
        """
==== OTCmd2 backtest recipe

Set the recipe that the chef will use, and make the ingredients from the feeds.
{{{
back recipe list                        - list the known recipes
back recipe get                         - show the current recipe
back recipe set RECIPE                  - set the current recipe
back recipe ingredients                 - make the ingredients
back recipe config                      - show the current recipe config
back recipe config KEY                  - show the current config of KEY
back recipe config KEY VAL              - set the current config of KEY to VAL
back recipe config tabview              - view the config with tabview
}}}
        """
        global dFEED_CACHE
        global sFEED_CACHE_KEY
        lArgs = self.lArgs
        oValues = self.oValues
        sDo = 'recipe'
        #!WTF local variable '__doc__' referenced before assignment
        self.dhelp['recipe'] = __doc__
        lArgs = self.lArgs

        import warnings
        with warnings.catch_warnings():
            warnings.filterwarnings('ignore') # ignore problems during import
            from OpenTrader.Omlettes import lKnownRecipes
        # self.vDebug("lKnownRecipes: " + repr(lKnownRecipes))
        # are ingredients under chef?
        _lCmds = ['set', 'list', 'get', 'make', 'ingredients', 'config']

        sCmd = str(lArgs[1])
        if sCmd == 'list':
            self.poutput("Known Recipes: %r" % (self.G(lKnownRecipes,)))
            return

        if sCmd == 'get' or (sCmd == 'set' and len(lArgs) == 2):
            self.poutput("Current Recipe: %s" % (self.ocmd2.sRecipe,))
            return

        assert len(lArgs) > 1, "ERROR: not enough args: " +sDo +str(_lCmds)
        assert sCmd in _lCmds, "ERROR: %s %s not in: %r " % (
            sDo, sCmd, _lCmds)

        if sCmd == 'config':
            oRecipe = oEnsureRecipe(self.ocmd2, oValues)
            self.ocmd2.vConfigOp(lArgs, oRecipe.oConfig)
            return

        if sCmd == 'set':
            assert len(lArgs) > 2, \
                   "ERROR: %s %s requires one of: %s" % (
                sDo, sCmd, '|'.join(lKnownRecipes))
            sNewRecipe = str(lArgs[2])
            assert sNewRecipe in lKnownRecipes, \
                   "ERROR: %s %s %s not in: %s" % (
                sDo, sCmd, sNewRecipe, '|'.join(lKnownRecipes))
            if self.ocmd2.sRecipe != sNewRecipe:
                self.ocmd2.sRecipe = sNewRecipe
                oRecipe = oEnsureRecipe(self.ocmd2, oValues, sNewRecipe=sNewRecipe)
            #? do we update the config file? - I think not
            #? self.ocmd2.oConfig['backtest']['sRecipe'] = sNewRecipe
            return

        # The following all require that a feed has been loaded
        assert sFEED_CACHE_KEY
        assert sFEED_CACHE_KEY in dFEED_CACHE
        _dCurrentFeedFrame = dFEED_CACHE[sFEED_CACHE_KEY]
        assert _dCurrentFeedFrame
        if sCmd == 'make' or sCmd == 'ingredients':
            assert _dCurrentFeedFrame
            oRecipe = oEnsureRecipe(self.ocmd2, oValues)
            # ugly
            dFeedParams = _dCurrentFeedFrame
            mFeedOhlc = _dCurrentFeedFrame['mFeedOhlc']
            dFeeds = dict(mFeedOhlc=mFeedOhlc, dFeedParams=dFeedParams)

            oRecipe.dMakeIngredients(dFeeds)
            assert oRecipe.dIngredients
            return

        self.vError("Unrecognized recipe command: " + str(lArgs) +'\n' +__doc__)
        return

    LCOMMANDS += ['chef']
    def backtest_chef(self):
        """
==== OTCmd2 backtest chef

Set the chef that we will use, and cook from the ingredients and the feeds.
{{{
back chef list                          - list the known chefs
back chef set                           - show the current chef
back chef set CHEF                      - set the current chef
back chef cook                          - cook the recipe by the chef
}}}
        """
        global dFEED_CACHE
        global sFEED_CACHE_KEY
        lArgs = self.lArgs
        sDo = 'chef'
        #!WTF local variable '__doc__' referenced before assignment
        self.dhelp['chef'] = __doc__
        lArgs = self.lArgs

        from OpenTrader.Omlettes import lKnownChefs
        # self.vDebug("lKnownChefs: " + repr(lKnownChefs))

        _lCmds = ['get', 'set', 'list', 'cook']
        assert len(lArgs) > 1, "ERROR: not enough args: " +sDo +str(_lCmds)
        sCmd = lArgs[1]

        if sCmd == 'list':
            self.poutput("Known Chefs: %r" % (lKnownChefs,))
            return

        if sCmd == 'get' or (sCmd == 'set' and len(lArgs) == 2):
            self.poutput("Current Chef: %s" % (self.ocmd2.sChef,))
            return

        assert sCmd in _lCmds, "ERROR: not in: " +sDo +str(_lCmds)

        if sCmd == 'set':
            assert len(lArgs) > 2, \
                   "ERROR: %s %s needs one of: %s" % (
                sDo, sCmd, '|'.join(lKnownChefs))
            sNewChef = str(lArgs[2])
            assert sNewChef in lKnownChefs, \
                   "ERROR: %s %s %s not in: %s" % (
                sDo, sCmd, sNewChef, '|'.join(lKnownChefs))
            if self.ocmd2.sChef != sNewChef:
                self.ocmd2.sChef = sNewChef
                oChef = oEnsureChef(self.ocmd2, oValues, sNewChef=sNewChef)
            #? do we update the config file? - I think not
            #? self.ocmd2.oConfig['backtest']['sChef'] = sNewChef
            return

        # The following all require that a feed has been loaded
        assert sFEED_CACHE_KEY
        assert sFEED_CACHE_KEY in dFEED_CACHE
        _dCurrentFeedFrame = dFEED_CACHE[sFEED_CACHE_KEY]
        assert _dCurrentFeedFrame

        # There's always a default provided of these
        oOm = oEnsureOmlette(self.ocmd2, oValues)
        oRecipe = oEnsureRecipe(self.ocmd2, oValues)
        oChefModule = oEnsureChef(self.ocmd2, oValues)

        if sCmd == 'cook':
            from OpenTrader.OTBackTest import oPyBacktestCook
            assert oRecipe.dIngredients
            # ugly
            dFeeds = _dCurrentFeedFrame

            oBt = oPyBacktestCook(dFeeds, oRecipe, oChefModule, oOm)
            assert oBt is not None
            if type(oBt) == str:
                raise RuntimeError(oBt)
            oOm.oBt = oBt
            # self.vDebug("Cooked " + oBt.sSummary())
            return

        self.vError("Unrecognized chef command: " + str(lArgs) +'\n' +__doc__)
        return

    # List the servings the chef has cooked, and dish out the servings
    LCOMMANDS += ['servings']
    def backtest_servings(self):
        """
==== OTCmd2 backtest servings

List the servings the chef has cooked, and dish out the servings.
{{{
back servings list            - list the servings that result from the recipe and chef
back servings signals         - show the signals: when to buy or sell
back servings trades          - show the trades: what was bought or sold
back servings positions       - show how the trades effected the positions
back servings equity          - show the results of the trades as equity differences
back servings reviews         - show the metrics and reviews of the trades
back servings tabview SERVING - view with tabview: the SERVING, or reviews
}}}
        """
        #? back reviews get/set/servings/tabview

        lArgs = self.lArgs
        sDo = 'servings'
        #!WTF local variable '__doc__' referenced before assignment
        self.dhelp['servings'] = __doc__
        lArgs = self.lArgs

        # There's always a default provided of these
        oOm = oEnsureOmlette(self.ocmd2, oValues)
        oRecipe = oEnsureRecipe(self.ocmd2, oValues)
        oChefModule = oEnsureChef(self.ocmd2, oValues)

        if not hasattr(oOm, 'oBt') or not oOm.oBt:
            self.vError("You must use \"chef cook\" before you can have servings")
            return
        oBt = oOm.oBt

        # ['signals', 'trades', 'positions', 'equity', 'reviews', 'trade_price']
        _lCmds = oChefModule.lProducedServings[:]
        if tabview and tabview not in _lCmds: _lCmds += ['tabview']

        if len(lArgs) == 1 or lArgs[1] == 'list':
            self.poutput("Produced Servings: %r" % (oChefModule.lProducedServings,))
            return

        assert len(lArgs) > 1, "ERROR: argument required" +sDo +str(_lCmds)
        sCmd = lArgs[1]
        # self.vDebug("lProducedServings: " + repr(_lCmds))
        assert sCmd in _lCmds, "ERROR: %s %s not in %r" % (
            sDo, sCmd, _lCmds)

        oFd = sys.stdout
        # There's always a default provided of these
        oOm = oEnsureOmlette(self.ocmd2, oValues)
        oRecipe = oEnsureRecipe(self.ocmd2, oValues)
        oChefModule = oEnsureChef(self.ocmd2, oValues)

        ## oFun = getattr(self.ocmd2.oBt, sCmd)
        ## self.poutput(oFun())
        if sCmd == 'signals':
            # this was the same as: oBt._mSignals = bt.mSignals() or oBt.signals
            oOm.oBt._mSignals = oRecipe.mSignals(oOm.oBt)
            oFd.write('INFO:  bt.signals found: %d\n' % len(oOm.oBt.signals))
            oOm.vAppendHdf('recipe/servings/mSignals', oOm.oBt.signals)
            return

        if sCmd == 'trades':
            # this was the same as: oBt._mTrades =  oBt.mTrades() or oBt.trades
            oOm.oBt._mTrades = oRecipe.mTrades(oOm.oBt)
            oFd.write('INFO:  bt.trades found: %d\n' % len(oOm.oBt.trades))
            oOm.vAppendHdf('recipe/servings/mTrades', oOm.oBt.trades)
            return

        if sCmd == 'positions':
            # this was the same as: oBt._rPositions = oBt.rPositions() or oBt.positions
            oOm.oBt._rPositions = oRecipe.rPositions(oOm.oBt, init_pos=0)
            oFd.write('INFO:  bt.positions found: %d\n' % len(oOm.oBt.positions))
            oOm.vAppendHdf('recipe/servings/rPositions', oOm.oBt.positions)
            return

        if sCmd == 'equity':
            # this was the same as: oBt._rEquity = oBt.rEquity() or oBt.equity
            oOm.oBt._rEquity = oRecipe.rEquity(oOm.oBt)
            oFd.write('INFO:  bt.equity found: %d\n' % len(oOm.oBt.equity))
            oOm.vAppendHdf('recipe/servings/rEquity', oOm.oBt.equity)
            return

        if sCmd == 'trade_price':
            # oFd.write('INFO:  bt.rTradePrice() found: %d\n' % len(oBt.rTradePrice()))
            oFd.write('INFO:  bt.trade_price found: %d\n' % len(oOm.oBt.trade_price))
            oOm.vAppendHdf('recipe/servings/rTradePrice', oOm.oBt.trade_price)
            return

        if sCmd == 'metrics' or sCmd == 'reviews':
            oOm.vSetTitleHdf('recipe/servings', oOm.oChefModule.sChef)
            #? Leave this as derived or store it? reviews?
            oOm.vSetMetadataHdf('recipe/servings', oOm.oBt.dSummary())
            oFd.write(oOm.oBt.sSummary())
            return

        if tabview and sCmd == 'tabview':
            assert len(lArgs) > 2, "ERROR: " +sDo +" " +sCmd \
                   +": serving required, one of: reviews " +str(oChefModule.lProducedServings)

            sServing = lArgs[2]
            if sServing in ['metrics', 'reviews']:
                l = [['Metric', 'Value']]
                l += oOm.oBt.lSummary()
                l.sort()
                # , hdr_rows=lHdrRows
                tabview.view(l, column_width='max')
                return

            assert sServing in oChefModule.lProducedServings
            mDf = getattr(oOm.oBt, sServing)
            # FixMe: need index timestamp for mva
            tabview.view(mDf)
            return

        self.vError("Unrecognized servings command: " + str(lArgs) +'\n' +__doc__)
        return

    # Plot the servings the chef has cooked, using matplotlib
    LCOMMANDS += ['plot']
    def backtest_plot(self):
        """
==== OTCmd2 backtest plot

Plot the servings the chef has cooked, using matplotlib.
{{{
back plot show      - show the current plot
back plot trades    - plot the trades
back plot equity    - plot the cumulative equity
}}}
        """
        lArgs = self.lArgs
        sDo = 'plot'
        #!WTF local variable '__doc__' referenced before assignment
        self.dhelp['plot'] = __doc__
        lArgs = self.lArgs

        __doc__ = sBACplot__doc__
        _lCmds = ['show', 'trades', 'equity']
        if not hasattr(oOm, 'oBt') or not oOm.oBt:
            self.vError("You must use \"chef cook\" before you can plot")
            return
        # FixMe:
        oBt = oOm.oBt

        assert len(lArgs) > 1, "ERROR: " +sDo +str(_lCmds)
        sCmd = lArgs[1]
        assert sCmd in _lCmds, "ERROR: " +sDo +str(_lCmds)

        import matplotlib
        import matplotlib.pylab as pylab
        if sCmd == 'show':
            pylab.show()
            return

        # FixMe:
        matplotlib.rcParams['figure.figsize'] = (10, 5)

        if sCmd == 'equity':
            # FixMe: derive the period from the sTimeFrame
            sPeriod = 'W'
            close_label = 'C'
            mOhlc = oRecipe.dIngredients['mOhlc']
            oChefModule.vPlotEquity(oBt.equity, mOhlc, sTitle="%s\nEquity" % repr(oBt),
                                    sPeriod=sPeriod,
                                    close_label=close_label,
                                    )
            pylab.show()
            return

        if sCmd == 'trades':
            oBt.vPlotTrades()
            pylab.legend(loc='lower left')
            pylab.show()

            return
        self.vError("Unrecognized plot command: " + str(lArgs))
        return

    def bexecute(self, lArgs, oValues):
        """bexecute executes the backtest command.
        """
        self.lArgs = lArgs
        self.oValues = oValues

        # self.vassert_args(lArgs, LCOMMANDS, imin=1)
        if self.bis_help(lArgs):
            return

        sDo = lArgs[0]
        # An omlette is an HDF5 file that saves all the data from a backtest
        if sDo in ['omlette']:
            oMeth = getattr(self, 'backtest_' +sDo)
            oMeth()
            return

        if oValues.sRecipe:
            self.ocmd2.sRecipe = oValues.sRecipe
            self.poutput("DEBUG: backtest recipe from values: " + oValues.sRecipe)
        elif not hasattr(self.ocmd2, 'sRecipe') or not self.ocmd2.sRecipe:
            self.ocmd2.sRecipe = self.ocmd2.oConfig['backtest']['recipe']
            self.poutput("WARN: backtest recipe from config: " + self.ocmd2.sRecipe)
        if oValues.sChef:
            self.ocmd2.sChef = oValues.sChef
            self.poutput("DEBUG: backtest chef from values: " + oValues.sChef)
        elif not hasattr(self.ocmd2, 'sChef') or not self.ocmd2.sChef:
            self.ocmd2.sChef = self.ocmd2.oConfig['backtest']['chef']
            self.poutput("WARN: backtest chef from config: " + self.ocmd2.sChef)

        if sDo in ['feed', 'recipe', 'chef']:
            oMeth = getattr(self, 'backtest_' +sDo)
            oMeth()
            return

        # Set the chef that we will use, and cook from the ingredients and the feeds
        oOm = oEnsureOmlette(self.ocmd2, oValues)
        oRecipe = oEnsureRecipe(self.ocmd2, oValues)
        oChefModule = oEnsureChef(self.ocmd2, oValues)

        if sDo in ['servings', 'plot']:
            oMeth = getattr(self, 'backtest_' +sDo)
            try:
                oMeth()
            except KeyboardInterrupt:
                pass
            except Exception as e:
                # This is still in the process of getting wired up and tested
                print(traceback.format_exc(10))
            return

        self.poutput("ERROR: Unrecognized backtest command: " + str(lArgs) +'\n' +__doc__)


def vDoBacktestCmd(self, lArgs, oValues):
    __doc__ = sBAC__doc__