From 03b004bfbe9e84a4c44d48cf3812f5ac2dd1e63c Mon Sep 17 00:00:00 2001 From: Lachyn Almazova Date: Sun, 11 Feb 2024 23:29:21 -0500 Subject: [PATCH 1/4] Updated _trades dataframe with 1D Indicator variables for Entry and Exit bars --- backtesting/_stats.py | 18 ++++++++++++++++++ backtesting/backtesting.py | 8 ++++++++ test.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 test.py diff --git a/backtesting/_stats.py b/backtesting/_stats.py index f2bb4f7c..26141025 100644 --- a/backtesting/_stats.py +++ b/backtesting/_stats.py @@ -67,6 +67,24 @@ def compute_stats( 'ExitTime': [t.exit_time for t in trades], 'Tag': [t.tag for t in trades], }) + + # Retrieve the DataFrame of all indicators from the strategy instance + indicators_df = strategy_instance.get_indicators_dataframe() + + # Iterate over the trades and get the indicator values + for i, trade in trades_df.iterrows(): + entry_bar = trade['EntryBar'] + exit_bar = trade['ExitBar'] + + # Get the indicators at the entry and exit bars + entry_indicators = indicators_df.loc[ohlc_data.index[entry_bar]] + exit_indicators = indicators_df.loc[ohlc_data.index[exit_bar]] + + # Add the indicator values to the trades_df + for column in entry_indicators.index: + trades_df.at[i, f'Entry_{column}'] = entry_indicators[column] + trades_df.at[i, f'Exit_{column}'] = exit_indicators[column] + trades_df['Duration'] = trades_df['ExitTime'] - trades_df['EntryTime'] del trades diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 9c168703..b0bde2e8 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -74,6 +74,14 @@ def _check_params(self, params): "can be optimized or run with.") setattr(self, k, v) return params + + def get_indicators_dataframe(self): + """ + Compile all indicator arrays into a DataFrame with the same index as the strategy data. + """ + + indicators_dict = {ind.name: ind for ind in self._indicators} + return pd.DataFrame(indicators_dict, index=self._data.index) def I(self, # noqa: E743 func: Callable, *args, diff --git a/test.py b/test.py new file mode 100644 index 00000000..aede446b --- /dev/null +++ b/test.py @@ -0,0 +1,28 @@ +from backtesting import Backtest, Strategy +from backtesting.lib import crossover + +from backtesting.test import SMA, GOOG + + +class SmaCross(Strategy): + n1 = 10 + n2 = 20 + + def init(self): + close = self.data.Close + self.sma1 = self.I(SMA, close, self.n1) + self.sma2 = self.I(SMA, close, self.n2) + + def next(self): + if crossover(self.sma1, self.sma2): + self.buy() + elif crossover(self.sma2, self.sma1): + self.sell() + + +bt = Backtest(GOOG, SmaCross, + cash=10000, commission=.002, + exclusive_orders=True) + +output = bt.run() +print(output['_trades']) \ No newline at end of file From 6d4c70cd18133fdb616371ba1a92ef0eb0475c00 Mon Sep 17 00:00:00 2001 From: Kernc Date: Wed, 22 Jan 2025 04:49:20 +0100 Subject: [PATCH 2/4] Shorter form, vectorized over index --- backtesting/_stats.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/backtesting/_stats.py b/backtesting/_stats.py index ac337958..404b3087 100644 --- a/backtesting/_stats.py +++ b/backtesting/_stats.py @@ -71,26 +71,14 @@ def compute_stats( 'EntryTime': [t.entry_time for t in trades], 'ExitTime': [t.exit_time for t in trades], }) - - # Retrieve the DataFrame of all indicators from the strategy instance - indicators_df = strategy_instance.get_indicators_dataframe() - - # Iterate over the trades and get the indicator values - for i, trade in trades_df.iterrows(): - entry_bar = trade['EntryBar'] - exit_bar = trade['ExitBar'] - - # Get the indicators at the entry and exit bars - entry_indicators = indicators_df.loc[ohlc_data.index[entry_bar]] - exit_indicators = indicators_df.loc[ohlc_data.index[exit_bar]] - - # Add the indicator values to the trades_df - for column in entry_indicators.index: - trades_df.at[i, f'Entry_{column}'] = entry_indicators[column] - trades_df.at[i, f'Exit_{column}'] = exit_indicators[column] - trades_df['Duration'] = trades_df['ExitTime'] - trades_df['EntryTime'] trades_df['Tag'] = [t.tag for t in trades] + + # Add indicator values + for ind in strategy_instance._indicators: + trades_df[f'Entry_{ind.name}'] = ind[trades_df['EntryBar'].values] + trades_df[f'Exit_{ind.name}'] = ind[trades_df['ExitBar'].values] + commissions = sum(t._commissions for t in trades) del trades From e91103b73a910262b59a2960579be1d74275e0f6 Mon Sep 17 00:00:00 2001 From: Kernc Date: Wed, 22 Jan 2025 04:49:50 +0100 Subject: [PATCH 3/4] Remove Strategy.get_indicators_dataframe() public helper method --- backtesting/backtesting.py | 8 -------- backtesting/test/_test.py | 7 ++++++- test.py | 28 ---------------------------- 3 files changed, 6 insertions(+), 37 deletions(-) delete mode 100644 test.py diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index bfb1bcd0..c3a6e0d5 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -77,14 +77,6 @@ def _check_params(self, params): "can be optimized or run with.") setattr(self, k, v) return params - - def get_indicators_dataframe(self): - """ - Compile all indicator arrays into a DataFrame with the same index as the strategy data. - """ - - indicators_dict = {ind.name: ind for ind in self._indicators} - return pd.DataFrame(indicators_dict, index=self._data.index) def I(self, # noqa: E743 func: Callable, *args, diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index d5373f1a..30fdb8a6 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -333,10 +333,15 @@ def almost_equal(a, b): self.assertEqual(len(stats['_trades']), 66) + indicator_columns = [ + f'{entry}_SMA(C,{n})' + for entry in ('Entry', 'Exit') + for n in (SmaCross.fast, SmaCross.slow)] self.assertSequenceEqual( sorted(stats['_trades'].columns), sorted(['Size', 'EntryBar', 'ExitBar', 'EntryPrice', 'ExitPrice', 'SL', 'TP', - 'PnL', 'ReturnPct', 'EntryTime', 'ExitTime', 'Duration', 'Tag'])) + 'PnL', 'ReturnPct', 'EntryTime', 'ExitTime', 'Duration', 'Tag', + *indicator_columns])) def test_compute_stats_bordercase(self): diff --git a/test.py b/test.py deleted file mode 100644 index aede446b..00000000 --- a/test.py +++ /dev/null @@ -1,28 +0,0 @@ -from backtesting import Backtest, Strategy -from backtesting.lib import crossover - -from backtesting.test import SMA, GOOG - - -class SmaCross(Strategy): - n1 = 10 - n2 = 20 - - def init(self): - close = self.data.Close - self.sma1 = self.I(SMA, close, self.n1) - self.sma2 = self.I(SMA, close, self.n2) - - def next(self): - if crossover(self.sma1, self.sma2): - self.buy() - elif crossover(self.sma2, self.sma1): - self.sell() - - -bt = Backtest(GOOG, SmaCross, - cash=10000, commission=.002, - exclusive_orders=True) - -output = bt.run() -print(output['_trades']) \ No newline at end of file From 3461dcfcf0eca2be96b0cc5f8bb1b560f74b9249 Mon Sep 17 00:00:00 2001 From: Kernc Date: Wed, 22 Jan 2025 05:06:25 +0100 Subject: [PATCH 4/4] Account for multi-dim indicators and/or no trades --- backtesting/_stats.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backtesting/_stats.py b/backtesting/_stats.py index 404b3087..c1d45cae 100644 --- a/backtesting/_stats.py +++ b/backtesting/_stats.py @@ -75,9 +75,13 @@ def compute_stats( trades_df['Tag'] = [t.tag for t in trades] # Add indicator values - for ind in strategy_instance._indicators: - trades_df[f'Entry_{ind.name}'] = ind[trades_df['EntryBar'].values] - trades_df[f'Exit_{ind.name}'] = ind[trades_df['ExitBar'].values] + if len(trades_df): + for ind in strategy_instance._indicators: + ind = np.atleast_2d(ind) + for i, values in enumerate(ind): # multi-d indicators + suffix = f'_{i}' if len(ind) > 1 else '' + trades_df[f'Entry_{ind.name}{suffix}'] = values[trades_df['EntryBar'].values] + trades_df[f'Exit_{ind.name}{suffix}'] = values[trades_df['ExitBar'].values] commissions = sum(t._commissions for t in trades) del trades