Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions backtesting/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,12 @@ def resample_apply(rule: str,
which is suitable for closing prices,
but you might prefer another (e.g. `"max"` for peaks, or similar).

Finally, any `*args` and `**kwargs` that are not already eaten by
implicit `backtesting.backtesting.Strategy.I` call
are passed to `func`.
Additional `*args` that are pandas objects or
`backtesting.backtesting.Strategy.data` arrays sharing `series`' index
are resampled alongside `series`, using the default `OHLCV_AGG` rule for
each input. Other `*args` and any `**kwargs` that are not already eaten by
implicit `backtesting.backtesting.Strategy.I` call are passed to `func`
unchanged.

For example, if we have a typical moving average function
`SMA(values, lookback_period)`, _hourly_ data source, and need to
Expand Down Expand Up @@ -292,13 +295,29 @@ def func(x, *_, **__):
'or a `Strategy.data.*` array'
series = series.s

if agg is None:
agg = OHLCV_AGG.get(getattr(series, 'name', ''), 'last')
if isinstance(series, pd.DataFrame):
agg = {column: OHLCV_AGG.get(column, 'last')
for column in series.columns}

resampled = series.resample(rule, label='right').agg(agg).dropna()
def _default_agg(data):
if isinstance(data, pd.DataFrame):
return {column: OHLCV_AGG.get(column, 'last')
for column in data.columns}
return OHLCV_AGG.get(getattr(data, 'name', ''), 'last')

resampled_args = list(args)
indexed_args = []
for i, arg in enumerate(args):
data = arg.s if isinstance(arg, _Array) else arg
if (isinstance(data, (pd.Series, pd.DataFrame)) and
data.index.equals(series.index)):
indexed_args.append((i, data.resample(rule, label='right').agg(
_default_agg(data))))

resampled = series.resample(rule, label='right').agg(
_default_agg(series) if agg is None else agg)
valid_index = resampled.dropna().index
for _, data in indexed_args:
valid_index = valid_index[valid_index.isin(data.dropna().index)]
resampled = resampled.loc[valid_index]
for i, data in indexed_args:
resampled_args[i] = data.loc[valid_index]
resampled.name = _as_str(series) + '[' + rule + ']'

# Check first few stack frames if we are being called from
Expand Down Expand Up @@ -331,7 +350,7 @@ def wrap_func(resampled, *args, **kwargs):

wrap_func.__name__ = func.__name__

array = strategy_I(wrap_func, resampled, *args, **kwargs)
array = strategy_I(wrap_func, resampled, *resampled_args, **kwargs)
return array


Expand Down
29 changes: 29 additions & 0 deletions backtesting/test/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,35 @@ def resets_index(*args):
res3 = resample_apply('D', lambda df: (df.Close, df.Close), EURUSD)
self.assertIsInstance(res3, pd.DataFrame)

def test_resample_apply_multiple_series(self):
data = EURUSD.iloc[:500]

def atr(high, low, close, periods):
self.assertTrue(high.index.equals(low.index))
self.assertTrue(high.index.equals(close.index))
previous_close = close.shift(1)
true_range = pd.concat([
high - low,
(high - previous_close).abs(),
(low - previous_close).abs(),
], axis=1).max(axis=1)
return true_range.rolling(periods).mean()

class MultiInputStrategy(Strategy):
def init(self):
self.atr = resample_apply(
'D', atr, self.data.High, self.data.Low, self.data.Close, 3)

def next(self):
pass

strategy = Backtest(data, MultiInputStrategy).run()._strategy
daily = data.resample('D', label='right').agg(OHLCV_AGG).dropna()
expected = atr(daily.High, daily.Low, daily.Close, 3)
expected = expected.reindex(
data.index.union(daily.index), method='ffill').reindex(data.index)
np.testing.assert_allclose(strategy.atr, expected, equal_nan=True)

def test_plot_heatmaps(self):
bt = Backtest(GOOG, SmaCross)
stats, heatmap = bt.optimize(fast=range(2, 7, 2),
Expand Down