Spaces:
Running
Running
File size: 7,357 Bytes
4ed8ca0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
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},
)
|