2018-02-09 07:35:38 +00:00
# pragma pylint: disable=missing-docstring, W0212, too-many-arguments
2017-11-14 21:15:24 +00:00
2018-02-09 07:35:38 +00:00
"""
This module contains the backtesting logic
"""
2018-03-25 19:37:14 +00:00
import logging
2018-03-29 18:16:25 +00:00
import operator
2018-03-17 21:43:36 +00:00
from argparse import Namespace
2018-07-18 18:08:55 +00:00
from datetime import datetime , timedelta
2018-07-04 07:31:35 +00:00
from typing import Any , Dict , List , NamedTuple , Optional , Tuple
2018-03-17 21:44:47 +00:00
2017-09-24 14:23:29 +00:00
import arrow
2018-07-16 12:01:02 +00:00
from pandas import DataFrame , to_datetime
2017-11-14 22:14:01 +00:00
from tabulate import tabulate
2017-09-28 21:26:28 +00:00
2018-01-10 07:51:36 +00:00
import freqtrade . optimize as optimize
2018-07-04 07:31:35 +00:00
from freqtrade import DependencyException , constants
2018-03-17 21:44:47 +00:00
from freqtrade . arguments import Arguments
from freqtrade . configuration import Configuration
2018-07-04 07:31:35 +00:00
from freqtrade . exchange import Exchange
2018-02-09 07:35:38 +00:00
from freqtrade . misc import file_dump_json
2018-07-28 21:23:18 +00:00
from freqtrade . optimize . backslapping import Backslapping
2017-09-28 21:26:28 +00:00
from freqtrade . persistence import Trade
2018-07-11 18:03:40 +00:00
from freqtrade . strategy . interface import SellType
2018-07-09 16:27:36 +00:00
from freqtrade . strategy . resolver import IStrategy , StrategyResolver
2018-07-14 23:45:06 +00:00
from collections import OrderedDict
2018-07-16 12:01:02 +00:00
import timeit
2018-07-16 13:16:18 +00:00
from time import sleep
2017-11-15 18:06:37 +00:00
2018-09-06 14:59:51 +00:00
import pdb
2017-11-15 18:06:37 +00:00
2018-03-25 19:37:14 +00:00
logger = logging . getLogger ( __name__ )
2018-06-10 11:15:25 +00:00
class BacktestResult ( NamedTuple ) :
"""
NamedTuple Defining BacktestResults inputs .
"""
pair : str
profit_percent : float
profit_abs : float
2018-06-10 11:32:07 +00:00
open_time : datetime
close_time : datetime
2018-06-10 18:52:42 +00:00
open_index : int
close_index : int
2018-06-10 11:15:25 +00:00
trade_duration : float
2018-06-10 11:37:53 +00:00
open_at_end : bool
2018-06-23 12:19:50 +00:00
open_rate : float
close_rate : float
2018-07-11 18:03:40 +00:00
sell_reason : SellType
2018-06-10 11:15:25 +00:00
2018-02-09 07:35:38 +00:00
class Backtesting ( object ) :
2017-11-14 22:14:01 +00:00
"""
2018-02-09 07:35:38 +00:00
Backtesting class , this class contains all the logic to run a backtest
To run a backtest :
backtesting = Backtesting ( config )
backtesting . start ( )
2017-11-14 22:14:01 +00:00
"""
2018-07-28 05:00:58 +00:00
2018-02-09 07:35:38 +00:00
def __init__ ( self , config : Dict [ str , Any ] ) - > None :
self . config = config
2018-07-09 16:27:36 +00:00
self . strategy : IStrategy = StrategyResolver ( self . config ) . strategy
2018-07-16 05:11:17 +00:00
self . ticker_interval = self . strategy . ticker_interval
self . tickerdata_to_dataframe = self . strategy . tickerdata_to_dataframe
2018-07-20 18:56:44 +00:00
self . advise_buy = self . strategy . advise_buy
self . advise_sell = self . strategy . advise_sell
2018-04-06 07:57:08 +00:00
# Reset keys for backtesting
2018-03-24 18:45:23 +00:00
self . config [ ' exchange ' ] [ ' key ' ] = ' '
self . config [ ' exchange ' ] [ ' secret ' ] = ' '
2018-04-10 15:20:27 +00:00
self . config [ ' exchange ' ] [ ' password ' ] = ' '
self . config [ ' exchange ' ] [ ' uid ' ] = ' '
self . config [ ' dry_run ' ] = True
2018-06-17 10:41:33 +00:00
self . exchange = Exchange ( self . config )
2018-06-22 18:04:07 +00:00
self . fee = self . exchange . get_fee ( )
2018-07-28 05:41:38 +00:00
2018-07-29 01:30:12 +00:00
self . stop_loss_value = self . strategy . stoploss
2018-07-14 22:54:23 +00:00
2018-07-16 14:16:35 +00:00
#### backslap config
'''
Numpy arrays are used for 100 x 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.
2018-07-17 08:12:21 +00:00
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
2018-07-28 21:23:18 +00:00
# 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
2018-08-01 01:10:23 +00:00
if ' backslap ' in config :
self . use_backslap = config [ ' backslap ' ] # Enable backslap - if false Orginal code is executed.
2018-07-28 05:41:38 +00:00
else :
2018-08-01 01:10:23 +00:00
self . use_backslap = False
2018-07-28 05:41:38 +00:00
2018-08-01 01:10:23 +00:00
logger . info ( " using backslap: {} " . format ( self . use_backslap ) )
2018-07-28 05:00:58 +00:00
2018-07-28 21:23:18 +00:00
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
2018-07-16 17:06:06 +00:00
self . debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap
2018-02-09 07:35:38 +00:00
2018-07-28 21:23:18 +00:00
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
2018-07-17 11:22:38 +00:00
2018-07-28 21:23:18 +00:00
self . stop_stops : int = 9999 # stop back testing any pair with this many stops, set to 999999 to not hit
2018-07-16 14:16:35 +00:00
2018-07-28 21:23:18 +00:00
self . backslap = Backslapping ( config )
2018-07-28 04:54:33 +00:00
2018-02-09 07:35:38 +00:00
@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
"""
2018-03-29 18:16:25 +00:00
timeframe = [
2018-07-25 14:04:25 +00:00
( arrow . get ( frame [ ' date ' ] . min ( ) ) , arrow . get ( frame [ ' date ' ] . max ( ) ) )
2018-03-29 18:16:25 +00:00
for frame in data . values ( )
]
return min ( timeframe , key = operator . itemgetter ( 0 ) ) [ 0 ] , \
2018-07-28 21:23:18 +00:00
max ( timeframe , key = operator . itemgetter ( 1 ) ) [ 1 ]
2018-02-09 07:35:38 +00:00
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
"""
2018-06-02 11:43:51 +00:00
stake_currency = str ( self . config . get ( ' stake_currency ' ) )
2018-02-09 07:35:38 +00:00
2018-07-19 11:14:21 +00:00
floatfmt = ( ' s ' , ' d ' , ' .2f ' , ' .2f ' , ' .8f ' , ' d ' , ' .1f ' , ' .1f ' )
2018-02-09 07:35:38 +00:00
tabular_data = [ ]
2018-09-06 14:59:51 +00:00
# headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
# 'total profit ' + stake_currency, 'avg duration', 'profit', 'loss', 'total loss ab', 'total profit ab', 'Risk Reward Ratio', 'Win Rate']
2018-07-08 17:55:04 +00:00
headers = [ ' pair ' , ' buy count ' , ' avg profit % ' , ' cum profit % ' ,
2018-09-06 14:59:51 +00:00
' total profit ' + stake_currency , ' avg duration ' , ' profit ' , ' loss ' , ' RRR ' , ' Win Rate % ' , ' Required RR ' ]
2018-02-09 07:35:38 +00:00
for pair in data :
2018-06-10 11:15:25 +00:00
result = results [ results . pair == pair ]
2018-09-06 14:59:51 +00:00
win_rate = ( len ( result [ result . profit_abs > 0 ] ) / len ( result . index ) ) if ( len ( result . index ) > 0 ) else None
2018-02-09 07:35:38 +00:00
tabular_data . append ( [
pair ,
len ( result . index ) ,
result . profit_percent . mean ( ) * 100.0 ,
2018-07-08 17:55:04 +00:00
result . profit_percent . sum ( ) * 100.0 ,
2018-06-10 11:15:25 +00:00
result . profit_abs . sum ( ) ,
2018-07-18 18:08:55 +00:00
str ( timedelta (
2018-07-19 11:14:21 +00:00
minutes = round ( result . trade_duration . mean ( ) ) ) ) if not result . empty else ' 0:00 ' ,
2018-06-10 11:15:25 +00:00
len ( result [ result . profit_abs > 0 ] ) ,
2018-09-06 14:59:51 +00:00
len ( result [ result . profit_abs < 0 ] ) ,
# result[result.profit_abs < 0]['profit_abs'].sum(),
# result[result.profit_abs > 0]['profit_abs'].sum(),
abs ( 1 / ( ( result [ result . profit_abs < 0 ] [ ' profit_abs ' ] . sum ( ) / len ( result [ result . profit_abs < 0 ] ) ) / ( result [ result . profit_abs > 0 ] [ ' profit_abs ' ] . sum ( ) / len ( result [ result . profit_abs > 0 ] ) ) ) ) ,
win_rate * 100 if win_rate else " nan " ,
( ( 1 / win_rate ) - 1 ) if win_rate else " nan "
2018-02-09 07:35:38 +00:00
] )
# Append Total
2017-11-14 22:14:01 +00:00
tabular_data . append ( [
2018-02-09 07:35:38 +00:00
' TOTAL ' ,
len ( results . index ) ,
results . profit_percent . mean ( ) * 100.0 ,
2018-07-11 12:50:04 +00:00
results . profit_percent . sum ( ) * 100.0 ,
2018-06-10 11:15:25 +00:00
results . profit_abs . sum ( ) ,
2018-07-18 18:08:55 +00:00
str ( timedelta (
2018-07-19 11:14:21 +00:00
minutes = round ( results . trade_duration . mean ( ) ) ) ) if not results . empty else ' 0:00 ' ,
2018-06-10 11:15:25 +00:00
len ( results [ results . profit_abs > 0 ] ) ,
len ( results [ results . profit_abs < 0 ] )
2017-11-14 22:14:01 +00:00
] )
2018-05-18 11:02:38 +00:00
return tabulate ( tabular_data , headers = headers , floatfmt = floatfmt , tablefmt = " pipe " )
2018-02-09 07:35:38 +00:00
2018-09-06 14:59:51 +00:00
def _generate_text_table_edge_positioning ( self , data : Dict [ str , Dict ] , results : DataFrame ) - > str :
2018-07-12 19:19:43 +00:00
"""
2018-09-06 14:59:51 +00:00
This is a temporary version of edge positioning calculation .
The function will be eventually moved to a plugin called Edge in order to calculate necessary WR , RRR and
other indictaors related to money management periodically ( each X minutes ) and keep it in a storage .
The calulation will be done per pair and per strategy .
2018-07-12 19:19:43 +00:00
"""
2018-09-06 14:59:51 +00:00
2018-07-12 19:19:43 +00:00
tabular_data = [ ]
2018-09-06 14:59:51 +00:00
headers = [ ' Number of trades ' , ' RRR ' , ' Win Rate % ' , ' Required RR ' ]
###
# The algorithm should be:
# 1) Removing outliers from dataframe. i.e. all profit_percent which are outside (mean -+ (2 * (standard deviation))).
# 2) Removing pairs with less than X trades (X defined in config).
# 3) Calculating RRR and WR.
# 4) Removing pairs for which WR and RRR are not in an acceptable range (e.x. WR > 95%).
# 5) Sorting the result based on the delta between required RR and RRR.
# Here we assume initial data in order to calculate position size.
# these values will be replaced by exchange info or config
for pair in data :
result = results [ results . pair == pair ]
# WinRate is calculated as follows: (Number of profitable trades) / (Total Trades)
win_rate = ( len ( result [ result . profit_abs > 0 ] ) / len ( result . index ) ) if ( len ( result . index ) > 0 ) else None
# Risk Reward Ratio is calculated as follows: 1 / ((total loss on losing trades / number of losing trades) / (total gain on profitable trades / number of winning trades))
risk_reward_ratio = abs ( 1 / ( ( result [ result . profit_abs < 0 ] [ ' profit_abs ' ] . sum ( ) / len ( result [ result . profit_abs < 0 ] ) ) / ( result [ result . profit_abs > 0 ] [ ' profit_abs ' ] . sum ( ) / len ( result [ result . profit_abs > 0 ] ) ) ) )
# Required Reward Ratio is (1 / WinRate) - 1
required_risk_reward = ( ( 1 / win_rate ) - 1 ) if win_rate else None
#pdb.set_trace()
tabular_data . append ( [
pair ,
len ( result . index ) ,
risk_reward_ratio ,
win_rate * 100 if win_rate else " nan " ,
required_risk_reward
] )
# for pair in data:
# result = results[results.pair == pair]
# win_rate = (len(result[result.profit_abs > 0]) / len(result.index)) if (len(result.index) > 0) else None
# 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]),
# # result[result.profit_abs < 0]['profit_abs'].sum(),
# # result[result.profit_abs > 0]['profit_abs'].sum(),
# abs(1 / ((result[result.profit_abs < 0]['profit_abs'].sum() / len(result[result.profit_abs < 0])) / (result[result.profit_abs > 0]['profit_abs'].sum() / len(result[result.profit_abs > 0])))),
# win_rate * 100 if win_rate else "nan",
# ((1 / win_rate) - 1) if win_rate else "nan"
# ])
#return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
2018-07-12 19:19:43 +00:00
return tabulate ( tabular_data , headers = headers , tablefmt = " pipe " )
2018-09-06 14:59:51 +00:00
2018-07-12 19:19:43 +00:00
def _generate_text_table_sell_reason ( self , data : Dict [ str , Dict ] , results : DataFrame ) - > str :
2018-07-29 11:07:11 +00:00
"""
2018-07-12 19:19:43 +00:00
Generate small table outlining Backtest results
2018-07-29 11:07:11 +00:00
"""
tabular_data = [ ]
2018-07-12 19:19:43 +00:00
headers = [ ' Sell Reason ' , ' Count ' ]
for reason , count in results [ ' sell_reason ' ] . value_counts ( ) . iteritems ( ) :
2018-07-29 01:30:12 +00:00
tabular_data . append ( [ reason . value , count ] )
2018-07-12 19:19:43 +00:00
return tabulate ( tabular_data , headers = headers , tablefmt = " pipe " )
2018-07-29 11:07:11 +00:00
2018-06-12 20:29:30 +00:00
def _store_backtest_result ( self , recordfilename : Optional [ str ] , results : DataFrame ) - > None :
2018-06-23 12:19:50 +00:00
records = [ ( t . pair , t . profit_percent , t . open_time . timestamp ( ) ,
t . close_time . timestamp ( ) , t . open_index - 1 , t . trade_duration ,
2018-07-11 18:32:56 +00:00
t . open_rate , t . close_rate , t . open_at_end , t . sell_reason . value )
2018-06-23 12:19:50 +00:00
for index , t in results . iterrows ( ) ]
2018-06-12 20:29:30 +00:00
if records :
logger . info ( ' Dumping backtest results to %s ' , recordfilename )
file_dump_json ( recordfilename , records )
2018-03-17 21:43:36 +00:00
def _get_sell_trade_entry (
self , pair : str , buy_row : DataFrame ,
2018-06-10 11:15:25 +00:00
partial_ticker : List , trade_count_lock : Dict , args : Dict ) - > Optional [ BacktestResult ] :
2018-03-17 21:43:36 +00:00
2018-02-09 07:35:38 +00:00
stake_amount = args [ ' stake_amount ' ]
max_open_trades = args . get ( ' max_open_trades ' , 0 )
trade = Trade (
2018-07-05 18:20:52 +00:00
open_rate = buy_row . open ,
2018-03-04 00:42:37 +00:00
open_date = buy_row . date ,
2018-02-09 07:35:38 +00:00
stake_amount = stake_amount ,
2018-03-04 00:42:37 +00:00
amount = stake_amount / buy_row . open ,
2018-06-22 18:04:07 +00:00
fee_open = self . fee ,
fee_close = self . fee
2018-02-09 07:35:38 +00:00
)
# calculate win/lose forwards from buy point
2018-03-04 00:42:37 +00:00
for sell_row in partial_ticker :
2018-02-09 07:35:38 +00:00
if max_open_trades > 0 :
# Increase trade_count_lock for every iteration
2018-03-04 00:42:37 +00:00
trade_count_lock [ sell_row . date ] = trade_count_lock . get ( sell_row . date , 0 ) + 1
buy_signal = sell_row . buy
2018-07-11 18:03:40 +00:00
sell = self . strategy . should_sell ( trade , sell_row . open , sell_row . date , buy_signal ,
2018-07-19 17:41:42 +00:00
sell_row . sell )
2018-07-12 20:21:52 +00:00
if sell . sell_flag :
2018-06-10 11:15:25 +00:00
return BacktestResult ( pair = pair ,
2018-07-05 18:20:52 +00:00
profit_percent = trade . calc_profit_percent ( rate = sell_row . open ) ,
profit_abs = trade . calc_profit ( rate = sell_row . open ) ,
2018-06-10 11:15:25 +00:00
open_time = buy_row . date ,
close_time = sell_row . date ,
2018-07-18 07:29:51 +00:00
trade_duration = int ( (
2018-07-29 01:30:12 +00:00
sell_row . date - buy_row . date ) . total_seconds ( ) / / 60 ) ,
2018-06-12 20:29:30 +00:00
open_index = buy_row . Index ,
close_index = sell_row . Index ,
2018-06-23 12:19:50 +00:00
open_at_end = False ,
2018-07-05 18:20:52 +00:00
open_rate = buy_row . open ,
2018-07-11 18:03:40 +00:00
close_rate = sell_row . open ,
2018-07-12 20:21:52 +00:00
sell_reason = sell . sell_type
2018-06-10 11:15:25 +00:00
)
2018-06-09 19:44:20 +00:00
if partial_ticker :
# no sell condition found - trade stil open at end of backtest period
sell_row = partial_ticker [ - 1 ]
2018-06-10 11:15:25 +00:00
btr = BacktestResult ( pair = pair ,
2018-07-05 18:20:52 +00:00
profit_percent = trade . calc_profit_percent ( rate = sell_row . open ) ,
profit_abs = trade . calc_profit ( rate = sell_row . open ) ,
2018-06-10 11:15:25 +00:00
open_time = buy_row . date ,
close_time = sell_row . date ,
2018-07-18 07:29:51 +00:00
trade_duration = int ( (
2018-07-29 01:30:12 +00:00
sell_row . date - buy_row . date ) . total_seconds ( ) / / 60 ) ,
2018-06-12 20:29:30 +00:00
open_index = buy_row . Index ,
close_index = sell_row . Index ,
2018-06-23 12:19:50 +00:00
open_at_end = True ,
2018-07-05 18:20:52 +00:00
open_rate = buy_row . open ,
2018-07-11 18:03:40 +00:00
close_rate = sell_row . open ,
sell_reason = SellType . FORCE_SELL
2018-06-10 11:15:25 +00:00
)
2018-06-13 17:43:33 +00:00
logger . debug ( ' Force_selling still open trade %s with %s perc - %s ' , btr . pair ,
btr . profit_percent , btr . profit_abs )
2018-06-10 11:15:25 +00:00
return btr
2018-02-09 07:35:38 +00:00
return None
2018-07-16 13:16:18 +00:00
def s ( self ) :
st = timeit . default_timer ( )
return st
def f ( self , st ) :
return ( timeit . default_timer ( ) - st )
2018-03-17 21:43:36 +00:00
def backtest ( self , args : Dict ) - > DataFrame :
2018-02-09 07:35:38 +00:00
"""
Implements 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 )
2018-07-17 18:26:59 +00:00
position_stacking : do we allow position stacking ? ( default : False )
2018-02-09 07:35:38 +00:00
: return : DataFrame
"""
2018-03-04 00:42:37 +00:00
2018-07-16 14:16:35 +00:00
use_backslap = self . use_backslap
debug_timing = self . debug_timing_main_loop
2018-07-16 15:48:06 +00:00
2018-07-28 21:23:18 +00:00
if use_backslap : # Use Back Slap code
return self . backslap . run ( args )
else : # use Original Back test code
2018-07-16 14:16:35 +00:00
########################## Original BT loop
2018-06-07 07:21:07 +00:00
2018-07-16 15:57:15 +00:00
headers = [ ' date ' , ' buy ' , ' open ' , ' close ' , ' sell ' ]
processed = args [ ' processed ' ]
max_open_trades = args . get ( ' max_open_trades ' , 0 )
2018-07-29 01:30:12 +00:00
position_stacking = args . get ( ' position_stacking ' , False )
2018-07-16 15:57:15 +00:00
trades = [ ]
trade_count_lock : Dict = { }
2018-06-07 07:21:07 +00:00
2018-07-16 14:16:35 +00:00
for pair , pair_data in processed . items ( ) :
2018-07-28 21:23:18 +00:00
if debug_timing : # Start timer
2018-07-16 14:16:35 +00:00
fl = self . s ( )
2018-06-06 10:56:08 +00:00
2018-07-16 14:16:35 +00:00
pair_data [ ' buy ' ] , pair_data [ ' sell ' ] = 0 , 0 # cleanup from previous run
2018-03-04 00:42:37 +00:00
2018-07-30 20:43:25 +00:00
ticker_data = self . advise_sell (
2018-07-29 18:36:03 +00:00
self . advise_buy ( pair_data , { ' pair ' : pair } ) , { ' pair ' : pair } ) [ headers ] . copy ( )
2018-06-07 07:21:07 +00:00
2018-07-16 14:16:35 +00:00
# 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 )
2018-06-07 07:21:07 +00:00
2018-07-16 14:16:35 +00:00
ticker_data . drop ( ticker_data . head ( 1 ) . index , inplace = True )
2018-06-06 10:56:08 +00:00
2018-07-28 21:23:18 +00:00
if debug_timing : # print time taken
2018-07-16 14:16:35 +00:00
flt = self . f ( fl )
2018-07-28 21:23:18 +00:00
# print("populate_buy_trend:", pair, round(flt, 10))
2018-07-16 14:16:35 +00:00
st = self . s ( )
2018-03-04 00:42:37 +00:00
2018-07-16 14:16:35 +00:00
# Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.)
ticker = [ x for x in ticker_data . itertuples ( ) ]
2018-07-16 13:16:18 +00:00
2018-07-16 14:16:35 +00:00
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
2018-03-04 00:42:37 +00:00
2018-07-17 18:26:59 +00:00
if not position_stacking :
2018-03-04 00:42:37 +00:00
if lock_pair_until is not None and row . date < = lock_pair_until :
2018-02-09 07:35:38 +00:00
continue
if max_open_trades > 0 :
# Check if max_open_trades has already been reached for the given date
2018-03-04 00:42:37 +00:00
if not trade_count_lock . get ( row . date , 0 ) < max_open_trades :
2018-02-09 07:35:38 +00:00
continue
2018-07-16 14:16:35 +00:00
trade_count_lock [ row . date ] = trade_count_lock . get ( row . date , 0 ) + 1
2018-07-15 17:03:47 +00:00
2018-07-16 14:16:35 +00:00
trade_entry = self . _get_sell_trade_entry ( pair , row , ticker [ index + 1 : ] ,
trade_count_lock , args )
2017-11-14 22:14:01 +00:00
2018-07-16 14:16:35 +00:00
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
2018-02-09 07:35:38 +00:00
2018-07-16 14:16:35 +00:00
if debug_timing : # print time taken
tt = self . f ( st )
print ( " Time to BackTest : " , pair , round ( tt , 10 ) )
print ( " ----------------------- " )
2018-06-09 19:44:20 +00:00
2018-07-16 14:16:35 +00:00
return DataFrame . from_records ( trades , columns = BacktestResult . _fields )
####################### Original BT loop end
2018-02-09 07:35:38 +00:00
def start ( self ) - > None :
"""
Run a backtesting end - to - end
: return : None
"""
2018-08-19 17:39:22 +00:00
data : Dict [ str , Any ] = { }
2018-02-09 07:35:38 +00:00
pairs = self . config [ ' exchange ' ] [ ' pair_whitelist ' ]
2018-03-25 19:37:14 +00:00
logger . info ( ' Using stake_currency: %s ... ' , self . config [ ' stake_currency ' ] )
logger . info ( ' Using stake_amount: %s ... ' , self . config [ ' stake_amount ' ] )
2018-02-09 07:35:38 +00:00
if self . config . get ( ' live ' ) :
2018-03-25 19:37:14 +00:00
logger . info ( ' Downloading data for all pairs in whitelist ... ' )
2018-08-19 17:39:22 +00:00
self . exchange . refresh_tickers ( pairs , self . ticker_interval )
data = self . exchange . klines
2018-02-09 07:35:38 +00:00
else :
2018-03-25 19:37:14 +00:00
logger . info ( ' Using local backtesting data (using whitelist in given config) ... ' )
2018-02-09 07:35:38 +00:00
2018-06-02 12:07:54 +00:00
timerange = Arguments . parse_timerange ( None if self . config . get (
' timerange ' ) is None else str ( self . config . get ( ' timerange ' ) ) )
2018-07-17 21:33:11 +00:00
2018-06-05 21:34:26 +00:00
data = optimize . load_data (
2018-02-09 07:35:38 +00:00
self . config [ ' datadir ' ] ,
pairs = pairs ,
ticker_interval = self . ticker_interval ,
refresh_pairs = self . config . get ( ' refresh_pairs ' , False ) ,
2018-06-17 20:11:32 +00:00
exchange = self . exchange ,
2018-02-09 07:35:38 +00:00
timerange = timerange
)
2018-08-01 01:10:23 +00:00
ld_files = self . s ( )
2018-06-11 17:50:43 +00:00
if not data :
logger . critical ( " No data found. Terminating. " )
return
2018-07-17 19:05:03 +00:00
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self . config . get ( ' use_max_market_positions ' , True ) :
2018-03-20 18:38:33 +00:00
max_open_trades = self . config [ ' max_open_trades ' ]
else :
2018-07-17 19:05:03 +00:00
logger . info ( ' Ignoring max_open_trades (--disable-max-market-positions was used) ... ' )
2018-03-20 18:38:33 +00:00
max_open_trades = 0
2018-02-09 07:35:38 +00:00
preprocessed = self . tickerdata_to_dataframe ( data )
2018-07-17 21:33:11 +00:00
t_t = self . f ( ld_files )
print ( " Load from json to file to df in mem took " , t_t )
2018-03-02 13:46:32 +00:00
2018-02-09 07:35:38 +00:00
# Print timeframe
min_date , max_date = self . get_timeframe ( preprocessed )
2018-03-25 19:37:14 +00:00
logger . info (
2018-02-09 07:35:38 +00:00
' Measuring data from %s up to %s ( %s days).. ' ,
min_date . isoformat ( ) ,
max_date . isoformat ( ) ,
( max_date - min_date ) . days
)
2018-07-27 21:01:52 +00:00
2018-02-09 07:35:38 +00:00
# Execute backtest and print results
results = self . backtest (
{
' stake_amount ' : self . config . get ( ' stake_amount ' ) ,
' processed ' : preprocessed ,
' max_open_trades ' : max_open_trades ,
2018-07-17 18:26:59 +00:00
' position_stacking ' : self . config . get ( ' position_stacking ' , False ) ,
2018-02-09 07:35:38 +00:00
}
)
2018-06-12 20:29:30 +00:00
if self . config . get ( ' export ' , False ) :
self . _store_backtest_result ( self . config . get ( ' exportfilename ' ) , results )
2018-07-16 15:57:15 +00:00
if self . use_backslap :
2018-09-06 14:59:51 +00:00
# logger.info(
# '\n====================================================== '
# 'BackSLAP REPORT'
# ' =======================================================\n'
# '%s',
# self._generate_text_table(
# data,
# results
# )
# )
2018-07-27 21:01:52 +00:00
logger . info (
2018-07-16 15:57:15 +00:00
' \n ====================================================== '
2018-09-06 14:59:51 +00:00
' Edge positionning REPORT '
2018-07-16 15:57:15 +00:00
' ======================================================= \n '
' %s ' ,
2018-09-06 14:59:51 +00:00
self . _generate_text_table_edge_positioning (
2018-07-16 15:57:15 +00:00
data ,
results
)
2018-07-27 21:01:52 +00:00
)
2018-07-28 21:23:18 +00:00
# optional print trades
2018-07-16 17:06:06 +00:00
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 )
2018-03-20 18:38:33 +00:00
2018-07-16 17:06:06 +00:00
def to_fwf ( df , fname ) :
content = tabulate ( df . values . tolist ( ) , list ( df . columns ) , floatfmt = " .8f " , tablefmt = ' psql ' )
print ( content )
2018-02-09 07:35:38 +00:00
2018-07-16 17:06:06 +00:00
DataFrame . to_fwf = to_fwf ( TradesFrame , " backslap.txt " )
2018-06-12 20:29:30 +00:00
2018-07-28 21:23:18 +00:00
# optional save trades
2018-07-16 17:48:11 +00:00
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 )
2018-02-09 07:35:38 +00:00
2018-07-16 17:48:11 +00:00
def to_fwf ( df , fname ) :
content = tabulate ( df . values . tolist ( ) , list ( df . columns ) , floatfmt = " .8f " , tablefmt = ' psql ' )
open ( fname , " w " ) . write ( content )
2018-07-12 19:19:43 +00:00
2018-07-16 17:48:11 +00:00
DataFrame . to_fwf = to_fwf ( TradesFrame , " backslap.txt " )
2018-02-09 07:35:38 +00:00
2018-07-16 15:57:15 +00:00
else :
logger . info (
' \n ================================================= '
' BACKTEST REPORT '
' ================================================== \n '
' %s ' ,
self . _generate_text_table (
data ,
results
)
2018-02-09 07:35:38 +00:00
)
2018-07-29 01:30:12 +00:00
2018-07-29 02:45:33 +00:00
if ' sell_reason ' in results . columns :
2018-07-29 01:30:12 +00:00
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! " )
2018-02-09 07:35:38 +00:00
2018-06-10 11:45:16 +00:00
logger . info (
2018-07-16 18:19:32 +00:00
' \n ' +
' LEFT OPEN TRADES REPORT ' . center ( 119 , ' = ' ) +
' \n %s ' ,
2018-06-10 11:45:16 +00:00
self . _generate_text_table (
data ,
2018-06-10 11:55:48 +00:00
results . loc [ results . open_at_end ]
2018-06-10 11:45:16 +00:00
)
2018-02-09 07:35:38 +00:00
)
2018-03-17 21:43:36 +00:00
def setup_configuration ( args : Namespace ) - > Dict [ str , Any ] :
2017-11-22 23:25:06 +00:00
"""
2018-02-09 07:35:38 +00:00
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 ' ] = ' '
2018-08-01 01:10:23 +00:00
config [ ' backslap ' ] = args . backslap
2018-05-25 14:04:08 +00:00
if config [ ' stake_amount ' ] == constants . UNLIMITED_STAKE_AMOUNT :
raise DependencyException ( ' stake amount could not be " %s " for backtesting ' %
constants . UNLIMITED_STAKE_AMOUNT )
2018-05-23 10:15:03 +00:00
2018-02-09 07:35:38 +00:00
return config
2018-03-17 21:43:36 +00:00
def start ( args : Namespace ) - > None :
2018-02-09 07:35:38 +00:00
"""
Start Backtesting script
: param args : Cli args from Arguments ( )
: return : None
2017-11-22 23:25:06 +00:00
"""
2018-02-09 07:35:38 +00:00
# Initialize configuration
config = setup_configuration ( args )
2018-03-25 19:41:25 +00:00
logger . info ( ' Starting freqtrade in Backtesting mode ' )
2018-02-09 07:35:38 +00:00
# Initialize backtesting object
backtesting = Backtesting ( config )
2018-09-06 14:59:51 +00:00
backtesting . start ( )