add async method

This commit is contained in:
Matthias 2018-08-14 13:21:15 +02:00
parent 721fb3e326
commit 2602cbe683
5 changed files with 428 additions and 448 deletions

View File

@ -161,14 +161,6 @@ class Arguments(object):
dest='exportfilename', dest='exportfilename',
metavar='PATH', 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 @staticmethod
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None: def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
@ -236,7 +228,7 @@ class Arguments(object):
Builds and attaches all subcommands Builds and attaches all subcommands
:return: None :return: None
""" """
from freqtrade.optimize import backtesting, hyperopt from freqtrade.optimize import backtesting, backslapping, hyperopt
subparsers = self.parser.add_subparsers(dest='subparser') subparsers = self.parser.add_subparsers(dest='subparser')
@ -246,6 +238,12 @@ class Arguments(object):
self.optimizer_shared_options(backtesting_cmd) self.optimizer_shared_options(backtesting_cmd)
self.backtesting_options(backtesting_cmd) self.backtesting_options(backtesting_cmd)
# Add backslapping subcommand
backslapping_cmd = subparsers.add_parser('backslapping', help='backslapping module')
backslapping_cmd.set_defaults(func=backslapping.start)
self.optimizer_shared_options(backslapping_cmd)
self.backtesting_options(backslapping_cmd)
# Add hyperopt subcommand # Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
hyperopt_cmd.set_defaults(func=hyperopt.start) hyperopt_cmd.set_defaults(func=hyperopt.start)

View File

@ -1,48 +1,35 @@
import timeit import timeit
from argparse import Namespace
import logging
from typing import Dict, Any from typing import Dict, Any
from pandas import DataFrame from pandas import DataFrame
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.optimize.optimize import IOptimize, BacktestResult, setup_configuration
from freqtrade.strategy import IStrategy from freqtrade.strategy import IStrategy
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import StrategyResolver from freqtrade.strategy.resolver import StrategyResolver
logger = logging.getLogger(__name__)
class Backslapping:
class Backslapping(IOptimize):
""" """
provides a quick way to evaluate strategies over a longer term of time provides a quick way to evaluate strategies over a longer term of time
""" """
def __init__(self, config: Dict[str, Any], exchange = None) -> None: def __init__(self, config: Dict[str, Any]) -> None:
""" """
constructor constructor
""" """
super().__init__(config)
self.config = config
self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.ticker_interval = self.strategy.ticker_interval
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
self.populate_buy_trend = self.strategy.populate_buy_trend
self.populate_sell_trend = self.strategy.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.fee = self.exchange.get_fee()
self.stop_loss_value = self.strategy.stoploss self.stop_loss_value = self.strategy.stoploss
#### backslap config # backslap config
''' '''
Numpy arrays are used for 100x speed up Numpy arrays are used for 100x speed up
We requires setting Int values for We requires setting Int values for
@ -81,7 +68,7 @@ class Backslapping:
def f(self, st): def f(self, st):
return (timeit.default_timer() - st) return (timeit.default_timer() - st)
def run(self,args): def run(self, args):
headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low']
processed = args['processed'] processed = args['processed']
@ -96,8 +83,8 @@ class Backslapping:
if self.debug_timing: # Start timer if self.debug_timing: # Start timer
fl = self.s() fl = self.s()
ticker_data = self.populate_sell_trend( ticker_data = self.advise_sell(self.advise_buy(pair_data, {'pair': pair}),
self.populate_buy_trend(pair_data))[headers].copy() {'pair': pair})[headers].copy()
if self.debug_timing: # print time taken if self.debug_timing: # print time taken
flt = self.f(fl) flt = self.f(fl)
@ -132,7 +119,7 @@ class Backslapping:
bslap_results_df = self.vector_fill_results_table(bslap_results_df, pair) bslap_results_df = self.vector_fill_results_table(bslap_results_df, pair)
else: else:
from freqtrade.optimize.backtesting import BacktestResult
bslap_results_df = [] bslap_results_df = []
bslap_results_df = DataFrame.from_records(bslap_results_df, columns=BacktestResult._fields) bslap_results_df = DataFrame.from_records(bslap_results_df, columns=BacktestResult._fields)
@ -221,13 +208,13 @@ class Backslapping:
""" """
The purpose of this def is to return the next "buy" = 1 The purpose of this def is to return the next "buy" = 1
after t_exit_ind. after t_exit_ind.
This function will also check is the stop limit for the pair has been reached. 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. 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 t_exit_ind is the index the last trade exited on
or 0 if first time around this loop. or 0 if first time around this loop.
stop_stops i stop_stops i
""" """
debug = self.debug debug = self.debug
@ -379,7 +366,7 @@ class Backslapping:
a) Find first buy index a) Find first buy index
b) Discover first stop and sell hit after buy index b) Discover first stop and sell hit after buy index
c) Chose first instance as trade exit c) Chose first instance as trade exit
Phase 2 Phase 2
2) Manage dynamic Stop and ROI Exit 2) Manage dynamic Stop and ROI Exit
a) Create trade slice from 1 a) Create trade slice from 1
@ -392,14 +379,14 @@ class Backslapping:
''' '''
0 - Find next buy entry 0 - Find next buy entry
Finds index for first (buy = 1) flag Finds index for first (buy = 1) flag
Requires: np_buy_arr - a 1D array of the 'buy' column. To find next "1" 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 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: np_buy_arr_len - length of pair array.
Requires: stops_stops - number of stops allowed before stop trading a pair Requires: stops_stops - number of stops allowed before stop trading a pair
Requires: stop_stop_counts - count of stops hit in the pair Requires: stop_stop_counts - count of stops hit in the pair
Provides: The next "buy" index after t_exit_ind 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 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) t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len, stop_stops, stop_stops_count)
@ -416,19 +403,19 @@ class Backslapping:
""" """
1 - Create views to search within for our open trade 1 - Create views to search within for our open trade
The views are 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: Numpy view is employed as:
1,000 faster than pandas searches 1,000 faster than pandas searches
Pandas cannot assure it will always return a view, it may make a slow copy. Pandas cannot assure it will always return a view, it may make a slow copy.
The view contains columns: The view contains columns:
buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5
Requires: np_bslap is our numpy array of the ticker DataFrame Requires: np_bslap is our numpy array of the ticker DataFrame
Requires: t_open_ind is the index row with the buy. 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 View of array after buy.
Provides: np_t_open_v_stop View of array after buy +1 Provides: np_t_open_v_stop View of array after buy +1
(Stop will search in here to prevent stopping in the past) (Stop will search in here to prevent stopping in the past)
""" """
np_t_open_v = np_bslap[t_open_ind:] np_t_open_v = np_bslap[t_open_ind:]
@ -446,13 +433,13 @@ class Backslapping:
''' '''
2 - Calculate our stop-loss price 2 - Calculate our stop-loss price
As stop is based on buy price of our trade As stop is based on buy price of our trade
- (BTO)Buys are Triggered On np_bto, typically the CLOSE of candle - (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. - (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. 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 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: np_bslap - is our numpy array of the ticker DataFrame
Requires: t_open_ind - is the index row with the first buy. Requires: t_open_ind - is the index row with the first buy.
Requires: p_stop - is the stop rate, ie. 0.99 is -1% Requires: p_stop - is the stop rate, ie. 0.99 is -1%
@ -469,9 +456,9 @@ class Backslapping:
''' '''
3 - Find candle STO is under Stop-Loss After Trade opened. 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 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_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_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 Requires: np_t_stop_pri The stop-loss price STO must fall under to trigger stop
@ -501,9 +488,9 @@ class Backslapping:
''' '''
4 - Find first sell index after trade open 4 - Find first sell index after trade open
First index in the view np_t_open_v where ['sell'] = 1 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: np_t_open_v - view of ticker_data from buy onwards
Requires: no_sell - integer '3', the buy column in the array 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 Provides: np_t_sell_ind index of view where first sell=1 after buy
@ -528,13 +515,13 @@ class Backslapping:
''' '''
5 - Determine which was hit first a stop or sell 5 - Determine which was hit first a stop or sell
To then use as exit index price-field (sell on buy, stop on stop) 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 STOP takes priority over SELL as would be 'in candle' from tick data
Sell would use Open from Next candle. Sell would use Open from Next candle.
So in a draw Stop would be hit first on ticker data in live 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: Validity of when types of trades may be executed can be summarised as:
Tick View Tick View
index index Buy Sell open low close high Stop price index index Buy Sell open low close high Stop price
open 2am 94 -1 0 0 ----- ------ ------ ----- ----- open 2am 94 -1 0 0 ----- ------ ------ ----- -----
@ -542,25 +529,25 @@ class Backslapping:
open 4am 96 1 0 1 Enter trgstop trg sel ROI out Stop out open 4am 96 1 0 1 Enter trgstop trg sel ROI out Stop out
open 5am 97 2 0 0 Exit ------ ------- ----- ----- open 5am 97 2 0 0 Exit ------ ------- ----- -----
open 6am 98 3 0 0 ----- ------ ------- ----- ----- open 6am 98 3 0 0 ----- ------ ------- ----- -----
-1 means not found till end of view i.e no valid Stop found. Exclude from match. -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. Stop tiggering and closing in 96-1, the candle we bought at OPEN in, is valid.
Buys and sells are triggered at candle close Buys and sells are triggered at candle close
Both will open their postions at the open of the next candle. i/e + 1 index 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 Stop and buy Indexes are on the view. To map to the ticker dataframe
the t_open_ind index should be summed. the t_open_ind index should be summed.
np_t_stop_ind: Stop Found index in view np_t_stop_ind: Stop Found index in view
t_exit_ind : Sell found in view t_exit_ind : Sell found in view
t_open_ind : Where view was started on ticker_data t_open_ind : Where view was started on ticker_data
TODO: fix this frig for logic 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 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_sell_ind as 9999999999 when -1 (not found)
cludge - Setting np_t_stop_ind as 9999999999 when -1 (not found) cludge - Setting np_t_stop_ind as 9999999999 when -1 (not found)
''' '''
if debug: if debug:
print("\n(5) numpy debug\nStop or Sell Logic Processing") print("\n(5) numpy debug\nStop or Sell Logic Processing")
@ -730,7 +717,7 @@ class Backslapping:
if t_exit_last >= t_exit_ind or t_exit_last == -1: if t_exit_last >= t_exit_ind or t_exit_last == -1:
""" """
Break loop and go on to next pair. Break loop and go on to next pair.
When last trade exit equals index of last exit, there is no When last trade exit equals index of last exit, there is no
opportunity to close any more trades. opportunity to close any more trades.
""" """
@ -763,7 +750,7 @@ class Backslapping:
bslap_result["open_rate"] = round(np_trade_enter_price, 15) bslap_result["open_rate"] = round(np_trade_enter_price, 15)
bslap_result["close_rate"] = round(np_trade_exit_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
bslap_result["sell_reason"] = t_exit_type #duplicated, but I don't care bslap_result["sell_reason"] = t_exit_type # duplicated, but I don't care
# append the dict to the list and print list # append the dict to the list and print list
bslap_pair_results.append(bslap_result) bslap_pair_results.append(bslap_result)
@ -787,3 +774,18 @@ class Backslapping:
# Send back List of trade dicts # Send back List of trade dicts
return bslap_pair_results return bslap_pair_results
def start(args: Namespace) -> None:
"""
Start Backtesting script
:param args: Cli args from Arguments()
:return: None
"""
# Initialize configuration
config = setup_configuration(args)
logger.info('Starting freqtrade in Backtesting mode')
# Initialize backtesting object
backslapping = Backslapping(config)
backslapping.start()

View File

@ -4,51 +4,21 @@
This module contains the backtesting logic This module contains the backtesting logic
""" """
import logging import logging
import operator
from argparse import Namespace from argparse import Namespace
from datetime import datetime, timedelta from typing import Any, Dict, List, Optional
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 import freqtrade.optimize as optimize
from freqtrade import DependencyException, constants from freqtrade.optimize.optimize import IOptimize, BacktestResult, setup_configuration
from freqtrade.arguments import Arguments 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 freqtrade.persistence import Trade
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
from collections import OrderedDict
import timeit
from time import sleep
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BacktestResult(NamedTuple): class Backtesting(IOptimize):
"""
NamedTuple Defining BacktestResults inputs.
"""
pair: str
profit_percent: float
profit_abs: float
open_time: datetime
close_time: datetime
open_index: int
close_index: int
trade_duration: float
open_at_end: bool
open_rate: float
close_rate: float
sell_reason: SellType
class Backtesting(object):
""" """
Backtesting class, this class contains all the logic to run a backtest Backtesting class, this class contains all the logic to run a backtest
@ -58,139 +28,7 @@ class Backtesting(object):
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.config = config super().__init__(config)
self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.ticker_interval = self.strategy.ticker_interval
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
self.advise_buy = self.strategy.advise_buy
self.advise_sell = self.strategy.advise_sell
# Reset keys for backtesting
self.config['exchange']['key'] = ''
self.config['exchange']['secret'] = ''
self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = ''
self.config['dry_run'] = True
self.exchange = Exchange(self.config)
self.fee = self.exchange.get_fee()
self.stop_loss_value = self.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
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
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
self.backslap = Backslapping(config)
@staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
timeframe = [
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
for frame in data.values()
]
return min(timeframe, key=operator.itemgetter(0))[0], \
max(timeframe, key=operator.itemgetter(1))[1]
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
tabular_data = []
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data:
result = results[results.pair == pair]
tabular_data.append([
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_percent.sum() * 100.0,
result.profit_abs.sum(),
str(timedelta(
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
len(result[result.profit_abs > 0]),
len(result[result.profit_abs < 0])
])
# Append Total
tabular_data.append([
'TOTAL',
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0,
results.profit_abs.sum(),
str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generate small table outlining Backtest results
"""
tabular_data = []
headers = ['Sell Reason', 'Count']
for reason, count in results['sell_reason'].value_counts().iteritems():
tabular_data.append([reason.value, count])
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
if records:
logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records)
def _get_sell_trade_entry( def _get_sell_trade_entry(
self, pair: str, buy_row: DataFrame, self, pair: str, buy_row: DataFrame,
@ -217,13 +55,14 @@ class Backtesting(object):
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal, sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
sell_row.sell) sell_row.sell)
if sell.sell_flag: if sell.sell_flag:
return BacktestResult(pair=pair, return BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.open), profit_percent=trade.calc_profit_percent(rate=sell_row.open),
profit_abs=trade.calc_profit(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open),
open_time=buy_row.date, open_time=buy_row.date,
close_time=sell_row.date, close_time=sell_row.date,
trade_duration=int(( trade_duration=int((
sell_row.date - buy_row.date).total_seconds() // 60), sell_row.date - buy_row.date).total_seconds() // 60),
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=False, open_at_end=False,
@ -240,7 +79,7 @@ class Backtesting(object):
open_time=buy_row.date, open_time=buy_row.date,
close_time=sell_row.date, close_time=sell_row.date,
trade_duration=int(( trade_duration=int((
sell_row.date - buy_row.date).total_seconds() // 60), sell_row.date - buy_row.date).total_seconds() // 60),
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=True, open_at_end=True,
@ -253,14 +92,7 @@ class Backtesting(object):
return btr return btr
return None return None
def s(self): def run(self, args: Dict) -> DataFrame:
st = timeit.default_timer()
return st
def f(self, st):
return (timeit.default_timer() - st)
def backtest(self, args: Dict) -> DataFrame:
""" """
Implements backtesting functionality Implements backtesting functionality
@ -275,50 +107,32 @@ class Backtesting(object):
position_stacking: do we allow position stacking? (default: False) position_stacking: do we allow position stacking? (default: False)
:return: DataFrame :return: DataFrame
""" """
headers = ['date', 'buy', 'open', 'close', 'sell']
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
position_stacking = args.get('position_stacking', False)
trades = []
trade_count_lock: Dict = {}
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
use_backslap = self.use_backslap ticker_data = self.advise_sell(
debug_timing = self.debug_timing_main_loop
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']
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
position_stacking = args.get('position_stacking', False)
trades = []
trade_count_lock: Dict = {}
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.advise_sell(
self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
# to avoid using data from future, we buy/sell with signal from previous candle # 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[:, 'buy'] = ticker_data['buy'].shift(1)
ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1)
ticker_data.drop(ticker_data.head(1).index, inplace=True) ticker_data.drop(ticker_data.head(1).index, inplace=True)
if debug_timing: # print time taken # Convert from Pandas to list for performance reasons
flt = self.f(fl) # (Looping Pandas is slow.)
# print("populate_buy_trend:", pair, round(flt, 10)) ticker = [x for x in ticker_data.itertuples()]
st = self.s()
# Convert from Pandas to list for performance reasons lock_pair_until = None
# (Looping Pandas is slow.) for index, row in enumerate(ticker):
ticker = [x for x in ticker_data.itertuples()] if row.buy == 0 or row.sell == 1:
continue # skip rows where no buy signal or that would immediately sell off
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 not position_stacking: if not position_stacking:
if lock_pair_until is not None and row.date <= lock_pair_until: if lock_pair_until is not None and row.date <= lock_pair_until:
@ -328,178 +142,22 @@ class Backtesting(object):
if not trade_count_lock.get(row.date, 0) < max_open_trades: if not trade_count_lock.get(row.date, 0) < max_open_trades:
continue continue
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 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_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
trade_count_lock, args) trade_count_lock, args)
if trade_entry: if trade_entry:
lock_pair_until = trade_entry.close_time lock_pair_until = trade_entry.close_time
trades.append(trade_entry) trades.append(trade_entry)
else: else:
# Set lock_pair_until to end of testing period if trade could not be closed # 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 # This happens only if the buy-signal was with the last candle
lock_pair_until = ticker_data.iloc[-1].date lock_pair_until = ticker_data.iloc[-1].date
if debug_timing: # print time taken return DataFrame.from_records(trades, columns=BacktestResult._fields)
tt = self.f(st)
print("Time to BackTest :", pair, round(tt, 10))
print("-----------------------")
return DataFrame.from_records(trades, columns=BacktestResult._fields)
####################### Original BT loop end
def start(self) -> None:
"""
Run a backtesting end-to-end
:return: None
"""
data = {}
pairs = self.config['exchange']['pair_whitelist']
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval)
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
timerange = Arguments.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = optimize.load_data(
self.config['datadir'],
pairs=pairs,
ticker_interval=self.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.exchange,
timerange=timerange
)
ld_files = self.s()
if not data:
logger.critical("No data found. Terminating.")
return
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
max_open_trades = self.config['max_open_trades']
else:
logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
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)
logger.info(
'Measuring data from %s up to %s (%s days)..',
min_date.isoformat(),
max_date.isoformat(),
(max_date - min_date).days
)
# Execute backtest and print results
results = self.backtest(
{
'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed,
'max_open_trades': max_open_trades,
'position_stacking': self.config.get('position_stacking', False),
}
)
if self.config.get('export', False):
self._store_backtest_result(self.config.get('exportfilename'), results)
if self.use_backslap:
logger.info(
'\n====================================================== '
'BackSLAP REPORT'
' =======================================================\n'
'%s',
self._generate_text_table(
data,
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)
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================================================= '
'BACKTEST REPORT'
' ==================================================\n'
'%s',
self._generate_text_table(
data,
results
)
)
if 'sell_reason' in results.columns:
logger.info(
'\n' +
' SELL READON STATS '.center(119, '=') +
'\n%s \n',
self._generate_text_table_sell_reason(data, results)
)
else:
logger.info("no sell reasons available!")
logger.info(
'\n' +
' LEFT OPEN TRADES REPORT '.center(119, '=') +
'\n%s',
self._generate_text_table(
data,
results.loc[results.open_at_end]
)
)
def setup_configuration(args: Namespace) -> Dict[str, Any]:
"""
Prepare the configuration for the backtesting
:param args: Cli args from Arguments()
:return: Configuration
"""
configuration = Configuration(args)
config = configuration.get_config()
# 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)
return config
def start(args: Namespace) -> None: def start(args: Namespace) -> None:

View File

@ -276,7 +276,7 @@ class Hyperopt(Backtesting):
self.strategy.stoploss = params['stoploss'] self.strategy.stoploss = params['stoploss']
processed = load(TICKERDATA_PICKLE) processed = load(TICKERDATA_PICKLE)
results = self.backtest( results = self.run(
{ {
'stake_amount': self.config['stake_amount'], 'stake_amount': self.config['stake_amount'],
'processed': processed, 'processed': processed,

View File

@ -0,0 +1,322 @@
# pragma pylint: disable=missing-docstring, W0212, too-many-arguments
"""
This module contains the backtesting logic
"""
import logging
import operator
from abc import ABC, abstractmethod
from argparse import Namespace
from copy import deepcopy
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
import arrow
from pandas import DataFrame
from tabulate import tabulate
from freqtrade import DependencyException, constants
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.exchange import Exchange
from freqtrade.misc import file_dump_json
import freqtrade.optimize as optimize
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
logger = logging.getLogger(__name__)
class BacktestResult(NamedTuple):
"""
NamedTuple Defining BacktestResults inputs.
"""
pair: str
profit_percent: float
profit_abs: float
open_time: datetime
close_time: datetime
open_index: int
close_index: int
trade_duration: float
open_at_end: bool
open_rate: float
close_rate: float
sell_reason: SellType
class IOptimize(ABC):
"""
Backtesting Abstract class, this class contains all the logic to run a backtest
To run a backtest:
backtesting = Backtesting(config)
backtesting.start()
"""
def __init__(self, config: Dict[str, Any]) -> None:
self.config = config
# Reset keys for backtesting
self.config['exchange']['key'] = ''
self.config['exchange']['secret'] = ''
self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = ''
self.config['dry_run'] = True
self.strategylist: List[IStrategy] = []
if self.config.get('strategy_list', None):
# Force one interval
self.ticker_interval = str(self.config.get('ticker_interval'))
for strat in list(self.config['strategy_list']):
stratconf = deepcopy(self.config)
stratconf['strategy'] = strat
self.strategylist.append(StrategyResolver(stratconf).strategy)
else:
# only one strategy
strat = StrategyResolver(self.config).strategy
self.strategylist.append(StrategyResolver(self.config).strategy)
# Load one strategy
self._set_strategy(self.strategylist[0])
self.exchange = Exchange(self.config)
self.fee = self.exchange.get_fee()
def _set_strategy(self, strategy):
"""
Load strategy into backtesting
"""
self.strategy = strategy
self.ticker_interval = self.config.get('ticker_interval')
self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe
self.advise_buy = strategy.advise_buy
self.advise_sell = strategy.advise_sell
def _get_timeframe(self, data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
timeframe = [
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
for frame in data.values()
]
return min(timeframe, key=operator.itemgetter(0))[0], \
max(timeframe, key=operator.itemgetter(1))[1]
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
tabular_data = []
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data:
result = results[results.pair == pair]
tabular_data.append([
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_percent.sum() * 100.0,
result.profit_abs.sum(),
str(timedelta(
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
len(result[result.profit_abs > 0]),
len(result[result.profit_abs < 0])
])
# Append Total
tabular_data.append([
'TOTAL',
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0,
results.profit_abs.sum(),
str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generate small table outlining Backtest results
"""
tabular_data = []
headers = ['Sell Reason', 'Count']
for reason, count in results['sell_reason'].value_counts().iteritems():
tabular_data.append([reason.value, count])
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
def _generate_text_table_strategy(self, all_results: dict) -> str:
"""
Generate summary table per strategy
"""
stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
tabular_data = []
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for strategy, results in all_results.items():
tabular_data.append([
strategy,
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0,
results.profit_abs.sum(),
str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
strategyname: Optional[str] = None) -> None:
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
if records:
if strategyname:
# Inject strategyname to filename
recname = Path(recordfilename)
recordfilename = str(Path.joinpath(
recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix))
logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records)
def start(self) -> None:
"""
Run a backtesting end-to-end
:return: None
"""
data = {}
pairs = self.config['exchange']['pair_whitelist']
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = self.exchange.get_candle_history(pair, self.ticker_interval)
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
timerange = Arguments.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = optimize.load_data(
self.config['datadir'],
pairs=pairs,
ticker_interval=self.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.exchange,
timerange=timerange
)
if not data:
logger.critical("No data found. Terminating.")
return
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
max_open_trades = self.config['max_open_trades']
else:
logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
max_open_trades = 0
all_results = {}
for strat in self.strategylist:
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
self._set_strategy(strat)
# need to reprocess data every time to populate signals
preprocessed = self.tickerdata_to_dataframe(data)
# Print timeframe
min_date, max_date = self._get_timeframe(preprocessed)
logger.info(
'Measuring data from %s up to %s (%s days)..',
min_date.isoformat(),
max_date.isoformat(),
(max_date - min_date).days
)
# Execute backtest and print results
all_results[self.strategy.get_strategy_name()] = self.run(
{
'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed,
'max_open_trades': max_open_trades,
'position_stacking': self.config.get('position_stacking', False),
}
)
for strategy, results in all_results.items():
if self.config.get('export', False):
self._store_backtest_result(self.config['exportfilename'], results,
strategy if len(self.strategylist) > 1 else None)
print(f"Result for strategy {strategy}")
print(' BACKTESTING REPORT '.center(119, '='))
print(self._generate_text_table(data, results))
print(' SELL REASON STATS '.center(119, '='))
print(self._generate_text_table_sell_reason(data, results))
print(' LEFT OPEN TRADES REPORT '.center(119, '='))
print(self._generate_text_table(data, results.loc[results.open_at_end]))
print()
if len(all_results) > 1:
# Print Strategy summary table
print(' Strategy Summary '.center(119, '='))
print(self._generate_text_table_strategy(all_results))
print('\nFor more details, please look at the detail tables above')
@abstractmethod
def run(self, args: Dict) -> DataFrame:
"""
Runs backtesting functionality.
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
Of course try to not have ugly code. By some accessor are sometime slower than functions.
Avoid, logging on this method
:param args: a dict containing:
stake_amount: btc amount to use for each trade
processed: a processed dictionary with format {pair, data}
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
position_stacking: do we allow position stacking? (default: False)
:return: DataFrame
"""
def setup_configuration(args: Namespace) -> Dict[str, Any]:
"""
Prepare the configuration for the backtesting
:param args: Cli args from Arguments()
:return: Configuration
"""
configuration = Configuration(args)
config = configuration.get_config()
# Ensure we do not use Exchange credentials
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT:
raise DependencyException('stake amount could not be "%s" for backtesting' %
constants.UNLIMITED_STAKE_AMOUNT)
return config