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 runstrat = 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))