For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

Function to calculate financial ratios for strategy and datas values.



  • This is not an indicator, but an added function that will return empyrical financial ratios at the end of the back test.

    This example is for an equal risk parity rebalancing algorithm that has five assets. A dataframe is returned that provides financial ratios for not only the strategy but for each of the datas. This works best with daily data. Two of the calculations are for annual returns and will throw an error if the test is less than one year.

                       Strategy  MSCI_ACWI  SP500  TBOND20   gold  iSharesREIT
    annual_return         0.048      0.080  0.114    0.024  0.027        0.055
    annual_volatility     0.069      0.150  0.123    0.130  0.136        0.142
    cagr                  0.048      0.080  0.114    0.024  0.027        0.055
    calmar                0.507      0.350  0.873    0.133  0.138        0.322
    cumm_return           0.148      0.261  0.381    0.073  0.084        0.174
    max_drawdown         -0.096     -0.230 -0.130   -0.179 -0.197       -0.171
    sharpe                0.719      0.590  0.938    0.246  0.265        0.449
    sortino               1.015      0.805  1.336    0.342  0.389        0.613
    tail_ratio            1.064      1.003  1.012    0.940  1.070        0.864
    

    The Strategy column is the result of the back test, and the other five columns are the datas used.

    To implement this, you need the following libraries:

    import pandas as pd
    import empyrical as ep     # https://github.com/quantopian/empyrical
    

    Add two analyzers:

    class CashMarket(bt.analyzers.Analyzer):
        """
        Analyzer returning cash and market values
        """
    
        def start(self):
            super(CashMarket, self).start()
    
        def create_analysis(self):
            self.rets = {}
            self.vals = 0.0
    
        def notify_cashvalue(self, cash, value):
            self.vals = (cash, value)
            self.rets[self.strategy.datetime.datetime()] = self.vals
    
        def get_analysis(self):
            return self.rets
    
    
    class OHLCV(bt.analyzers.Analyzer):
        """This analyzer reports the OHLCV of each of datas.
        Params:
          - timeframe (default: ``None``)
            If ``None`` then the timeframe of the 1st data of the system will be
            used
          - compression (default: ``None``)
            Only used for sub-day timeframes to for example work on an hourly
            timeframe by specifying "TimeFrame.Minutes" and 60 as compression
            If ``None`` then the compression of the 1st data of the system will be
            used
        Methods:
          - get_analysis
            Returns a dictionary with returns as values and the datetime points for
            each return as keys
        """
    
        def start(self):
            tf = min(d._timeframe for d in self.datas)
            self._usedate = tf >= bt.TimeFrame.Days
    
        def next(self):
            # Cycle through the datas at each next and store the OHLCV
            # (d.l.lines.__len__())
            pvals = {}
            # Some datas I use have only 'Close' value used for signalling. The try statement
            # avoids errors on these datas.
            for d in self.datas:
                try:
                    d.open[0]
                    d.high[0]
                    d.low[0]
                    d.volume[0]
                except:
                    continue
                else:
                    pvals = [d.open[0], d.high[0], d.low[0], d.close[0], d.volume[0]]
    
                    if self._usedate:
                        self.rets[(self.strategy.datetime.date(), d._name)] = pvals
                    else:
                        self.rets[(self.strategy.datetime.datetime(), d._name)] = pvals
    
        def get_analysis(self):
            return self.rets
    

    Add function at the top level for returning financial ratios.

    def fin_funcs(returns_series, risk_free_rate=0):
        """
        Financial calculations taken from Quantopians Empirical Library.
    
        :param df: pd.Series containing daily returns calculated on a percentage change and also by log scale.
        :return: Dictionary of financial ratios both for percent change returns and log returns.
        """
        returns_pct = returns_series
    
        # Calculate each of the functions.
        annual_return_pct = ep.annual_return(
            returns_pct, period="daily", annualization=None
        )
        cumm_return_pct = ep.cum_returns(returns_pct, starting_value=0).iloc[-1]
        cagr_pct = ep.cagr(returns_pct, period="daily", annualization=None)
        sharpe_pct = ep.sharpe_ratio(
            returns_pct, risk_free=risk_free_rate, period="daily", annualization=None
        )
        annual_volatility_pct = ep.annual_volatility(
            returns_pct, period="daily", alpha=2.0, annualization=None
        )
        max_drawdown_pct = ep.max_drawdown(returns_pct)
        calmar_pct = ep.calmar_ratio(returns_pct, period="daily", annualization=None)
        sortino_pct = ep.sortino_ratio(
            returns_pct,
            required_return=0,
            period="daily",
            annualization=None,
            _downside_risk=None,
        )
        tail_ratio_pct = ep.tail_ratio(returns_pct)
    
        # Collect ratios into dictionary.
        financials = {
            "annual_return": annual_return_pct,
            "cumm_return": cumm_return_pct,
            "cagr": cagr_pct,
            "sharpe": sharpe_pct,
            "annual_volatility": annual_volatility_pct,
            "max_drawdown": max_drawdown_pct,
            "calmar": calmar_pct,
            "sortino": sortino_pct,
            "tail_ratio": tail_ratio_pct,
        }
    
        return financials
    

    Add analyzers...

    cerebro.addanalyzer(CashMarket, _name="cash_market")
    cerebro.addanalyzer(OHLCV, _name="ohlcv")
    

    After running cerebro...
    Cerebro run

    strat = cerebro.run(**eval("dict(" + args.cerebro + ")"))
    

    Create dictionary for collecting all the financial results to then convert to pandas dataframe.
    First collect the strategy results, then the datas results.

    fin_results = {}
    
    # Calculate the financial functions for the strategy results
    # First, get market values for the algorithm, second item in list in dict from analyzer cash_market
    dict_mv = strat[0].analyzers.getbyname("cash_market").get_analysis()
    
    # Create lists for values and keys for pd.Series, then create pd.Series with pct_change.
    v = [x[1] for x in dict_mv.values()]
    d = [x.date() for x in dict_mv.keys()]
    returns_series = pd.Series(v, index=d, name="Strategy")
    returns_series = returns_series[int(args.rperiod) :].pct_change()
    
    # Call fin_funcs and get dictionary back. Add to new dictionary tracking all the financials.
    fin_results["Strategy"] = fin_funcs(returns_series)
    

    Then collect data for each datas and get financial ratios.

    # Get the OHLCV for each data.
    dict_ohlcv = strat[0].analyzers.getbyname("ohlcv").get_analysis()
    
    # Create a dataframe from the analyzer ohlcv, percent change by date.
    df = (
        pd.DataFrame(pd.DataFrame.from_dict(dict_ohlcv).unstack())
        .loc[pd.IndexSlice[:, :, 3], :]
        .droplevel(2)
        .reset_index()
        .pivot(index="level_0", columns="level_1", values=0)
        .pct_change()
    )
    
    # Get financial ratios for each security column and add to fin_results dictionary.
    for n in range(len(df.columns)):
        fin_results[df.columns[n]] = fin_funcs(df.iloc[:, n])
    

    Convert results dictionary to a DataFrame for presenting results.

    df_results = pd.DataFrame.from_dict(fin_results)
    print(df_results.round(3))
    

Log in to reply
 

});