finaich / finrobot /functional /quantitative.py
wangzerui's picture
Upload folder using huggingface_hub
4ed8ca0 verified
raw
history blame
7.36 kB
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},
)