def backtest( strategy, data, # Treated as csv path is str, and dataframe of pd.DataFrame commission=COMMISSION_PER_TRANSACTION, init_cash=INIT_CASH, plot=True, fractional=False, verbose=1, sort_by="rnorm", sentiments=[], strats={}, # Only used when strategy = "multi" return_history=False, return_plot=False, channel="", symbol="", allow_short=False, short_max=1.5, figsize=(30, 15), data_class=None, data_kwargs={}, plot_kwargs={}, fig=None, **kwargs, ): """Backtest financial data with a specified trading strategy Parameters ---------------- strategy : str or an instance of `fastquant.strategies.base.BaseStrategy` see list of accepted strategy keys below data : pandas.DataFrame dataframe with at least close price indexed with time commission : float commission per transaction [0, 1] init_cash : float initial cash (currency implied from `data`) plot : bool show plot backtrader (disabled if `strategy`=="multi") verbose : int Verbose can take values: [0, 1, 2, 3], with increasing levels of verbosity (default=1). sort_by : str sort result by given metric (default='rnorm') sentiments : pandas.DataFrame df of sentiment [0, 1] indexed by time (applicable if `strategy`=='senti') strats : dict dictionary of strategy parameters (applicable if `strategy`=='multi') return_history : bool return history of transactions (i.e. buy and sell timestamps) (default=False) return_plot: bool return the plot (if you want to save the plot) (default=True) channel : str Channel to be used for notifications - e.g. "slack" (default=None) symbol : str Symbol to be referenced in the channel notification if not None (default=None) allow_short : bool Whether to allow short selling, with max set as `short_max` times the portfolio value (default=False) short_max : float The maximum short position allowable as a ratio relative to the portfolio value at that time point (default=1.5) figsize : tuple The size of the figure to be displayed at the end of the backtest (default=(30, 15)) data_class : bt.feed.DataBase Custom backtrader database to be used as a parent class instead bt.feed. (default=None) data_kwargs : dict Datafeed keyword arguments (empty dict by default) plot_kwargs : dict Argument for function cerebro.plot() (empty dict by default) {0} """ # Setting initial support for 1 cpu # Return the full strategy object to get all run information cerebro = bt.Cerebro(stdstats=False, maxcpus=1, optreturn=False) cerebro.addobserver(bt.observers.Broker) cerebro.addobserver(bt.observers.Trades) cerebro.addobserver(bt.observers.BuySell) # Convert all non iterables and strings into lists kwargs = { k: v if isinstance(v, Iterable) and not isinstance(v, str) else [v] for k, v in kwargs.items() } # Add logging parameters based on the `verbose` parameter logging_params = get_logging_params(verbose) kwargs.update(logging_params) # Add Strategy strat_names = [] strat_name = None if strategy == "multi" and strats is not None: for strat, params in strats.items(): cerebro.optstrategy( STRATEGY_MAPPING[strat], init_cash=[init_cash], commission=commission, channel=channel, symbol=symbol, allow_short=allow_short, fractional=fractional, short_max=short_max, **params, ) strat_names.append(strat) else: # Allow instance of BaseStrategy or from the predefined mapping if not isinstance(strategy, str) and issubclass(strategy, bt.Strategy): strat_name = (strategy.__name__ if hasattr(strategy, "__name__") else str(strategy)) else: strat_name = strategy strategy = STRATEGY_MAPPING[strategy] cerebro.optstrategy( strategy, init_cash=[init_cash], commission=commission, channel=channel, symbol=symbol, fractional=fractional, allow_short=allow_short, short_max=short_max, **kwargs, ) strat_names.append(strat_name) # Apply Total, Average, Compound and Annualized Returns calculated using a logarithmic approach cerebro.addanalyzer(btanalyzers.Returns, _name="returns") cerebro.addanalyzer(btanalyzers.SharpeRatio, _name="mysharpe") cerebro.addanalyzer(btanalyzers.DrawDown, _name="drawdown") cerebro.addanalyzer(btanalyzers.TimeDrawDown, _name="timedraw") cerebro.addanalyzer(btanalyzers.TradeAnalyzer, _name="tradeanalyzer") #trade analyzer cerebro.broker.setcommission(commission=commission) # Initalize and verify data pd_data, data, data_format_dict = initalize_data(data, strat_name, symbol, data_class, sentiments, data_kwargs) cerebro.adddata(pd_data) cerebro.broker.setcash(init_cash) # Allows us to set buy price based on next day closing # (technically impossible, but reasonable assuming you use all your money to buy market at the end of the next day) cerebro.broker.set_coc(True) if verbose > 0: print("Starting Portfolio Value: %.2f" % cerebro.broker.getvalue()) # clock the start of the process tstart = time.time() stratruns = cerebro.run() # clock the end of the process tend = time.time() if verbose > 0: # print out the result print("Time used (seconds):", str(tend - tstart)) # Get History, Optimal Parameters and Strategy Metrics sorted_combined_df, optim_params, history_dict = analyze_strategies( init_cash, stratruns, data, strat_names, strategy, strats, sort_by, return_history, verbose, **kwargs, ) # Plot if plot and strategy != "multi": # Plot only with the optimal parameters when multiple strategy runs are required if sorted_combined_df.shape[0] != 1: if verbose > 0: print("=============================================") print("Plotting backtest for optimal parameters ...") _, fig = backtest( strategy, data, plot=plot, verbose=0, sort_by=sort_by, return_plot=return_plot, plot_kwargs=plot_kwargs, **optim_params, ) else: fig = plot_results(cerebro, data_format_dict, figsize, **plot_kwargs) if return_history and return_plot: return sorted_combined_df, history_dict, fig elif return_history: return sorted_combined_df, history_dict elif return_plot: return sorted_combined_df, fig else: return sorted_combined_df
def backtest( strategy, data, # Treated as csv path is str, and dataframe of pd.DataFrame commission=COMMISSION_PER_TRANSACTION, init_cash=INIT_CASH, plot=True, verbose=1, sort_by="rnorm", sentiments=None, strats=None, # Only used when strategy = "multi" data_format=None, # No longer needed but will leave for now to warn removal in a coming release return_history=False, channel=None, symbol=None, allow_short=False, short_max=1.5, figsize=(30, 15), **kwargs, ): """Backtest financial data with a specified trading strategy Parameters ---------------- strategy : str or an instance of `fastquant.strategies.base.BaseStrategy` see list of accepted strategy keys below data : pandas.DataFrame dataframe with at least close price indexed with time commission : float commission per transaction [0, 1] init_cash : float initial cash (currency implied from `data`) plot : bool show plot backtrader (disabled if `strategy`=="multi") sort_by : str sort result by given metric (default='rnorm') sentiments : pandas.DataFrame df of sentiment [0, 1] indexed by time (applicable if `strategy`=='senti') strats : dict dictionary of strategy parameters (applicable if `strategy`=='multi') return_history : bool return history of transactions (i.e. buy and sell timestamps) (default=False) channel : str Channel to be used for last day notification - e.g. "slack" (default=None) verbose : int Verbose can take values: [0, 1, 2, 3], with increasing levels of verbosity (default=1). symbol : str Symbol to be referenced in the channel notification if not None (default=None) {0} """ if data_format: errmsg = "Warning: data_format argument is no longer needed since formatting is now purely automated based on column names!" errmsg += "\nWe will be removing this argument in a coming release!" warnings.warn(errmsg, DeprecationWarning) print(errmsg) # Setting inital support for 1 cpu # Return the full strategy object to get all run information cerebro = bt.Cerebro(stdstats=False, maxcpus=1, optreturn=False) cerebro.addobserver(bt.observers.Broker) cerebro.addobserver(bt.observers.Trades) cerebro.addobserver(bt.observers.BuySell) # Convert all non iterables and strings into lists kwargs = { k: v if isinstance(v, Iterable) and not isinstance(v, str) else [v] for k, v in kwargs.items() } # Add logging parameters based on the `verbose` parameter logging_params = get_logging_params(verbose) kwargs.update(logging_params) # Add Strategy strat_names = [] strat_name = None if strategy == "multi" and strats is not None: for strat, params in strats.items(): cerebro.optstrategy( STRATEGY_MAPPING[strat], init_cash=[init_cash], commission=commission, channel=None, symbol=None, allow_short=allow_short, short_max=short_max, **params, ) strat_names.append(strat) else: # Allow instance of BaseStrategy or from the predefined mapping if not isinstance(strategy, str) and issubclass(strategy, bt.Strategy): strat_name = str(strategy) else: strat_name = strategy strategy = STRATEGY_MAPPING[strategy] cerebro.optstrategy( strategy, init_cash=[init_cash], commission=commission, channel=None, symbol=None, allow_short=allow_short, short_max=short_max, **kwargs, ) strat_names.append(strat_name) # Apply Total, Average, Compound and Annualized Returns calculated using a logarithmic approach cerebro.addanalyzer(btanalyzers.Returns, _name="returns") cerebro.addanalyzer(btanalyzers.SharpeRatio, _name="mysharpe") cerebro.addanalyzer(btanalyzers.DrawDown, _name="drawdown") cerebro.addanalyzer(btanalyzers.TimeDrawDown, _name="timedraw") cerebro.broker.setcommission(commission=commission) # Initalize and verify data pd_data, data, data_format_dict = initalize_data( data, strat_name, sentiments ) cerebro.adddata(pd_data) cerebro.broker.setcash(init_cash) # Allows us to set buy price based on next day closing # (technically impossible, but reasonable assuming you use all your money to buy market at the end of the next day) cerebro.broker.set_coc(True) if verbose > 0: print("Starting Portfolio Value: %.2f" % cerebro.broker.getvalue()) # clock the start of the process tstart = time.time() stratruns = cerebro.run() # clock the end of the process tend = time.time() if verbose > 0: # print out the result print("Time used (seconds):", str(tend - tstart)) # Get History, Optimal Parameters and Strategy Metrics sorted_combined_df, optim_params, history_dict = analyze_strategies( stratruns, data, strat_names, strategy, strats, sort_by, return_history, verbose, **kwargs, ) # Plot if plot and strategy != "multi": # Plot only with the optimal parameters when multiple strategy runs are required if sorted_combined_df.shape[0] != 1: if verbose > 0: print("=============================================") print("Plotting backtest for optimal parameters ...") backtest( strategy, data, plot=plot, verbose=0, sort_by=sort_by, **optim_params, ) else: plot_results(cerebro, data_format_dict, figsize) if return_history: return sorted_combined_df, history_dict else: return sorted_combined_df