From 2dc270e98b439f104ae184f6647baa64f0428794 Mon Sep 17 00:00:00 2001 From: ipergiove Date: Sat, 21 Oct 2023 23:46:04 -0400 Subject: [PATCH 1/5] iterate single step --- backtesting/backtesting.py | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 9c168703..8b962d70 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -1662,3 +1662,85 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None, reverse_indicators=reverse_indicators, show_legend=show_legend, open_browser=open_browser) + + def initialize(self, **kwargs): + """ + Initialize the backtest with the given parameters. Keyword arguments are interpreted as strategy parameters. + + :param kwargs: Strategy parameters + """ + data = _Data(self._data.copy(deep=False)) + broker: _Broker = self._broker(data=data) + strategy: Strategy = self._strategy(broker, data, kwargs) + + strategy.init() + data._update() # Strategy.init might have changed/added to data.df + + # Indicators used in Strategy.next() + indicator_attrs = {attr: indicator + for attr, indicator in strategy.__dict__.items() + if isinstance(indicator, _Indicator)}.items() + + # Skip first few candles where indicators are still "warming up" + # +1 to have at least two entries available + start = 1 + max((np.isnan(indicator.astype(float)).argmin(axis=-1).max() + for _, indicator in indicator_attrs), default=0) + + self._step_data = data + self._step_broker = broker + self._step_strategy = strategy + self._step_time = start + self._step_indicator_attrs = indicator_attrs + + def next(self): + """ + Move the backtest one time step forward and return the results for the current step. + + :return: Results and statistics for the current time step + :rtype: pd.Series + """ + + # Disable "invalid value encountered in ..." warnings. Comparison + # np.nan >= 3 is not invalid; it's False. + # with np.errstate(invalid='ignore'): + + if self._step_time < len(self._data): + # Prepare data and indicators for `next` call + self._step_data._set_length(self._step_time + 1) + for attr, indicator in self._step_indicator_attrs: + # Slice indicator on the last dimension (case of 2d indicator) + setattr(self._step_strategy, attr, indicator[..., :self._step_time + 1]) + + # Handle orders processing and broker stuff + try: + self._step_broker.next() + except _OutOfMoneyError: + pass + + # Next tick, a moment before bar close + self._step_strategy.next() + self._step_time += 1 + else: + # Close any remaining open trades so they produce some stats + for trade in self._step_broker.trades: + trade.close() + + # Re-run broker one last time to handle orders placed in the last strategy + # iteration. Use the same OHLC values as in the last broker iteration. + if self._step_time < len(self._data): + try_(self._step_broker.next, exception=_OutOfMoneyError) + + # Set data back to full length + # for future `indicator._opts['data'].index` calls to work + self._step_data._set_length(len(self._data)) + + + equity = pd.Series(self._step_broker._equity).bfill().fillna(self._step_broker._cash).values + results = compute_stats( + trades=self._step_broker.closed_trades, + equity=equity, + ohlc_data=self._data, + risk_free_rate=0.0, + strategy_instance=self._step_strategy, + ) + return results \ No newline at end of file From d73119e0b6a5b3be5e8e728fe3b27e7b25eff39d Mon Sep 17 00:00:00 2001 From: ipergiove Date: Tue, 9 Jan 2024 22:29:32 +0100 Subject: [PATCH 2/5] kwargs and done --- backtesting/backtesting.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 8b962d70..6144e3b1 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -1692,7 +1692,7 @@ def initialize(self, **kwargs): self._step_time = start self._step_indicator_attrs = indicator_attrs - def next(self): + def next(self, done:bool|None=None, **kwargs): """ Move the backtest one time step forward and return the results for the current step. @@ -1718,9 +1718,10 @@ def next(self): pass # Next tick, a moment before bar close - self._step_strategy.next() + self._step_strategy.next(**kwargs) self._step_time += 1 - else: + + if done==True: # Close any remaining open trades so they produce some stats for trade in self._step_broker.trades: trade.close() From 43d4412ce2f24d464c9a6a05aba282855d208198 Mon Sep 17 00:00:00 2001 From: ipergiove Date: Thu, 25 Jan 2024 16:18:51 +0100 Subject: [PATCH 3/5] test iteration --- backtesting/test/_iteration.py | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 backtesting/test/_iteration.py diff --git a/backtesting/test/_iteration.py b/backtesting/test/_iteration.py new file mode 100644 index 00000000..85e042a7 --- /dev/null +++ b/backtesting/test/_iteration.py @@ -0,0 +1,40 @@ +from backtesting import Backtest +from backtesting import Strategy +from backtesting.test import GOOG, SMA +from backtesting.lib import crossover +import types + +class SmaCross(Strategy): + # Define the two MA lags as *class variables* + # for later optimization + n1 = 10 + n2 = 20 + + def init(self): + # Precompute the two moving averages + self.sma1 = self.I(SMA, self.data.Close, self.n1) + self.sma2 = self.I(SMA, self.data.Close, self.n2) + + def next(self): + # If sma1 crosses above sma2, close any existing + # short trades, and buy the asset + if crossover(self.sma1, self.sma2): + self.position.close() + self.buy() + + # Else, if sma1 crosses below sma2, close any existing + # long trades, and sell the asset + elif crossover(self.sma2, self.sma1): + self.position.close() + self.sell() + +bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002) +# stats = bt.run() +bt.initialize() + +while True: + stats = bt.next() + if not isinstance(stats, types.NoneType): + break +print(stats) +bt.plot(results=stats, open_browser=True) From 753f945faab42949cff576ba562e3ce3b9847313 Mon Sep 17 00:00:00 2001 From: ipergiove Date: Thu, 25 Jan 2024 17:10:50 +0100 Subject: [PATCH 4/5] add function type --- backtesting/backtesting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 6144e3b1..61627cab 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -1663,7 +1663,7 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None, show_legend=show_legend, open_browser=open_browser) - def initialize(self, **kwargs): + def initialize(self, **kwargs) -> None: """ Initialize the backtest with the given parameters. Keyword arguments are interpreted as strategy parameters. @@ -1692,7 +1692,7 @@ def initialize(self, **kwargs): self._step_time = start self._step_indicator_attrs = indicator_attrs - def next(self, done:bool|None=None, **kwargs): + def next(self, done:bool|None=None, **kwargs) -> None|pd.Series: """ Move the backtest one time step forward and return the results for the current step. @@ -1718,6 +1718,7 @@ def next(self, done:bool|None=None, **kwargs): pass # Next tick, a moment before bar close + # passing kwargs to be used in the strategy class self._step_strategy.next(**kwargs) self._step_time += 1 From d7f6046bafe486d3d5a6f63a6bb0f731940caa8b Mon Sep 17 00:00:00 2001 From: ipergiove Date: Thu, 25 Jan 2024 17:39:23 +0100 Subject: [PATCH 5/5] action iin test strategy --- backtesting/test/_iteration.py | 43 +++++++++++++++------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/backtesting/test/_iteration.py b/backtesting/test/_iteration.py index 85e042a7..0360617c 100644 --- a/backtesting/test/_iteration.py +++ b/backtesting/test/_iteration.py @@ -3,37 +3,32 @@ from backtesting.test import GOOG, SMA from backtesting.lib import crossover import types +import random +random.seed(0) -class SmaCross(Strategy): - # Define the two MA lags as *class variables* - # for later optimization - n1 = 10 - n2 = 20 - +class TestStrategy(Strategy): def init(self): - # Precompute the two moving averages - self.sma1 = self.I(SMA, self.data.Close, self.n1) - self.sma2 = self.I(SMA, self.data.Close, self.n2) - - def next(self): - # If sma1 crosses above sma2, close any existing - # short trades, and buy the asset - if crossover(self.sma1, self.sma2): - self.position.close() - self.buy() + print("Init", self.equity) + + def next(self, action=None): + # uncomment if you want to test run() + # if not action: + # action = random.randint(0, 1) + if action!=None: + if action == 0: + self.buy() + elif action == 1: + self.position.close() - # Else, if sma1 crosses below sma2, close any existing - # long trades, and sell the asset - elif crossover(self.sma2, self.sma1): - self.position.close() - self.sell() -bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002) +bt = Backtest(GOOG, TestStrategy, cash=10_000, commission=.002) + # stats = bt.run() -bt.initialize() +bt.initialize() while True: - stats = bt.next() + action = random.randint(0, 1) + stats = bt.next(action=action) if not isinstance(stats, types.NoneType): break print(stats)