import os import json import importlib import yfinance as yf import backtrader as bt from backtrader.strategies import SMA_CrossOver from typing import Annotated, List, Tuple from matplotlib import pyplot as plt from pprint import pformat from IPython import get_ipython class DeployedCapitalAnalyzer(bt.Analyzer): def start(self): self.deployed_capital = [] self.initial_cash = self.strategy.broker.get_cash() # Initial cash in account def notify_order(self, order): if order.status in [order.Completed]: if order.isbuy(): self.deployed_capital.append(order.executed.price * order.executed.size) elif order.issell(): self.deployed_capital.append(order.executed.price * order.executed.size) def stop(self): total_deployed = sum(self.deployed_capital) final_cash = self.strategy.broker.get_value() net_profit = final_cash - self.initial_cash if total_deployed > 0: self.retn = net_profit / total_deployed else: self.retn = 0 def get_analysis(self): return {"return_on_deployed_capital": self.retn} class BackTraderUtils: def back_test( ticker_symbol: Annotated[ str, "Ticker symbol of the stock (e.g., 'AAPL' for Apple)" ], start_date: Annotated[ str, "Start date of the historical data in 'YYYY-MM-DD' format" ], end_date: Annotated[ str, "End date of the historical data in 'YYYY-MM-DD' format" ], strategy: Annotated[ str, "BackTrader Strategy class to be backtested. Can be pre-defined or custom. Pre-defined options: 'SMA_CrossOver'. If custom, provide module path and class name as a string like 'my_module:TestStrategy'.", ], strategy_params: Annotated[ str, "Additional parameters to be passed to the strategy class formatted as json string. E.g. {'fast': 10, 'slow': 30} for SMACross.", ] = "", sizer: Annotated[ int | str | None, "Sizer used for backtesting. Can be a fixed number or a custom Sizer class. If input is integer, a corresponding fixed sizer will be applied. If custom, provide module path and class name as a string like 'my_module:TestSizer'.", ] = None, sizer_params: Annotated[ str, "Additional parameters to be passed to the sizer class formatted as json string.", ] = "", indicator: Annotated[ str | None, "Custom indicator class added to strategy. Provide module path and class name as a string like 'my_module:TestIndicator'.", ] = None, indicator_params: Annotated[ str, "Additional parameters to be passed to the indicator class formatted as json string.", ] = "", cash: Annotated[ float, "Initial cash amount for the backtest. Default to 10000.0" ] = 10000.0, save_fig: Annotated[ str | None, "Path to save the plot of backtest results. Default to None." ] = None, ) -> str: """ Use the Backtrader library to backtest a trading strategy on historical stock data. """ cerebro = bt.Cerebro() if strategy == "SMA_CrossOver": strategy_class = SMA_CrossOver else: assert ( ":" in strategy ), "Custom strategy should be module path and class name separated by a colon." module_path, class_name = strategy.split(":") module = importlib.import_module(module_path) strategy_class = getattr(module, class_name) strategy_params = json.loads(strategy_params) if strategy_params else {} cerebro.addstrategy(strategy_class, **strategy_params) # Create a data feed data = bt.feeds.PandasData( dataname=yf.download(ticker_symbol, start_date, end_date, auto_adjust=True) ) cerebro.adddata(data) # Add the data feed # Set our desired cash start cerebro.broker.setcash(cash) # Set the size of the trades if sizer is not None: if isinstance(sizer, int): cerebro.addsizer(bt.sizers.FixedSize, stake=sizer) else: assert ( ":" in sizer ), "Custom sizer should be module path and class name separated by a colon." module_path, class_name = sizer.split(":") module = importlib.import_module(module_path) sizer_class = getattr(module, class_name) sizer_params = json.loads(sizer_params) if sizer_params else {} cerebro.addsizer(sizer_class, **sizer_params) # Set additional indicator if indicator is not None: assert ( ":" in indicator ), "Custom indicator should be module path and class name separated by a colon." module_path, class_name = indicator.split(":") module = importlib.import_module(module_path) indicator_class = getattr(module, class_name) indicator_params = json.loads(indicator_params) if indicator_params else {} cerebro.addindicator(indicator_class, **indicator_params) # Attach analyzers cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe_ratio") cerebro.addanalyzer(bt.analyzers.DrawDown, _name="draw_down") cerebro.addanalyzer(bt.analyzers.Returns, _name="returns") cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trade_analyzer") # cerebro.addanalyzer(DeployedCapitalAnalyzer, _name="deployed_capital") stats_dict = {"Starting Portfolio Value:": cerebro.broker.getvalue()} results = cerebro.run() # run it all first_strategy = results[0] # Access analysis results stats_dict["Final Portfolio Value"] = cerebro.broker.getvalue() # stats_dict["Deployed Capital"] = pformat( # first_strategy.analyzers.deployed_capital.get_analysis(), indent=4 # ) stats_dict["Sharpe Ratio"] = ( first_strategy.analyzers.sharpe_ratio.get_analysis() ) stats_dict["Drawdown"] = first_strategy.analyzers.draw_down.get_analysis() stats_dict["Returns"] = first_strategy.analyzers.returns.get_analysis() stats_dict["Trade Analysis"] = ( first_strategy.analyzers.trade_analyzer.get_analysis() ) if save_fig: directory = os.path.dirname(save_fig) if directory: os.makedirs(directory, exist_ok=True) plt.figure(figsize=(12, 8)) cerebro.plot() plt.savefig(save_fig) plt.close() return "Back Test Finished. Results: \n" + pformat(stats_dict, indent=2) if __name__ == "__main__": # Example usage: start_date = "2011-01-01" end_date = "2012-12-31" ticker = "MSFT" # BackTraderUtils.back_test( # ticker, start_date, end_date, "SMA_CrossOver", {"fast": 10, "slow": 30} # ) BackTraderUtils.back_test( ticker, start_date, end_date, "test_module:TestStrategy", {"exitbars": 5}, )