From 90e3c387577cbc6992c9e8825dd86b0619a8dbac Mon Sep 17 00:00:00 2001 From: creslinux Date: Sat, 14 Jul 2018 22:54:23 +0000 Subject: [PATCH 01/31] First cut, Bslap science project replacement for freqtrade backtest analysis - appprox 300-500x quicker to execute - fixes stop on close take close price bug in FT Bslap is configurable but by default stops are triggerd on low and pay stop price Not implimented dynamic stops or roi --- freqtrade/optimize/backtesting.py | 495 ++++++++++++++++++++++++++++-- 1 file changed, 463 insertions(+), 32 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 05bcdf4b7..14e4fe1f5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,6 +21,8 @@ from freqtrade.configuration import Configuration from freqtrade.exchange import Exchange from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade +from profilehooks import profile +from freqtrade.strategy.resolver import IStrategy, StrategyResolver logger = logging.getLogger(__name__) @@ -67,6 +69,8 @@ class Backtesting(object): self.exchange = Exchange(self.config) self.fee = self.exchange.get_fee() + self.stop_loss_value = self.analyze.strategy.stoploss + @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: """ @@ -186,6 +190,7 @@ class Backtesting(object): return btr return None + @profile def backtest(self, args: Dict) -> DataFrame: """ Implements backtesting functionality @@ -201,55 +206,481 @@ class Backtesting(object): realistic: do we try to simulate realistic trades? (default: True) :return: DataFrame """ - headers = ['date', 'buy', 'open', 'close', 'sell'] + headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] processed = args['processed'] max_open_trades = args.get('max_open_trades', 0) realistic = args.get('realistic', False) trades = [] trade_count_lock: Dict = {} + ########################### Call out BSlap instead of using FT + bslap_results: list = [] + last_bslap_resultslist = [] for pair, pair_data in processed.items(): - pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run - ticker_data = self.populate_sell_trend( - self.populate_buy_trend(pair_data))[headers].copy() + self.populate_buy_trend(pair_data))[headers].copy() - # to avoid using data from future, we buy/sell with signal from previous candle - ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) - ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) + bslap_pair_results = self.backslap_pair(ticker_data, pair) - ticker_data.drop(ticker_data.head(1).index, inplace=True) + last_bslap_results = bslap_results + bslap_results = last_bslap_results + bslap_pair_results + bslap_results_df = DataFrame(bslap_results) + print(bslap_results_df.dtypes()) - # Convert from Pandas to list for performance reasons - # (Looping Pandas is slow.) - ticker = [x for x in ticker_data.itertuples()] + return bslap_results_df + ########################### Original BT loop + # for pair, pair_data in processed.items(): + # pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run + # + # ticker_data = self.populate_sell_trend( + # self.populate_buy_trend(pair_data))[headers].copy() + # + # # to avoid using data from future, we buy/sell with signal from previous candle + # ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) + # ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) + # + # ticker_data.drop(ticker_data.head(1).index, inplace=True) + # + # # Convert from Pandas to list for performance reasons + # # (Looping Pandas is slow.) + # ticker = [x for x in ticker_data.itertuples()] + # + # lock_pair_until = None + # for index, row in enumerate(ticker): + # if row.buy == 0 or row.sell == 1: + # continue # skip rows where no buy signal or that would immediately sell off + # + # if realistic: + # if lock_pair_until is not None and row.date <= lock_pair_until: + # continue + # if max_open_trades > 0: + # # Check if max_open_trades has already been reached for the given date + # if not trade_count_lock.get(row.date, 0) < max_open_trades: + # continue + # + # trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 + # + # trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:], + # trade_count_lock, args) + # + # + # if trade_entry: + # lock_pair_until = trade_entry.close_time + # trades.append(trade_entry) + # else: + # # Set lock_pair_until to end of testing period if trade could not be closed + # # This happens only if the buy-signal was with the last candle + # lock_pair_until = ticker_data.iloc[-1].date + # + # return DataFrame.from_records(trades, columns=BacktestResult._fields) + ######################## Original BT loop end - lock_pair_until = None - for index, row in enumerate(ticker): - if row.buy == 0 or row.sell == 1: - continue # skip rows where no buy signal or that would immediately sell off + def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int): + import utils_find_1st as utf1st + """ + The purpose of this def is to return the next "buy" = 1 + after t_exit_ind. - if realistic: - if lock_pair_until is not None and row.date <= lock_pair_until: - continue - if max_open_trades > 0: - # Check if max_open_trades has already been reached for the given date - if not trade_count_lock.get(row.date, 0) < max_open_trades: - continue + t_exit_ind is the index the last trade exited on + or 0 if first time around this loop. + """ + t_open_ind: int - trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 + # Create a view on our buy index starting after last trade exit + # Search for next buy + np_buy_arr_v = np_buy_arr[t_exit_ind:] + t_open_ind = utf1st.find_1st(np_buy_arr_v, 1, utf1st.cmp_equal) + t_open_ind = t_open_ind + t_exit_ind # Align numpy index + return t_open_ind - trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:], - trade_count_lock, args) + def backslap_pair(self, ticker_data, pair): + import pandas as pd + import numpy as np + import timeit + import utils_find_1st as utf1st - if trade_entry: - lock_pair_until = trade_entry.close_time - trades.append(trade_entry) + stop = self.stop_loss_value + p_stop = (-stop + 1) # What stop really means, e.g 0.01 is 0.99 of price + + ### backslap debug wrap + debug_2loops = False # only loop twice, for faster debug + debug_timing = False # print timing for each step + debug = False # print values, to check accuracy + if debug: + from pandas import set_option + set_option('display.max_rows', 5000) + set_option('display.max_columns', 8) + pd.set_option('display.width', 1000) + pd.set_option('max_colwidth', 40) + pd.set_option('precision', 12) + + def s(): + st = timeit.default_timer() + return st + + def f(st): + return (timeit.default_timer() - st) + #### backslap config + """ + A couple legacy Pandas vars still used for pretty debug output. + If have debug enabled the code uses these fields for dataframe output + + Ensure bto, sto, sco are aligned with Numpy values next + to align debug and actual. Options are: + buy - open - close - sell - high - low - np_stop_pri + """ + bto = buys_triggered_on = "close" + sto = stops_triggered_on = "low" ## Should be low, FT uses close + sco = stops_calculated_on = "np_stop_pri" ## should use np_stop_pri, FT uses close + ''' + Numpy arrays are used for 100x speed up + We requires setting Int values for + buy stop triggers and stop calculated on + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - stop 6 + ''' + np_buy: int = 0 + np_open: int = 1 + np_close: int = 2 + np_sell: int = 3 + np_high: int = 4 + np_low: int = 5 + np_stop: int = 6 + np_bto: int = np_close # buys_triggered_on - should be close + np_bco: int = np_open # buys calculated on - open of the next candle. + np_sto: int = np_low # stops_triggered_on - Should be low, FT uses close + np_sco: int = np_stop # stops_calculated_on - Should be stop, FT uses close + # + ### End Config + + pair: str = pair + loop: int = 1 + + #ticker_data: DataFrame = ticker_dfs[t_file] + bslap: DataFrame = ticker_data + + # Build a single dimension numpy array from "buy" index for faster search + # (500x faster than pandas) + np_buy_arr = bslap['buy'].values + np_buy_arr_len: int = len(np_buy_arr) + + # use numpy array for faster searches in loop, 20x faster than pandas + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + np_bslap = np.array(bslap[['buy', 'open', 'close', 'sell', 'high', 'low']]) + + loop: int = 0 # how many time around the loop + t_exit_ind = 0 # Start loop from first index + t_exit_last = 0 # To test for exit + + st = s() # Start timer for processing dataframe + if debug: + print('Processing:', pair) + + # Results will be stored in a list of dicts + bslap_pair_results: list = [] + bslap_result: dict = {} + + while t_exit_ind < np_buy_arr_len: + loop = loop + 1 + if debug or debug_timing: + print("----------------------------------------------------------- Loop", loop, pair) + if debug_2loops: + if loop == 4: + break + ''' + Dev phases + Phase 1 + 1) Manage buy, sell, stop enter/exit + a) Find first buy index + b) Discover first stop and sell hit after buy index + c) Chose first intance as trade exit + + Phase 2 + 2) Manage dynamic Stop and ROI Exit + a) Create trade slice from 1 + b) search within trade slice for dynamice stop hit + c) search within trade slice for ROI hit + ''' + + ''' + Finds index for first buy = 1 flag, use .values numpy array for speed + + Create a slice, from first buy index onwards. + Slice will be used to find exit conditions after trade open + ''' + if debug_timing: + st = s() + + t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind) + + if debug_timing: + t_t = f(st) + print("0-numpy", str.format('{0:.17f}', t_t)) + st = s() + + ''' + Calculate np_t_stop_pri (Trade Stop Price) based on the buy price + + As stop in based on buy price we are interested in buy + - Buys are Triggered On np_bto, typically the CLOSE of candle + - Buys are Calculated On np_bco, default is OPEN of the next candle. + as we only see the CLOSE after it has happened. + The assumption is we have bought at first available price, the OPEN + ''' + np_t_stop_pri = (np_bslap[t_open_ind + 1, np_bco] * p_stop) + + if debug_timing: + t_t = f(st) + print("1-numpy", str.format('{0:.17f}', t_t)) + st = s() + + """ + 1)Create a View from our open trade forward + + The view is our search space for the next Stop or Sell + We use a numpy view: + Using a numpy for speed on views, 1,000 faster than pandas + Pandas cannot assure it will always return a view, copies are + 3 orders of magnitude slower + + The view contains columns: + buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + """ + np_t_open_v = np_bslap[t_open_ind:] + + if debug_timing: + t_t = f(st) + print("2-numpy", str.format('{0:.17f}', t_t)) + st = s() + + ''' + Find first stop index after Trade Open: + + First index in np_t_open_v (numpy view of bslap dataframe) + Using a numpy view a orders of magnitude faster + + where [np_sto] (stop tiggered on variable: "close", "low" etc) < np_t_stop_pri + ''' + np_t_stop_ind = utf1st.find_1st(np_t_open_v[:, np_sto], + np_t_stop_pri, + utf1st.cmp_smaller) \ + + t_open_ind + + if debug_timing: + t_t = f(st) + print("3-numpy", str.format('{0:.17f}', t_t)) + st = s() + + ''' + Find first sell index after trade open + + First index in t_open_slice where ['sell'] = 1 + ''' + # Use numpy array for faster search for sell + # Sell uses column 3. + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + # Numpy searches 25-35x quicker than pandas on this data + + np_t_sell_ind = utf1st.find_1st(np_t_open_v[:, np_sell], + 1, utf1st.cmp_equal) \ + + t_open_ind + + if debug_timing: + t_t = f(st) + print("4-numpy", str.format('{0:.17f}', t_t)) + st = s() + + ''' + Determine which was hit first stop or sell, use as exit + + STOP takes priority over SELL as would be 'in candle' from tick data + Sell would use Open from Next candle. + So in a draw Stop would be hit first on ticker data in live + ''' + if np_t_stop_ind <= np_t_sell_ind: + t_exit_ind = np_t_stop_ind # Set Exit row index + t_exit_type = 'stop' # Set Exit type (sell|stop) + np_t_exit_pri = np_sco # The price field our STOP exit will use + else: + # move sell onto next candle, we only look back on sell + # will use the open price later. + t_exit_ind = np_t_sell_ind # Set Exit row index + t_exit_type = 'sell' # Set Exit type (sell|stop) + np_t_exit_pri = np_open # The price field our SELL exit will use + + if debug_timing: + t_t = f(st) + print("5-logic", str.format('{0:.17f}', t_t)) + st = s() + + if debug: + ''' + Print out the buys, stops, sells + Include Line before and after to for easy + Human verification + ''' + # Combine the np_t_stop_pri value to bslap dataframe to make debug + # life easy. This is the currenct stop price based on buy price_ + # Don't care about performance in debug + # (add an extra column if printing as df has date in col1 not in npy) + bslap['np_stop_pri'] = np_t_stop_pri + + # Buy + print("=================== BUY ", pair) + print("Numpy Array BUY Index is:", t_open_ind) + print("DataFrame BUY Index is:", t_open_ind + 1, "displaying DF \n") + print("HINT, BUY trade should use OPEN price from next candle, i.e ", t_open_ind + 2, "\n") + op_is = t_open_ind - 1 # Print open index start, line before + op_if = t_open_ind + 3 # Print open index finish, line after + print(bslap.iloc[op_is:op_if], "\n") + print(bslap.iloc[t_open_ind + 1]['date']) + + # Stop - Stops trigger price sto, and price received sco. (Stop Trigger|Calculated On) + print("=================== STOP ", pair) + print("Numpy Array STOP Index is:", np_t_stop_ind) + print("DataFrame STOP Index is:", np_t_stop_ind + 1, "displaying DF \n") + print("First Stop Index after Trade open in candle", np_t_stop_ind + 1, ": \n", + str.format('{0:.17f}', bslap.iloc[np_t_stop_ind][sto]), + "is less than", str.format('{0:.17f}', np_t_stop_pri)) + print("Tokens will be sold at rate:", str.format('{0:.17f}', bslap.iloc[np_t_stop_ind][sco])) + print("HINT, STOPs should close in-candle, i.e", np_t_stop_ind + 1, + ": As live STOPs are not linked to O-C times") + + st_is = np_t_stop_ind - 1 # Print stop index start, line before + st_if = np_t_stop_ind + 2 # Print stop index finish, line after + print(bslap.iloc[st_is:st_if], "\n") + + # Sell + print("=================== SELL ", pair) + print("Numpy Array SELL Index is:", np_t_sell_ind) + print("DataFrame SELL Index is:", np_t_sell_ind + 1, "displaying DF \n") + print("First Sell Index after Trade open in in candle", np_t_sell_ind + 1) + print("HINT, if exit is SELL (not stop) trade should use OPEN price from next candle", + np_t_sell_ind + 2, "\n") + sl_is = np_t_sell_ind - 1 # Print sell index start, line before + sl_if = np_t_sell_ind + 3 # Print sell index finish, line after + print(bslap.iloc[sl_is:sl_if], "\n") + + # Chosen Exit (stop or sell) + print("=================== EXIT ", pair) + print("Exit type is :", t_exit_type) + # print((bslap.iloc[t_exit_ind], "\n")) + print("trade exit price field is", np_t_exit_pri, "\n") + + ''' + Trade entry is always the next candles "open" price + We trigger on close, so cannot see that till after + its closed. + + The exception to this is a STOP which is calculated in candle + ''' + if debug_timing: + t_t = f(st) + print("6-depra", str.format('{0:.17f}', t_t)) + st = s() + + ## use numpy view "np_t_open_v" for speed. Columns are + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + # exception is 6 which is use the stop value. + + np_trade_enter_price = np_bslap[t_open_ind + 1, np_open] + if t_exit_type == 'stop': + if np_t_exit_pri == 6: + np_trade_exit_price = np_t_stop_pri else: - # Set lock_pair_until to end of testing period if trade could not be closed - # This happens only if the buy-signal was with the last candle - lock_pair_until = ticker_data.iloc[-1].date + np_trade_exit_price = np_bslap[t_exit_ind, np_t_exit_pri] + if t_exit_type == 'sell': + np_trade_exit_price = np_bslap[t_exit_ind + 1, np_t_exit_pri] + + if debug_timing: + t_t = f(st) + print("7-numpy", str.format('{0:.17f}', t_t)) + st = s() + + if debug: + print("//////////////////////////////////////////////") + print("+++++++++++++++++++++++++++++++++ Trade Enter ") + print("np_trade Enterprice is ", str.format('{0:.17f}', np_trade_enter_price)) + print("--------------------------------- Trade Exit ") + print("Trade Exit Type is ", t_exit_type) + print("np_trade Exit Price is", str.format('{0:.17f}', np_trade_exit_price)) + print("//////////////////////////////////////////////") + + # Loop control - catch no closed trades. + if debug: + print("---------------------------------------- end of loop", loop - 1, + " Dataframe Exit Index is: ", t_exit_ind) + print("Exit Index Last, Exit Index Now Are: ", t_exit_last, t_exit_ind) + + if t_exit_last >= t_exit_ind: + """ + When last trade exit equals index of last exit, there is no + opportunity to close any more trades. + + Break loop and go on to next pair. + + TODO + add handing here to record none closed open trades + """ + + if debug: + print(bslap_pair_results) + + break + else: + """ + Add trade to backtest looking results list of dicts + Loop back to look for more trades. + """ + # Index will change if incandle stop or look back as close Open and Sell + if t_exit_type == 'stop': + close_index: int = t_exit_ind + 1 + elif t_exit_type == 'sell': + close_index: int = t_exit_ind + 2 + else: + close_index: int = t_exit_ind + 1 + # Munge the date / delta + start_date: str = bslap.iloc[t_open_ind + 1]['date'] + end_date: str = bslap.iloc[close_index]['date'] + + def __datetime(date_str): + return datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S+00:00') + + # trade_start = __datetime(start_date) + # trade_end = __datetime(end_date) + # trade_mins = (trade_end - trade_start).total_seconds() / 60 + # build trade dictionary + bslap_result["pair"] = pair + bslap_result["profit_percent"] = ( np_trade_exit_price - np_trade_enter_price) // np_trade_enter_price * 100 + bslap_result["profit_abs"] = "" + bslap_result["open_time"] = start_date + bslap_result["close_time"] = end_date + bslap_result["open_index"] = t_open_ind + 1 + bslap_result["close_index"] = close_index + # bslap_result["trade_duration"] = trade_mins + bslap_result["open_at_end"] = False + bslap_result["open_rate"] = str.format('{0:.10f}', np_trade_enter_price) + bslap_result["close_rate"] = str.format('{0:.10f}', np_trade_exit_price) + bslap_result["exit_type"] = t_exit_type + # Add trade dictionary to list + bslap_pair_results.append(bslap_result) + if debug: + print(bslap_pair_results) + + """ + Loop back to start. t_exit_last becomes where loop + will seek to open new trades from. + Push index on 1 to not open on close + """ + t_exit_last = t_exit_ind + 1 + + if debug_timing: + t_t = f(st) + print("8", str.format('{0:.17f}', t_t)) + + # Send back List of trade dicts + return bslap_pair_results + + - return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: """ From 07175ebc5a9d3fc2a068bbd21727f356e00c1930 Mon Sep 17 00:00:00 2001 From: creslinux Date: Sat, 14 Jul 2018 23:45:06 +0000 Subject: [PATCH 02/31] up --- freqtrade/optimize/backtesting.py | 37 ++++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 14e4fe1f5..9110694ce 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -22,7 +22,7 @@ from freqtrade.exchange import Exchange from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from profilehooks import profile -from freqtrade.strategy.resolver import IStrategy, StrategyResolver +from collections import OrderedDict logger = logging.getLogger(__name__) @@ -190,7 +190,7 @@ class Backtesting(object): return btr return None - @profile + def backtest(self, args: Dict) -> DataFrame: """ Implements backtesting functionality @@ -218,15 +218,18 @@ class Backtesting(object): for pair, pair_data in processed.items(): ticker_data = self.populate_sell_trend( self.populate_buy_trend(pair_data))[headers].copy() + ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) + ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) bslap_pair_results = self.backslap_pair(ticker_data, pair) last_bslap_results = bslap_results bslap_results = last_bslap_results + bslap_pair_results - bslap_results_df = DataFrame(bslap_results) - print(bslap_results_df.dtypes()) + #bslap_results_df = DataFrame(bslap_results) - return bslap_results_df + res = DataFrame.from_records(bslap_results, columns=BacktestResult._fields) + print(res) + return res ########################### Original BT loop # for pair, pair_data in processed.items(): # pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run @@ -297,14 +300,15 @@ class Backtesting(object): import numpy as np import timeit import utils_find_1st as utf1st + from datetime import datetime stop = self.stop_loss_value p_stop = (-stop + 1) # What stop really means, e.g 0.01 is 0.99 of price ### backslap debug wrap - debug_2loops = False # only loop twice, for faster debug + debug_2loops = True # only loop twice, for faster debug debug_timing = False # print timing for each step - debug = False # print values, to check accuracy + debug = True # print values, to check accuracy if debug: from pandas import set_option set_option('display.max_rows', 5000) @@ -638,24 +642,25 @@ class Backtesting(object): else: close_index: int = t_exit_ind + 1 # Munge the date / delta - start_date: str = bslap.iloc[t_open_ind + 1]['date'] - end_date: str = bslap.iloc[close_index]['date'] + start_date = bslap.iloc[t_open_ind + 1]['date'] + end_date = bslap.iloc[close_index]['date'] - def __datetime(date_str): - return datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S+00:00') + # def __datetime(date_str): + # return datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S+00:00') + + trade_start = start_date + trade_end = end_date + trade_mins = (trade_end - trade_start).total_seconds() / 60 - # trade_start = __datetime(start_date) - # trade_end = __datetime(end_date) - # trade_mins = (trade_end - trade_start).total_seconds() / 60 # build trade dictionary bslap_result["pair"] = pair - bslap_result["profit_percent"] = ( np_trade_exit_price - np_trade_enter_price) // np_trade_enter_price * 100 + bslap_result["profit_percent"] = ( np_trade_exit_price - np_trade_enter_price)/np_trade_enter_price bslap_result["profit_abs"] = "" bslap_result["open_time"] = start_date bslap_result["close_time"] = end_date bslap_result["open_index"] = t_open_ind + 1 bslap_result["close_index"] = close_index - # bslap_result["trade_duration"] = trade_mins + bslap_result["trade_duration"] = trade_mins bslap_result["open_at_end"] = False bslap_result["open_rate"] = str.format('{0:.10f}', np_trade_enter_price) bslap_result["close_rate"] = str.format('{0:.10f}', np_trade_exit_price) From 71c3106f8f94cd480c404b20d46c8b8abf555142 Mon Sep 17 00:00:00 2001 From: creslinux Date: Sun, 15 Jul 2018 09:30:01 +0000 Subject: [PATCH 03/31] Added ABS and Fees Fixed Index Alignment that was off moving from scratch to FT Fixed Stoploss, its a negative in FT, had been using positve stop -1 in scratch --- freqtrade/optimize/backtesting.py | 77 ++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9110694ce..3cfb588b9 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -214,22 +214,28 @@ class Backtesting(object): trade_count_lock: Dict = {} ########################### Call out BSlap instead of using FT bslap_results: list = [] - last_bslap_resultslist = [] + last_bslap_results: list = [] + for pair, pair_data in processed.items(): ticker_data = self.populate_sell_trend( self.populate_buy_trend(pair_data))[headers].copy() - ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) - ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) + ticker_data.drop(ticker_data.head(1).index, inplace=True) + + # #dump same DFs to disk for offline testing in scratch + # f_pair:str = pair + # csv = f_pair.replace("/", "_") + # csv="/Users/creslin/PycharmProjects/freqtrade_new/frames/" + csv + # ticker_data.to_csv(csv, sep='\t', encoding='utf-8') + + #call bslap - results are a list of dicts bslap_pair_results = self.backslap_pair(ticker_data, pair) - last_bslap_results = bslap_results bslap_results = last_bslap_results + bslap_pair_results - #bslap_results_df = DataFrame(bslap_results) - res = DataFrame.from_records(bslap_results, columns=BacktestResult._fields) - print(res) - return res + bslap_results_df = DataFrame(bslap_results, columns=BacktestResult._fields) + return bslap_results_df + ########################### Original BT loop # for pair, pair_data in processed.items(): # pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run @@ -302,13 +308,28 @@ class Backtesting(object): import utils_find_1st as utf1st from datetime import datetime - stop = self.stop_loss_value - p_stop = (-stop + 1) # What stop really means, e.g 0.01 is 0.99 of price - ### backslap debug wrap debug_2loops = True # only loop twice, for faster debug debug_timing = False # print timing for each step debug = True # print values, to check accuracy + + # Read Stop Loss Values and Stake + stop = self.stop_loss_value + p_stop = (stop + 1) # What stop really means, e.g 0.01 is 0.99 of price + stake = self.config.get('stake_amount') + + # Set fees + # TODO grab these from the environment, do not hard set + # Fees + open_fee = 0.05 + close_fee = 0.05 + + if debug: + print("Stop is ", stop, "value from stragey file") + print("p_stop is", p_stop, "value used to multiply to entry price") + print("Stake is,", stake, "the sum of currency to spend per trade") + print("The open fee is", open_fee, "The close fee is", close_fee) + if debug: from pandas import set_option set_option('display.max_rows', 5000) @@ -385,9 +406,10 @@ class Backtesting(object): while t_exit_ind < np_buy_arr_len: loop = loop + 1 if debug or debug_timing: - print("----------------------------------------------------------- Loop", loop, pair) + print("-- T_exit_Ind - Numpy Index is", t_exit_ind, " ----------------------- Loop", loop, pair) if debug_2loops: - if loop == 4: + if loop == 2: + print("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++Loop debug max met - breaking") break ''' Dev phases @@ -541,10 +563,10 @@ class Backtesting(object): print("=================== STOP ", pair) print("Numpy Array STOP Index is:", np_t_stop_ind) print("DataFrame STOP Index is:", np_t_stop_ind + 1, "displaying DF \n") - print("First Stop Index after Trade open in candle", np_t_stop_ind + 1, ": \n", + print("First Stop after Trade open in candle", t_open_ind + 1, "is ", np_t_stop_ind + 1,": \n", str.format('{0:.17f}', bslap.iloc[np_t_stop_ind][sto]), "is less than", str.format('{0:.17f}', np_t_stop_pri)) - print("Tokens will be sold at rate:", str.format('{0:.17f}', bslap.iloc[np_t_stop_ind][sco])) + print("If stop is first exit match sell rate is :", str.format('{0:.17f}', bslap.iloc[np_t_stop_ind][sco])) print("HINT, STOPs should close in-candle, i.e", np_t_stop_ind + 1, ": As live STOPs are not linked to O-C times") @@ -610,7 +632,7 @@ class Backtesting(object): # Loop control - catch no closed trades. if debug: - print("---------------------------------------- end of loop", loop - 1, + print("---------------------------------------- end of loop", loop, " Dataframe Exit Index is: ", t_exit_ind) print("Exit Index Last, Exit Index Now Are: ", t_exit_last, t_exit_ind) @@ -641,23 +663,26 @@ class Backtesting(object): close_index: int = t_exit_ind + 2 else: close_index: int = t_exit_ind + 1 - # Munge the date / delta - start_date = bslap.iloc[t_open_ind + 1]['date'] - end_date = bslap.iloc[close_index]['date'] + # Munge the date / delta (bt already date formats...just subract) + trade_start = bslap.iloc[t_open_ind + 1]['date'] + trade_end = bslap.iloc[close_index]['date'] # def __datetime(date_str): # return datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S+00:00') - - trade_start = start_date - trade_end = end_date trade_mins = (trade_end - trade_start).total_seconds() / 60 + # Profit ABS. + # sumrecieved((rate * numTokens) * fee) - sumpaid ((rate * numTokens) * fee) + sumpaid: float = (np_trade_enter_price * stake) * open_fee + sumrecieved: float = (np_trade_exit_price * stake) * close_fee + profit_abs: float = sumrecieved - sumpaid + # build trade dictionary bslap_result["pair"] = pair - bslap_result["profit_percent"] = ( np_trade_exit_price - np_trade_enter_price)/np_trade_enter_price - bslap_result["profit_abs"] = "" - bslap_result["open_time"] = start_date - bslap_result["close_time"] = end_date + bslap_result["profit_percent"] = (np_trade_exit_price - np_trade_enter_price) / np_trade_enter_price + bslap_result["profit_abs"] = str.format('{0:.10f}', profit_abs) + bslap_result["open_time"] = trade_start + bslap_result["close_time"] = trade_end bslap_result["open_index"] = t_open_ind + 1 bslap_result["close_index"] = close_index bslap_result["trade_duration"] = trade_mins From 4e68362d46d6ca7c3aa96ac8a5fa090d18ca3c4f Mon Sep 17 00:00:00 2001 From: creslinux Date: Sun, 15 Jul 2018 10:33:00 +0000 Subject: [PATCH 04/31] Works with reporting output Bugs Calculating % prof ok, but abs wrong BAT/BTC DF is very broken all OHLC are the same - but exposes a buy after stop on last row "oddness" to be investigated / handled --- freqtrade/optimize/backtesting.py | 61 +++++++++++++++++-------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3cfb588b9..9698ef471 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -190,7 +190,7 @@ class Backtesting(object): return btr return None - + @profile def backtest(self, args: Dict) -> DataFrame: """ Implements backtesting functionality @@ -309,9 +309,9 @@ class Backtesting(object): from datetime import datetime ### backslap debug wrap - debug_2loops = True # only loop twice, for faster debug + debug_2loops = False # only loop twice, for faster debug debug_timing = False # print timing for each step - debug = True # print values, to check accuracy + debug = False # print values, to check accuracy # Read Stop Loss Values and Stake stop = self.stop_loss_value @@ -354,8 +354,10 @@ class Backtesting(object): buy - open - close - sell - high - low - np_stop_pri """ bto = buys_triggered_on = "close" - sto = stops_triggered_on = "low" ## Should be low, FT uses close - sco = stops_calculated_on = "np_stop_pri" ## should use np_stop_pri, FT uses close + # sto = stops_triggered_on = "low" ## Should be low, FT uses close + # sco = stops_calculated_on = "np_stop_pri" ## should use np_stop_pri, FT uses close + sto = stops_triggered_on = "close" ## Should be low, FT uses close + sco = stops_calculated_on = "close" ## should use np_stop_pri, FT uses close ''' Numpy arrays are used for 100x speed up We requires setting Int values for @@ -371,8 +373,10 @@ class Backtesting(object): np_stop: int = 6 np_bto: int = np_close # buys_triggered_on - should be close np_bco: int = np_open # buys calculated on - open of the next candle. - np_sto: int = np_low # stops_triggered_on - Should be low, FT uses close - np_sco: int = np_stop # stops_calculated_on - Should be stop, FT uses close + #np_sto: int = np_low # stops_triggered_on - Should be low, FT uses close + #np_sco: int = np_stop # stops_calculated_on - Should be stop, FT uses close + np_sto: int = np_close # stops_triggered_on - Should be low, FT uses close + np_sco: int = np_close # stops_calculated_on - Should be stop, FT uses close # ### End Config @@ -408,7 +412,7 @@ class Backtesting(object): if debug or debug_timing: print("-- T_exit_Ind - Numpy Index is", t_exit_ind, " ----------------------- Loop", loop, pair) if debug_2loops: - if loop == 2: + if loop == 3: print("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++Loop debug max met - breaking") break ''' @@ -673,23 +677,25 @@ class Backtesting(object): # Profit ABS. # sumrecieved((rate * numTokens) * fee) - sumpaid ((rate * numTokens) * fee) - sumpaid: float = (np_trade_enter_price * stake) * open_fee - sumrecieved: float = (np_trade_exit_price * stake) * close_fee - profit_abs: float = sumrecieved - sumpaid + sumpaid: float = (np_trade_enter_price * stake) + sumpaid_fee: float = sumpaid * open_fee + sumrecieved: float = (np_trade_exit_price * stake) + sumrecieved_fee: float = sumrecieved * close_fee + profit_abs: float = sumrecieved - sumpaid - sumpaid_fee - sumrecieved_fee # build trade dictionary bslap_result["pair"] = pair bslap_result["profit_percent"] = (np_trade_exit_price - np_trade_enter_price) / np_trade_enter_price - bslap_result["profit_abs"] = str.format('{0:.10f}', profit_abs) + bslap_result["profit_abs"] = round(profit_abs, 15) bslap_result["open_time"] = trade_start bslap_result["close_time"] = trade_end - bslap_result["open_index"] = t_open_ind + 1 + bslap_result["open_index"] = t_open_ind + 2 # +1 between np and df, +1 as we buy on next. bslap_result["close_index"] = close_index bslap_result["trade_duration"] = trade_mins bslap_result["open_at_end"] = False - bslap_result["open_rate"] = str.format('{0:.10f}', np_trade_enter_price) - bslap_result["close_rate"] = str.format('{0:.10f}', np_trade_exit_price) - bslap_result["exit_type"] = t_exit_type + bslap_result["open_rate"] = round(np_trade_enter_price, 15) + bslap_result["close_rate"] = round(np_trade_exit_price, 15) + #bslap_result["exit_type"] = t_exit_type # Add trade dictionary to list bslap_pair_results.append(bslap_result) if debug: @@ -704,7 +710,7 @@ class Backtesting(object): if debug_timing: t_t = f(st) - print("8", str.format('{0:.17f}', t_t)) + print("8+trade", str.format('{0:.17f}', t_t)) # Send back List of trade dicts return bslap_pair_results @@ -785,16 +791,17 @@ class Backtesting(object): ) ) - logger.info( - '\n=============================================== ' - 'LEFT OPEN TRADES REPORT' - ' ===============================================\n' - '%s', - self._generate_text_table( - data, - results.loc[results.open_at_end] - ) - ) + ## TODO. Catch open trades for this report. + # logger.info( + # '\n=============================================== ' + # 'LEFT OPEN TRADES REPORT' + # ' ===============================================\n' + # '%s', + # self._generate_text_table( + # data, + # results.loc[results.open_at_end] + # ) + # ) def setup_configuration(args: Namespace) -> Dict[str, Any]: From a8b62a21ccce9bc7e79687d86167e9559b9b8962 Mon Sep 17 00:00:00 2001 From: creslinux Date: Sun, 15 Jul 2018 17:03:47 +0000 Subject: [PATCH 05/31] hmm --- freqtrade/optimize/backtesting.py | 153 +++++++++++++++++++++++------- 1 file changed, 120 insertions(+), 33 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9698ef471..045b61a39 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -190,7 +190,6 @@ class Backtesting(object): return btr return None - @profile def backtest(self, args: Dict) -> DataFrame: """ Implements backtesting functionality @@ -233,7 +232,13 @@ class Backtesting(object): last_bslap_results = bslap_results bslap_results = last_bslap_results + bslap_pair_results + # Switch List of Trade Dicts (bslap_results) to Dataframe + # Fill missing, calculable columns, profit, duration , abs etc. bslap_results_df = DataFrame(bslap_results, columns=BacktestResult._fields) + bslap_results_df = self.vector_fill_results_table(bslap_results_df) + + print(bslap_results_df.dtypes) + return bslap_results_df ########################### Original BT loop @@ -283,6 +288,69 @@ class Backtesting(object): # return DataFrame.from_records(trades, columns=BacktestResult._fields) ######################## Original BT loop end + def vector_fill_results_table(self, bslap_results_df: DataFrame): + """ + The Results frame contains a number of columns that are calculable + from othe columns. These are left blank till all rows are added, + to be populated in single vector calls. + + Columns to be populated are: + - Profit + - trade duration + - profit abs + + :param bslap_results Dataframe + :return: bslap_results Dataframe + """ + import pandas as pd + debug = True + + # stake and fees + stake = self.config.get('stake_amount') + # TODO grab these from the environment, do not hard set + open_fee = 0.05 + close_fee = 0.05 + + if debug: + print("Stake is,", stake, "the sum of currency to spend per trade") + print("The open fee is", open_fee, "The close fee is", close_fee) + if debug: + from pandas import set_option + set_option('display.max_rows', 5000) + set_option('display.max_columns', 8) + pd.set_option('display.width', 1000) + pd.set_option('max_colwidth', 40) + pd.set_option('precision', 12) + + # Align with BT + bslap_results_df['open_time'] = pd.to_datetime(bslap_results_df['open_time']) + bslap_results_df['close_time'] = pd.to_datetime(bslap_results_df['close_time']) + + # Populate duration + bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] + if debug: + print(bslap_results_df[['open_time', 'close_time', 'trade_duration']]) + + ## Spends, Takes, Profit, Absolute Profit + # Buy Price + bslap_results_df['buy_sum'] = stake * bslap_results_df['open_rate'] + bslap_results_df['buy_fee'] = bslap_results_df['buy_sum'] * open_fee + bslap_results_df['buy_spend'] = bslap_results_df['buy_sum'] + bslap_results_df['buy_fee'] + # Sell price + bslap_results_df['sell_sum'] = stake * bslap_results_df['close_rate'] + bslap_results_df['sell_fee'] = bslap_results_df['sell_sum'] * close_fee + bslap_results_df['sell_take'] = bslap_results_df['sell_sum'] - bslap_results_df['sell_fee'] + # profit_percent + bslap_results_df['profit_percent'] = bslap_results_df['sell_take'] / bslap_results_df['buy_spend'] - 1 + # Absolute profit + bslap_results_df['profit_abs'] = bslap_results_df['sell_take'] - bslap_results_df['buy_spend'] + + if debug: + print("\n") + print(bslap_results_df[['buy_spend', 'sell_take', 'profit_percent', 'profit_abs']]) + + return bslap_results_df + def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int): import utils_find_1st as utf1st """ @@ -301,6 +369,7 @@ class Backtesting(object): t_open_ind = t_open_ind + t_exit_ind # Align numpy index return t_open_ind + @profile def backslap_pair(self, ticker_data, pair): import pandas as pd import numpy as np @@ -316,19 +385,10 @@ class Backtesting(object): # Read Stop Loss Values and Stake stop = self.stop_loss_value p_stop = (stop + 1) # What stop really means, e.g 0.01 is 0.99 of price - stake = self.config.get('stake_amount') - - # Set fees - # TODO grab these from the environment, do not hard set - # Fees - open_fee = 0.05 - close_fee = 0.05 if debug: print("Stop is ", stop, "value from stragey file") print("p_stop is", p_stop, "value used to multiply to entry price") - print("Stake is,", stake, "the sum of currency to spend per trade") - print("The open fee is", open_fee, "The close fee is", close_fee) if debug: from pandas import set_option @@ -395,6 +455,12 @@ class Backtesting(object): # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 np_bslap = np.array(bslap[['buy', 'open', 'close', 'sell', 'high', 'low']]) + # Build a numpy list of date-times. + # We use these when building the trade + # The rationale is to address a value from a pandas cell is thousands of + # times more expensive. Processing time went X25 when trying to use any data from pandas + np_bslap_dates = bslap['date'].values + loop: int = 0 # how many time around the loop t_exit_ind = 0 # Start loop from first index t_exit_last = 0 # To test for exit @@ -657,9 +723,13 @@ class Backtesting(object): break else: """ - Add trade to backtest looking results list of dicts - Loop back to look for more trades. - """ + Add trade to backtest looking results list of dicts + Loop back to look for more trades. + """ + if debug_timing: + t_t = f(st) + print("8a-IfEls", str.format('{0:.17f}', t_t)) + st = s() # Index will change if incandle stop or look back as close Open and Sell if t_exit_type == 'stop': close_index: int = t_exit_ind + 1 @@ -668,39 +738,56 @@ class Backtesting(object): else: close_index: int = t_exit_ind + 1 - # Munge the date / delta (bt already date formats...just subract) - trade_start = bslap.iloc[t_open_ind + 1]['date'] - trade_end = bslap.iloc[close_index]['date'] - # def __datetime(date_str): - # return datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S+00:00') - trade_mins = (trade_end - trade_start).total_seconds() / 60 + if debug_timing: + t_t = f(st) + print("8b-Index", str.format('{0:.17f}', t_t)) + st = s() - # Profit ABS. - # sumrecieved((rate * numTokens) * fee) - sumpaid ((rate * numTokens) * fee) - sumpaid: float = (np_trade_enter_price * stake) - sumpaid_fee: float = sumpaid * open_fee - sumrecieved: float = (np_trade_exit_price * stake) - sumrecieved_fee: float = sumrecieved * close_fee - profit_abs: float = sumrecieved - sumpaid - sumpaid_fee - sumrecieved_fee + # # Profit ABS. + # # sumrecieved((rate * numTokens) * fee) - sumpaid ((rate * numTokens) * fee) + # sumpaid: float = (np_trade_enter_price * stake) + # sumpaid_fee: float = sumpaid * open_fee + # sumrecieved: float = (np_trade_exit_price * stake) + # sumrecieved_fee: float = sumrecieved * close_fee + # profit_abs: float = sumrecieved - sumpaid - sumpaid_fee - sumrecieved_fee + + if debug_timing: + t_t = f(st) + print("8d---ABS", str.format('{0:.17f}', t_t)) + st = s() + + # Build trade dictionary + ## In general if a field can be calculated later from other fields leave blank here + ## Its X(numer of trades faster) to calc all in a single vector than 1 trade at a time - # build trade dictionary bslap_result["pair"] = pair - bslap_result["profit_percent"] = (np_trade_exit_price - np_trade_enter_price) / np_trade_enter_price - bslap_result["profit_abs"] = round(profit_abs, 15) - bslap_result["open_time"] = trade_start - bslap_result["close_time"] = trade_end + bslap_result["profit_percent"] = "1" # To be 1 vector calc across trades when loop complete + bslap_result["profit_abs"] = "1" # To be 1 vector calc across trades when loop complete + bslap_result["open_time"] = np_bslap_dates[t_open_ind + 1] # use numpy array, pandas 20x slower + bslap_result["close_time"] = np_bslap_dates[close_index] # use numpy array, pandas 20x slower bslap_result["open_index"] = t_open_ind + 2 # +1 between np and df, +1 as we buy on next. bslap_result["close_index"] = close_index - bslap_result["trade_duration"] = trade_mins + bslap_result["trade_duration"] = "1" # To be 1 vector calc across trades when loop complete bslap_result["open_at_end"] = False bslap_result["open_rate"] = round(np_trade_enter_price, 15) bslap_result["close_rate"] = round(np_trade_exit_price, 15) - #bslap_result["exit_type"] = t_exit_type + bslap_result["exit_type"] = t_exit_type + + if debug_timing: + t_t = f(st) + print("8e-trade", str.format('{0:.17f}', t_t)) + st = s() # Add trade dictionary to list bslap_pair_results.append(bslap_result) + if debug: print(bslap_pair_results) + if debug_timing: + t_t = f(st) + print("8f--list", str.format('{0:.17f}', t_t)) + st = s() + """ Loop back to start. t_exit_last becomes where loop will seek to open new trades from. From 7174f27eb85296ef1886e3ebc0f71c5c3e7ec0d0 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 12:01:02 +0000 Subject: [PATCH 06/31] Rewrite to used algned numpy/dataframes updated logic added vector fill for abs/profit/duration in single hit on results. --- freqtrade/optimize/backtesting.py | 663 +++++++++++++++++------------- 1 file changed, 370 insertions(+), 293 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 045b61a39..2629ed5fa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,7 +10,7 @@ from datetime import datetime from typing import Any, Dict, List, NamedTuple, Optional, Tuple import arrow -from pandas import DataFrame +from pandas import DataFrame, to_datetime from tabulate import tabulate import freqtrade.optimize as optimize @@ -23,6 +23,7 @@ from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from profilehooks import profile from collections import OrderedDict +import timeit logger = logging.getLogger(__name__) @@ -190,6 +191,7 @@ class Backtesting(object): return btr return None + @profile def backtest(self, args: Dict) -> DataFrame: """ Implements backtesting functionality @@ -234,10 +236,13 @@ class Backtesting(object): # Switch List of Trade Dicts (bslap_results) to Dataframe # Fill missing, calculable columns, profit, duration , abs etc. - bslap_results_df = DataFrame(bslap_results, columns=BacktestResult._fields) - bslap_results_df = self.vector_fill_results_table(bslap_results_df) + bslap_results_df = DataFrame(bslap_results) + bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) + bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) + ### don't use this, itll drop exit type field + # bslap_results_df = DataFrame(bslap_results, columns=BacktestResult._fields) - print(bslap_results_df.dtypes) + bslap_results_df = self.vector_fill_results_table(bslap_results_df) return bslap_results_df @@ -303,14 +308,13 @@ class Backtesting(object): :return: bslap_results Dataframe """ import pandas as pd - debug = True + debug = False # stake and fees - stake = self.config.get('stake_amount') - # TODO grab these from the environment, do not hard set - open_fee = 0.05 - close_fee = 0.05 - + stake = 0.015 + # 0.05% is 0.00,05 + open_fee = 0.0000 + close_fee = 0.0000 if debug: print("Stake is,", stake, "the sum of currency to spend per trade") print("The open fee is", open_fee, "The close fee is", close_fee) @@ -322,10 +326,6 @@ class Backtesting(object): pd.set_option('max_colwidth', 40) pd.set_option('precision', 12) - # Align with BT - bslap_results_df['open_time'] = pd.to_datetime(bslap_results_df['open_time']) - bslap_results_df['close_time'] = pd.to_datetime(bslap_results_df['close_time']) - # Populate duration bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] if debug: @@ -347,29 +347,46 @@ class Backtesting(object): if debug: print("\n") - print(bslap_results_df[['buy_spend', 'sell_take', 'profit_percent', 'profit_abs']]) + print(bslap_results_df[ + ['buy_sum', 'buy_fee', 'buy_spend', 'sell_sum', 'sell_take', 'profit_percent', 'profit_abs', + 'exit_type']]) return bslap_results_df def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int): import utils_find_1st as utf1st """ - The purpose of this def is to return the next "buy" = 1 - after t_exit_ind. + The purpose of this def is to return the next "buy" = 1 + after t_exit_ind. - t_exit_ind is the index the last trade exited on - or 0 if first time around this loop. - """ + t_exit_ind is the index the last trade exited on + or 0 if first time around this loop. + """ + # Timers, to be called if in debug + def s(): + st = timeit.default_timer() + return st + def f(st): + return (timeit.default_timer() - st) + + st = s() t_open_ind: int - # Create a view on our buy index starting after last trade exit - # Search for next buy + """ + Create a view on our buy index starting after last trade exit + Search for next buy + """ np_buy_arr_v = np_buy_arr[t_exit_ind:] t_open_ind = utf1st.find_1st(np_buy_arr_v, 1, utf1st.cmp_equal) - t_open_ind = t_open_ind + t_exit_ind # Align numpy index + + ''' + If -1 is returned no buy has been found, preserve the value + ''' + if t_open_ind != -1: # send back the -1 if no buys found. otherwise update index + t_open_ind = t_open_ind + t_exit_ind # Align numpy index + return t_open_ind - @profile def backslap_pair(self, ticker_data, pair): import pandas as pd import numpy as np @@ -397,27 +414,12 @@ class Backtesting(object): pd.set_option('display.width', 1000) pd.set_option('max_colwidth', 40) pd.set_option('precision', 12) - def s(): st = timeit.default_timer() return st - def f(st): return (timeit.default_timer() - st) #### backslap config - """ - A couple legacy Pandas vars still used for pretty debug output. - If have debug enabled the code uses these fields for dataframe output - - Ensure bto, sto, sco are aligned with Numpy values next - to align debug and actual. Options are: - buy - open - close - sell - high - low - np_stop_pri - """ - bto = buys_triggered_on = "close" - # sto = stops_triggered_on = "low" ## Should be low, FT uses close - # sco = stops_calculated_on = "np_stop_pri" ## should use np_stop_pri, FT uses close - sto = stops_triggered_on = "close" ## Should be low, FT uses close - sco = stops_calculated_on = "close" ## should use np_stop_pri, FT uses close ''' Numpy arrays are used for 100x speed up We requires setting Int values for @@ -441,7 +443,6 @@ class Backtesting(object): ### End Config pair: str = pair - loop: int = 1 #ticker_data: DataFrame = ticker_dfs[t_file] bslap: DataFrame = ticker_data @@ -482,223 +483,342 @@ class Backtesting(object): print("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++Loop debug max met - breaking") break ''' - Dev phases - Phase 1 - 1) Manage buy, sell, stop enter/exit - a) Find first buy index - b) Discover first stop and sell hit after buy index - c) Chose first intance as trade exit + Dev phases + Phase 1 + 1) Manage buy, sell, stop enter/exit + a) Find first buy index + b) Discover first stop and sell hit after buy index + c) Chose first instance as trade exit + + Phase 2 + 2) Manage dynamic Stop and ROI Exit + a) Create trade slice from 1 + b) search within trade slice for dynamice stop hit + c) search within trade slice for ROI hit + ''' - Phase 2 - 2) Manage dynamic Stop and ROI Exit - a) Create trade slice from 1 - b) search within trade slice for dynamice stop hit - c) search within trade slice for ROI hit - ''' - - ''' - Finds index for first buy = 1 flag, use .values numpy array for speed - - Create a slice, from first buy index onwards. - Slice will be used to find exit conditions after trade open - ''' if debug_timing: st = s() - + ''' + 0 - Find next buy entry + Finds index for first (buy = 1) flag + + Requires: np_buy_arr - a 1D array of the 'buy' column. To find next "1" + Required: t_exit_ind - Either 0, first loop. Or The index we last exited on + Provides: The next "buy" index after t_exit_ind + + If -1 is returned no buy has been found in remainder of array, skip to exit loop + ''' t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind) + if debug: + print("\n(0) numpy debug \nnp_get_t_open, has returned the next valid buy index as", t_open_ind) + print("If -1 there are no valid buys in the remainder of ticker data. Skipping to end of loop") if debug_timing: t_t = f(st) print("0-numpy", str.format('{0:.17f}', t_t)) st = s() - ''' - Calculate np_t_stop_pri (Trade Stop Price) based on the buy price + if t_open_ind != -1: - As stop in based on buy price we are interested in buy - - Buys are Triggered On np_bto, typically the CLOSE of candle - - Buys are Calculated On np_bco, default is OPEN of the next candle. - as we only see the CLOSE after it has happened. - The assumption is we have bought at first available price, the OPEN - ''' - np_t_stop_pri = (np_bslap[t_open_ind + 1, np_bco] * p_stop) + """ + 1 - Create view to search within for our open trade + + The view is our search space for the next Stop or Sell + Numpy view is employed as: + 1,000 faster than pandas searches + Pandas cannot assure it will always return a view, it may make a slow copy. + + The view contains columns: + buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + + Requires: np_bslap is our numpy array of the ticker DataFrame + Requires: t_open_ind is the index row with the buy. + Provided: np_t_open_v View of array after trade. + """ + np_t_open_v = np_bslap[t_open_ind:] - if debug_timing: - t_t = f(st) - print("1-numpy", str.format('{0:.17f}', t_t)) - st = s() + if debug: + print("\n(1) numpy debug \nNumpy view row 0 is now Ticker_Data Index", t_open_ind) + print("Numpy View: Buy - Open - Close - Sell - High - Low") + print("Row 0", np_t_open_v[0]) + print("Row 1", np_t_open_v[1], ) + if debug_timing: + t_t = f(st) + print("2-numpy", str.format('{0:.17f}', t_t)) + st = s() - """ - 1)Create a View from our open trade forward - - The view is our search space for the next Stop or Sell - We use a numpy view: - Using a numpy for speed on views, 1,000 faster than pandas - Pandas cannot assure it will always return a view, copies are - 3 orders of magnitude slower - - The view contains columns: - buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - """ - np_t_open_v = np_bslap[t_open_ind:] - - if debug_timing: - t_t = f(st) - print("2-numpy", str.format('{0:.17f}', t_t)) - st = s() - - ''' - Find first stop index after Trade Open: - - First index in np_t_open_v (numpy view of bslap dataframe) - Using a numpy view a orders of magnitude faster - - where [np_sto] (stop tiggered on variable: "close", "low" etc) < np_t_stop_pri - ''' - np_t_stop_ind = utf1st.find_1st(np_t_open_v[:, np_sto], - np_t_stop_pri, - utf1st.cmp_smaller) \ - + t_open_ind - - if debug_timing: - t_t = f(st) - print("3-numpy", str.format('{0:.17f}', t_t)) - st = s() - - ''' - Find first sell index after trade open - - First index in t_open_slice where ['sell'] = 1 - ''' - # Use numpy array for faster search for sell - # Sell uses column 3. - # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - # Numpy searches 25-35x quicker than pandas on this data - - np_t_sell_ind = utf1st.find_1st(np_t_open_v[:, np_sell], - 1, utf1st.cmp_equal) \ - + t_open_ind - - if debug_timing: - t_t = f(st) - print("4-numpy", str.format('{0:.17f}', t_t)) - st = s() - - ''' - Determine which was hit first stop or sell, use as exit - - STOP takes priority over SELL as would be 'in candle' from tick data - Sell would use Open from Next candle. - So in a draw Stop would be hit first on ticker data in live - ''' - if np_t_stop_ind <= np_t_sell_ind: - t_exit_ind = np_t_stop_ind # Set Exit row index - t_exit_type = 'stop' # Set Exit type (sell|stop) - np_t_exit_pri = np_sco # The price field our STOP exit will use - else: - # move sell onto next candle, we only look back on sell - # will use the open price later. - t_exit_ind = np_t_sell_ind # Set Exit row index - t_exit_type = 'sell' # Set Exit type (sell|stop) - np_t_exit_pri = np_open # The price field our SELL exit will use - - if debug_timing: - t_t = f(st) - print("5-logic", str.format('{0:.17f}', t_t)) - st = s() - - if debug: ''' - Print out the buys, stops, sells - Include Line before and after to for easy - Human verification + 2 - Calculate our stop-loss price + + As stop is based on buy price of our trade + - (BTO)Buys are Triggered On np_bto, typically the CLOSE of candle + - (BCO)Buys are Calculated On np_bco, default is OPEN of the next candle. + This is as we only see the CLOSE after it has happened. + The back test assumption is we have bought at first available price, the OPEN + + Requires: np_bslap - is our numpy array of the ticker DataFrame + Requires: t_open_ind - is the index row with the first buy. + Requires: p_stop - is the stop rate, ie. 0.99 is -1% + Provides: np_t_stop_pri - The value stop-loss will be triggered on ''' - # Combine the np_t_stop_pri value to bslap dataframe to make debug - # life easy. This is the currenct stop price based on buy price_ - # Don't care about performance in debug - # (add an extra column if printing as df has date in col1 not in npy) - bslap['np_stop_pri'] = np_t_stop_pri + np_t_stop_pri = (np_bslap[t_open_ind + 1, np_bco] * p_stop) - # Buy - print("=================== BUY ", pair) - print("Numpy Array BUY Index is:", t_open_ind) - print("DataFrame BUY Index is:", t_open_ind + 1, "displaying DF \n") - print("HINT, BUY trade should use OPEN price from next candle, i.e ", t_open_ind + 2, "\n") - op_is = t_open_ind - 1 # Print open index start, line before - op_if = t_open_ind + 3 # Print open index finish, line after - print(bslap.iloc[op_is:op_if], "\n") - print(bslap.iloc[t_open_ind + 1]['date']) + if debug: + print("\n(2) numpy debug\nStop-Loss has been calculated at:", np_t_stop_pri) + if debug_timing: + t_t = f(st) + print("2-numpy", str.format('{0:.17f}', t_t)) + st = s() - # Stop - Stops trigger price sto, and price received sco. (Stop Trigger|Calculated On) - print("=================== STOP ", pair) - print("Numpy Array STOP Index is:", np_t_stop_ind) - print("DataFrame STOP Index is:", np_t_stop_ind + 1, "displaying DF \n") - print("First Stop after Trade open in candle", t_open_ind + 1, "is ", np_t_stop_ind + 1,": \n", - str.format('{0:.17f}', bslap.iloc[np_t_stop_ind][sto]), - "is less than", str.format('{0:.17f}', np_t_stop_pri)) - print("If stop is first exit match sell rate is :", str.format('{0:.17f}', bslap.iloc[np_t_stop_ind][sco])) - print("HINT, STOPs should close in-candle, i.e", np_t_stop_ind + 1, - ": As live STOPs are not linked to O-C times") + ''' + 3 - Find candle STO is under Stop-Loss After Trade opened. + + where [np_sto] (stop tiggered on variable: "close", "low" etc) < np_t_stop_pri + + Requires: np_t_open_v Numpy view of ticker_data after trade open + Requires: np_sto User Var(STO)StopTriggeredOn. Typically set to "low" or "close" + Requires: np_t_stop_pri The stop-loss price STO must fall under to trigger stop + Provides: np_t_stop_ind The first candle after trade open where STO is under stop-loss + ''' + np_t_stop_ind = utf1st.find_1st(np_t_open_v[:, np_sto], + np_t_stop_pri, + utf1st.cmp_smaller) - st_is = np_t_stop_ind - 1 # Print stop index start, line before - st_if = np_t_stop_ind + 2 # Print stop index finish, line after - print(bslap.iloc[st_is:st_if], "\n") + if debug: + print("\n(3) numpy debug\nNext view index with STO (stop trigger on) under Stop-Loss is", np_t_stop_ind, + ". STO is using field", np_sto, + "\nFrom key: buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5\n") - # Sell - print("=================== SELL ", pair) - print("Numpy Array SELL Index is:", np_t_sell_ind) - print("DataFrame SELL Index is:", np_t_sell_ind + 1, "displaying DF \n") - print("First Sell Index after Trade open in in candle", np_t_sell_ind + 1) - print("HINT, if exit is SELL (not stop) trade should use OPEN price from next candle", - np_t_sell_ind + 2, "\n") - sl_is = np_t_sell_ind - 1 # Print sell index start, line before - sl_if = np_t_sell_ind + 3 # Print sell index finish, line after - print(bslap.iloc[sl_is:sl_if], "\n") + print("If -1 returned there is no stop found to end of view, then next two array lines are garbage") + print("Row", np_t_stop_ind, np_t_open_v[np_t_stop_ind]) + print("Row", np_t_stop_ind + 1, np_t_open_v[np_t_stop_ind + 1]) + if debug_timing: + t_t = f(st) + print("3-numpy", str.format('{0:.17f}', t_t)) + st = s() - # Chosen Exit (stop or sell) - print("=================== EXIT ", pair) - print("Exit type is :", t_exit_type) - # print((bslap.iloc[t_exit_ind], "\n")) - print("trade exit price field is", np_t_exit_pri, "\n") + ''' + 4 - Find first sell index after trade open + + First index in the view np_t_open_v where ['sell'] = 1 + + Requires: np_t_open_v - view of ticker_data from buy onwards + Requires: no_sell - integer '3', the buy column in the array + Provides: np_t_sell_ind index of view where first sell=1 after buy + ''' + # Use numpy array for faster search for sell + # Sell uses column 3. + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + # Numpy searches 25-35x quicker than pandas on this data - ''' - Trade entry is always the next candles "open" price - We trigger on close, so cannot see that till after - its closed. + np_t_sell_ind = utf1st.find_1st(np_t_open_v[:, np_sell], + 1, utf1st.cmp_equal) + if debug: + print("\n(4) numpy debug\nNext view index with sell = 1 is ", np_t_sell_ind) + print("If 0 or less is returned there is no sell found to end of view, then next lines garbage") + print("Row", np_t_sell_ind, np_t_open_v[np_t_stop_ind]) + print("Row", np_t_sell_ind + 1, np_t_open_v[np_t_stop_ind + 1]) + if debug_timing: + t_t = f(st) + print("4-numpy", str.format('{0:.17f}', t_t)) + st = s() - The exception to this is a STOP which is calculated in candle - ''' - if debug_timing: - t_t = f(st) - print("6-depra", str.format('{0:.17f}', t_t)) - st = s() + ''' + 5 - Determine which was hit first a stop or sell + To then use as exit index price-field (sell on buy, stop on stop) + + STOP takes priority over SELL as would be 'in candle' from tick data + Sell would use Open from Next candle. + So in a draw Stop would be hit first on ticker data in live + + Validity of when types of trades may be executed can be summarised as: + + Tick View + index index Buy Sell open low close high Stop price + open 2am 94 -1 0 0 ----- ------ ------ ----- ----- + open 3am 95 0 1 0 ----- ------ trg buy ----- ----- + open 4am 96 1 0 1 Enter trgstop trg sel ROI out Stop out + open 5am 97 2 0 0 Exit ------ ------- ----- ----- + open 6am 98 3 0 0 ----- ------ ------- ----- ----- + + -1 means not found till end of view i.e no valid Stop found. Exclude from match. + Stop tiggering in 1, candle we bought at OPEN is valid. + + Buys and sells are triggered at candle close + Both with action their postions at the open of the next candle Index + 1 + + Stop and buy Indexes are on the view. To map to the ticker dataframe + the t_open_ind index should be summed. + + np_t_stop_ind: Stop Found index in view + t_exit_ind : Sell found in view + t_open_ind : Where view was started on ticker_data + + TODO: fix this frig for logig test,, case/switch/dictionary would be better... + more so when later testing many options, dynamic stop / roi etc + cludge - Im setting np_t_sell_ind as 9999999999 when -1 (not found) + cludge - Im setting np_t_stop_ind as 9999999999 when -1 (not found) + + ''' + if debug: + print("\n(5) numpy debug\nStop or Sell Logic Processing") - ## use numpy view "np_t_open_v" for speed. Columns are - # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - # exception is 6 which is use the stop value. + # cludge for logic test (-1) means it was not found, set crazy high to lose < test + np_t_sell_ind = 99999999 if np_t_sell_ind <= 0 else np_t_sell_ind + np_t_stop_ind = 99999999 if np_t_stop_ind == -1 else np_t_stop_ind - np_trade_enter_price = np_bslap[t_open_ind + 1, np_open] - if t_exit_type == 'stop': - if np_t_exit_pri == 6: - np_trade_exit_price = np_t_stop_pri + # Stoploss trigger found before a sell =1 + if np_t_stop_ind < 99999999 and np_t_stop_ind <= np_t_sell_ind: + t_exit_ind = t_open_ind + np_t_stop_ind # Set Exit row index + t_exit_type = 'stop' # Set Exit type (stop) + np_t_exit_pri = np_sco # The price field our STOP exit will use + if debug: + print("Type STOP is first exit condition. " + "At view index:", np_t_stop_ind, ". Ticker data exit index is", t_exit_ind) + + # Buy = 1 found before a stoploss triggered + elif np_t_sell_ind < 99999999 and np_t_sell_ind < np_t_stop_ind: + # move sell onto next candle, we only look back on sell + # will use the open price later. + t_exit_ind = t_open_ind + np_t_sell_ind + 1 # Set Exit row index + t_exit_type = 'sell' # Set Exit type (sell) + np_t_exit_pri = np_open # The price field our SELL exit will use + if debug: + print("Type SELL is first exit condition. " + "At view index", np_t_sell_ind, ". Ticker data exit index is", t_exit_ind) + + # No stop or buy left in view - set t_exit_last -1 to handle gracefully else: + t_exit_last: int = -1 # Signal loop to exit, no buys or sells found. + t_exit_type = "No Exit" + np_t_exit_pri = 999 # field price should be calculated on. 999 a non-existent column + if debug: + print("No valid STOP or SELL found. Signalling t_exit_last to gracefully exit") + + # TODO: fix having to cludge/uncludge this .. + # Undo cludge + np_t_sell_ind = -1 if np_t_sell_ind == 99999999 else np_t_sell_ind + np_t_stop_ind = -1 if np_t_stop_ind == 99999999 else np_t_stop_ind + + if debug_timing: + t_t = f(st) + print("5-logic", str.format('{0:.17f}', t_t)) + st = s() + + if debug: + ''' + Print out the buys, stops, sells + Include Line before and after to for easy + Human verification + ''' + # Combine the np_t_stop_pri value to bslap dataframe to make debug + # life easy. This is the current stop price based on buy price_ + # This is slow but don't care about performance in debug + # + # When referencing equiv np_column, as example np_sto, its 5 in numpy and 6 in df, so +1 + # as there is no data column in the numpy array. + bslap['np_stop_pri'] = np_t_stop_pri + + # Buy + print("\n\nDATAFRAME DEBUG =================== BUY ", pair) + print("Numpy Array BUY Index is:", 0) + print("DataFrame BUY Index is:", t_open_ind, "displaying DF \n") + print("HINT, BUY trade should use OPEN price from next candle, i.e ", t_open_ind + 1) + op_is = t_open_ind - 1 # Print open index start, line before + op_if = t_open_ind + 3 # Print open index finish, line after + print(bslap.iloc[op_is:op_if], "\n") + + # Stop - Stops trigger price np_sto (+1 for pandas column), and price received np_sco +1. (Stop Trigger|Calculated On) + if np_t_stop_ind < 0: + print("DATAFRAME DEBUG =================== STOP ", pair) + print("No STOPS were found until the end of ticker data file\n") + else: + print("DATAFRAME DEBUG =================== STOP ", pair) + print("Numpy Array STOP Index is:", np_t_stop_ind, "View starts at index", t_open_ind) + df_stop_index = (t_open_ind + np_t_stop_ind) + + print("DataFrame STOP Index is:", df_stop_index, "displaying DF \n") + print("First Stoploss trigger after Trade entered at OPEN in candle", t_open_ind + 1, "is ", + df_stop_index, ": \n", + str.format('{0:.17f}', bslap.iloc[df_stop_index][np_sto + 1]), + "is less than", str.format('{0:.17f}', np_t_stop_pri)) + + print("A stoploss exit will be calculated at rate:", + str.format('{0:.17f}', bslap.iloc[df_stop_index][np_sco + 1])) + + print("\nHINT, STOPs should exit in-candle, i.e", df_stop_index, + ": As live STOPs are not linked to O-C times") + + st_is = df_stop_index - 1 # Print stop index start, line before + st_if = df_stop_index + 2 # Print stop index finish, line after + print(bslap.iloc[st_is:st_if], "\n") + + # Sell + if np_t_sell_ind < 0: + print("DATAFRAME DEBUG =================== SELL ", pair) + print("No SELLS were found till the end of ticker data file\n") + else: + print("DATAFRAME DEBUG =================== SELL ", pair) + print("Numpy View SELL Index is:", np_t_sell_ind, "View starts at index", t_open_ind) + df_sell_index = (t_open_ind + np_t_sell_ind) + + print("DataFrame SELL Index is:", df_sell_index, "displaying DF \n") + print("First Sell Index after Trade open is in candle", df_sell_index) + print("HINT, if exit is SELL (not stop) trade should use OPEN price from next candle", + df_sell_index + 1) + sl_is = df_sell_index - 1 # Print sell index start, line before + sl_if = df_sell_index + 3 # Print sell index finish, line after + print(bslap.iloc[sl_is:sl_if], "\n") + + # Chosen Exit (stop or sell) + + print("DATAFRAME DEBUG =================== EXIT ", pair) + print("Exit type is :", t_exit_type) + print("trade exit price field is", np_t_exit_pri, "\n") + + if debug_timing: + t_t = f(st) + print("6-depra", str.format('{0:.17f}', t_t)) + st = s() + + ## use numpy view "np_t_open_v" for speed. Columns are + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + # exception is 6 which is use the stop value. + + # TODO no! this is hard coded bleh fix this open + np_trade_enter_price = np_bslap[t_open_ind + 1, np_open] + if t_exit_type == 'stop': + if np_t_exit_pri == 6: + np_trade_exit_price = np_t_stop_pri + else: + np_trade_exit_price = np_bslap[t_exit_ind, np_t_exit_pri] + if t_exit_type == 'sell': np_trade_exit_price = np_bslap[t_exit_ind, np_t_exit_pri] - if t_exit_type == 'sell': - np_trade_exit_price = np_bslap[t_exit_ind + 1, np_t_exit_pri] - if debug_timing: - t_t = f(st) - print("7-numpy", str.format('{0:.17f}', t_t)) - st = s() + # Catch no exit found + if t_exit_type == "No Exit": + np_trade_exit_price = 0 - if debug: - print("//////////////////////////////////////////////") - print("+++++++++++++++++++++++++++++++++ Trade Enter ") - print("np_trade Enterprice is ", str.format('{0:.17f}', np_trade_enter_price)) - print("--------------------------------- Trade Exit ") - print("Trade Exit Type is ", t_exit_type) - print("np_trade Exit Price is", str.format('{0:.17f}', np_trade_exit_price)) - print("//////////////////////////////////////////////") + if debug_timing: + t_t = f(st) + print("7-numpy", str.format('{0:.17f}', t_t)) + st = s() + + if debug: + print("//////////////////////////////////////////////") + print("+++++++++++++++++++++++++++++++++ Trade Enter ") + print("np_trade Enter Price is ", str.format('{0:.17f}', np_trade_enter_price)) + print("--------------------------------- Trade Exit ") + print("Trade Exit Type is ", t_exit_type) + print("np_trade Exit Price is", str.format('{0:.17f}', np_trade_exit_price)) + print("//////////////////////////////////////////////") + + else: # no buys were found, step 0 returned -1 + # Gracefully exit the loop + t_exit_last == -1 + if debug: + print("\n(E) No buys were found in remaining ticker file. Exiting", pair) # Loop control - catch no closed trades. if debug: @@ -706,87 +826,47 @@ class Backtesting(object): " Dataframe Exit Index is: ", t_exit_ind) print("Exit Index Last, Exit Index Now Are: ", t_exit_last, t_exit_ind) - if t_exit_last >= t_exit_ind: + if t_exit_last >= t_exit_ind or t_exit_last == -1: """ + Break loop and go on to next pair. + When last trade exit equals index of last exit, there is no opportunity to close any more trades. - - Break loop and go on to next pair. - - TODO - add handing here to record none closed open trades """ - + # TODO :add handing here to record none closed open trades if debug: print(bslap_pair_results) - break else: """ - Add trade to backtest looking results list of dicts - Loop back to look for more trades. - """ - if debug_timing: - t_t = f(st) - print("8a-IfEls", str.format('{0:.17f}', t_t)) - st = s() - # Index will change if incandle stop or look back as close Open and Sell - if t_exit_type == 'stop': - close_index: int = t_exit_ind + 1 - elif t_exit_type == 'sell': - close_index: int = t_exit_ind + 2 - else: - close_index: int = t_exit_ind + 1 - - if debug_timing: - t_t = f(st) - print("8b-Index", str.format('{0:.17f}', t_t)) - st = s() - - # # Profit ABS. - # # sumrecieved((rate * numTokens) * fee) - sumpaid ((rate * numTokens) * fee) - # sumpaid: float = (np_trade_enter_price * stake) - # sumpaid_fee: float = sumpaid * open_fee - # sumrecieved: float = (np_trade_exit_price * stake) - # sumrecieved_fee: float = sumrecieved * close_fee - # profit_abs: float = sumrecieved - sumpaid - sumpaid_fee - sumrecieved_fee - - if debug_timing: - t_t = f(st) - print("8d---ABS", str.format('{0:.17f}', t_t)) - st = s() - + Add trade to backtest looking results list of dicts + Loop back to look for more trades. + """ # Build trade dictionary ## In general if a field can be calculated later from other fields leave blank here - ## Its X(numer of trades faster) to calc all in a single vector than 1 trade at a time + ## Its X(number of trades faster) to calc all in a single vector than 1 trade at a time + # create a new dict + close_index: int = t_exit_ind + bslap_result = {} # Must have at start or we end up with a list of multiple same last result bslap_result["pair"] = pair - bslap_result["profit_percent"] = "1" # To be 1 vector calc across trades when loop complete - bslap_result["profit_abs"] = "1" # To be 1 vector calc across trades when loop complete + bslap_result["profit_percent"] = "" # To be 1 vector calc across trades when loop complete + bslap_result["profit_abs"] = "" # To be 1 vector calc across trades when loop complete bslap_result["open_time"] = np_bslap_dates[t_open_ind + 1] # use numpy array, pandas 20x slower bslap_result["close_time"] = np_bslap_dates[close_index] # use numpy array, pandas 20x slower - bslap_result["open_index"] = t_open_ind + 2 # +1 between np and df, +1 as we buy on next. + bslap_result["open_index"] = t_open_ind + 1 # +1 as we buy on next. bslap_result["close_index"] = close_index - bslap_result["trade_duration"] = "1" # To be 1 vector calc across trades when loop complete + bslap_result["trade_duration"] = "" # To be 1 vector calc across trades when loop complete bslap_result["open_at_end"] = False bslap_result["open_rate"] = round(np_trade_enter_price, 15) bslap_result["close_rate"] = round(np_trade_exit_price, 15) bslap_result["exit_type"] = t_exit_type - - if debug_timing: - t_t = f(st) - print("8e-trade", str.format('{0:.17f}', t_t)) - st = s() - # Add trade dictionary to list + # append the dict to the list and print list bslap_pair_results.append(bslap_result) if debug: - print(bslap_pair_results) - - if debug_timing: - t_t = f(st) - print("8f--list", str.format('{0:.17f}', t_t)) - st = s() + print("The trade dict is: \n", bslap_result) + print("Trades dicts in list after append are: \n ", bslap_pair_results) """ Loop back to start. t_exit_last becomes where loop @@ -802,9 +882,6 @@ class Backtesting(object): # Send back List of trade dicts return bslap_pair_results - - - def start(self) -> None: """ Run a backtesting end-to-end @@ -868,9 +945,9 @@ class Backtesting(object): self._store_backtest_result(self.config.get('exportfilename'), results) logger.info( - '\n================================================= ' - 'BACKTESTING REPORT' - ' ==================================================\n' + '\n====================================================== ' + 'BackSLAP REPORT' + ' =======================================================\n' '%s', self._generate_text_table( data, From eed29a6b8af3a1746c16cb142f6e418072ce6042 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 13:16:18 +0000 Subject: [PATCH 07/31] update --- freqtrade/optimize/backtesting.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2629ed5fa..d17b7b246 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -24,6 +24,7 @@ from freqtrade.persistence import Trade from profilehooks import profile from collections import OrderedDict import timeit +from time import sleep logger = logging.getLogger(__name__) @@ -191,7 +192,14 @@ class Backtesting(object): return btr return None - @profile + def s(self): + st = timeit.default_timer() + return st + + def f(self, st): + return (timeit.default_timer() - st) + + def backtest(self, args: Dict) -> DataFrame: """ Implements backtesting functionality @@ -207,21 +215,31 @@ class Backtesting(object): realistic: do we try to simulate realistic trades? (default: True) :return: DataFrame """ + debug_timing = False + headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] processed = args['processed'] max_open_trades = args.get('max_open_trades', 0) realistic = args.get('realistic', False) trades = [] trade_count_lock: Dict = {} + ########################### Call out BSlap instead of using FT bslap_results: list = [] - last_bslap_results: list = [] + for pair, pair_data in processed.items(): + if debug_timing: # Start timer + fl = self.s() + ticker_data = self.populate_sell_trend( self.populate_buy_trend(pair_data))[headers].copy() ticker_data.drop(ticker_data.head(1).index, inplace=True) + if debug_timing: # print time taken + flt = self.f(fl) + print("populate_buy_trend:", pair, round(flt, 10)) + st = self.st() # #dump same DFs to disk for offline testing in scratch # f_pair:str = pair @@ -234,11 +252,18 @@ class Backtesting(object): last_bslap_results = bslap_results bslap_results = last_bslap_results + bslap_pair_results + if debug_timing: # print time taken + tt = self.f(st) + print("Time to Back Slap :", pair, round(tt,10)) + print("-----------------------") + + # Switch List of Trade Dicts (bslap_results) to Dataframe # Fill missing, calculable columns, profit, duration , abs etc. bslap_results_df = DataFrame(bslap_results) bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) + ### don't use this, itll drop exit type field # bslap_results_df = DataFrame(bslap_results, columns=BacktestResult._fields) From fb0edd71ff166a353613a1f14de42a27163ea92b Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 14:16:35 +0000 Subject: [PATCH 08/31] in tech test --- freqtrade/optimize/backtesting.py | 276 ++++++++++++++++++------------ 1 file changed, 171 insertions(+), 105 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d17b7b246..bbb52d167 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -73,6 +73,35 @@ class Backtesting(object): self.stop_loss_value = self.analyze.strategy.stoploss + #### backslap config + ''' + Numpy arrays are used for 100x speed up + We requires setting Int values for + buy stop triggers and stop calculated on + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - stop 6 + ''' + self.np_buy: int = 0 + self.np_open: int = 1 + self.np_close: int = 2 + self.np_sell: int = 3 + self.np_high: int = 4 + self.np_low: int = 5 + self.np_stop: int = 6 + self.np_bto: int = self.np_close # buys_triggered_on - should be close + self.np_bco: int = self.np_open # buys calculated on - open of the next candle. + # self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close + # self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close + self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close + self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close + + self.use_backslap = True # Enable backslap + self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended + self.debug_timing = False # Stages within Backslap + self.debug_2loops = False # Limit each pair to two loops, useful when debugging + self.debug_vector = False # Debug vector calcs + self.debug_timing_main_loop = False # print overall timing per pair + + @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: """ @@ -215,7 +244,7 @@ class Backtesting(object): realistic: do we try to simulate realistic trades? (default: True) :return: DataFrame """ - debug_timing = False + headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] processed = args['processed'] @@ -224,99 +253,114 @@ class Backtesting(object): trades = [] trade_count_lock: Dict = {} - ########################### Call out BSlap instead of using FT - bslap_results: list = [] + use_backslap = self.use_backslap + debug_timing = self.debug_timing_main_loop + if use_backslap: # Use Back Slap code + ########################### Call out BSlap Loop instead of Original BT code + bslap_results: list = [] + for pair, pair_data in processed.items(): + if debug_timing: # Start timer + fl = self.s() + + ticker_data = self.populate_sell_trend( + self.populate_buy_trend(pair_data))[headers].copy() + + if debug_timing: # print time taken + flt = self.f(fl) + #print("populate_buy_trend:", pair, round(flt, 10)) + st = self.s() + + # #dump same DFs to disk for offline testing in scratch + # f_pair:str = pair + # csv = f_pair.replace("/", "_") + # csv="/Users/creslin/PycharmProjects/freqtrade_new/frames/" + csv + # ticker_data.to_csv(csv, sep='\t', encoding='utf-8') + + #call bslap - results are a list of dicts + bslap_pair_results = self.backslap_pair(ticker_data, pair) + last_bslap_results = bslap_results + bslap_results = last_bslap_results + bslap_pair_results + + if debug_timing: # print time taken + tt = self.f(st) + print("Time to BackSlap :", pair, round(tt,10)) + print("-----------------------") - for pair, pair_data in processed.items(): - if debug_timing: # Start timer - fl = self.s() + # Switch List of Trade Dicts (bslap_results) to Dataframe + # Fill missing, calculable columns, profit, duration , abs etc. + bslap_results_df = DataFrame(bslap_results) + bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) + bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) - ticker_data = self.populate_sell_trend( + ### don't use this, itll drop exit type field + # bslap_results_df = DataFrame(bslap_results, columns=BacktestResult._fields) + + bslap_results_df = self.vector_fill_results_table(bslap_results_df) + + return bslap_results_df + + else: # use Original Back test code + ########################## Original BT loop + + for pair, pair_data in processed.items(): + if debug_timing: # Start timer + fl = self.s() + + pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run + + ticker_data = self.populate_sell_trend( self.populate_buy_trend(pair_data))[headers].copy() - ticker_data.drop(ticker_data.head(1).index, inplace=True) - if debug_timing: # print time taken - flt = self.f(fl) - print("populate_buy_trend:", pair, round(flt, 10)) - st = self.st() + # to avoid using data from future, we buy/sell with signal from previous candle + ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) + ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) - # #dump same DFs to disk for offline testing in scratch - # f_pair:str = pair - # csv = f_pair.replace("/", "_") - # csv="/Users/creslin/PycharmProjects/freqtrade_new/frames/" + csv - # ticker_data.to_csv(csv, sep='\t', encoding='utf-8') + ticker_data.drop(ticker_data.head(1).index, inplace=True) - #call bslap - results are a list of dicts - bslap_pair_results = self.backslap_pair(ticker_data, pair) - last_bslap_results = bslap_results - bslap_results = last_bslap_results + bslap_pair_results + if debug_timing: # print time taken + flt = self.f(fl) + #print("populate_buy_trend:", pair, round(flt, 10)) + st = self.s() - if debug_timing: # print time taken - tt = self.f(st) - print("Time to Back Slap :", pair, round(tt,10)) - print("-----------------------") + # Convert from Pandas to list for performance reasons + # (Looping Pandas is slow.) + ticker = [x for x in ticker_data.itertuples()] + + lock_pair_until = None + for index, row in enumerate(ticker): + if row.buy == 0 or row.sell == 1: + continue # skip rows where no buy signal or that would immediately sell off + + if realistic: + if lock_pair_until is not None and row.date <= lock_pair_until: + continue + if max_open_trades > 0: + # Check if max_open_trades has already been reached for the given date + if not trade_count_lock.get(row.date, 0) < max_open_trades: + continue + + trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 + + trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:], + trade_count_lock, args) - # Switch List of Trade Dicts (bslap_results) to Dataframe - # Fill missing, calculable columns, profit, duration , abs etc. - bslap_results_df = DataFrame(bslap_results) - bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) - bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) + if trade_entry: + lock_pair_until = trade_entry.close_time + trades.append(trade_entry) + else: + # Set lock_pair_until to end of testing period if trade could not be closed + # This happens only if the buy-signal was with the last candle + lock_pair_until = ticker_data.iloc[-1].date - ### don't use this, itll drop exit type field - # bslap_results_df = DataFrame(bslap_results, columns=BacktestResult._fields) + if debug_timing: # print time taken + tt = self.f(st) + print("Time to BackTest :", pair, round(tt, 10)) + print("-----------------------") - bslap_results_df = self.vector_fill_results_table(bslap_results_df) - - return bslap_results_df - - ########################### Original BT loop - # for pair, pair_data in processed.items(): - # pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run - # - # ticker_data = self.populate_sell_trend( - # self.populate_buy_trend(pair_data))[headers].copy() - # - # # to avoid using data from future, we buy/sell with signal from previous candle - # ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) - # ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) - # - # ticker_data.drop(ticker_data.head(1).index, inplace=True) - # - # # Convert from Pandas to list for performance reasons - # # (Looping Pandas is slow.) - # ticker = [x for x in ticker_data.itertuples()] - # - # lock_pair_until = None - # for index, row in enumerate(ticker): - # if row.buy == 0 or row.sell == 1: - # continue # skip rows where no buy signal or that would immediately sell off - # - # if realistic: - # if lock_pair_until is not None and row.date <= lock_pair_until: - # continue - # if max_open_trades > 0: - # # Check if max_open_trades has already been reached for the given date - # if not trade_count_lock.get(row.date, 0) < max_open_trades: - # continue - # - # trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 - # - # trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:], - # trade_count_lock, args) - # - # - # if trade_entry: - # lock_pair_until = trade_entry.close_time - # trades.append(trade_entry) - # else: - # # Set lock_pair_until to end of testing period if trade could not be closed - # # This happens only if the buy-signal was with the last candle - # lock_pair_until = ticker_data.iloc[-1].date - # - # return DataFrame.from_records(trades, columns=BacktestResult._fields) - ######################## Original BT loop end + return DataFrame.from_records(trades, columns=BacktestResult._fields) + ####################### Original BT loop end def vector_fill_results_table(self, bslap_results_df: DataFrame): """ @@ -333,13 +377,18 @@ class Backtesting(object): :return: bslap_results Dataframe """ import pandas as pd - debug = False + debug = self.debug_vector + + # stake and fees + # stake = 0.015 + # 0.05% is 0.0005 + #fee = 0.001 + + stake = self.config.get('stake_amount') + fee = self.fee + open_fee = fee / 2 + close_fee = fee / 2 - # stake and fees - stake = 0.015 - # 0.05% is 0.00,05 - open_fee = 0.0000 - close_fee = 0.0000 if debug: print("Stake is,", stake, "the sum of currency to spend per trade") print("The open fee is", open_fee, "The close fee is", close_fee) @@ -420,9 +469,12 @@ class Backtesting(object): from datetime import datetime ### backslap debug wrap - debug_2loops = False # only loop twice, for faster debug - debug_timing = False # print timing for each step - debug = False # print values, to check accuracy + # debug_2loops = False # only loop twice, for faster debug + # debug_timing = False # print timing for each step + # debug = False # print values, to check accuracy + debug_2loops = self.debug_2loops # only loop twice, for faster debug + debug_timing = self.debug_timing # print timing for each step + debug = self.debug # print values, to check accuracy # Read Stop Loss Values and Stake stop = self.stop_loss_value @@ -451,20 +503,34 @@ class Backtesting(object): buy stop triggers and stop calculated on # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - stop 6 ''' - np_buy: int = 0 - np_open: int = 1 - np_close: int = 2 - np_sell: int = 3 - np_high: int = 4 - np_low: int = 5 - np_stop: int = 6 - np_bto: int = np_close # buys_triggered_on - should be close - np_bco: int = np_open # buys calculated on - open of the next candle. - #np_sto: int = np_low # stops_triggered_on - Should be low, FT uses close - #np_sco: int = np_stop # stops_calculated_on - Should be stop, FT uses close - np_sto: int = np_close # stops_triggered_on - Should be low, FT uses close - np_sco: int = np_close # stops_calculated_on - Should be stop, FT uses close - # + # np_buy: int = 0 + # np_open: int = 1 + # np_close: int = 2 + # np_sell: int = 3 + # np_high: int = 4 + # np_low: int = 5 + # np_stop: int = 6 + # np_bto: int = np_close # buys_triggered_on - should be close + # np_bco: int = np_open # buys calculated on - open of the next candle. + # #np_sto: int = np_low # stops_triggered_on - Should be low, FT uses close + # #np_sco: int = np_stop # stops_calculated_on - Should be stop, FT uses close + # np_sto: int = np_close # stops_triggered_on - Should be low, FT uses close + # np_sco: int = np_close # stops_calculated_on - Should be stop, FT uses close + + ####### + # Use vars set at top of backtest + np_buy: int = self.np_buy + np_open: int = self.np_open + np_close: int = self.np_close + np_sell: int = self.np_sell + np_high: int = self.np_high + np_low: int = self.np_low + np_stop: int = self.np_stop + np_bto: int = self.np_bto # buys_triggered_on - should be close + np_bco: int = self.np_bco # buys calculated on - open of the next candle. + np_sto: int = self.np_sto # stops_triggered_on - Should be low, FT uses close + np_sco: int = self.np_sco # stops_calculated_on - Should be stop, FT uses close + ### End Config pair: str = pair From 5aaf454f120e75f4e228ece32ba5a396aa3446b4 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 15:48:06 +0000 Subject: [PATCH 09/31] GAS trades verified from candle data to excel by hand All pass 3 sells 1 stop loss --- freqtrade/optimize/backtesting.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bbb52d167..f254bc8a4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -95,10 +95,10 @@ class Backtesting(object): self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close self.use_backslap = True # Enable backslap - self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended + self.debug = True # Main debug enable, very print heavy, enable 2 loops recommended self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging - self.debug_vector = False # Debug vector calcs + self.debug_vector = True # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair @@ -255,6 +255,7 @@ class Backtesting(object): use_backslap = self.use_backslap debug_timing = self.debug_timing_main_loop + if use_backslap: # Use Back Slap code ########################### Call out BSlap Loop instead of Original BT code bslap_results: list = [] @@ -395,7 +396,7 @@ class Backtesting(object): if debug: from pandas import set_option set_option('display.max_rows', 5000) - set_option('display.max_columns', 8) + set_option('display.max_columns', 10) pd.set_option('display.width', 1000) pd.set_option('max_colwidth', 40) pd.set_option('precision', 12) @@ -422,8 +423,7 @@ class Backtesting(object): if debug: print("\n") print(bslap_results_df[ - ['buy_sum', 'buy_fee', 'buy_spend', 'sell_sum', 'sell_take', 'profit_percent', 'profit_abs', - 'exit_type']]) + ['buy_sum', 'buy_fee', 'buy_spend', 'sell_sum','sell_fee', 'sell_take', 'profit_percent', 'profit_abs', 'exit_type']]) return bslap_results_df @@ -708,8 +708,8 @@ class Backtesting(object): if debug: print("\n(4) numpy debug\nNext view index with sell = 1 is ", np_t_sell_ind) print("If 0 or less is returned there is no sell found to end of view, then next lines garbage") - print("Row", np_t_sell_ind, np_t_open_v[np_t_stop_ind]) - print("Row", np_t_sell_ind + 1, np_t_open_v[np_t_stop_ind + 1]) + print("Row", np_t_sell_ind, np_t_open_v[np_t_sell_ind]) + print("Row", np_t_sell_ind + 1, np_t_open_v[np_t_sell_ind + 1]) if debug_timing: t_t = f(st) print("4-numpy", str.format('{0:.17f}', t_t)) From 4a39a754f4811aa4531e8ce0a30019d391a72979 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 15:57:15 +0000 Subject: [PATCH 10/31] Fixed: self.use_backslap = Bool on line97 If self.use_backslap = True Backslap executes If self.use_backslap = False Original Backtest Code executes --- freqtrade/optimize/backtesting.py | 57 ++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f254bc8a4..191616c6c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -94,12 +94,12 @@ class Backtesting(object): self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close - self.use_backslap = True # Enable backslap + self.use_backslap = True # Enable backslap - if false Orginal code is executed. self.debug = True # Main debug enable, very print heavy, enable 2 loops recommended self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging self.debug_vector = True # Debug vector calcs - self.debug_timing_main_loop = False # print overall timing per pair + self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap @staticmethod @@ -245,18 +245,18 @@ class Backtesting(object): :return: DataFrame """ - - headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] - processed = args['processed'] - max_open_trades = args.get('max_open_trades', 0) - realistic = args.get('realistic', False) - trades = [] - trade_count_lock: Dict = {} - use_backslap = self.use_backslap debug_timing = self.debug_timing_main_loop if use_backslap: # Use Back Slap code + + headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] + processed = args['processed'] + max_open_trades = args.get('max_open_trades', 0) + realistic = args.get('realistic', False) + trades = [] + trade_count_lock: Dict = {} + ########################### Call out BSlap Loop instead of Original BT code bslap_results: list = [] for pair, pair_data in processed.items(): @@ -304,6 +304,13 @@ class Backtesting(object): else: # use Original Back test code ########################## Original BT loop + headers = ['date', 'buy', 'open', 'close', 'sell'] + processed = args['processed'] + max_open_trades = args.get('max_open_trades', 0) + realistic = args.get('realistic', False) + trades = [] + trade_count_lock: Dict = {} + for pair, pair_data in processed.items(): if debug_timing: # Start timer fl = self.s() @@ -1035,16 +1042,28 @@ class Backtesting(object): if self.config.get('export', False): self._store_backtest_result(self.config.get('exportfilename'), results) - logger.info( - '\n====================================================== ' - 'BackSLAP REPORT' - ' =======================================================\n' - '%s', - self._generate_text_table( - data, - results + if self.use_backslap: + logger.info( + '\n====================================================== ' + 'BackSLAP REPORT' + ' =======================================================\n' + '%s', + self._generate_text_table( + data, + results + ) + ) + else: + logger.info( + '\n================================================= ' + 'BACKTEST REPORT' + ' ==================================================\n' + '%s', + self._generate_text_table( + data, + results + ) ) - ) ## TODO. Catch open trades for this report. # logger.info( From 0f3339f74f30dec2910fc4c36e401bb390fc5053 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 16:09:42 +0000 Subject: [PATCH 11/31] use ujson to load ticker files 30% faster from disk. --- freqtrade/optimize/__init__.py | 24 ++++++++++++++++++++---- requirements.txt | 7 +++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index e806ff2b8..90dda79e2 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -10,9 +10,16 @@ import arrow from freqtrade import misc, constants, OperationalException from freqtrade.exchange import Exchange from freqtrade.arguments import TimeRange +import importlib +ujson_found = importlib.util.find_spec("ujson") +if ujson_found is not None: + import ujson logger = logging.getLogger(__name__) +if ujson_found is not None: + logger.debug('Loaded UltraJson ujson in optimize.py') + def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: if not tickerlist: @@ -63,11 +70,17 @@ def load_tickerdata_file( if os.path.isfile(gzipfile): logger.debug('Loading ticker data from file %s', gzipfile) with gzip.open(gzipfile) as tickerdata: - pairdata = json.load(tickerdata) + if ujson_found is not None: + pairdata = ujson.load(tickerdata, precise_float=True) + else: + pairdata = json.load(tickerdata) elif os.path.isfile(file): logger.debug('Loading ticker data from file %s', file) with open(file) as tickerdata: - pairdata = json.load(tickerdata) + if ujson_found is not None: + pairdata = ujson.load(tickerdata, precise_float=True) + else: + pairdata = json.load(tickerdata) else: return None @@ -163,7 +176,10 @@ def load_cached_data_for_updating(filename: str, # read the cached file if os.path.isfile(filename): with open(filename, "rt") as file: - data = json.load(file) + if ujson_found is not None: + data = ujson.load(file, precise_float=True) + else: + data = json.load(file) # remove the last item, because we are not sure if it is correct # it could be fetched when the candle was incompleted if data: @@ -226,4 +242,4 @@ def download_backtesting_testdata(datadir: str, logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) logger.debug("New End: %s", misc.format_ms_time(data[-1][0])) - misc.file_dump_json(filename, data) + misc.file_dump_json(filename, data) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3fb91888c..99b471b1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,10 @@ scikit-optimize==0.5.2 # Required for plotting data #plotly==3.0.0 + +# find first, C search in arrays +py_find_1st==1.1.1 + +#Load ticker files 30% faster +ujson==1.35 + From 059aceb582e04c2a64c5370ffb5381e7c1956385 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 16:12:33 +0000 Subject: [PATCH 12/31] Disabled full debug on in last commit Switched Stops to trigger on Low Switched Stops to pay stop-rate not close. --- freqtrade/optimize/backtesting.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 191616c6c..3aabfc2ad 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -89,17 +89,17 @@ class Backtesting(object): self.np_stop: int = 6 self.np_bto: int = self.np_close # buys_triggered_on - should be close self.np_bco: int = self.np_open # buys calculated on - open of the next candle. - # self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close - # self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close - self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close - self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close + self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close + self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close + #self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close + #self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close self.use_backslap = True # Enable backslap - if false Orginal code is executed. - self.debug = True # Main debug enable, very print heavy, enable 2 loops recommended + self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging - self.debug_vector = True # Debug vector calcs - self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap + self.debug_vector = False # Debug vector calcs + self.debug_timing_main_loop = True # print overall timing per pair - works in Backtest and Backslap @staticmethod From 885a653439bd49e8d5b25d75422f2559dfc225aa Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 16:18:54 +0000 Subject: [PATCH 13/31] Disabled full debug on in last commit Switched Stops to trigger on Low Switched Stops to pay stop-rate not close. --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3aabfc2ad..774d4b954 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -98,7 +98,7 @@ class Backtesting(object): self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging - self.debug_vector = False # Debug vector calcs + self.debug_vector = True # Debug vector calcs self.debug_timing_main_loop = True # print overall timing per pair - works in Backtest and Backslap From 99d16e82c0a4109060abe8e5675b6231bfa2b710 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 16:30:11 +0000 Subject: [PATCH 14/31] disable time calcs output on vector displaying in debug. Excessive. --- freqtrade/optimize/backtesting.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 774d4b954..0fd21b14e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -89,10 +89,10 @@ class Backtesting(object): self.np_stop: int = 6 self.np_bto: int = self.np_close # buys_triggered_on - should be close self.np_bco: int = self.np_open # buys calculated on - open of the next candle. - self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close - self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close - #self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close - #self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close + #self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close + #self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close + self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close + self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close self.use_backslap = True # Enable backslap - if false Orginal code is executed. self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended @@ -410,8 +410,8 @@ class Backtesting(object): # Populate duration bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] - if debug: - print(bslap_results_df[['open_time', 'close_time', 'trade_duration']]) + # if debug: + # print(bslap_results_df[['open_time', 'close_time', 'trade_duration']]) ## Spends, Takes, Profit, Absolute Profit # Buy Price From ec1960530b98a2a20d4c2dc6204c2d06765bebd2 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 17:06:06 +0000 Subject: [PATCH 15/31] Added Show trades option If true, prints trades ordered by date after summary. Useful for spotting trends. --- freqtrade/optimize/backtesting.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0fd21b14e..85e42b91a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -94,12 +94,14 @@ class Backtesting(object): self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close - self.use_backslap = True # Enable backslap - if false Orginal code is executed. - self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended - self.debug_timing = False # Stages within Backslap - self.debug_2loops = False # Limit each pair to two loops, useful when debugging - self.debug_vector = True # Debug vector calcs - self.debug_timing_main_loop = True # print overall timing per pair - works in Backtest and Backslap + self.use_backslap = True # Enable backslap - if false Orginal code is executed. + self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended + self.debug_timing = False # Stages within Backslap + self.debug_2loops = False # Limit each pair to two loops, useful when debugging + self.debug_vector = False # Debug vector calcs + self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap + + self.backslap_show_trades = True #prints trades in addition to summary report, also saves to backslap.txt @staticmethod @@ -1053,6 +1055,17 @@ class Backtesting(object): results ) ) + if self.backslap_show_trades: + TradesFrame = results.filter(['open_time', 'pair', 'exit_type', 'profit_percent', 'profit_abs', + 'buy_spend', 'sell_take', 'trade_duration', 'close_time'], axis=1) + + def to_fwf(df, fname): + content = tabulate(df.values.tolist(), list(df.columns), floatfmt=".8f", tablefmt='psql') + print(content) + open(fname, "w").write(content) + + DataFrame.to_fwf = to_fwf(TradesFrame, "backslap.txt") + else: logger.info( '\n================================================= ' From 8d5da4e6ad9ffc93b7ea0b9c6b49cbfa4406ce56 Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 17:48:11 +0000 Subject: [PATCH 16/31] changed defaults Seperated save trades and print trades options. --- freqtrade/optimize/backtesting.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 85e42b91a..43e05d70b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -101,7 +101,8 @@ class Backtesting(object): self.debug_vector = False # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap - self.backslap_show_trades = True #prints trades in addition to summary report, also saves to backslap.txt + self.backslap_show_trades = False # prints trades in addition to summary report + self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt @staticmethod @@ -1055,17 +1056,25 @@ class Backtesting(object): results ) ) + #optional print trades if self.backslap_show_trades: TradesFrame = results.filter(['open_time', 'pair', 'exit_type', 'profit_percent', 'profit_abs', 'buy_spend', 'sell_take', 'trade_duration', 'close_time'], axis=1) - def to_fwf(df, fname): content = tabulate(df.values.tolist(), list(df.columns), floatfmt=".8f", tablefmt='psql') print(content) - open(fname, "w").write(content) DataFrame.to_fwf = to_fwf(TradesFrame, "backslap.txt") + #optional save trades + if self.backslap_save_trades: + TradesFrame = results.filter(['open_time', 'pair', 'exit_type', 'profit_percent', 'profit_abs', + 'buy_spend', 'sell_take', 'trade_duration', 'close_time'], axis=1) + def to_fwf(df, fname): + content = tabulate(df.values.tolist(), list(df.columns), floatfmt=".8f", tablefmt='psql') + open(fname, "w").write(content) + DataFrame.to_fwf = to_fwf(TradesFrame, "backslap.txt") + else: logger.info( '\n================================================= ' From 3b0cb7bc333c980523e38abd5d70ba9c6a24113f Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 18:06:31 +0000 Subject: [PATCH 17/31] Added ujson and py_find_1st to setup.py --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index cd0574fa2..4ec541674 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,8 @@ setup(name='freqtrade', 'cachetools', 'coinmarketcap', 'scikit-optimize', + 'ujson' + 'py_find_1st' ], include_package_data=True, zip_safe=False, From 357c8c0ba0ed902f1757844b0fffb94be45ecc3f Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 18:32:41 +0000 Subject: [PATCH 18/31] sensible defaults --- freqtrade/optimize/backtesting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 43e05d70b..eb002a7ec 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -89,14 +89,14 @@ class Backtesting(object): self.np_stop: int = 6 self.np_bto: int = self.np_close # buys_triggered_on - should be close self.np_bco: int = self.np_open # buys calculated on - open of the next candle. - #self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close - #self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close - self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close - self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close + self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close + self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close + #self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close + #self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close self.use_backslap = True # Enable backslap - if false Orginal code is executed. self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended - self.debug_timing = False # Stages within Backslap + self.debug_timing = True # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging self.debug_vector = False # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap From a31391734798c2f0107b259c9e0b9cf54788c7bf Mon Sep 17 00:00:00 2001 From: creslinux Date: Mon, 16 Jul 2018 18:59:48 +0000 Subject: [PATCH 19/31] Handle a buy on the last candle We will never see this, as buy is on close which is the end of backtest e.g there is no next candle OPEN to buy at, or on --- freqtrade/optimize/backtesting.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index eb002a7ec..d6e4d1daf 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -89,10 +89,10 @@ class Backtesting(object): self.np_stop: int = 6 self.np_bto: int = self.np_close # buys_triggered_on - should be close self.np_bco: int = self.np_open # buys calculated on - open of the next candle. - self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close - self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close - #self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close - #self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close + #self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close + #self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close + self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close + self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close self.use_backslap = True # Enable backslap - if false Orginal code is executed. self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended @@ -437,7 +437,7 @@ class Backtesting(object): return bslap_results_df - def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int): + def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int, np_buy_arr_len: int): import utils_find_1st as utf1st """ The purpose of this def is to return the next "buy" = 1 @@ -469,6 +469,9 @@ class Backtesting(object): if t_open_ind != -1: # send back the -1 if no buys found. otherwise update index t_open_ind = t_open_ind + t_exit_ind # Align numpy index + if t_open_ind == np_buy_arr_len -1 : # If buy found on last candle ignore, there is no OPEN in next to use + t_open_ind = -1 # -1 ends the loop + return t_open_ind def backslap_pair(self, ticker_data, pair): @@ -606,11 +609,12 @@ class Backtesting(object): Requires: np_buy_arr - a 1D array of the 'buy' column. To find next "1" Required: t_exit_ind - Either 0, first loop. Or The index we last exited on + Requires: np_buy_arr_len - length of pair array. Provides: The next "buy" index after t_exit_ind If -1 is returned no buy has been found in remainder of array, skip to exit loop ''' - t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind) + t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len) if debug: print("\n(0) numpy debug \nnp_get_t_open, has returned the next valid buy index as", t_open_ind) From baaf0a5b216d61462dbe55e3ac5b0168ece2ae16 Mon Sep 17 00:00:00 2001 From: creslinux Date: Tue, 17 Jul 2018 08:12:21 +0000 Subject: [PATCH 20/31] Handle when 0 trades are found in any pairs being tested. --- freqtrade/optimize/backtesting.py | 55 ++++++++++++++----------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d6e4d1daf..3cdc9366b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -89,14 +89,14 @@ class Backtesting(object): self.np_stop: int = 6 self.np_bto: int = self.np_close # buys_triggered_on - should be close self.np_bco: int = self.np_open # buys calculated on - open of the next candle. - #self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close - #self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close - self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close - self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close + self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close + self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close + #self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close + #self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close - self.use_backslap = True # Enable backslap - if false Orginal code is executed. + self.use_backslap = True # Enable backslap - if false Orginal code is executed. self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended - self.debug_timing = True # Stages within Backslap + self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging self.debug_vector = False # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap @@ -294,13 +294,16 @@ class Backtesting(object): # Switch List of Trade Dicts (bslap_results) to Dataframe # Fill missing, calculable columns, profit, duration , abs etc. bslap_results_df = DataFrame(bslap_results) - bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) - bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) - ### don't use this, itll drop exit type field - # bslap_results_df = DataFrame(bslap_results, columns=BacktestResult._fields) + if len(bslap_results_df) > 0: # Only post process a frame if it has a record + bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) + bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) + + bslap_results_df = self.vector_fill_results_table(bslap_results_df, pair) + else: + bslap_results_df = [] + bslap_results_df= DataFrame.from_records(bslap_results_df, columns=BacktestResult._fields) - bslap_results_df = self.vector_fill_results_table(bslap_results_df) return bslap_results_df @@ -370,10 +373,12 @@ class Backtesting(object): print("Time to BackTest :", pair, round(tt, 10)) print("-----------------------") + print("trades") + return DataFrame.from_records(trades, columns=BacktestResult._fields) ####################### Original BT loop end - def vector_fill_results_table(self, bslap_results_df: DataFrame): + def vector_fill_results_table(self, bslap_results_df: DataFrame, pair: str): """ The Results frame contains a number of columns that are calculable from othe columns. These are left blank till all rows are added, @@ -411,6 +416,7 @@ class Backtesting(object): pd.set_option('max_colwidth', 40) pd.set_option('precision', 12) + #Ony do Math on a dataframe that has an open; No result pairs contain the pair string only # Populate duration bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] # if debug: @@ -516,19 +522,7 @@ class Backtesting(object): buy stop triggers and stop calculated on # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - stop 6 ''' - # np_buy: int = 0 - # np_open: int = 1 - # np_close: int = 2 - # np_sell: int = 3 - # np_high: int = 4 - # np_low: int = 5 - # np_stop: int = 6 - # np_bto: int = np_close # buys_triggered_on - should be close - # np_bco: int = np_open # buys calculated on - open of the next candle. - # #np_sto: int = np_low # stops_triggered_on - Should be low, FT uses close - # #np_sco: int = np_stop # stops_calculated_on - Should be stop, FT uses close - # np_sto: int = np_close # stops_triggered_on - Should be low, FT uses close - # np_sco: int = np_close # stops_calculated_on - Should be stop, FT uses close + ####### # Use vars set at top of backtest @@ -748,10 +742,10 @@ class Backtesting(object): open 6am 98 3 0 0 ----- ------ ------- ----- ----- -1 means not found till end of view i.e no valid Stop found. Exclude from match. - Stop tiggering in 1, candle we bought at OPEN is valid. + Stop tiggering and closing in 96-1, the candle we bought at OPEN in, is valid. Buys and sells are triggered at candle close - Both with action their postions at the open of the next candle Index + 1 + Both will open their postions at the open of the next candle. i/e + 1 index Stop and buy Indexes are on the view. To map to the ticker dataframe the t_open_ind index should be summed. @@ -760,10 +754,10 @@ class Backtesting(object): t_exit_ind : Sell found in view t_open_ind : Where view was started on ticker_data - TODO: fix this frig for logig test,, case/switch/dictionary would be better... + TODO: fix this frig for logic test,, case/switch/dictionary would be better... more so when later testing many options, dynamic stop / roi etc - cludge - Im setting np_t_sell_ind as 9999999999 when -1 (not found) - cludge - Im setting np_t_stop_ind as 9999999999 when -1 (not found) + cludge - Setting np_t_sell_ind as 9999999999 when -1 (not found) + cludge - Setting np_t_stop_ind as 9999999999 when -1 (not found) ''' if debug: @@ -939,6 +933,7 @@ class Backtesting(object): opportunity to close any more trades. """ # TODO :add handing here to record none closed open trades + if debug: print(bslap_pair_results) break From ed4bf32f2af3d6dfd1dc08aa6e7aa5de5e28031a Mon Sep 17 00:00:00 2001 From: creslinux Date: Tue, 17 Jul 2018 10:59:17 +0000 Subject: [PATCH 21/31] Fixed Stop closing in Index 0 when buy opening on Index 1 --- freqtrade/optimize/backtesting.py | 41 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3cdc9366b..c0259c8fc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -95,10 +95,10 @@ class Backtesting(object): #self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close self.use_backslap = True # Enable backslap - if false Orginal code is executed. - self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended + self.debug = True # Main debug enable, very print heavy, enable 2 loops recommended self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging - self.debug_vector = False # Debug vector calcs + self.debug_vector = True # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap self.backslap_show_trades = False # prints trades in addition to summary report @@ -296,8 +296,10 @@ class Backtesting(object): bslap_results_df = DataFrame(bslap_results) if len(bslap_results_df) > 0: # Only post process a frame if it has a record - bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) - bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) + # bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) + # bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) + # if debug: + # print("open_time and close_time converted to datetime columns") bslap_results_df = self.vector_fill_results_table(bslap_results_df, pair) else: @@ -373,7 +375,6 @@ class Backtesting(object): print("Time to BackTest :", pair, round(tt, 10)) print("-----------------------") - print("trades") return DataFrame.from_records(trades, columns=BacktestResult._fields) ####################### Original BT loop end @@ -393,6 +394,7 @@ class Backtesting(object): :return: bslap_results Dataframe """ import pandas as pd + import numpy as np debug = self.debug_vector # stake and fees @@ -416,8 +418,6 @@ class Backtesting(object): pd.set_option('max_colwidth', 40) pd.set_option('precision', 12) - #Ony do Math on a dataframe that has an open; No result pairs contain the pair string only - # Populate duration bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] # if debug: # print(bslap_results_df[['open_time', 'close_time', 'trade_duration']]) @@ -621,9 +621,9 @@ class Backtesting(object): if t_open_ind != -1: """ - 1 - Create view to search within for our open trade + 1 - Create views to search within for our open trade - The view is our search space for the next Stop or Sell + The views are our search space for the next Stop or Sell Numpy view is employed as: 1,000 faster than pandas searches Pandas cannot assure it will always return a view, it may make a slow copy. @@ -633,9 +633,12 @@ class Backtesting(object): Requires: np_bslap is our numpy array of the ticker DataFrame Requires: t_open_ind is the index row with the buy. - Provided: np_t_open_v View of array after trade. + Provides: np_t_open_v View of array after buy. + Provides: np_t_open_v_stop View of array after buy +1 + (Stop will search in here to prevent stopping in the past) """ np_t_open_v = np_bslap[t_open_ind:] + np_t_open_v_stop = np_bslap[t_open_ind +1:] if debug: print("\n(1) numpy debug \nNumpy view row 0 is now Ticker_Data Index", t_open_ind) @@ -675,23 +678,27 @@ class Backtesting(object): where [np_sto] (stop tiggered on variable: "close", "low" etc) < np_t_stop_pri - Requires: np_t_open_v Numpy view of ticker_data after trade open + Requires: np_t_open_v_stop Numpy view of ticker_data after buy row +1 (when trade was opened) Requires: np_sto User Var(STO)StopTriggeredOn. Typically set to "low" or "close" Requires: np_t_stop_pri The stop-loss price STO must fall under to trigger stop Provides: np_t_stop_ind The first candle after trade open where STO is under stop-loss ''' - np_t_stop_ind = utf1st.find_1st(np_t_open_v[:, np_sto], + np_t_stop_ind = utf1st.find_1st(np_t_open_v_stop[:, np_sto], np_t_stop_pri, utf1st.cmp_smaller) + # plus 1 as np_t_open_v_stop is 1 ahead of view np_t_open_v, used from here on out. + np_t_stop_ind = np_t_stop_ind +1 + + if debug: - print("\n(3) numpy debug\nNext view index with STO (stop trigger on) under Stop-Loss is", np_t_stop_ind, + print("\n(3) numpy debug\nNext view index with STO (stop trigger on) under Stop-Loss is", np_t_stop_ind -1, ". STO is using field", np_sto, "\nFrom key: buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5\n") - print("If -1 returned there is no stop found to end of view, then next two array lines are garbage") - print("Row", np_t_stop_ind, np_t_open_v[np_t_stop_ind]) - print("Row", np_t_stop_ind + 1, np_t_open_v[np_t_stop_ind + 1]) + print("If -1 or 0 returned there is no stop found to end of view, then next two array lines are garbage") + print("Row", np_t_stop_ind -1 , np_t_open_v[np_t_stop_ind]) + print("Row", np_t_stop_ind , np_t_open_v[np_t_stop_ind + 1]) if debug_timing: t_t = f(st) print("3-numpy", str.format('{0:.17f}', t_t)) @@ -765,7 +772,7 @@ class Backtesting(object): # cludge for logic test (-1) means it was not found, set crazy high to lose < test np_t_sell_ind = 99999999 if np_t_sell_ind <= 0 else np_t_sell_ind - np_t_stop_ind = 99999999 if np_t_stop_ind == -1 else np_t_stop_ind + np_t_stop_ind = 99999999 if np_t_stop_ind <= 0 else np_t_stop_ind # Stoploss trigger found before a sell =1 if np_t_stop_ind < 99999999 and np_t_stop_ind <= np_t_sell_ind: From 8cea0517eb4e225553afbc8bc450be97a214ef11 Mon Sep 17 00:00:00 2001 From: creslinux Date: Tue, 17 Jul 2018 11:22:38 +0000 Subject: [PATCH 22/31] Added stop_stops stop_stops is an int value when number of stops in a pair reached the int the pair is stopped trading. This allows backtest to align with my pre_trade_mgt that does the same in dry and live operations --- freqtrade/optimize/backtesting.py | 32 ++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c0259c8fc..8bc6efb4d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -95,14 +95,16 @@ class Backtesting(object): #self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close self.use_backslap = True # Enable backslap - if false Orginal code is executed. - self.debug = True # Main debug enable, very print heavy, enable 2 loops recommended + self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging - self.debug_vector = True # Debug vector calcs + self.debug_vector = False # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap - self.backslap_show_trades = False # prints trades in addition to summary report - self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt + self.backslap_show_trades = True # prints trades in addition to summary report + self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt + + self.stop_stops: int = 9999 # stop back testing any pair with this many stops, set to 999999 to not hit @staticmethod @@ -389,7 +391,6 @@ class Backtesting(object): - Profit - trade duration - profit abs - :param bslap_results Dataframe :return: bslap_results Dataframe """ @@ -443,14 +444,19 @@ class Backtesting(object): return bslap_results_df - def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int, np_buy_arr_len: int): + def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int, np_buy_arr_len: int, stop_stops: int, stop_stops_count: int): import utils_find_1st as utf1st """ The purpose of this def is to return the next "buy" = 1 after t_exit_ind. + + This function will also check is the stop limit for the pair has been reached. + if stop_stops is the limit and stop_stops_count it the number of times the stop has been hit. t_exit_ind is the index the last trade exited on or 0 if first time around this loop. + + stop_stops i """ # Timers, to be called if in debug def s(): @@ -478,6 +484,10 @@ class Backtesting(object): if t_open_ind == np_buy_arr_len -1 : # If buy found on last candle ignore, there is no OPEN in next to use t_open_ind = -1 # -1 ends the loop + if stop_stops_count >= stop_stops: # if maximum number of stops allowed in a pair is hit, exit loop + t_open_ind = -1 # -1 ends the loop + print("Max stop limit ", stop_stops, "reached. Moving to next pair") + return t_open_ind def backslap_pair(self, ticker_data, pair): @@ -564,6 +574,9 @@ class Backtesting(object): t_exit_ind = 0 # Start loop from first index t_exit_last = 0 # To test for exit + stop_stops = self.stop_stops # Int of stops within a pair to stop trading a pair at + stop_stops_count = 0 # stop counter per pair + st = s() # Start timer for processing dataframe if debug: print('Processing:', pair) @@ -604,11 +617,13 @@ class Backtesting(object): Requires: np_buy_arr - a 1D array of the 'buy' column. To find next "1" Required: t_exit_ind - Either 0, first loop. Or The index we last exited on Requires: np_buy_arr_len - length of pair array. + Requires: stops_stops - number of stops allowed before stop trading a pair + Requires: stop_stop_counts - count of stops hit in the pair Provides: The next "buy" index after t_exit_ind If -1 is returned no buy has been found in remainder of array, skip to exit loop ''' - t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len) + t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len, stop_stops, stop_stops_count) if debug: print("\n(0) numpy debug \nnp_get_t_open, has returned the next valid buy index as", t_open_ind) @@ -971,6 +986,9 @@ class Backtesting(object): # append the dict to the list and print list bslap_pair_results.append(bslap_result) + if t_exit_type is "stop": + stop_stops_count = stop_stops_count + 1 + if debug: print("The trade dict is: \n", bslap_result) print("Trades dicts in list after append are: \n ", bslap_pair_results) From 3184c85dca50f60c263d8f7d3c4981d41a132e56 Mon Sep 17 00:00:00 2001 From: creslinux Date: Tue, 17 Jul 2018 21:33:11 +0000 Subject: [PATCH 23/31] default settings to trigger low, take stop --- freqtrade/optimize/backtesting.py | 55 +++++++++++++++++-------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8bc6efb4d..6cd271534 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -101,7 +101,7 @@ class Backtesting(object): self.debug_vector = False # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap - self.backslap_show_trades = True # prints trades in addition to summary report + self.backslap_show_trades = False # prints trades in addition to summary report self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt self.stop_stops: int = 9999 # stop back testing any pair with this many stops, set to 999999 to not hit @@ -414,33 +414,35 @@ class Backtesting(object): if debug: from pandas import set_option set_option('display.max_rows', 5000) - set_option('display.max_columns', 10) + set_option('display.max_columns', 20) pd.set_option('display.width', 1000) pd.set_option('max_colwidth', 40) pd.set_option('precision', 12) bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] - # if debug: - # print(bslap_results_df[['open_time', 'close_time', 'trade_duration']]) ## Spends, Takes, Profit, Absolute Profit + print(bslap_results_df) # Buy Price - bslap_results_df['buy_sum'] = stake * bslap_results_df['open_rate'] - bslap_results_df['buy_fee'] = bslap_results_df['buy_sum'] * open_fee - bslap_results_df['buy_spend'] = bslap_results_df['buy_sum'] + bslap_results_df['buy_fee'] + bslap_results_df['buy_vol'] = stake / bslap_results_df['open_rate'] # How many target are we buying + bslap_results_df['buy_fee'] = stake * open_fee + bslap_results_df['buy_spend'] = stake + bslap_results_df['buy_fee'] # How much we're spending + # Sell price - bslap_results_df['sell_sum'] = stake * bslap_results_df['close_rate'] - bslap_results_df['sell_fee'] = bslap_results_df['sell_sum'] * close_fee + bslap_results_df['sell_sum'] = bslap_results_df['buy_vol'] * bslap_results_df['close_rate'] + bslap_results_df['sell_fee'] = bslap_results_df['sell_sum'] * close_fee bslap_results_df['sell_take'] = bslap_results_df['sell_sum'] - bslap_results_df['sell_fee'] # profit_percent - bslap_results_df['profit_percent'] = bslap_results_df['sell_take'] / bslap_results_df['buy_spend'] - 1 + bslap_results_df['profit_percent'] = (bslap_results_df['sell_take'] - bslap_results_df['buy_spend']) \ + / bslap_results_df['buy_spend'] # Absolute profit bslap_results_df['profit_abs'] = bslap_results_df['sell_take'] - bslap_results_df['buy_spend'] + if debug: print("\n") print(bslap_results_df[ - ['buy_sum', 'buy_fee', 'buy_spend', 'sell_sum','sell_fee', 'sell_take', 'profit_percent', 'profit_abs', 'exit_type']]) + ['buy_vol', 'buy_fee', 'buy_spend', 'sell_sum','sell_fee', 'sell_take', 'profit_percent', 'profit_abs', 'exit_type']]) return bslap_results_df @@ -458,6 +460,8 @@ class Backtesting(object): stop_stops i """ + debug = self.debug + # Timers, to be called if in debug def s(): st = timeit.default_timer() @@ -486,7 +490,8 @@ class Backtesting(object): if stop_stops_count >= stop_stops: # if maximum number of stops allowed in a pair is hit, exit loop t_open_ind = -1 # -1 ends the loop - print("Max stop limit ", stop_stops, "reached. Moving to next pair") + if debug: + print("Max stop limit ", stop_stops, "reached. Moving to next pair") return t_open_ind @@ -1026,6 +1031,8 @@ class Backtesting(object): timerange = Arguments.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) + + ld_files = self.s() data = optimize.load_data( self.config['datadir'], pairs=pairs, @@ -1046,6 +1053,8 @@ class Backtesting(object): max_open_trades = 0 preprocessed = self.tickerdata_to_dataframe(data) + t_t = self.f(ld_files) + print("Load from json to file to df in mem took", t_t) # Print timeframe min_date, max_date = self.get_timeframe(preprocessed) @@ -1110,18 +1119,16 @@ class Backtesting(object): results ) ) - - ## TODO. Catch open trades for this report. - # logger.info( - # '\n=============================================== ' - # 'LEFT OPEN TRADES REPORT' - # ' ===============================================\n' - # '%s', - # self._generate_text_table( - # data, - # results.loc[results.open_at_end] - # ) - # ) + logger.info( + '\n=============================================== ' + 'LEFT OPEN TRADES REPORT' + ' ===============================================\n' + '%s', + self._generate_text_table( + data, + results.loc[results.open_at_end] + ) + ) def setup_configuration(args: Namespace) -> Dict[str, Any]: From 79f931f29639e05232ba1044cebceaf2e6a3e332 Mon Sep 17 00:00:00 2001 From: creslin <34645187+creslinux@users.noreply.github.com> Date: Wed, 25 Jul 2018 19:49:25 +0000 Subject: [PATCH 24/31] Update backtesting.py --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c0259c8fc..636a95b2d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -95,10 +95,10 @@ class Backtesting(object): #self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close self.use_backslap = True # Enable backslap - if false Orginal code is executed. - self.debug = True # Main debug enable, very print heavy, enable 2 loops recommended + self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging - self.debug_vector = True # Debug vector calcs + self.debug_vector = False # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap self.backslap_show_trades = False # prints trades in addition to summary report From 482b85182ad77a517e79d98d772f93f1f2536061 Mon Sep 17 00:00:00 2001 From: creslin <34645187+creslinux@users.noreply.github.com> Date: Thu, 26 Jul 2018 06:53:43 +0000 Subject: [PATCH 25/31] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4ec541674..bfe01e6fe 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ setup(name='freqtrade', 'cachetools', 'coinmarketcap', 'scikit-optimize', - 'ujson' + 'ujson', 'py_find_1st' ], include_package_data=True, From e39ae45d2f5ff7c954b7a45201bf4a941c6c55cd Mon Sep 17 00:00:00 2001 From: creslinux Date: Thu, 26 Jul 2018 18:40:45 +0000 Subject: [PATCH 26/31] Some reason did not push this... vector calcs redone. --- freqtrade/optimize/backtesting.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6cd271534..5b4487762 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -419,6 +419,12 @@ class Backtesting(object): pd.set_option('max_colwidth', 40) pd.set_option('precision', 12) + # # Get before + # csv = "cryptosher_before_debug" + # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + + bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] ## Spends, Takes, Profit, Absolute Profit @@ -438,6 +444,10 @@ class Backtesting(object): # Absolute profit bslap_results_df['profit_abs'] = bslap_results_df['sell_take'] - bslap_results_df['buy_spend'] + # # Get After + # csv="cryptosher_after_debug" + # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + if debug: print("\n") From 2dc3d6a6b857d558beb8b0291f3921d0670783f2 Mon Sep 17 00:00:00 2001 From: creslin <34645187+creslinux@users.noreply.github.com> Date: Thu, 26 Jul 2018 18:42:20 +0000 Subject: [PATCH 27/31] Update backtesting.py --- freqtrade/optimize/backtesting.py | 89 +++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 636a95b2d..5b4487762 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -98,11 +98,13 @@ class Backtesting(object): self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging - self.debug_vector = False # Debug vector calcs + self.debug_vector = False # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap self.backslap_show_trades = False # prints trades in addition to summary report - self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt + self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt + + self.stop_stops: int = 9999 # stop back testing any pair with this many stops, set to 999999 to not hit @staticmethod @@ -389,7 +391,6 @@ class Backtesting(object): - Profit - trade duration - profit abs - :param bslap_results Dataframe :return: bslap_results Dataframe """ @@ -413,45 +414,64 @@ class Backtesting(object): if debug: from pandas import set_option set_option('display.max_rows', 5000) - set_option('display.max_columns', 10) + set_option('display.max_columns', 20) pd.set_option('display.width', 1000) pd.set_option('max_colwidth', 40) pd.set_option('precision', 12) + # # Get before + # csv = "cryptosher_before_debug" + # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + + bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] - # if debug: - # print(bslap_results_df[['open_time', 'close_time', 'trade_duration']]) ## Spends, Takes, Profit, Absolute Profit + print(bslap_results_df) # Buy Price - bslap_results_df['buy_sum'] = stake * bslap_results_df['open_rate'] - bslap_results_df['buy_fee'] = bslap_results_df['buy_sum'] * open_fee - bslap_results_df['buy_spend'] = bslap_results_df['buy_sum'] + bslap_results_df['buy_fee'] + bslap_results_df['buy_vol'] = stake / bslap_results_df['open_rate'] # How many target are we buying + bslap_results_df['buy_fee'] = stake * open_fee + bslap_results_df['buy_spend'] = stake + bslap_results_df['buy_fee'] # How much we're spending + # Sell price - bslap_results_df['sell_sum'] = stake * bslap_results_df['close_rate'] - bslap_results_df['sell_fee'] = bslap_results_df['sell_sum'] * close_fee + bslap_results_df['sell_sum'] = bslap_results_df['buy_vol'] * bslap_results_df['close_rate'] + bslap_results_df['sell_fee'] = bslap_results_df['sell_sum'] * close_fee bslap_results_df['sell_take'] = bslap_results_df['sell_sum'] - bslap_results_df['sell_fee'] # profit_percent - bslap_results_df['profit_percent'] = bslap_results_df['sell_take'] / bslap_results_df['buy_spend'] - 1 + bslap_results_df['profit_percent'] = (bslap_results_df['sell_take'] - bslap_results_df['buy_spend']) \ + / bslap_results_df['buy_spend'] # Absolute profit bslap_results_df['profit_abs'] = bslap_results_df['sell_take'] - bslap_results_df['buy_spend'] + # # Get After + # csv="cryptosher_after_debug" + # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + + if debug: print("\n") print(bslap_results_df[ - ['buy_sum', 'buy_fee', 'buy_spend', 'sell_sum','sell_fee', 'sell_take', 'profit_percent', 'profit_abs', 'exit_type']]) + ['buy_vol', 'buy_fee', 'buy_spend', 'sell_sum','sell_fee', 'sell_take', 'profit_percent', 'profit_abs', 'exit_type']]) return bslap_results_df - def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int, np_buy_arr_len: int): + def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int, np_buy_arr_len: int, stop_stops: int, stop_stops_count: int): import utils_find_1st as utf1st """ The purpose of this def is to return the next "buy" = 1 after t_exit_ind. + + This function will also check is the stop limit for the pair has been reached. + if stop_stops is the limit and stop_stops_count it the number of times the stop has been hit. t_exit_ind is the index the last trade exited on or 0 if first time around this loop. + + stop_stops i """ + debug = self.debug + # Timers, to be called if in debug def s(): st = timeit.default_timer() @@ -478,6 +498,11 @@ class Backtesting(object): if t_open_ind == np_buy_arr_len -1 : # If buy found on last candle ignore, there is no OPEN in next to use t_open_ind = -1 # -1 ends the loop + if stop_stops_count >= stop_stops: # if maximum number of stops allowed in a pair is hit, exit loop + t_open_ind = -1 # -1 ends the loop + if debug: + print("Max stop limit ", stop_stops, "reached. Moving to next pair") + return t_open_ind def backslap_pair(self, ticker_data, pair): @@ -564,6 +589,9 @@ class Backtesting(object): t_exit_ind = 0 # Start loop from first index t_exit_last = 0 # To test for exit + stop_stops = self.stop_stops # Int of stops within a pair to stop trading a pair at + stop_stops_count = 0 # stop counter per pair + st = s() # Start timer for processing dataframe if debug: print('Processing:', pair) @@ -604,11 +632,13 @@ class Backtesting(object): Requires: np_buy_arr - a 1D array of the 'buy' column. To find next "1" Required: t_exit_ind - Either 0, first loop. Or The index we last exited on Requires: np_buy_arr_len - length of pair array. + Requires: stops_stops - number of stops allowed before stop trading a pair + Requires: stop_stop_counts - count of stops hit in the pair Provides: The next "buy" index after t_exit_ind If -1 is returned no buy has been found in remainder of array, skip to exit loop ''' - t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len) + t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len, stop_stops, stop_stops_count) if debug: print("\n(0) numpy debug \nnp_get_t_open, has returned the next valid buy index as", t_open_ind) @@ -971,6 +1001,9 @@ class Backtesting(object): # append the dict to the list and print list bslap_pair_results.append(bslap_result) + if t_exit_type is "stop": + stop_stops_count = stop_stops_count + 1 + if debug: print("The trade dict is: \n", bslap_result) print("Trades dicts in list after append are: \n ", bslap_pair_results) @@ -1008,6 +1041,8 @@ class Backtesting(object): timerange = Arguments.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) + + ld_files = self.s() data = optimize.load_data( self.config['datadir'], pairs=pairs, @@ -1028,6 +1063,8 @@ class Backtesting(object): max_open_trades = 0 preprocessed = self.tickerdata_to_dataframe(data) + t_t = self.f(ld_files) + print("Load from json to file to df in mem took", t_t) # Print timeframe min_date, max_date = self.get_timeframe(preprocessed) @@ -1092,18 +1129,16 @@ class Backtesting(object): results ) ) - - ## TODO. Catch open trades for this report. - # logger.info( - # '\n=============================================== ' - # 'LEFT OPEN TRADES REPORT' - # ' ===============================================\n' - # '%s', - # self._generate_text_table( - # data, - # results.loc[results.open_at_end] - # ) - # ) + logger.info( + '\n=============================================== ' + 'LEFT OPEN TRADES REPORT' + ' ===============================================\n' + '%s', + self._generate_text_table( + data, + results.loc[results.open_at_end] + ) + ) def setup_configuration(args: Namespace) -> Dict[str, Any]: From 0372485cf025aa3b4c7e5484d695899681094f05 Mon Sep 17 00:00:00 2001 From: creslinux Date: Thu, 26 Jul 2018 19:17:00 +0000 Subject: [PATCH 28/31] Some reason did not push this... vector calcs redone. --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5b4487762..36ecc6826 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -423,12 +423,12 @@ class Backtesting(object): # csv = "cryptosher_before_debug" # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') - bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] ## Spends, Takes, Profit, Absolute Profit - print(bslap_results_df) + # print(bslap_results_df) # Buy Price bslap_results_df['buy_vol'] = stake / bslap_results_df['open_rate'] # How many target are we buying bslap_results_df['buy_fee'] = stake * open_fee From 1a673c6ac94e036b9f66343bce9c8d4cb237d91f Mon Sep 17 00:00:00 2001 From: Gert Date: Sat, 28 Jul 2018 14:23:18 -0700 Subject: [PATCH 29/31] working on moving backslap --- freqtrade/optimize/backslapping.py | 785 +++++++++++++++++++++++++++++ freqtrade/optimize/backtesting.py | 745 +-------------------------- 2 files changed, 810 insertions(+), 720 deletions(-) create mode 100644 freqtrade/optimize/backslapping.py diff --git a/freqtrade/optimize/backslapping.py b/freqtrade/optimize/backslapping.py new file mode 100644 index 000000000..dff55648f --- /dev/null +++ b/freqtrade/optimize/backslapping.py @@ -0,0 +1,785 @@ +import timeit +from typing import Dict, Any + +from pandas import DataFrame + +from freqtrade.analyze import Analyze +from freqtrade.exchange import Exchange + + +class Backslapping: + """ + provides a quick way to evaluate strategies over a longer term of time + """ + + def __init__(self, config: Dict[str, Any], exchange = None) -> None: + """ + constructor + """ + + self.config = config + self.analyze = Analyze(self.config) + self.ticker_interval = self.analyze.strategy.ticker_interval + self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe + self.populate_buy_trend = self.analyze.populate_buy_trend + self.populate_sell_trend = self.analyze.populate_sell_trend + + ### + # + ### + if exchange is None: + self.config['exchange']['secret'] = '' + self.config['exchange']['password'] = '' + self.config['exchange']['uid'] = '' + self.config['dry_run'] = True + self.exchange = Exchange(self.config) + else: + self.exchange = exchange + + self.fee = self.exchange.get_fee() + + self.stop_loss_value = self.analyze.strategy.stoploss + + #### backslap config + ''' + Numpy arrays are used for 100x speed up + We requires setting Int values for + buy stop triggers and stop calculated on + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - stop 6 + ''' + self.np_buy: int = 0 + self.np_open: int = 1 + self.np_close: int = 2 + self.np_sell: int = 3 + self.np_high: int = 4 + self.np_low: int = 5 + self.np_stop: int = 6 + self.np_bto: int = self.np_close # buys_triggered_on - should be close + self.np_bco: int = self.np_open # buys calculated on - open of the next candle. + self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close + self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close + # self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close + # self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close + + self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended + self.debug_timing = False # Stages within Backslap + self.debug_2loops = False # Limit each pair to two loops, useful when debugging + self.debug_vector = False # Debug vector calcs + self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap + + self.backslap_show_trades = False # prints trades in addition to summary report + self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt + + self.stop_stops: int = 9999 # stop back testing any pair with this many stops, set to 999999 to not hit + + def s(self): + st = timeit.default_timer() + return st + + def f(self, st): + return (timeit.default_timer() - st) + + def run(self,args): + + headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] + processed = args['processed'] + max_open_trades = args.get('max_open_trades', 0) + realistic = args.get('realistic', False) + trades = [] + trade_count_lock: Dict = {} + + ########################### Call out BSlap Loop instead of Original BT code + bslap_results: list = [] + for pair, pair_data in processed.items(): + if self.debug_timing: # Start timer + fl = self.s() + + ticker_data = self.populate_sell_trend( + self.populate_buy_trend(pair_data))[headers].copy() + + if self.debug_timing: # print time taken + flt = self.f(fl) + # print("populate_buy_trend:", pair, round(flt, 10)) + st = self.s() + + # #dump same DFs to disk for offline testing in scratch + # f_pair:str = pair + # csv = f_pair.replace("/", "_") + # csv="/Users/creslin/PycharmProjects/freqtrade_new/frames/" + csv + # ticker_data.to_csv(csv, sep='\t', encoding='utf-8') + + # call bslap - results are a list of dicts + bslap_pair_results = self.backslap_pair(ticker_data, pair) + last_bslap_results = bslap_results + bslap_results = last_bslap_results + bslap_pair_results + + if self.debug_timing: # print time taken + tt = self.f(st) + print("Time to BackSlap :", pair, round(tt, 10)) + print("-----------------------") + + # Switch List of Trade Dicts (bslap_results) to Dataframe + # Fill missing, calculable columns, profit, duration , abs etc. + bslap_results_df = DataFrame(bslap_results) + + if len(bslap_results_df) > 0: # Only post process a frame if it has a record + # bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) + # bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) + # if debug: + # print("open_time and close_time converted to datetime columns") + + bslap_results_df = self.vector_fill_results_table(bslap_results_df, pair) + else: + from freqtrade.optimize.backtesting import BacktestResult + + bslap_results_df = [] + bslap_results_df = DataFrame.from_records(bslap_results_df, columns=BacktestResult._fields) + + return bslap_results_df + + def vector_fill_results_table(self, bslap_results_df: DataFrame, pair: str): + """ + The Results frame contains a number of columns that are calculable + from othe columns. These are left blank till all rows are added, + to be populated in single vector calls. + + Columns to be populated are: + - Profit + - trade duration + - profit abs + :param bslap_results Dataframe + :return: bslap_results Dataframe + """ + import pandas as pd + import numpy as np + debug = self.debug_vector + + # stake and fees + # stake = 0.015 + # 0.05% is 0.0005 + # fee = 0.001 + + stake = self.config.get('stake_amount') + fee = self.fee + open_fee = fee / 2 + close_fee = fee / 2 + + if debug: + print("Stake is,", stake, "the sum of currency to spend per trade") + print("The open fee is", open_fee, "The close fee is", close_fee) + if debug: + from pandas import set_option + set_option('display.max_rows', 5000) + set_option('display.max_columns', 20) + pd.set_option('display.width', 1000) + pd.set_option('max_colwidth', 40) + pd.set_option('precision', 12) + + # # Get before + # csv = "cryptosher_before_debug" + # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + + # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + + bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] + + ## Spends, Takes, Profit, Absolute Profit + # print(bslap_results_df) + # Buy Price + bslap_results_df['buy_vol'] = stake / bslap_results_df['open_rate'] # How many target are we buying + bslap_results_df['buy_fee'] = stake * open_fee + bslap_results_df['buy_spend'] = stake + bslap_results_df['buy_fee'] # How much we're spending + + # Sell price + bslap_results_df['sell_sum'] = bslap_results_df['buy_vol'] * bslap_results_df['close_rate'] + bslap_results_df['sell_fee'] = bslap_results_df['sell_sum'] * close_fee + bslap_results_df['sell_take'] = bslap_results_df['sell_sum'] - bslap_results_df['sell_fee'] + # profit_percent + bslap_results_df['profit_percent'] = (bslap_results_df['sell_take'] - bslap_results_df['buy_spend']) \ + / bslap_results_df['buy_spend'] + # Absolute profit + bslap_results_df['profit_abs'] = bslap_results_df['sell_take'] - bslap_results_df['buy_spend'] + + # # Get After + # csv="cryptosher_after_debug" + # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') + + if debug: + print("\n") + print(bslap_results_df[ + ['buy_vol', 'buy_fee', 'buy_spend', 'sell_sum', 'sell_fee', 'sell_take', 'profit_percent', + 'profit_abs', 'exit_type']]) + + return bslap_results_df + + def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int, np_buy_arr_len: int, stop_stops: int, + stop_stops_count: int): + import utils_find_1st as utf1st + """ + The purpose of this def is to return the next "buy" = 1 + after t_exit_ind. + + This function will also check is the stop limit for the pair has been reached. + if stop_stops is the limit and stop_stops_count it the number of times the stop has been hit. + + t_exit_ind is the index the last trade exited on + or 0 if first time around this loop. + + stop_stops i + """ + debug = self.debug + + # Timers, to be called if in debug + def s(): + st = timeit.default_timer() + return st + + def f(st): + return (timeit.default_timer() - st) + + st = s() + t_open_ind: int + + """ + Create a view on our buy index starting after last trade exit + Search for next buy + """ + np_buy_arr_v = np_buy_arr[t_exit_ind:] + t_open_ind = utf1st.find_1st(np_buy_arr_v, 1, utf1st.cmp_equal) + + ''' + If -1 is returned no buy has been found, preserve the value + ''' + if t_open_ind != -1: # send back the -1 if no buys found. otherwise update index + t_open_ind = t_open_ind + t_exit_ind # Align numpy index + + if t_open_ind == np_buy_arr_len - 1: # If buy found on last candle ignore, there is no OPEN in next to use + t_open_ind = -1 # -1 ends the loop + + if stop_stops_count >= stop_stops: # if maximum number of stops allowed in a pair is hit, exit loop + t_open_ind = -1 # -1 ends the loop + if debug: + print("Max stop limit ", stop_stops, "reached. Moving to next pair") + + return t_open_ind + + def backslap_pair(self, ticker_data, pair): + import pandas as pd + import numpy as np + import timeit + import utils_find_1st as utf1st + from datetime import datetime + + ### backslap debug wrap + # debug_2loops = False # only loop twice, for faster debug + # debug_timing = False # print timing for each step + # debug = False # print values, to check accuracy + debug_2loops = self.debug_2loops # only loop twice, for faster debug + debug_timing = self.debug_timing # print timing for each step + debug = self.debug # print values, to check accuracy + + # Read Stop Loss Values and Stake + stop = self.stop_loss_value + p_stop = (stop + 1) # What stop really means, e.g 0.01 is 0.99 of price + + if debug: + print("Stop is ", stop, "value from stragey file") + print("p_stop is", p_stop, "value used to multiply to entry price") + + if debug: + from pandas import set_option + set_option('display.max_rows', 5000) + set_option('display.max_columns', 8) + pd.set_option('display.width', 1000) + pd.set_option('max_colwidth', 40) + pd.set_option('precision', 12) + + def s(): + st = timeit.default_timer() + return st + + def f(st): + return (timeit.default_timer() - st) + + #### backslap config + ''' + Numpy arrays are used for 100x speed up + We requires setting Int values for + buy stop triggers and stop calculated on + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - stop 6 + ''' + + ####### + # Use vars set at top of backtest + np_buy: int = self.np_buy + np_open: int = self.np_open + np_close: int = self.np_close + np_sell: int = self.np_sell + np_high: int = self.np_high + np_low: int = self.np_low + np_stop: int = self.np_stop + np_bto: int = self.np_bto # buys_triggered_on - should be close + np_bco: int = self.np_bco # buys calculated on - open of the next candle. + np_sto: int = self.np_sto # stops_triggered_on - Should be low, FT uses close + np_sco: int = self.np_sco # stops_calculated_on - Should be stop, FT uses close + + ### End Config + + pair: str = pair + + # ticker_data: DataFrame = ticker_dfs[t_file] + bslap: DataFrame = ticker_data + + # Build a single dimension numpy array from "buy" index for faster search + # (500x faster than pandas) + np_buy_arr = bslap['buy'].values + np_buy_arr_len: int = len(np_buy_arr) + + # use numpy array for faster searches in loop, 20x faster than pandas + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + np_bslap = np.array(bslap[['buy', 'open', 'close', 'sell', 'high', 'low']]) + + # Build a numpy list of date-times. + # We use these when building the trade + # The rationale is to address a value from a pandas cell is thousands of + # times more expensive. Processing time went X25 when trying to use any data from pandas + np_bslap_dates = bslap['date'].values + + loop: int = 0 # how many time around the loop + t_exit_ind = 0 # Start loop from first index + t_exit_last = 0 # To test for exit + + stop_stops = self.stop_stops # Int of stops within a pair to stop trading a pair at + stop_stops_count = 0 # stop counter per pair + + st = s() # Start timer for processing dataframe + if debug: + print('Processing:', pair) + + # Results will be stored in a list of dicts + bslap_pair_results: list = [] + bslap_result: dict = {} + + while t_exit_ind < np_buy_arr_len: + loop = loop + 1 + if debug or debug_timing: + print("-- T_exit_Ind - Numpy Index is", t_exit_ind, " ----------------------- Loop", loop, pair) + if debug_2loops: + if loop == 3: + print( + "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++Loop debug max met - breaking") + break + ''' + Dev phases + Phase 1 + 1) Manage buy, sell, stop enter/exit + a) Find first buy index + b) Discover first stop and sell hit after buy index + c) Chose first instance as trade exit + + Phase 2 + 2) Manage dynamic Stop and ROI Exit + a) Create trade slice from 1 + b) search within trade slice for dynamice stop hit + c) search within trade slice for ROI hit + ''' + + if debug_timing: + st = s() + ''' + 0 - Find next buy entry + Finds index for first (buy = 1) flag + + Requires: np_buy_arr - a 1D array of the 'buy' column. To find next "1" + Required: t_exit_ind - Either 0, first loop. Or The index we last exited on + Requires: np_buy_arr_len - length of pair array. + Requires: stops_stops - number of stops allowed before stop trading a pair + Requires: stop_stop_counts - count of stops hit in the pair + Provides: The next "buy" index after t_exit_ind + + If -1 is returned no buy has been found in remainder of array, skip to exit loop + ''' + t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len, stop_stops, stop_stops_count) + + if debug: + print("\n(0) numpy debug \nnp_get_t_open, has returned the next valid buy index as", t_open_ind) + print("If -1 there are no valid buys in the remainder of ticker data. Skipping to end of loop") + if debug_timing: + t_t = f(st) + print("0-numpy", str.format('{0:.17f}', t_t)) + st = s() + + if t_open_ind != -1: + + """ + 1 - Create views to search within for our open trade + + The views are our search space for the next Stop or Sell + Numpy view is employed as: + 1,000 faster than pandas searches + Pandas cannot assure it will always return a view, it may make a slow copy. + + The view contains columns: + buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + + Requires: np_bslap is our numpy array of the ticker DataFrame + Requires: t_open_ind is the index row with the buy. + Provides: np_t_open_v View of array after buy. + Provides: np_t_open_v_stop View of array after buy +1 + (Stop will search in here to prevent stopping in the past) + """ + np_t_open_v = np_bslap[t_open_ind:] + np_t_open_v_stop = np_bslap[t_open_ind + 1:] + + if debug: + print("\n(1) numpy debug \nNumpy view row 0 is now Ticker_Data Index", t_open_ind) + print("Numpy View: Buy - Open - Close - Sell - High - Low") + print("Row 0", np_t_open_v[0]) + print("Row 1", np_t_open_v[1], ) + if debug_timing: + t_t = f(st) + print("2-numpy", str.format('{0:.17f}', t_t)) + st = s() + + ''' + 2 - Calculate our stop-loss price + + As stop is based on buy price of our trade + - (BTO)Buys are Triggered On np_bto, typically the CLOSE of candle + - (BCO)Buys are Calculated On np_bco, default is OPEN of the next candle. + This is as we only see the CLOSE after it has happened. + The back test assumption is we have bought at first available price, the OPEN + + Requires: np_bslap - is our numpy array of the ticker DataFrame + Requires: t_open_ind - is the index row with the first buy. + Requires: p_stop - is the stop rate, ie. 0.99 is -1% + Provides: np_t_stop_pri - The value stop-loss will be triggered on + ''' + np_t_stop_pri = (np_bslap[t_open_ind + 1, np_bco] * p_stop) + + if debug: + print("\n(2) numpy debug\nStop-Loss has been calculated at:", np_t_stop_pri) + if debug_timing: + t_t = f(st) + print("2-numpy", str.format('{0:.17f}', t_t)) + st = s() + + ''' + 3 - Find candle STO is under Stop-Loss After Trade opened. + + where [np_sto] (stop tiggered on variable: "close", "low" etc) < np_t_stop_pri + + Requires: np_t_open_v_stop Numpy view of ticker_data after buy row +1 (when trade was opened) + Requires: np_sto User Var(STO)StopTriggeredOn. Typically set to "low" or "close" + Requires: np_t_stop_pri The stop-loss price STO must fall under to trigger stop + Provides: np_t_stop_ind The first candle after trade open where STO is under stop-loss + ''' + np_t_stop_ind = utf1st.find_1st(np_t_open_v_stop[:, np_sto], + np_t_stop_pri, + utf1st.cmp_smaller) + + # plus 1 as np_t_open_v_stop is 1 ahead of view np_t_open_v, used from here on out. + np_t_stop_ind = np_t_stop_ind + 1 + + if debug: + print("\n(3) numpy debug\nNext view index with STO (stop trigger on) under Stop-Loss is", + np_t_stop_ind - 1, + ". STO is using field", np_sto, + "\nFrom key: buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5\n") + + print( + "If -1 or 0 returned there is no stop found to end of view, then next two array lines are garbage") + print("Row", np_t_stop_ind - 1, np_t_open_v[np_t_stop_ind]) + print("Row", np_t_stop_ind, np_t_open_v[np_t_stop_ind + 1]) + if debug_timing: + t_t = f(st) + print("3-numpy", str.format('{0:.17f}', t_t)) + st = s() + + ''' + 4 - Find first sell index after trade open + + First index in the view np_t_open_v where ['sell'] = 1 + + Requires: np_t_open_v - view of ticker_data from buy onwards + Requires: no_sell - integer '3', the buy column in the array + Provides: np_t_sell_ind index of view where first sell=1 after buy + ''' + # Use numpy array for faster search for sell + # Sell uses column 3. + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + # Numpy searches 25-35x quicker than pandas on this data + + np_t_sell_ind = utf1st.find_1st(np_t_open_v[:, np_sell], + 1, utf1st.cmp_equal) + if debug: + print("\n(4) numpy debug\nNext view index with sell = 1 is ", np_t_sell_ind) + print("If 0 or less is returned there is no sell found to end of view, then next lines garbage") + print("Row", np_t_sell_ind, np_t_open_v[np_t_sell_ind]) + print("Row", np_t_sell_ind + 1, np_t_open_v[np_t_sell_ind + 1]) + if debug_timing: + t_t = f(st) + print("4-numpy", str.format('{0:.17f}', t_t)) + st = s() + + ''' + 5 - Determine which was hit first a stop or sell + To then use as exit index price-field (sell on buy, stop on stop) + + STOP takes priority over SELL as would be 'in candle' from tick data + Sell would use Open from Next candle. + So in a draw Stop would be hit first on ticker data in live + + Validity of when types of trades may be executed can be summarised as: + + Tick View + index index Buy Sell open low close high Stop price + open 2am 94 -1 0 0 ----- ------ ------ ----- ----- + open 3am 95 0 1 0 ----- ------ trg buy ----- ----- + open 4am 96 1 0 1 Enter trgstop trg sel ROI out Stop out + open 5am 97 2 0 0 Exit ------ ------- ----- ----- + open 6am 98 3 0 0 ----- ------ ------- ----- ----- + + -1 means not found till end of view i.e no valid Stop found. Exclude from match. + Stop tiggering and closing in 96-1, the candle we bought at OPEN in, is valid. + + Buys and sells are triggered at candle close + Both will open their postions at the open of the next candle. i/e + 1 index + + Stop and buy Indexes are on the view. To map to the ticker dataframe + the t_open_ind index should be summed. + + np_t_stop_ind: Stop Found index in view + t_exit_ind : Sell found in view + t_open_ind : Where view was started on ticker_data + + TODO: fix this frig for logic test,, case/switch/dictionary would be better... + more so when later testing many options, dynamic stop / roi etc + cludge - Setting np_t_sell_ind as 9999999999 when -1 (not found) + cludge - Setting np_t_stop_ind as 9999999999 when -1 (not found) + + ''' + if debug: + print("\n(5) numpy debug\nStop or Sell Logic Processing") + + # cludge for logic test (-1) means it was not found, set crazy high to lose < test + np_t_sell_ind = 99999999 if np_t_sell_ind <= 0 else np_t_sell_ind + np_t_stop_ind = 99999999 if np_t_stop_ind <= 0 else np_t_stop_ind + + # Stoploss trigger found before a sell =1 + if np_t_stop_ind < 99999999 and np_t_stop_ind <= np_t_sell_ind: + t_exit_ind = t_open_ind + np_t_stop_ind # Set Exit row index + t_exit_type = 'stop' # Set Exit type (stop) + np_t_exit_pri = np_sco # The price field our STOP exit will use + if debug: + print("Type STOP is first exit condition. " + "At view index:", np_t_stop_ind, ". Ticker data exit index is", t_exit_ind) + + # Buy = 1 found before a stoploss triggered + elif np_t_sell_ind < 99999999 and np_t_sell_ind < np_t_stop_ind: + # move sell onto next candle, we only look back on sell + # will use the open price later. + t_exit_ind = t_open_ind + np_t_sell_ind + 1 # Set Exit row index + t_exit_type = 'sell' # Set Exit type (sell) + np_t_exit_pri = np_open # The price field our SELL exit will use + if debug: + print("Type SELL is first exit condition. " + "At view index", np_t_sell_ind, ". Ticker data exit index is", t_exit_ind) + + # No stop or buy left in view - set t_exit_last -1 to handle gracefully + else: + t_exit_last: int = -1 # Signal loop to exit, no buys or sells found. + t_exit_type = "No Exit" + np_t_exit_pri = 999 # field price should be calculated on. 999 a non-existent column + if debug: + print("No valid STOP or SELL found. Signalling t_exit_last to gracefully exit") + + # TODO: fix having to cludge/uncludge this .. + # Undo cludge + np_t_sell_ind = -1 if np_t_sell_ind == 99999999 else np_t_sell_ind + np_t_stop_ind = -1 if np_t_stop_ind == 99999999 else np_t_stop_ind + + if debug_timing: + t_t = f(st) + print("5-logic", str.format('{0:.17f}', t_t)) + st = s() + + if debug: + ''' + Print out the buys, stops, sells + Include Line before and after to for easy + Human verification + ''' + # Combine the np_t_stop_pri value to bslap dataframe to make debug + # life easy. This is the current stop price based on buy price_ + # This is slow but don't care about performance in debug + # + # When referencing equiv np_column, as example np_sto, its 5 in numpy and 6 in df, so +1 + # as there is no data column in the numpy array. + bslap['np_stop_pri'] = np_t_stop_pri + + # Buy + print("\n\nDATAFRAME DEBUG =================== BUY ", pair) + print("Numpy Array BUY Index is:", 0) + print("DataFrame BUY Index is:", t_open_ind, "displaying DF \n") + print("HINT, BUY trade should use OPEN price from next candle, i.e ", t_open_ind + 1) + op_is = t_open_ind - 1 # Print open index start, line before + op_if = t_open_ind + 3 # Print open index finish, line after + print(bslap.iloc[op_is:op_if], "\n") + + # Stop - Stops trigger price np_sto (+1 for pandas column), and price received np_sco +1. (Stop Trigger|Calculated On) + if np_t_stop_ind < 0: + print("DATAFRAME DEBUG =================== STOP ", pair) + print("No STOPS were found until the end of ticker data file\n") + else: + print("DATAFRAME DEBUG =================== STOP ", pair) + print("Numpy Array STOP Index is:", np_t_stop_ind, "View starts at index", t_open_ind) + df_stop_index = (t_open_ind + np_t_stop_ind) + + print("DataFrame STOP Index is:", df_stop_index, "displaying DF \n") + print("First Stoploss trigger after Trade entered at OPEN in candle", t_open_ind + 1, "is ", + df_stop_index, ": \n", + str.format('{0:.17f}', bslap.iloc[df_stop_index][np_sto + 1]), + "is less than", str.format('{0:.17f}', np_t_stop_pri)) + + print("A stoploss exit will be calculated at rate:", + str.format('{0:.17f}', bslap.iloc[df_stop_index][np_sco + 1])) + + print("\nHINT, STOPs should exit in-candle, i.e", df_stop_index, + ": As live STOPs are not linked to O-C times") + + st_is = df_stop_index - 1 # Print stop index start, line before + st_if = df_stop_index + 2 # Print stop index finish, line after + print(bslap.iloc[st_is:st_if], "\n") + + # Sell + if np_t_sell_ind < 0: + print("DATAFRAME DEBUG =================== SELL ", pair) + print("No SELLS were found till the end of ticker data file\n") + else: + print("DATAFRAME DEBUG =================== SELL ", pair) + print("Numpy View SELL Index is:", np_t_sell_ind, "View starts at index", t_open_ind) + df_sell_index = (t_open_ind + np_t_sell_ind) + + print("DataFrame SELL Index is:", df_sell_index, "displaying DF \n") + print("First Sell Index after Trade open is in candle", df_sell_index) + print("HINT, if exit is SELL (not stop) trade should use OPEN price from next candle", + df_sell_index + 1) + sl_is = df_sell_index - 1 # Print sell index start, line before + sl_if = df_sell_index + 3 # Print sell index finish, line after + print(bslap.iloc[sl_is:sl_if], "\n") + + # Chosen Exit (stop or sell) + + print("DATAFRAME DEBUG =================== EXIT ", pair) + print("Exit type is :", t_exit_type) + print("trade exit price field is", np_t_exit_pri, "\n") + + if debug_timing: + t_t = f(st) + print("6-depra", str.format('{0:.17f}', t_t)) + st = s() + + ## use numpy view "np_t_open_v" for speed. Columns are + # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 + # exception is 6 which is use the stop value. + + # TODO no! this is hard coded bleh fix this open + np_trade_enter_price = np_bslap[t_open_ind + 1, np_open] + if t_exit_type == 'stop': + if np_t_exit_pri == 6: + np_trade_exit_price = np_t_stop_pri + else: + np_trade_exit_price = np_bslap[t_exit_ind, np_t_exit_pri] + if t_exit_type == 'sell': + np_trade_exit_price = np_bslap[t_exit_ind, np_t_exit_pri] + + # Catch no exit found + if t_exit_type == "No Exit": + np_trade_exit_price = 0 + + if debug_timing: + t_t = f(st) + print("7-numpy", str.format('{0:.17f}', t_t)) + st = s() + + if debug: + print("//////////////////////////////////////////////") + print("+++++++++++++++++++++++++++++++++ Trade Enter ") + print("np_trade Enter Price is ", str.format('{0:.17f}', np_trade_enter_price)) + print("--------------------------------- Trade Exit ") + print("Trade Exit Type is ", t_exit_type) + print("np_trade Exit Price is", str.format('{0:.17f}', np_trade_exit_price)) + print("//////////////////////////////////////////////") + + else: # no buys were found, step 0 returned -1 + # Gracefully exit the loop + t_exit_last == -1 + if debug: + print("\n(E) No buys were found in remaining ticker file. Exiting", pair) + + # Loop control - catch no closed trades. + if debug: + print("---------------------------------------- end of loop", loop, + " Dataframe Exit Index is: ", t_exit_ind) + print("Exit Index Last, Exit Index Now Are: ", t_exit_last, t_exit_ind) + + if t_exit_last >= t_exit_ind or t_exit_last == -1: + """ + Break loop and go on to next pair. + + When last trade exit equals index of last exit, there is no + opportunity to close any more trades. + """ + # TODO :add handing here to record none closed open trades + + if debug: + print(bslap_pair_results) + break + else: + """ + Add trade to backtest looking results list of dicts + Loop back to look for more trades. + """ + # Build trade dictionary + ## In general if a field can be calculated later from other fields leave blank here + ## Its X(number of trades faster) to calc all in a single vector than 1 trade at a time + + # create a new dict + close_index: int = t_exit_ind + bslap_result = {} # Must have at start or we end up with a list of multiple same last result + bslap_result["pair"] = pair + bslap_result["profit_percent"] = "" # To be 1 vector calc across trades when loop complete + bslap_result["profit_abs"] = "" # To be 1 vector calc across trades when loop complete + bslap_result["open_time"] = np_bslap_dates[t_open_ind + 1] # use numpy array, pandas 20x slower + bslap_result["close_time"] = np_bslap_dates[close_index] # use numpy array, pandas 20x slower + bslap_result["open_index"] = t_open_ind + 1 # +1 as we buy on next. + bslap_result["close_index"] = close_index + bslap_result["trade_duration"] = "" # To be 1 vector calc across trades when loop complete + bslap_result["open_at_end"] = False + bslap_result["open_rate"] = round(np_trade_enter_price, 15) + bslap_result["close_rate"] = round(np_trade_exit_price, 15) + bslap_result["exit_type"] = t_exit_type + # append the dict to the list and print list + bslap_pair_results.append(bslap_result) + + if t_exit_type is "stop": + stop_stops_count = stop_stops_count + 1 + + if debug: + print("The trade dict is: \n", bslap_result) + print("Trades dicts in list after append are: \n ", bslap_pair_results) + + """ + Loop back to start. t_exit_last becomes where loop + will seek to open new trades from. + Push index on 1 to not open on close + """ + t_exit_last = t_exit_ind + 1 + + if debug_timing: + t_t = f(st) + print("8+trade", str.format('{0:.17f}', t_t)) + + # Send back List of trade dicts + return bslap_pair_results diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 36ecc6826..b68c544f2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -20,6 +20,7 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.exchange import Exchange from freqtrade.misc import file_dump_json +from freqtrade.optimize.backslapping import Backslapping from freqtrade.persistence import Trade from profilehooks import profile from collections import OrderedDict @@ -54,6 +55,7 @@ class Backtesting(object): backtesting = Backtesting(config) backtesting.start() """ + def __init__(self, config: Dict[str, Any]) -> None: self.config = config self.analyze = Analyze(self.config) @@ -91,21 +93,22 @@ class Backtesting(object): self.np_bco: int = self.np_open # buys calculated on - open of the next candle. self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close - #self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close - #self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close + # self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close + # self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close - self.use_backslap = True # Enable backslap - if false Orginal code is executed. - self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended - self.debug_timing = False # Stages within Backslap - self.debug_2loops = False # Limit each pair to two loops, useful when debugging - self.debug_vector = False # Debug vector calcs + self.use_backslap = True # Enable backslap - if false Orginal code is executed. + self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended + self.debug_timing = False # Stages within Backslap + self.debug_2loops = False # Limit each pair to two loops, useful when debugging + self.debug_vector = False # Debug vector calcs self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap - self.backslap_show_trades = False # prints trades in addition to summary report - self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt + self.backslap_show_trades = False # prints trades in addition to summary report + self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt - self.stop_stops: int = 9999 # stop back testing any pair with this many stops, set to 999999 to not hit + self.stop_stops: int = 9999 # stop back testing any pair with this many stops, set to 999999 to not hit + self.backslap = Backslapping(config) @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: @@ -119,7 +122,7 @@ class Backtesting(object): for frame in data.values() ] return min(timeframe, key=operator.itemgetter(0))[0], \ - max(timeframe, key=operator.itemgetter(1))[1] + max(timeframe, key=operator.itemgetter(1))[1] def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str: """ @@ -193,7 +196,6 @@ class Backtesting(object): buy_signal = sell_row.buy if self.analyze.should_sell(trade, sell_row.open, sell_row.date, buy_signal, sell_row.sell): - return BacktestResult(pair=pair, profit_percent=trade.calc_profit_percent(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open), @@ -233,7 +235,6 @@ class Backtesting(object): def f(self, st): return (timeit.default_timer() - st) - def backtest(self, args: Dict) -> DataFrame: """ Implements backtesting functionality @@ -253,65 +254,9 @@ class Backtesting(object): use_backslap = self.use_backslap debug_timing = self.debug_timing_main_loop - if use_backslap: # Use Back Slap code - - headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] - processed = args['processed'] - max_open_trades = args.get('max_open_trades', 0) - realistic = args.get('realistic', False) - trades = [] - trade_count_lock: Dict = {} - - ########################### Call out BSlap Loop instead of Original BT code - bslap_results: list = [] - for pair, pair_data in processed.items(): - if debug_timing: # Start timer - fl = self.s() - - ticker_data = self.populate_sell_trend( - self.populate_buy_trend(pair_data))[headers].copy() - - if debug_timing: # print time taken - flt = self.f(fl) - #print("populate_buy_trend:", pair, round(flt, 10)) - st = self.s() - - # #dump same DFs to disk for offline testing in scratch - # f_pair:str = pair - # csv = f_pair.replace("/", "_") - # csv="/Users/creslin/PycharmProjects/freqtrade_new/frames/" + csv - # ticker_data.to_csv(csv, sep='\t', encoding='utf-8') - - #call bslap - results are a list of dicts - bslap_pair_results = self.backslap_pair(ticker_data, pair) - last_bslap_results = bslap_results - bslap_results = last_bslap_results + bslap_pair_results - - if debug_timing: # print time taken - tt = self.f(st) - print("Time to BackSlap :", pair, round(tt,10)) - print("-----------------------") - - - # Switch List of Trade Dicts (bslap_results) to Dataframe - # Fill missing, calculable columns, profit, duration , abs etc. - bslap_results_df = DataFrame(bslap_results) - - if len(bslap_results_df) > 0: # Only post process a frame if it has a record - # bslap_results_df['open_time'] = to_datetime(bslap_results_df['open_time']) - # bslap_results_df['close_time'] = to_datetime(bslap_results_df['close_time']) - # if debug: - # print("open_time and close_time converted to datetime columns") - - bslap_results_df = self.vector_fill_results_table(bslap_results_df, pair) - else: - bslap_results_df = [] - bslap_results_df= DataFrame.from_records(bslap_results_df, columns=BacktestResult._fields) - - - return bslap_results_df - - else: # use Original Back test code + if use_backslap: # Use Back Slap code + return self.backslap.run(args) + else: # use Original Back test code ########################## Original BT loop headers = ['date', 'buy', 'open', 'close', 'sell'] @@ -322,7 +267,7 @@ class Backtesting(object): trade_count_lock: Dict = {} for pair, pair_data in processed.items(): - if debug_timing: # Start timer + if debug_timing: # Start timer fl = self.s() pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run @@ -336,9 +281,9 @@ class Backtesting(object): ticker_data.drop(ticker_data.head(1).index, inplace=True) - if debug_timing: # print time taken + if debug_timing: # print time taken flt = self.f(fl) - #print("populate_buy_trend:", pair, round(flt, 10)) + # print("populate_buy_trend:", pair, round(flt, 10)) st = self.s() # Convert from Pandas to list for performance reasons @@ -363,7 +308,6 @@ class Backtesting(object): trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:], trade_count_lock, args) - if trade_entry: lock_pair_until = trade_entry.close_time trades.append(trade_entry) @@ -377,651 +321,9 @@ class Backtesting(object): print("Time to BackTest :", pair, round(tt, 10)) print("-----------------------") - return DataFrame.from_records(trades, columns=BacktestResult._fields) ####################### Original BT loop end - def vector_fill_results_table(self, bslap_results_df: DataFrame, pair: str): - """ - The Results frame contains a number of columns that are calculable - from othe columns. These are left blank till all rows are added, - to be populated in single vector calls. - - Columns to be populated are: - - Profit - - trade duration - - profit abs - :param bslap_results Dataframe - :return: bslap_results Dataframe - """ - import pandas as pd - import numpy as np - debug = self.debug_vector - - # stake and fees - # stake = 0.015 - # 0.05% is 0.0005 - #fee = 0.001 - - stake = self.config.get('stake_amount') - fee = self.fee - open_fee = fee / 2 - close_fee = fee / 2 - - if debug: - print("Stake is,", stake, "the sum of currency to spend per trade") - print("The open fee is", open_fee, "The close fee is", close_fee) - if debug: - from pandas import set_option - set_option('display.max_rows', 5000) - set_option('display.max_columns', 20) - pd.set_option('display.width', 1000) - pd.set_option('max_colwidth', 40) - pd.set_option('precision', 12) - - # # Get before - # csv = "cryptosher_before_debug" - # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') - - # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') - - bslap_results_df['trade_duration'] = bslap_results_df['close_time'] - bslap_results_df['open_time'] - - ## Spends, Takes, Profit, Absolute Profit - # print(bslap_results_df) - # Buy Price - bslap_results_df['buy_vol'] = stake / bslap_results_df['open_rate'] # How many target are we buying - bslap_results_df['buy_fee'] = stake * open_fee - bslap_results_df['buy_spend'] = stake + bslap_results_df['buy_fee'] # How much we're spending - - # Sell price - bslap_results_df['sell_sum'] = bslap_results_df['buy_vol'] * bslap_results_df['close_rate'] - bslap_results_df['sell_fee'] = bslap_results_df['sell_sum'] * close_fee - bslap_results_df['sell_take'] = bslap_results_df['sell_sum'] - bslap_results_df['sell_fee'] - # profit_percent - bslap_results_df['profit_percent'] = (bslap_results_df['sell_take'] - bslap_results_df['buy_spend']) \ - / bslap_results_df['buy_spend'] - # Absolute profit - bslap_results_df['profit_abs'] = bslap_results_df['sell_take'] - bslap_results_df['buy_spend'] - - # # Get After - # csv="cryptosher_after_debug" - # bslap_results_df.to_csv(csv, sep='\t', encoding='utf-8') - - - if debug: - print("\n") - print(bslap_results_df[ - ['buy_vol', 'buy_fee', 'buy_spend', 'sell_sum','sell_fee', 'sell_take', 'profit_percent', 'profit_abs', 'exit_type']]) - - return bslap_results_df - - def np_get_t_open_ind(self, np_buy_arr, t_exit_ind: int, np_buy_arr_len: int, stop_stops: int, stop_stops_count: int): - import utils_find_1st as utf1st - """ - The purpose of this def is to return the next "buy" = 1 - after t_exit_ind. - - This function will also check is the stop limit for the pair has been reached. - if stop_stops is the limit and stop_stops_count it the number of times the stop has been hit. - - t_exit_ind is the index the last trade exited on - or 0 if first time around this loop. - - stop_stops i - """ - debug = self.debug - - # Timers, to be called if in debug - def s(): - st = timeit.default_timer() - return st - def f(st): - return (timeit.default_timer() - st) - - st = s() - t_open_ind: int - - """ - Create a view on our buy index starting after last trade exit - Search for next buy - """ - np_buy_arr_v = np_buy_arr[t_exit_ind:] - t_open_ind = utf1st.find_1st(np_buy_arr_v, 1, utf1st.cmp_equal) - - ''' - If -1 is returned no buy has been found, preserve the value - ''' - if t_open_ind != -1: # send back the -1 if no buys found. otherwise update index - t_open_ind = t_open_ind + t_exit_ind # Align numpy index - - if t_open_ind == np_buy_arr_len -1 : # If buy found on last candle ignore, there is no OPEN in next to use - t_open_ind = -1 # -1 ends the loop - - if stop_stops_count >= stop_stops: # if maximum number of stops allowed in a pair is hit, exit loop - t_open_ind = -1 # -1 ends the loop - if debug: - print("Max stop limit ", stop_stops, "reached. Moving to next pair") - - return t_open_ind - - def backslap_pair(self, ticker_data, pair): - import pandas as pd - import numpy as np - import timeit - import utils_find_1st as utf1st - from datetime import datetime - - ### backslap debug wrap - # debug_2loops = False # only loop twice, for faster debug - # debug_timing = False # print timing for each step - # debug = False # print values, to check accuracy - debug_2loops = self.debug_2loops # only loop twice, for faster debug - debug_timing = self.debug_timing # print timing for each step - debug = self.debug # print values, to check accuracy - - # Read Stop Loss Values and Stake - stop = self.stop_loss_value - p_stop = (stop + 1) # What stop really means, e.g 0.01 is 0.99 of price - - if debug: - print("Stop is ", stop, "value from stragey file") - print("p_stop is", p_stop, "value used to multiply to entry price") - - if debug: - from pandas import set_option - set_option('display.max_rows', 5000) - set_option('display.max_columns', 8) - pd.set_option('display.width', 1000) - pd.set_option('max_colwidth', 40) - pd.set_option('precision', 12) - def s(): - st = timeit.default_timer() - return st - def f(st): - return (timeit.default_timer() - st) - #### backslap config - ''' - Numpy arrays are used for 100x speed up - We requires setting Int values for - buy stop triggers and stop calculated on - # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - stop 6 - ''' - - - ####### - # Use vars set at top of backtest - np_buy: int = self.np_buy - np_open: int = self.np_open - np_close: int = self.np_close - np_sell: int = self.np_sell - np_high: int = self.np_high - np_low: int = self.np_low - np_stop: int = self.np_stop - np_bto: int = self.np_bto # buys_triggered_on - should be close - np_bco: int = self.np_bco # buys calculated on - open of the next candle. - np_sto: int = self.np_sto # stops_triggered_on - Should be low, FT uses close - np_sco: int = self.np_sco # stops_calculated_on - Should be stop, FT uses close - - ### End Config - - pair: str = pair - - #ticker_data: DataFrame = ticker_dfs[t_file] - bslap: DataFrame = ticker_data - - # Build a single dimension numpy array from "buy" index for faster search - # (500x faster than pandas) - np_buy_arr = bslap['buy'].values - np_buy_arr_len: int = len(np_buy_arr) - - # use numpy array for faster searches in loop, 20x faster than pandas - # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - np_bslap = np.array(bslap[['buy', 'open', 'close', 'sell', 'high', 'low']]) - - # Build a numpy list of date-times. - # We use these when building the trade - # The rationale is to address a value from a pandas cell is thousands of - # times more expensive. Processing time went X25 when trying to use any data from pandas - np_bslap_dates = bslap['date'].values - - loop: int = 0 # how many time around the loop - t_exit_ind = 0 # Start loop from first index - t_exit_last = 0 # To test for exit - - stop_stops = self.stop_stops # Int of stops within a pair to stop trading a pair at - stop_stops_count = 0 # stop counter per pair - - st = s() # Start timer for processing dataframe - if debug: - print('Processing:', pair) - - # Results will be stored in a list of dicts - bslap_pair_results: list = [] - bslap_result: dict = {} - - while t_exit_ind < np_buy_arr_len: - loop = loop + 1 - if debug or debug_timing: - print("-- T_exit_Ind - Numpy Index is", t_exit_ind, " ----------------------- Loop", loop, pair) - if debug_2loops: - if loop == 3: - print("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++Loop debug max met - breaking") - break - ''' - Dev phases - Phase 1 - 1) Manage buy, sell, stop enter/exit - a) Find first buy index - b) Discover first stop and sell hit after buy index - c) Chose first instance as trade exit - - Phase 2 - 2) Manage dynamic Stop and ROI Exit - a) Create trade slice from 1 - b) search within trade slice for dynamice stop hit - c) search within trade slice for ROI hit - ''' - - if debug_timing: - st = s() - ''' - 0 - Find next buy entry - Finds index for first (buy = 1) flag - - Requires: np_buy_arr - a 1D array of the 'buy' column. To find next "1" - Required: t_exit_ind - Either 0, first loop. Or The index we last exited on - Requires: np_buy_arr_len - length of pair array. - Requires: stops_stops - number of stops allowed before stop trading a pair - Requires: stop_stop_counts - count of stops hit in the pair - Provides: The next "buy" index after t_exit_ind - - If -1 is returned no buy has been found in remainder of array, skip to exit loop - ''' - t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len, stop_stops, stop_stops_count) - - if debug: - print("\n(0) numpy debug \nnp_get_t_open, has returned the next valid buy index as", t_open_ind) - print("If -1 there are no valid buys in the remainder of ticker data. Skipping to end of loop") - if debug_timing: - t_t = f(st) - print("0-numpy", str.format('{0:.17f}', t_t)) - st = s() - - if t_open_ind != -1: - - """ - 1 - Create views to search within for our open trade - - The views are our search space for the next Stop or Sell - Numpy view is employed as: - 1,000 faster than pandas searches - Pandas cannot assure it will always return a view, it may make a slow copy. - - The view contains columns: - buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - - Requires: np_bslap is our numpy array of the ticker DataFrame - Requires: t_open_ind is the index row with the buy. - Provides: np_t_open_v View of array after buy. - Provides: np_t_open_v_stop View of array after buy +1 - (Stop will search in here to prevent stopping in the past) - """ - np_t_open_v = np_bslap[t_open_ind:] - np_t_open_v_stop = np_bslap[t_open_ind +1:] - - if debug: - print("\n(1) numpy debug \nNumpy view row 0 is now Ticker_Data Index", t_open_ind) - print("Numpy View: Buy - Open - Close - Sell - High - Low") - print("Row 0", np_t_open_v[0]) - print("Row 1", np_t_open_v[1], ) - if debug_timing: - t_t = f(st) - print("2-numpy", str.format('{0:.17f}', t_t)) - st = s() - - ''' - 2 - Calculate our stop-loss price - - As stop is based on buy price of our trade - - (BTO)Buys are Triggered On np_bto, typically the CLOSE of candle - - (BCO)Buys are Calculated On np_bco, default is OPEN of the next candle. - This is as we only see the CLOSE after it has happened. - The back test assumption is we have bought at first available price, the OPEN - - Requires: np_bslap - is our numpy array of the ticker DataFrame - Requires: t_open_ind - is the index row with the first buy. - Requires: p_stop - is the stop rate, ie. 0.99 is -1% - Provides: np_t_stop_pri - The value stop-loss will be triggered on - ''' - np_t_stop_pri = (np_bslap[t_open_ind + 1, np_bco] * p_stop) - - if debug: - print("\n(2) numpy debug\nStop-Loss has been calculated at:", np_t_stop_pri) - if debug_timing: - t_t = f(st) - print("2-numpy", str.format('{0:.17f}', t_t)) - st = s() - - ''' - 3 - Find candle STO is under Stop-Loss After Trade opened. - - where [np_sto] (stop tiggered on variable: "close", "low" etc) < np_t_stop_pri - - Requires: np_t_open_v_stop Numpy view of ticker_data after buy row +1 (when trade was opened) - Requires: np_sto User Var(STO)StopTriggeredOn. Typically set to "low" or "close" - Requires: np_t_stop_pri The stop-loss price STO must fall under to trigger stop - Provides: np_t_stop_ind The first candle after trade open where STO is under stop-loss - ''' - np_t_stop_ind = utf1st.find_1st(np_t_open_v_stop[:, np_sto], - np_t_stop_pri, - utf1st.cmp_smaller) - - # plus 1 as np_t_open_v_stop is 1 ahead of view np_t_open_v, used from here on out. - np_t_stop_ind = np_t_stop_ind +1 - - - if debug: - print("\n(3) numpy debug\nNext view index with STO (stop trigger on) under Stop-Loss is", np_t_stop_ind -1, - ". STO is using field", np_sto, - "\nFrom key: buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5\n") - - print("If -1 or 0 returned there is no stop found to end of view, then next two array lines are garbage") - print("Row", np_t_stop_ind -1 , np_t_open_v[np_t_stop_ind]) - print("Row", np_t_stop_ind , np_t_open_v[np_t_stop_ind + 1]) - if debug_timing: - t_t = f(st) - print("3-numpy", str.format('{0:.17f}', t_t)) - st = s() - - ''' - 4 - Find first sell index after trade open - - First index in the view np_t_open_v where ['sell'] = 1 - - Requires: np_t_open_v - view of ticker_data from buy onwards - Requires: no_sell - integer '3', the buy column in the array - Provides: np_t_sell_ind index of view where first sell=1 after buy - ''' - # Use numpy array for faster search for sell - # Sell uses column 3. - # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - # Numpy searches 25-35x quicker than pandas on this data - - np_t_sell_ind = utf1st.find_1st(np_t_open_v[:, np_sell], - 1, utf1st.cmp_equal) - if debug: - print("\n(4) numpy debug\nNext view index with sell = 1 is ", np_t_sell_ind) - print("If 0 or less is returned there is no sell found to end of view, then next lines garbage") - print("Row", np_t_sell_ind, np_t_open_v[np_t_sell_ind]) - print("Row", np_t_sell_ind + 1, np_t_open_v[np_t_sell_ind + 1]) - if debug_timing: - t_t = f(st) - print("4-numpy", str.format('{0:.17f}', t_t)) - st = s() - - ''' - 5 - Determine which was hit first a stop or sell - To then use as exit index price-field (sell on buy, stop on stop) - - STOP takes priority over SELL as would be 'in candle' from tick data - Sell would use Open from Next candle. - So in a draw Stop would be hit first on ticker data in live - - Validity of when types of trades may be executed can be summarised as: - - Tick View - index index Buy Sell open low close high Stop price - open 2am 94 -1 0 0 ----- ------ ------ ----- ----- - open 3am 95 0 1 0 ----- ------ trg buy ----- ----- - open 4am 96 1 0 1 Enter trgstop trg sel ROI out Stop out - open 5am 97 2 0 0 Exit ------ ------- ----- ----- - open 6am 98 3 0 0 ----- ------ ------- ----- ----- - - -1 means not found till end of view i.e no valid Stop found. Exclude from match. - Stop tiggering and closing in 96-1, the candle we bought at OPEN in, is valid. - - Buys and sells are triggered at candle close - Both will open their postions at the open of the next candle. i/e + 1 index - - Stop and buy Indexes are on the view. To map to the ticker dataframe - the t_open_ind index should be summed. - - np_t_stop_ind: Stop Found index in view - t_exit_ind : Sell found in view - t_open_ind : Where view was started on ticker_data - - TODO: fix this frig for logic test,, case/switch/dictionary would be better... - more so when later testing many options, dynamic stop / roi etc - cludge - Setting np_t_sell_ind as 9999999999 when -1 (not found) - cludge - Setting np_t_stop_ind as 9999999999 when -1 (not found) - - ''' - if debug: - print("\n(5) numpy debug\nStop or Sell Logic Processing") - - # cludge for logic test (-1) means it was not found, set crazy high to lose < test - np_t_sell_ind = 99999999 if np_t_sell_ind <= 0 else np_t_sell_ind - np_t_stop_ind = 99999999 if np_t_stop_ind <= 0 else np_t_stop_ind - - # Stoploss trigger found before a sell =1 - if np_t_stop_ind < 99999999 and np_t_stop_ind <= np_t_sell_ind: - t_exit_ind = t_open_ind + np_t_stop_ind # Set Exit row index - t_exit_type = 'stop' # Set Exit type (stop) - np_t_exit_pri = np_sco # The price field our STOP exit will use - if debug: - print("Type STOP is first exit condition. " - "At view index:", np_t_stop_ind, ". Ticker data exit index is", t_exit_ind) - - # Buy = 1 found before a stoploss triggered - elif np_t_sell_ind < 99999999 and np_t_sell_ind < np_t_stop_ind: - # move sell onto next candle, we only look back on sell - # will use the open price later. - t_exit_ind = t_open_ind + np_t_sell_ind + 1 # Set Exit row index - t_exit_type = 'sell' # Set Exit type (sell) - np_t_exit_pri = np_open # The price field our SELL exit will use - if debug: - print("Type SELL is first exit condition. " - "At view index", np_t_sell_ind, ". Ticker data exit index is", t_exit_ind) - - # No stop or buy left in view - set t_exit_last -1 to handle gracefully - else: - t_exit_last: int = -1 # Signal loop to exit, no buys or sells found. - t_exit_type = "No Exit" - np_t_exit_pri = 999 # field price should be calculated on. 999 a non-existent column - if debug: - print("No valid STOP or SELL found. Signalling t_exit_last to gracefully exit") - - # TODO: fix having to cludge/uncludge this .. - # Undo cludge - np_t_sell_ind = -1 if np_t_sell_ind == 99999999 else np_t_sell_ind - np_t_stop_ind = -1 if np_t_stop_ind == 99999999 else np_t_stop_ind - - if debug_timing: - t_t = f(st) - print("5-logic", str.format('{0:.17f}', t_t)) - st = s() - - if debug: - ''' - Print out the buys, stops, sells - Include Line before and after to for easy - Human verification - ''' - # Combine the np_t_stop_pri value to bslap dataframe to make debug - # life easy. This is the current stop price based on buy price_ - # This is slow but don't care about performance in debug - # - # When referencing equiv np_column, as example np_sto, its 5 in numpy and 6 in df, so +1 - # as there is no data column in the numpy array. - bslap['np_stop_pri'] = np_t_stop_pri - - # Buy - print("\n\nDATAFRAME DEBUG =================== BUY ", pair) - print("Numpy Array BUY Index is:", 0) - print("DataFrame BUY Index is:", t_open_ind, "displaying DF \n") - print("HINT, BUY trade should use OPEN price from next candle, i.e ", t_open_ind + 1) - op_is = t_open_ind - 1 # Print open index start, line before - op_if = t_open_ind + 3 # Print open index finish, line after - print(bslap.iloc[op_is:op_if], "\n") - - # Stop - Stops trigger price np_sto (+1 for pandas column), and price received np_sco +1. (Stop Trigger|Calculated On) - if np_t_stop_ind < 0: - print("DATAFRAME DEBUG =================== STOP ", pair) - print("No STOPS were found until the end of ticker data file\n") - else: - print("DATAFRAME DEBUG =================== STOP ", pair) - print("Numpy Array STOP Index is:", np_t_stop_ind, "View starts at index", t_open_ind) - df_stop_index = (t_open_ind + np_t_stop_ind) - - print("DataFrame STOP Index is:", df_stop_index, "displaying DF \n") - print("First Stoploss trigger after Trade entered at OPEN in candle", t_open_ind + 1, "is ", - df_stop_index, ": \n", - str.format('{0:.17f}', bslap.iloc[df_stop_index][np_sto + 1]), - "is less than", str.format('{0:.17f}', np_t_stop_pri)) - - print("A stoploss exit will be calculated at rate:", - str.format('{0:.17f}', bslap.iloc[df_stop_index][np_sco + 1])) - - print("\nHINT, STOPs should exit in-candle, i.e", df_stop_index, - ": As live STOPs are not linked to O-C times") - - st_is = df_stop_index - 1 # Print stop index start, line before - st_if = df_stop_index + 2 # Print stop index finish, line after - print(bslap.iloc[st_is:st_if], "\n") - - # Sell - if np_t_sell_ind < 0: - print("DATAFRAME DEBUG =================== SELL ", pair) - print("No SELLS were found till the end of ticker data file\n") - else: - print("DATAFRAME DEBUG =================== SELL ", pair) - print("Numpy View SELL Index is:", np_t_sell_ind, "View starts at index", t_open_ind) - df_sell_index = (t_open_ind + np_t_sell_ind) - - print("DataFrame SELL Index is:", df_sell_index, "displaying DF \n") - print("First Sell Index after Trade open is in candle", df_sell_index) - print("HINT, if exit is SELL (not stop) trade should use OPEN price from next candle", - df_sell_index + 1) - sl_is = df_sell_index - 1 # Print sell index start, line before - sl_if = df_sell_index + 3 # Print sell index finish, line after - print(bslap.iloc[sl_is:sl_if], "\n") - - # Chosen Exit (stop or sell) - - print("DATAFRAME DEBUG =================== EXIT ", pair) - print("Exit type is :", t_exit_type) - print("trade exit price field is", np_t_exit_pri, "\n") - - if debug_timing: - t_t = f(st) - print("6-depra", str.format('{0:.17f}', t_t)) - st = s() - - ## use numpy view "np_t_open_v" for speed. Columns are - # buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - # exception is 6 which is use the stop value. - - # TODO no! this is hard coded bleh fix this open - np_trade_enter_price = np_bslap[t_open_ind + 1, np_open] - if t_exit_type == 'stop': - if np_t_exit_pri == 6: - np_trade_exit_price = np_t_stop_pri - else: - np_trade_exit_price = np_bslap[t_exit_ind, np_t_exit_pri] - if t_exit_type == 'sell': - np_trade_exit_price = np_bslap[t_exit_ind, np_t_exit_pri] - - # Catch no exit found - if t_exit_type == "No Exit": - np_trade_exit_price = 0 - - if debug_timing: - t_t = f(st) - print("7-numpy", str.format('{0:.17f}', t_t)) - st = s() - - if debug: - print("//////////////////////////////////////////////") - print("+++++++++++++++++++++++++++++++++ Trade Enter ") - print("np_trade Enter Price is ", str.format('{0:.17f}', np_trade_enter_price)) - print("--------------------------------- Trade Exit ") - print("Trade Exit Type is ", t_exit_type) - print("np_trade Exit Price is", str.format('{0:.17f}', np_trade_exit_price)) - print("//////////////////////////////////////////////") - - else: # no buys were found, step 0 returned -1 - # Gracefully exit the loop - t_exit_last == -1 - if debug: - print("\n(E) No buys were found in remaining ticker file. Exiting", pair) - - # Loop control - catch no closed trades. - if debug: - print("---------------------------------------- end of loop", loop, - " Dataframe Exit Index is: ", t_exit_ind) - print("Exit Index Last, Exit Index Now Are: ", t_exit_last, t_exit_ind) - - if t_exit_last >= t_exit_ind or t_exit_last == -1: - """ - Break loop and go on to next pair. - - When last trade exit equals index of last exit, there is no - opportunity to close any more trades. - """ - # TODO :add handing here to record none closed open trades - - if debug: - print(bslap_pair_results) - break - else: - """ - Add trade to backtest looking results list of dicts - Loop back to look for more trades. - """ - # Build trade dictionary - ## In general if a field can be calculated later from other fields leave blank here - ## Its X(number of trades faster) to calc all in a single vector than 1 trade at a time - - # create a new dict - close_index: int = t_exit_ind - bslap_result = {} # Must have at start or we end up with a list of multiple same last result - bslap_result["pair"] = pair - bslap_result["profit_percent"] = "" # To be 1 vector calc across trades when loop complete - bslap_result["profit_abs"] = "" # To be 1 vector calc across trades when loop complete - bslap_result["open_time"] = np_bslap_dates[t_open_ind + 1] # use numpy array, pandas 20x slower - bslap_result["close_time"] = np_bslap_dates[close_index] # use numpy array, pandas 20x slower - bslap_result["open_index"] = t_open_ind + 1 # +1 as we buy on next. - bslap_result["close_index"] = close_index - bslap_result["trade_duration"] = "" # To be 1 vector calc across trades when loop complete - bslap_result["open_at_end"] = False - bslap_result["open_rate"] = round(np_trade_enter_price, 15) - bslap_result["close_rate"] = round(np_trade_exit_price, 15) - bslap_result["exit_type"] = t_exit_type - # append the dict to the list and print list - bslap_pair_results.append(bslap_result) - - if t_exit_type is "stop": - stop_stops_count = stop_stops_count + 1 - - if debug: - print("The trade dict is: \n", bslap_result) - print("Trades dicts in list after append are: \n ", bslap_pair_results) - - """ - Loop back to start. t_exit_last becomes where loop - will seek to open new trades from. - Push index on 1 to not open on close - """ - t_exit_last = t_exit_ind + 1 - - if debug_timing: - t_t = f(st) - print("8+trade", str.format('{0:.17f}', t_t)) - - # Send back List of trade dicts - return bslap_pair_results - def start(self) -> None: """ Run a backtesting end-to-end @@ -1099,23 +401,26 @@ class Backtesting(object): results ) ) - #optional print trades + # optional print trades if self.backslap_show_trades: TradesFrame = results.filter(['open_time', 'pair', 'exit_type', 'profit_percent', 'profit_abs', 'buy_spend', 'sell_take', 'trade_duration', 'close_time'], axis=1) + def to_fwf(df, fname): content = tabulate(df.values.tolist(), list(df.columns), floatfmt=".8f", tablefmt='psql') print(content) DataFrame.to_fwf = to_fwf(TradesFrame, "backslap.txt") - #optional save trades + # optional save trades if self.backslap_save_trades: TradesFrame = results.filter(['open_time', 'pair', 'exit_type', 'profit_percent', 'profit_abs', 'buy_spend', 'sell_take', 'trade_duration', 'close_time'], axis=1) + def to_fwf(df, fname): content = tabulate(df.values.tolist(), list(df.columns), floatfmt=".8f", tablefmt='psql') open(fname, "w").write(content) + DataFrame.to_fwf = to_fwf(TradesFrame, "backslap.txt") else: From ab66fe1b724cc50c883005282a08bda0f538327d Mon Sep 17 00:00:00 2001 From: Gert Date: Sat, 28 Jul 2018 19:45:33 -0700 Subject: [PATCH 30/31] prepared for tracking signals --- freqtrade/optimize/backslapping.py | 16 +++++++++------- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backslapping.py b/freqtrade/optimize/backslapping.py index 3eee5db37..b16515942 100644 --- a/freqtrade/optimize/backslapping.py +++ b/freqtrade/optimize/backslapping.py @@ -5,6 +5,7 @@ from pandas import DataFrame from freqtrade.exchange import Exchange from freqtrade.strategy import IStrategy +from freqtrade.strategy.interface import SellType from freqtrade.strategy.resolver import StrategyResolver @@ -571,7 +572,7 @@ class Backslapping: # Stoploss trigger found before a sell =1 if np_t_stop_ind < 99999999 and np_t_stop_ind <= np_t_sell_ind: t_exit_ind = t_open_ind + np_t_stop_ind # Set Exit row index - t_exit_type = 'stop' # Set Exit type (stop) + t_exit_type = SellType.STOP_LOSS # Set Exit type (stop) np_t_exit_pri = np_sco # The price field our STOP exit will use if debug: print("Type STOP is first exit condition. " @@ -582,7 +583,7 @@ class Backslapping: # move sell onto next candle, we only look back on sell # will use the open price later. t_exit_ind = t_open_ind + np_t_sell_ind + 1 # Set Exit row index - t_exit_type = 'sell' # Set Exit type (sell) + t_exit_type = SellType.SELL_SIGNAL # Set Exit type (sell) np_t_exit_pri = np_open # The price field our SELL exit will use if debug: print("Type SELL is first exit condition. " @@ -591,7 +592,7 @@ class Backslapping: # No stop or buy left in view - set t_exit_last -1 to handle gracefully else: t_exit_last: int = -1 # Signal loop to exit, no buys or sells found. - t_exit_type = "No Exit" + t_exit_type = SellType.NONE np_t_exit_pri = 999 # field price should be calculated on. 999 a non-existent column if debug: print("No valid STOP or SELL found. Signalling t_exit_last to gracefully exit") @@ -688,16 +689,16 @@ class Backslapping: # TODO no! this is hard coded bleh fix this open np_trade_enter_price = np_bslap[t_open_ind + 1, np_open] - if t_exit_type == 'stop': + if t_exit_type == SellType.STOP_LOSS: if np_t_exit_pri == 6: np_trade_exit_price = np_t_stop_pri else: np_trade_exit_price = np_bslap[t_exit_ind, np_t_exit_pri] - if t_exit_type == 'sell': + if t_exit_type == SellType.SELL_SIGNAL: np_trade_exit_price = np_bslap[t_exit_ind, np_t_exit_pri] # Catch no exit found - if t_exit_type == "No Exit": + if t_exit_type == SellType.NONE: np_trade_exit_price = 0 if debug_timing: @@ -762,10 +763,11 @@ class Backslapping: bslap_result["open_rate"] = round(np_trade_enter_price, 15) bslap_result["close_rate"] = round(np_trade_exit_price, 15) bslap_result["exit_type"] = t_exit_type + bslap_result["sell_reason"] = t_exit_type #duplicated, but I don't care # append the dict to the list and print list bslap_pair_results.append(bslap_result) - if t_exit_type is "stop": + if t_exit_type is SellType.STOP_LOSS: stop_stops_count = stop_stops_count + 1 if debug: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4ec3ac82a..8585a2628 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -455,7 +455,7 @@ class Backtesting(object): ) ) - if 'Sell Reason' in results.columns: + if 'sell_reason' in results.columns: logger.info( '\n' + ' SELL READON STATS '.center(119, '=') + From 04d5e857e2e9bc49d66055c1ec25088c45265e59 Mon Sep 17 00:00:00 2001 From: Gert Date: Tue, 31 Jul 2018 18:10:23 -0700 Subject: [PATCH 31/31] added option to easily switch between backtesting and backslapping from the commandline option --- freqtrade/arguments.py | 8 ++++++++ freqtrade/optimize/backtesting.py | 12 +++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 022a2c739..1f6de2052 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -161,6 +161,14 @@ class Arguments(object): dest='exportfilename', metavar='PATH', ) + parser.add_argument( + '--backslap', + help="Utilize the Backslapping approach instead of the default Backtesting. This should provide more " + "accurate results, unless you are utilizing Min/Max function in your strategy.", + required=False, + dest='backslap', + action='store_true' + ) @staticmethod def optimizer_shared_options(parser: argparse.ArgumentParser) -> None: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 688b760ca..6ba6d7e3e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -98,7 +98,13 @@ class Backtesting(object): # self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close # self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close - self.use_backslap = True # Enable backslap - if false Orginal code is executed. + if 'backslap' in config: + self.use_backslap = config['backslap'] # Enable backslap - if false Orginal code is executed. + else: + self.use_backslap = False + + logger.info("using backslap: {}".format(self.use_backslap)) + self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended self.debug_timing = False # Stages within Backslap self.debug_2loops = False # Limit each pair to two loops, useful when debugging @@ -364,7 +370,6 @@ class Backtesting(object): timerange = Arguments.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) - ld_files = self.s() data = optimize.load_data( self.config['datadir'], pairs=pairs, @@ -374,6 +379,7 @@ class Backtesting(object): timerange=timerange ) + ld_files = self.s() if not data: logger.critical("No data found. Terminating.") return @@ -489,7 +495,7 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]: # Ensure we do not use Exchange credentials config['exchange']['key'] = '' config['exchange']['secret'] = '' - + config['backslap'] = args.backslap if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: raise DependencyException('stake amount could not be "%s" for backtesting' % constants.UNLIMITED_STAKE_AMOUNT)