Initial support for backtesting with short

This commit is contained in:
Matthias 2021-08-23 21:15:56 +02:00
parent 3e8164bfca
commit 7373b39015

View File

@ -37,13 +37,16 @@ logger = logging.getLogger(__name__)
# Indexes for backtest tuples # Indexes for backtest tuples
DATE_IDX = 0 DATE_IDX = 0
BUY_IDX = 1 OPEN_IDX = 1
OPEN_IDX = 2 HIGH_IDX = 2
CLOSE_IDX = 3 LOW_IDX = 3
SELL_IDX = 4 CLOSE_IDX = 4
LOW_IDX = 5 BUY_IDX = 5
HIGH_IDX = 6 SELL_IDX = 6
BUY_TAG_IDX = 7 SHORT_IDX = 7
ESHORT_IDX = 8
BUY_TAG_IDX = 9
SHORT_TAG_IDX = 10
class Backtesting: class Backtesting:
@ -215,7 +218,8 @@ class Backtesting:
""" """
# Every change to this headers list must evaluate further usages of the resulting tuple # Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top # and eventually change the constants for indexes at the top
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short']
data: Dict = {} data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed)) self.progress.init_step(BacktestState.CONVERT, len(processed))
@ -223,13 +227,21 @@ class Backtesting:
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
self.check_abort() self.check_abort()
self.progress.increment() self.progress.increment()
has_buy_tag = 'buy_tag' in pair_data has_buy_tag = 'long_tag' in pair_data
headers = headers + ['buy_tag'] if has_buy_tag else headers has_short_tag = 'short_tag' in pair_data
headers = headers + ['long_tag'] if has_buy_tag else headers
headers = headers + ['short_tag'] if has_short_tag else headers
if not pair_data.empty: if not pair_data.empty:
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist # Cleanup from prior runs
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist pair_data.loc[:, 'buy'] = 0 # TODO: Should be renamed to enter_long
pair_data.loc[:, 'enter_short'] = 0
pair_data.loc[:, 'sell'] = 0 # TODO: should be renamed to exit_long
pair_data.loc[:, 'exit_short'] = 0
# pair_data.loc[:, 'sell'] = 0
if has_buy_tag: if has_buy_tag:
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist pair_data.loc[:, 'long_tag'] = None # cleanup if buy_tag is exist
if has_short_tag:
pair_data.loc[:, 'short_tag'] = None # cleanup if short_tag is exist
df_analyzed = self.strategy.advise_sell( df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), self.strategy.advise_buy(pair_data, {'pair': pair}),
@ -240,10 +252,12 @@ class Backtesting:
startup_candles=self.required_startup) startup_candles=self.required_startup)
# To avoid using data from future, we use buy/sell signals shifted # To avoid using data from future, we use buy/sell signals shifted
# from the previous candle # from the previous candle
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'enter_long'] = df_analyzed.loc[:, 'enter_long'].shift(1)
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) df_analyzed.loc[:, 'enter_short'] = df_analyzed.loc[:, 'enter_short'].shift(1)
df_analyzed.loc[:, 'exit_long'] = df_analyzed.loc[:, 'exit_long'].shift(1)
df_analyzed.loc[:, 'exit_short'] = df_analyzed.loc[:, 'exit_short'].shift(1)
if has_buy_tag: if has_buy_tag:
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) df_analyzed.loc[:, 'long_tag'] = df_analyzed.loc[:, 'long_tag'].shift(1)
df_analyzed.drop(df_analyzed.head(1).index, inplace=True) df_analyzed.drop(df_analyzed.head(1).index, inplace=True)
@ -322,7 +336,7 @@ class Backtesting:
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
# TODO: short exits
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX],
sell_row[SELL_IDX], sell_row[SELL_IDX],
@ -349,7 +363,7 @@ class Backtesting:
return None return None
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: def _enter_trade(self, pair: str, row: List, direction: str) -> Optional[LocalTrade]:
try: try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None) stake_amount = self.wallets.get_trade_stake_amount(pair, None)
except DependencyException: except DependencyException:
@ -389,6 +403,7 @@ class Backtesting:
is_open=True, is_open=True,
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
exchange=self._exchange_name, exchange=self._exchange_name,
is_short=(direction == 'short'),
) )
return trade return trade
return None return None
@ -422,6 +437,20 @@ class Backtesting:
self.rejected_trades += 1 self.rejected_trades += 1
return False return False
def check_for_trade_entry(self, row) -> Optional[str]:
enter_long = row[BUY_IDX] == 1
exit_long = row[SELL_IDX] == 1
enter_short = row[SHORT_IDX] == 1
exit_short = row[ESHORT_IDX] == 1
if enter_long == 1 and not any([exit_long, enter_short]):
# Long
return 'long'
if enter_short == 1 and not any([exit_short, enter_long]):
# Short
return 'short'
return None
def backtest(self, processed: Dict, def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime, start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False, max_open_trades: int = 0, position_stacking: bool = False,
@ -482,15 +511,15 @@ class Backtesting:
# without positionstacking, we can only have one open trade per pair. # without positionstacking, we can only have one open trade per pair.
# max_open_trades must be respected # max_open_trades must be respected
# don't open on the last row # don't open on the last row
trade_dir = self.check_for_trade_entry(row)
if ( if (
(position_stacking or len(open_trades[pair]) == 0) (position_stacking or len(open_trades[pair]) == 0)
and self.trade_slot_available(max_open_trades, open_trade_count_start) and self.trade_slot_available(max_open_trades, open_trade_count_start)
and tmp != end_date and tmp != end_date
and row[BUY_IDX] == 1 and trade_dir is not None
and row[SELL_IDX] != 1
and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) and not PairLocks.is_pair_locked(pair, row[DATE_IDX])
): ):
trade = self._enter_trade(pair, row) trade = self._enter_trade(pair, row, trade_dir)
if trade: if trade:
# TODO: hacky workaround to avoid opening > max_open_trades # TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behaviour - not sure if this is correct # This emulates previous behaviour - not sure if this is correct