def _maybe_resample_data(resample_rule, df, indicators, equity_data, trades): if isinstance(resample_rule, str): freq = resample_rule else: if len(df) < _MAX_CANDLES: return df, indicators, equity_data, trades from_index = dict(day=-2, hour=-6, minute=1, second=0, millisecond=0, microsecond=0, nanosecond=0)[df.index.resolution] FREQS = ('1T', '5T', '10T', '15T', '30T', '1H', '2H', '4H', '8H', '1D', '1W', '1M') freq = next((f for f in FREQS[from_index:] if len(df.resample(f)) <= _MAX_CANDLES), FREQS[-1]) warnings.warn("Data contains too many candlesticks to plot; downsampling to {!r}. " "See `Backtest.plot(resample=...)`".format(freq)) from .lib import OHLCV_AGG, TRADES_AGG, _EQUITY_AGG df = df.resample(freq, label='right').agg(OHLCV_AGG).dropna() indicators = [_Indicator(i.df.resample(freq, label='right').mean() .dropna().reindex(df.index).values.T, **dict(i._opts, name=i.name, # HACK: override `data` for its index data=pd.Series(np.nan, index=df.index))) for i in indicators] assert not indicators or indicators[0].df.index.equals(df.index) equity_data = equity_data.resample(freq, label='right').agg(_EQUITY_AGG).dropna(how='all') assert equity_data.index.equals(df.index) def _weighted_returns(s, trades=trades): df = trades.loc[s.index] return ((df['Size'].abs() * df['ReturnPct']) / df['Size'].abs().sum()).sum() def _group_trades(column): def f(s, new_index=df.index.astype(np.int64), bars=trades[column]): if s.size: # Via int64 because on pandas recently broken datetime mean_time = int(bars.loc[s.index].view('i8').mean()) new_bar_idx = new_index.get_loc(mean_time, method='nearest') return new_bar_idx return f if len(trades): # Avoid pandas "resampling on Int64 index" error trades = trades.assign(count=1).resample(freq, on='ExitTime', label='right').agg(dict( TRADES_AGG, ReturnPct=_weighted_returns, count='sum', EntryBar=_group_trades('EntryTime'), ExitBar=_group_trades('ExitTime'), )).dropna() return df, indicators, equity_data, trades
def test_as_str(self): def func(): pass class Class: pass self.assertEqual(_as_str('4'), '4') self.assertEqual(_as_str(4), '4') self.assertEqual(_as_str(_Indicator([1, 2], name='x')), 'x') self.assertEqual(_as_str(func), 'func') self.assertEqual(_as_str(Class), 'Class') self.assertEqual(_as_str(lambda x: x), '') for s in ('Open', 'High', 'Low', 'Close'): self.assertEqual(_as_str(_Array([1], name=s)), s[0])
def test_as_str(self): def func(): pass class Class: def __call__(self): pass self.assertEqual(_as_str('4'), '4') self.assertEqual(_as_str(4), '4') self.assertEqual(_as_str(_Indicator([1, 2], name='x')), 'x') self.assertEqual(_as_str(func), 'func') self.assertEqual(_as_str(Class), 'Class') self.assertEqual(_as_str(Class()), 'Class') self.assertEqual(_as_str(pd.Series([1, 2], name='x')), 'x') self.assertEqual(_as_str(pd.DataFrame()), 'df') self.assertEqual(_as_str(lambda x: x), 'λ') for s in ('Open', 'High', 'Low', 'Close', 'Volume'): self.assertEqual(_as_str(_Array([1], name=s)), s[0])
def _maybe_resample_data(resample_rule, df, indicators, equity_data, trades): if isinstance(resample_rule, str): freq = resample_rule else: if resample_rule is False or len(df) <= _MAX_CANDLES: return df, indicators, equity_data, trades minutes_to_freq = { 1: "1T", 5: "5T", 10: "10T", 15: "15T", 30: "30T", 60: "1H", 120: "2H", 240: "4H", 480: "8H", 1440: "1D", 10080: "1W", 43800: "1M", } index_timedelta = df.index[-1] - df.index[0] req_minutes = (index_timedelta / 10000).total_seconds() // 60 freq = [v for k, v in minutes_to_freq.items() if k >= req_minutes] freq = freq[0] if freq else "1M" warnings.warn( f'Data contains too many candlesticks to plot; downsampling to {freq!r}. ' 'See `Backtest.plot(resample=...)`') from .lib import TRADES_AGG, _EQUITY_AGG from backtesting.ohlc_helpers import OHLCV_AGG df = df.resample(freq, label='right').agg(OHLCV_AGG).dropna() indicators = [ _Indicator( i.df.resample(freq, label='right').mean().dropna().reindex( df.index).values.T, **dict( i._opts, name=i.name, # Replace saved index with the resampled one index=df.index), ) for i in indicators ] assert not indicators or indicators[0].df.index.equals(df.index) equity_data = equity_data.resample( freq, label='right').agg(_EQUITY_AGG).dropna(how='all') assert equity_data.index.equals(df.index) def _weighted_returns(s, trades=trades): df = trades.loc[s.index] return ((df['Size'].abs() * df['ReturnPct']) / df['Size'].abs().sum()).sum() def _group_trades(column): def f(s, new_index=df.index.astype(np.int64), bars=trades[column]): if s.size: # Via int64 because on pandas recently broken datetime mean_time = int(bars.loc[s.index].view('i8').mean()) new_bar_idx = new_index.get_loc(mean_time, method='nearest') return new_bar_idx return f if len(trades): # Avoid pandas "resampling on Int64 index" error trades = (trades.assign(count=1).resample( freq, on='ExitTime', label='right').agg( dict( TRADES_AGG, ReturnPct=_weighted_returns, count='sum', EntryBar=_group_trades('EntryTime'), ExitBar=_group_trades('ExitTime'), )).dropna()) return df, indicators, equity_data, trades
def _maybe_resample_data(resample_rule, df, indicators, equity_data, trades): if isinstance(resample_rule, str): freq = resample_rule else: if resample_rule is False or len(df) <= _MAX_CANDLES: return df, indicators, equity_data, trades freq_minutes = pd.Series({ "1T": 1, "5T": 5, "10T": 10, "15T": 15, "30T": 30, "1H": 60, "2H": 60 * 2, "4H": 60 * 4, "8H": 60 * 8, "1D": 60 * 24, "1W": 60 * 24 * 7, "1M": np.inf, }) timespan = df.index[-1] - df.index[0] require_minutes = (timespan / _MAX_CANDLES).total_seconds() // 60 freq = freq_minutes.where( freq_minutes >= require_minutes).first_valid_index() warnings.warn( f"Data contains too many candlesticks to plot; downsampling to {freq!r}. " "See `Backtest.plot(resample=...)`") from .lib import OHLCV_AGG, TRADES_AGG, _EQUITY_AGG df = df.resample(freq, label='right').agg(OHLCV_AGG).dropna() indicators = [ _Indicator( i.df.resample(freq, label='right').mean().dropna().reindex( df.index).values.T, **dict( i._opts, name=i.name, # Replace saved index with the resampled one index=df.index)) for i in indicators ] assert not indicators or indicators[0].df.index.equals(df.index) equity_data = equity_data.resample( freq, label='right').agg(_EQUITY_AGG).dropna(how='all') assert equity_data.index.equals(df.index) def _weighted_returns(s, trades=trades): df = trades.loc[s.index] return ((df['Size'].abs() * df['ReturnPct']) / df['Size'].abs().sum()).sum() def _group_trades(column): def f(s, new_index=pd.Index(df.index.view(int)), bars=trades[column]): if s.size: # Via int64 because on pandas recently broken datetime mean_time = int(bars.loc[s.index].view(int).mean()) new_bar_idx = new_index.get_loc(mean_time, method='nearest') return new_bar_idx return f if len(trades): # Avoid pandas "resampling on Int64 index" error trades = trades.assign(count=1).resample( freq, on='ExitTime', label='right').agg( dict( TRADES_AGG, ReturnPct=_weighted_returns, count='sum', EntryBar=_group_trades('EntryTime'), ExitBar=_group_trades('ExitTime'), )).dropna() return df, indicators, equity_data, trades
def I( self, func: Callable, *args, name=None, plot=True, overlay=None, color=None, scatter=False, **kwargs, ) -> np.ndarray: if name is None: params = ','.join(filter(None, map(_as_str, chain(args, kwargs.values())))) func_name = _as_str(func) name = f'{func_name}({params})' if params else f'{func_name}' else: name = name.format( *map(_as_str, args), **dict(zip(kwargs.keys(), map(_as_str, kwargs.values()))), ) try: value = func(*args, **kwargs) except Exception as e: raise RuntimeError(f'I "{name}" errored with exception: {e}') if isinstance(value, pd.DataFrame): value = value.values.T if value is not None: value = try_(lambda: np.asarray(value, order='C'), None) is_arraylike = value is not None # Optionally flip the array if the user returned e.g. `df.values` if is_arraylike and np.argmax(value.shape) == 0: value = value.T if not is_arraylike or not 1 <= value.ndim <= 2 or value.shape[-1] != len(self._data.Close): raise ValueError( 'Indicators must return (optionally a tuple of) numpy.arrays of same ' f'length as `data` (data shape: {self._data.Close.shape}; indicator "{name}"' f'shape: {getattr(value, "shape" , "")}, returned value: {value})' ) if plot and overlay is None and np.issubdtype(value.dtype, np.number): x = value / self._data.Close # By default, overlay if strong majority of indicator values # is within 30% of Close with np.errstate(invalid='ignore'): overlay = ((x < 1.4) & (x > 0.6)).mean() > 0.6 value = _Indicator( value, name=name, plot=plot, overlay=overlay, color=color, scatter=scatter, index=self.data.index, ) self._indicators.append(value) return value