Spaces:
Sleeping
Sleeping
| 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}, | |
| ) | |