From e5da1487f7d76f2970a7d2f069189c32230223e6 Mon Sep 17 00:00:00 2001 From: arkershaw <721253+arkershaw@users.noreply.github.com> Date: Sat, 15 Mar 2025 11:13:16 +0700 Subject: [PATCH 01/10] Restore original unit of fractional backtest for results/plot. --- backtesting/lib.py | 54 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/backtesting/lib.py b/backtesting/lib.py index 9079b0ea..1ac751ed 100644 --- a/backtesting/lib.py +++ b/backtesting/lib.py @@ -24,7 +24,7 @@ import numpy as np import pandas as pd -from ._plotting import plot_heatmaps as _plot_heatmaps +from ._plotting import plot_heatmaps as _plot_heatmaps, plot from ._stats import compute_stats as _compute_stats from ._util import SharedMemoryManager, _Array, _as_str, _batch, _tqdm from .backtesting import Backtest, Strategy @@ -537,8 +537,60 @@ def __init__(self, data = data.copy() data[['Open', 'High', 'Low', 'Close']] *= fractional_unit data['Volume'] /= fractional_unit + self._fractional_unit = fractional_unit super().__init__(data, *args, **kwargs) + def run(self, **kwargs) -> pd.Series: + result = super().run(**kwargs) + + trades: pd.DataFrame = result['_trades'] + trades['Size'] *= self._fractional_unit + trades[['EntryPrice', 'ExitPrice', 'TP', 'SL']] /= self._fractional_unit + + indicators = result['_strategy']._indicators + for indicator in indicators: + is_overlay = indicator._opts['overlay'] + if np.all(is_overlay): + indicator /= self._fractional_unit + + return result + + def plot(self, *, results: pd.Series = None, filename=None, plot_width=None, + plot_equity=True, plot_return=False, plot_pl=True, + plot_volume=True, plot_drawdown=False, plot_trades=True, + smooth_equity=False, relative_equity=True, + superimpose: Union[bool, str] = True, + resample=True, reverse_indicators=False, + show_legend=True, open_browser=True): + + data = self._data.copy() + data[['Open', 'High', 'Low', 'Close']] /= self._fractional_unit + data['Volume'] *= self._fractional_unit + + if results is None: + if self._results is None: + raise RuntimeError('First issue `backtest.run()` to obtain results.') + results = self._results + + return plot( + results=results, + df=data, + indicators=results._strategy._indicators, + filename=filename, + plot_width=plot_width, + plot_equity=plot_equity, + plot_return=plot_return, + plot_pl=plot_pl, + plot_volume=plot_volume, + plot_drawdown=plot_drawdown, + plot_trades=plot_trades, + smooth_equity=smooth_equity, + relative_equity=relative_equity, + superimpose=superimpose, + resample=resample, + reverse_indicators=reverse_indicators, + show_legend=show_legend, + open_browser=open_browser) # Prevent pdoc3 documenting __init__ signature of Strategy subclasses for cls in list(globals().values()): From 9092e191d4d3e3c4202d9b9c366ef6948089a4b3 Mon Sep 17 00:00:00 2001 From: arkershaw <721253+arkershaw@users.noreply.github.com> Date: Sat, 15 Mar 2025 11:13:39 +0700 Subject: [PATCH 02/10] Add pytest requirement. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index b0ad94c5..ea9a7a07 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ 'jupyter_client', # for nbconvert ], 'test': [ + 'pytest', 'matplotlib', 'scikit-learn', 'sambo', From 45f78b85fc706f44470e8ddb077cf6b172a8a9f1 Mon Sep 17 00:00:00 2001 From: arkershaw <721253+arkershaw@users.noreply.github.com> Date: Sat, 15 Mar 2025 12:11:18 +0700 Subject: [PATCH 03/10] Add modified values to test. --- backtesting/lib.py | 1 + backtesting/test/_test.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/backtesting/lib.py b/backtesting/lib.py index 1ac751ed..b55ae71e 100644 --- a/backtesting/lib.py +++ b/backtesting/lib.py @@ -592,6 +592,7 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None, show_legend=show_legend, open_browser=open_browser) + # Prevent pdoc3 documenting __init__ signature of Strategy subclasses for cls in list(globals().values()): if isinstance(cls, type) and issubclass(cls, Strategy): diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index eeb46d40..ca3f7e10 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -932,6 +932,12 @@ def test_FractionalBacktest(self): ubtc_bt = FractionalBacktest(BTCUSD['2015':], SmaCross, fractional_unit=1/1e6, cash=100) stats = ubtc_bt.run(fast=2, slow=3) self.assertEqual(stats['# Trades'], 41) + trades = stats['_trades'] + self.assertEqual(len(trades), 41) + first_trade = trades[['Size', 'EntryPrice', 'ExitPrice']].head(1) + self.assertEqual(first_trade['Size'][0], -0.422493) # Fractional value -422493 + self.assertAlmostEqual(first_trade['EntryPrice'][0], 236.69) # Fractional value 0.000236689 + self.assertAlmostEqual(first_trade['ExitPrice'][0], 261.7) # Fractional value 0.000261699 def test_MultiBacktest(self): btm = MultiBacktest([GOOG, EURUSD, BTCUSD], SmaCross, cash=100_000) From ec64e4779d1db644140a888169a2e4266bc21cc5 Mon Sep 17 00:00:00 2001 From: arkershaw <721253+arkershaw@users.noreply.github.com> Date: Sat, 22 Mar 2025 13:24:08 +0700 Subject: [PATCH 04/10] Revert "Add pytest requirement." This reverts commit 9092e191d4d3e3c4202d9b9c366ef6948089a4b3. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index ea9a7a07..b0ad94c5 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,6 @@ 'jupyter_client', # for nbconvert ], 'test': [ - 'pytest', 'matplotlib', 'scikit-learn', 'sambo', From 1b49e0a9760ed4e0dffe7e9b9e2ed1e41e6febdd Mon Sep 17 00:00:00 2001 From: arkershaw <721253+arkershaw@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:55:26 +0700 Subject: [PATCH 05/10] Add test for indicator scaling. --- backtesting/lib.py | 3 +-- backtesting/test/_test.py | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backtesting/lib.py b/backtesting/lib.py index b55ae71e..9b17ac2a 100644 --- a/backtesting/lib.py +++ b/backtesting/lib.py @@ -549,8 +549,7 @@ def run(self, **kwargs) -> pd.Series: indicators = result['_strategy']._indicators for indicator in indicators: - is_overlay = indicator._opts['overlay'] - if np.all(is_overlay): + if indicator._opts['overlay']: indicator /= self._fractional_unit return result diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index ca3f7e10..2bfecf42 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -934,10 +934,14 @@ def test_FractionalBacktest(self): self.assertEqual(stats['# Trades'], 41) trades = stats['_trades'] self.assertEqual(len(trades), 41) - first_trade = trades[['Size', 'EntryPrice', 'ExitPrice']].head(1) + first_trade = trades[['Size', 'EntryPrice', 'ExitPrice', 'EntryBar']].head(1) self.assertEqual(first_trade['Size'][0], -0.422493) # Fractional value -422493 self.assertAlmostEqual(first_trade['EntryPrice'][0], 236.69) # Fractional value 0.000236689 self.assertAlmostEqual(first_trade['ExitPrice'][0], 261.7) # Fractional value 0.000261699 + indicators = stats['_strategy']._indicators + self.assertEqual(len(indicators), 2) + self.assertAlmostEqual(indicators[0][first_trade['EntryBar'][0]], 234.14, places=2) # Fractional value 0.000234139 + self.assertAlmostEqual(indicators[1][first_trade['EntryBar'][0]], 237.07, places=2) # Fractional value 0.000237067 def test_MultiBacktest(self): btm = MultiBacktest([GOOG, EURUSD, BTCUSD], SmaCross, cash=100_000) From 70280a2cced250f311e4c33867216a2d0d16c786 Mon Sep 17 00:00:00 2001 From: arkershaw <721253+arkershaw@users.noreply.github.com> Date: Sat, 22 Mar 2025 19:28:01 +0700 Subject: [PATCH 06/10] Test params for fractional backtest plot. --- backtesting/test/_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index 2bfecf42..50c0eb0a 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -647,6 +647,8 @@ def test_file_size(self): def test_params(self): bt = Backtest(GOOG.iloc[:100], SmaCross) bt.run() + fbt = FractionalBacktest(GOOG.iloc[:100], SmaCross, fractional_unit=1/1e6) + fbt.run() with _tempfile() as f: for p in dict(plot_volume=False, # noqa: C408 plot_equity=False, @@ -662,6 +664,7 @@ def test_params(self): show_legend=False).items(): with self.subTest(param=p[0]): bt.plot(**dict([p]), filename=f, open_browser=False) + fbt.plot(**dict([p]), filename=f, open_browser=False) def test_hide_legend(self): bt = Backtest(GOOG.iloc[:100], SmaCross) From d6b0ac88b099861ec196fb76a38e333bb72684cb Mon Sep 17 00:00:00 2001 From: arkershaw <721253+arkershaw@users.noreply.github.com> Date: Sun, 23 Mar 2025 14:38:17 +0700 Subject: [PATCH 07/10] Refactor scaling of OHLC data to avoid duplication. --- backtesting/backtesting.py | 5 ++++- backtesting/lib.py | 43 ++++++-------------------------------- backtesting/test/_test.py | 8 +++++-- 3 files changed, 16 insertions(+), 40 deletions(-) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 25d98f52..7ced15b6 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -1623,6 +1623,9 @@ def _mp_task(arg): for shmem in shm: shmem.close() + def _get_plot_data(self) -> pd.DataFrame: + return self._data + def plot(self, *, results: pd.Series = None, filename=None, plot_width=None, plot_equity=True, plot_return=False, plot_pl=True, plot_volume=True, plot_drawdown=False, plot_trades=True, @@ -1716,7 +1719,7 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None, return plot( results=results, - df=self._data, + df=self._get_plot_data(), indicators=results._strategy._indicators, filename=filename, plot_width=plot_width, diff --git a/backtesting/lib.py b/backtesting/lib.py index 9b17ac2a..84ae0f16 100644 --- a/backtesting/lib.py +++ b/backtesting/lib.py @@ -24,7 +24,7 @@ import numpy as np import pandas as pd -from ._plotting import plot_heatmaps as _plot_heatmaps, plot +from ._plotting import plot_heatmaps as _plot_heatmaps from ._stats import compute_stats as _compute_stats from ._util import SharedMemoryManager, _Array, _as_str, _batch, _tqdm from .backtesting import Backtest, Strategy @@ -554,42 +554,11 @@ def run(self, **kwargs) -> pd.Series: return result - def plot(self, *, results: pd.Series = None, filename=None, plot_width=None, - plot_equity=True, plot_return=False, plot_pl=True, - plot_volume=True, plot_drawdown=False, plot_trades=True, - smooth_equity=False, relative_equity=True, - superimpose: Union[bool, str] = True, - resample=True, reverse_indicators=False, - show_legend=True, open_browser=True): - - data = self._data.copy() - data[['Open', 'High', 'Low', 'Close']] /= self._fractional_unit - data['Volume'] *= self._fractional_unit - - if results is None: - if self._results is None: - raise RuntimeError('First issue `backtest.run()` to obtain results.') - results = self._results - - return plot( - results=results, - df=data, - indicators=results._strategy._indicators, - filename=filename, - plot_width=plot_width, - plot_equity=plot_equity, - plot_return=plot_return, - plot_pl=plot_pl, - plot_volume=plot_volume, - plot_drawdown=plot_drawdown, - plot_trades=plot_trades, - smooth_equity=smooth_equity, - relative_equity=relative_equity, - superimpose=superimpose, - resample=resample, - reverse_indicators=reverse_indicators, - show_legend=show_legend, - open_browser=open_browser) + def _get_plot_data(self) -> pd.DataFrame: + plot_data = self._data.copy() + plot_data[['Open', 'High', 'Low', 'Close']] /= self._fractional_unit + plot_data['Volume'] *= self._fractional_unit + return plot_data # Prevent pdoc3 documenting __init__ signature of Strategy subclasses diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index 50c0eb0a..f0e88823 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -943,8 +943,12 @@ def test_FractionalBacktest(self): self.assertAlmostEqual(first_trade['ExitPrice'][0], 261.7) # Fractional value 0.000261699 indicators = stats['_strategy']._indicators self.assertEqual(len(indicators), 2) - self.assertAlmostEqual(indicators[0][first_trade['EntryBar'][0]], 234.14, places=2) # Fractional value 0.000234139 - self.assertAlmostEqual(indicators[1][first_trade['EntryBar'][0]], 237.07, places=2) # Fractional value 0.000237067 + self.assertAlmostEqual( + indicators[0][first_trade['EntryBar'][0]], 234.14, places=2 + ) # Fractional value 0.000234139 + self.assertAlmostEqual( + indicators[1][first_trade['EntryBar'][0]], 237.07, places=2 + ) # Fractional value 0.000237067 def test_MultiBacktest(self): btm = MultiBacktest([GOOG, EURUSD, BTCUSD], SmaCross, cash=100_000) From 174044451dc5345394f51665766c2e01ce56632f Mon Sep 17 00:00:00 2001 From: Kernc Date: Sun, 30 Mar 2025 08:05:36 +0200 Subject: [PATCH 08/10] REF: Fractionalize data df JIT before bt.run --- backtesting/backtesting.py | 5 +---- backtesting/lib.py | 21 +++++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 7ced15b6..25d98f52 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -1623,9 +1623,6 @@ def _mp_task(arg): for shmem in shm: shmem.close() - def _get_plot_data(self) -> pd.DataFrame: - return self._data - def plot(self, *, results: pd.Series = None, filename=None, plot_width=None, plot_equity=True, plot_return=False, plot_pl=True, plot_volume=True, plot_drawdown=False, plot_trades=True, @@ -1719,7 +1716,7 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None, return plot( results=results, - df=self._get_plot_data(), + df=self._data, indicators=results._strategy._indicators, filename=filename, plot_width=plot_width, diff --git a/backtesting/lib.py b/backtesting/lib.py index 84ae0f16..70866213 100644 --- a/backtesting/lib.py +++ b/backtesting/lib.py @@ -26,7 +26,7 @@ from ._plotting import plot_heatmaps as _plot_heatmaps from ._stats import compute_stats as _compute_stats -from ._util import SharedMemoryManager, _Array, _as_str, _batch, _tqdm +from ._util import SharedMemoryManager, _Array, _as_str, _batch, _tqdm, patch from .backtesting import Backtest, Strategy __pdoc__ = {} @@ -534,14 +534,17 @@ def __init__(self, 'Use `FractionalBacktest(..., fractional_unit=)`.', category=DeprecationWarning, stacklevel=2) fractional_unit = 1 / kwargs.pop('satoshi') - data = data.copy() - data[['Open', 'High', 'Low', 'Close']] *= fractional_unit - data['Volume'] /= fractional_unit self._fractional_unit = fractional_unit - super().__init__(data, *args, **kwargs) + with warnings.catch_warnings(record=True): + warnings.filterwarnings(action='ignore', message='frac') + super().__init__(data, *args, **kwargs) def run(self, **kwargs) -> pd.Series: - result = super().run(**kwargs) + data = self._data.copy() + data[['Open', 'High', 'Low', 'Close']] *= self._fractional_unit + data['Volume'] /= self._fractional_unit + with patch(self, '_data', data): + result = super().run(**kwargs) trades: pd.DataFrame = result['_trades'] trades['Size'] *= self._fractional_unit @@ -554,12 +557,6 @@ def run(self, **kwargs) -> pd.Series: return result - def _get_plot_data(self) -> pd.DataFrame: - plot_data = self._data.copy() - plot_data[['Open', 'High', 'Low', 'Close']] /= self._fractional_unit - plot_data['Volume'] *= self._fractional_unit - return plot_data - # Prevent pdoc3 documenting __init__ signature of Strategy subclasses for cls in list(globals().values()): From a7c716ae52fafeaec5e42dbbf0e8a60f7a337a20 Mon Sep 17 00:00:00 2001 From: Kernc Date: Sun, 30 Mar 2025 08:26:55 +0200 Subject: [PATCH 09/10] TST: Simplify test --- backtesting/test/_test.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index f0e88823..0ffb78e3 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -937,18 +937,9 @@ def test_FractionalBacktest(self): self.assertEqual(stats['# Trades'], 41) trades = stats['_trades'] self.assertEqual(len(trades), 41) - first_trade = trades[['Size', 'EntryPrice', 'ExitPrice', 'EntryBar']].head(1) - self.assertEqual(first_trade['Size'][0], -0.422493) # Fractional value -422493 - self.assertAlmostEqual(first_trade['EntryPrice'][0], 236.69) # Fractional value 0.000236689 - self.assertAlmostEqual(first_trade['ExitPrice'][0], 261.7) # Fractional value 0.000261699 - indicators = stats['_strategy']._indicators - self.assertEqual(len(indicators), 2) - self.assertAlmostEqual( - indicators[0][first_trade['EntryBar'][0]], 234.14, places=2 - ) # Fractional value 0.000234139 - self.assertAlmostEqual( - indicators[1][first_trade['EntryBar'][0]], 237.07, places=2 - ) # Fractional value 0.000237067 + trade = trades.iloc[0] + self.assertAlmostEqual(trade['EntryPrice'], 236.69) + self.assertAlmostEqual(stats['_strategy']._indicators[0][trade['EntryBar']], 234.14) def test_MultiBacktest(self): btm = MultiBacktest([GOOG, EURUSD, BTCUSD], SmaCross, cash=100_000) From 5aa08c4dd23f989387204e064c817a0c9415b7a2 Mon Sep 17 00:00:00 2001 From: Kernc Date: Sun, 30 Mar 2025 08:35:34 +0200 Subject: [PATCH 10/10] TST: Simplify another test --- backtesting/test/_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index f1e5f6e3..366d54c4 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -649,8 +649,6 @@ def test_file_size(self): def test_params(self): bt = Backtest(GOOG.iloc[:100], SmaCross) bt.run() - fbt = FractionalBacktest(GOOG.iloc[:100], SmaCross, fractional_unit=1/1e6) - fbt.run() with _tempfile() as f: for p in dict(plot_volume=False, # noqa: C408 plot_equity=False, @@ -666,7 +664,6 @@ def test_params(self): show_legend=False).items(): with self.subTest(param=p[0]): bt.plot(**dict([p]), filename=f, open_browser=False) - fbt.plot(**dict([p]), filename=f, open_browser=False) def test_hide_legend(self): bt = Backtest(GOOG.iloc[:100], SmaCross)