From 309186911590630cb01bdd503da840c9eca8518c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 14:30:14 +0100 Subject: [PATCH 01/61] refactor get_close_rate out of get_sell_trade-entry --- freqtrade/optimize/backtesting.py | 44 +++++++++++++++++-------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d9fb1f2d1..c7d25cb7b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -261,6 +261,29 @@ class Backtesting: ticker[pair] = [x for x in ticker_data.itertuples()] return ticker + def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float: + # Special handling if high or low hit STOP_LOSS or ROI + if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + # Set close_rate to stoploss + closerate = trade.stop_loss + elif sell.sell_type == (SellType.ROI): + roi = self.strategy.min_roi_reached_entry(trade_dur) + if roi is not None: + # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) + closerate = - (trade.open_rate * roi + trade.open_rate * + (1 + trade.fee_open)) / (trade.fee_close - 1) + + # Use the maximum between closerate and low as we + # cannot sell outside of a candle. + # Applies when using {"xx": -1} as roi to force sells after xx minutes + closerate = max(closerate, sell_row.low) + else: + # This should not be reached... + closerate = sell_row.open + else: + closerate = sell_row.open + return closerate + def _get_sell_trade_entry( self, pair: str, buy_row: DataFrame, partial_ticker: List, trade_count_lock: Dict, @@ -287,26 +310,7 @@ class Backtesting: sell_row.sell, low=sell_row.low, high=sell_row.high) if sell.sell_flag: trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) - # Special handling if high or low hit STOP_LOSS or ROI - if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - # Set close_rate to stoploss - closerate = trade.stop_loss - elif sell.sell_type == (SellType.ROI): - roi = self.strategy.min_roi_reached_entry(trade_dur) - if roi is not None: - # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - closerate = - (trade.open_rate * roi + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) - - # Use the maximum between closerate and low as we - # cannot sell outside of a candle. - # Applies when using {"xx": -1} as roi to force sells after xx minutes - closerate = max(closerate, sell_row.low) - else: - # This should not be reached... - closerate = sell_row.open - else: - closerate = sell_row.open + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) return BacktestResult(pair=pair, profit_percent=trade.calc_profit_percent(rate=closerate), From 1e6f9f9fe20cab92711b06f929e5641991c8ea60 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 15:13:49 +0100 Subject: [PATCH 02/61] Add testcase for negative ROI sell using open --- tests/optimize/test_backtest_detail.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 3f6cc8c9a..e269009b1 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -265,6 +265,22 @@ tc16 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) +# Test 17: Buy, hold for 120 mins, then forcesell using roi=-1 +# Causes negative profit even though sell-reason is ROI. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# Uses open as sell-rate (special case) - since the roi-time is a multiple of the ticker interval. +tc17 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5050, 6172, 0, 0], + [3, 4980, 5000, 4940, 4962, 6172, 0, 0], # ForceSell on ROI (roi=-1) + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + TESTS = [ tc0, tc1, @@ -283,6 +299,7 @@ TESTS = [ tc14, tc15, tc16, + tc17, ] From 3163cbdf8a56f2eafefa5736945087327752ed10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 15:18:12 +0100 Subject: [PATCH 03/61] Apply special case for negative ROI --- freqtrade/optimize/backtesting.py | 10 ++++++++-- freqtrade/strategy/interface.py | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c7d25cb7b..6e7d36368 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -267,7 +267,7 @@ class Backtesting: # Set close_rate to stoploss closerate = trade.stop_loss elif sell.sell_type == (SellType.ROI): - roi = self.strategy.min_roi_reached_entry(trade_dur) + roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) if roi is not None: # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) closerate = - (trade.open_rate * roi + trade.open_rate * @@ -275,8 +275,14 @@ class Backtesting: # Use the maximum between closerate and low as we # cannot sell outside of a candle. - # Applies when using {"xx": -1} as roi to force sells after xx minutes + # Applies when a new ROI setting comes in place and the whole candle is above that. closerate = max(closerate, sell_row.low) + if roi == -1 and roi_entry % self.timeframe_mins == 0: + # When forceselling with ROI=-1, the roi-entry will always be "on the entry". + # If that entry is a multiple of the timeframe (so on open) + # - we'll use open instead of close + closerate = max(closerate, sell_row.open) + else: # This should not be reached... closerate = sell_row.open diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e208138e7..2b3a6194f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -394,7 +394,7 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - def min_roi_reached_entry(self, trade_dur: int) -> Optional[float]: + def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: """ Based on trade duration defines the ROI entry that may have been reached. :param trade_dur: trade duration in minutes @@ -403,9 +403,9 @@ class IStrategy(ABC): # Get highest entry in ROI dict where key <= trade-duration roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) if not roi_list: - return None + return None, None roi_entry = max(roi_list) - return self.minimal_roi[roi_entry] + return roi_entry, self.minimal_roi[roi_entry] def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: """ @@ -415,7 +415,7 @@ class IStrategy(ABC): """ # Check if time matches and current rate is above threshold trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) - roi = self.min_roi_reached_entry(trade_dur) + _, roi = self.min_roi_reached_entry(trade_dur) if roi is None: return False else: From 189835b9635e54ce10c88ae02ba0ce4936c7749d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 15:26:10 +0100 Subject: [PATCH 04/61] Add documentation for ROI-1 case --- docs/backtesting.md | 5 ++++- docs/configuration.md | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 68782bb9c..017289905 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -194,7 +194,10 @@ Since backtesting lacks some detailed information about what happens within a ca - Buys happen at open-price - Sell signal sells happen at open-price of the following candle - Low happens before high for stoploss, protecting capital first. -- ROI sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) +- ROI + - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) + - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit + - Forcesells caused by `=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Stoploss sells happen exactly at stoploss price, even if low was lower - Trailing stoploss - High happens first - adjusting stoploss diff --git a/docs/configuration.md b/docs/configuration.md index 5ad1a886e..5bebbcfcd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -169,6 +169,9 @@ This parameter can be set in either Strategy or Configuration file. If you use i `minimal_roi` value from the strategy file. If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal roi is disabled unless your trade generates 1000% profit. +!!! Note "Special case to forcesell after a specific time" + A special case presents using `"": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell. + ### Understand stoploss Go to the [stoploss documentation](stoploss.md) for more details. From 45d12dbc83fd6a24f1d7b28bcf6bf5022053d454 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 15:28:56 +0100 Subject: [PATCH 05/61] Avoid a few calculations during backtesting --- freqtrade/optimize/backtesting.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6e7d36368..7b5bd34b4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -262,13 +262,22 @@ class Backtesting: return ticker def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float: + """ + Get close rate for backtesting result + """ # Special handling if high or low hit STOP_LOSS or ROI if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): # Set close_rate to stoploss - closerate = trade.stop_loss + return trade.stop_loss elif sell.sell_type == (SellType.ROI): roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) if roi is not None: + if roi == -1 and roi_entry % self.timeframe_mins == 0: + # When forceselling with ROI=-1, the roi-entry will always be "on the entry". + # If that entry is a multiple of the timeframe (so on open) + # - we'll use open instead of close + return sell_row.open + # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) closerate = - (trade.open_rate * roi + trade.open_rate * (1 + trade.fee_open)) / (trade.fee_close - 1) @@ -276,19 +285,13 @@ class Backtesting: # Use the maximum between closerate and low as we # cannot sell outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. - closerate = max(closerate, sell_row.low) - if roi == -1 and roi_entry % self.timeframe_mins == 0: - # When forceselling with ROI=-1, the roi-entry will always be "on the entry". - # If that entry is a multiple of the timeframe (so on open) - # - we'll use open instead of close - closerate = max(closerate, sell_row.open) + return max(closerate, sell_row.low) else: # This should not be reached... - closerate = sell_row.open + return sell_row.open else: - closerate = sell_row.open - return closerate + return sell_row.open def _get_sell_trade_entry( self, pair: str, buy_row: DataFrame, From de33ec425086e1aacc093b6cea0694ca549db5cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Dec 2019 16:52:12 +0100 Subject: [PATCH 06/61] use sell_row.open also when the active ROI value just changed --- freqtrade/optimize/backtesting.py | 19 ++++++---- tests/optimize/test_backtest_detail.py | 50 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7b5bd34b4..4e86acdc6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -273,19 +273,26 @@ class Backtesting: roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) if roi is not None: if roi == -1 and roi_entry % self.timeframe_mins == 0: - # When forceselling with ROI=-1, the roi-entry will always be "on the entry". - # If that entry is a multiple of the timeframe (so on open) + # When forceselling with ROI=-1, the roi time will always be equal to trade_dur. + # If that entry is a multiple of the timeframe (so on candle open) # - we'll use open instead of close return sell_row.open # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - closerate = - (trade.open_rate * roi + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) + close_rate = - (trade.open_rate * roi + trade.open_rate * + (1 + trade.fee_open)) / (trade.fee_close - 1) - # Use the maximum between closerate and low as we + if (trade_dur > 0 and trade_dur == roi_entry + and roi_entry % self.timeframe_mins == 0 + and sell_row.open > close_rate): + # new ROI entry came into effect. + # use Open rate if open_rate > calculated sell rate + return sell_row.open + + # Use the maximum between close_rate and low as we # cannot sell outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. - return max(closerate, sell_row.low) + return max(close_rate, sell_row.low) else: # This should not be reached... diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e269009b1..eff236a5e 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -281,6 +281,53 @@ tc17 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) + +# Test 18: Buy, hold for 120 mins, then drop ROI to 1%, causing a sell in candle 3. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# uses open_rate as sell-price +tc18 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5200, 6172, 0, 0], + [3, 5200, 5220, 4940, 4962, 6172, 0, 0], # Sell on ROI (sells on open) + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.04, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 19: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# uses calculated ROI (1%) as sell rate, otherwise identical to tc18 +tc19 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5200, 6172, 0, 0], + [3, 5000, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.01, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 20: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# uses calculated ROI (1%) as sell rate, otherwise identical to tc18 +tc20 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5200, 6172, 0, 0], + [3, 5200, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "119": 0.01}, profit_perc=0.01, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + + TESTS = [ tc0, tc1, @@ -300,6 +347,9 @@ TESTS = [ tc15, tc16, tc17, + tc18, + tc19, + tc20, ] From 7c7ca1cb90f6f5468cf286920462b117a3b63f99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Dec 2019 07:12:37 +0100 Subject: [PATCH 07/61] Remove min (plural) from codebase --- freqtrade/data/history.py | 6 +- freqtrade/optimize/backtesting.py | 6 +- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/optimize/hyperopt_interface.py | 6 +- tests/conftest.py | 127 ++++++++++++++++++++++- tests/optimize/test_backtest_detail.py | 2 +- tests/optimize/test_hyperopt.py | 2 +- 7 files changed, 138 insertions(+), 13 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 865893a66..ddcae89ab 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -463,7 +463,7 @@ def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow] def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, - max_date: datetime, timeframe_mins: int) -> bool: + max_date: datetime, timeframe_min: int) -> bool: """ Validates preprocessed backtesting data for missing values and shows warnings about it that. @@ -471,10 +471,10 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, :param pair: pair used for log output. :param min_date: start-date of the data :param max_date: end-date of the data - :param timeframe_mins: ticker Timeframe in minutes + :param timeframe_min: ticker Timeframe in minutes """ # total difference in minutes / timeframe-minutes - expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_mins) + expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_min) found_missing = False dflen = len(data) if dflen < expected_frames: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d9fb1f2d1..637115dfd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -87,7 +87,7 @@ class Backtesting: raise OperationalException("Ticker-interval needs to be set in either configuration " "or as cli argument `--ticker-interval 5m`") self.timeframe = str(self.config.get('ticker_interval')) - self.timeframe_mins = timeframe_to_minutes(self.timeframe) + self.timeframe_min = timeframe_to_minutes(self.timeframe) # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) @@ -378,7 +378,7 @@ class Backtesting: lock_pair_until: Dict = {} # Indexes per pair, so some pairs are allowed to have a missing start. indexes: Dict = {} - tmp = start_date + timedelta(minutes=self.timeframe_mins) + tmp = start_date + timedelta(minutes=self.timeframe_min) # Loop timerange and get candle for each pair at that point in time while tmp < end_date: @@ -430,7 +430,7 @@ class Backtesting: lock_pair_until[pair] = end_date.datetime # Move time one configured time_interval ahead. - tmp += timedelta(minutes=self.timeframe_mins) + tmp += timedelta(minutes=self.timeframe_min) return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e52cb8faf..597fd7f22 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -427,7 +427,7 @@ class Hyperopt: f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " - f"Avg duration {results_metrics['duration']:5.1f} mins." + f"Avg duration {results_metrics['duration']:5.1f} min." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 958fb8d66..2a88462e9 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -106,7 +106,7 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - timeframe_mins = timeframe_to_minutes(IHyperOpt.ticker_interval) + timeframe_min = timeframe_to_minutes(IHyperOpt.ticker_interval) # We define here limits for the ROI space parameters automagically adapted to the # timeframe used by the bot: @@ -117,8 +117,8 @@ class IHyperOpt(ABC): # # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() # method for the 5m ticker interval. - roi_t_scale = timeframe_mins / 5 - roi_p_scale = math.log1p(timeframe_mins) / math.log1p(5) + roi_t_scale = timeframe_min / 5 + roi_p_scale = math.log1p(timeframe_min) / math.log1p(5) roi_limits = { 'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha), 'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha), diff --git a/tests/conftest.py b/tests/conftest.py index 020c29eb1..82111528e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1418,4 +1418,129 @@ def open_trade(): @pytest.fixture def hyperopt_results(): - return [{'loss': 0.4366182531161519, 'params_dict': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 mins.', 'total_profit': -0.00125625, 'current_epoch': 1, 'is_initial_point': True, 'is_best': True}, {'loss': 20.0, 'params_dict': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, 'stoploss': {'stoploss': -0.338070047333259}}, 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 mins.', 'total_profit': 6.185e-05, 'current_epoch': 2, 'is_initial_point': True, 'is_best': False}, {'loss': 14.241196856510731, 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 mins.', 'total_profit': -0.13639474, 'current_epoch': 3, 'is_initial_point': True, 'is_best': False}, {'loss': 100000, 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan mins.', 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False}, {'loss': 0.22195522184191518, 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 mins.', 'total_profit': -0.002480140000000001, 'current_epoch': 5, 'is_initial_point': True, 'is_best': True}, {'loss': 0.545315889154162, 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 mins.', 'total_profit': -0.0041773, 'current_epoch': 6, 'is_initial_point': True, 'is_best': False}, {'loss': 4.713497421432944, 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, 'stoploss': {'stoploss': -0.14613268022709905}}, 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 mins.', 'total_profit': -0.06339929, 'current_epoch': 7, 'is_initial_point': True, 'is_best': False}, {'loss': 20.0, 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 mins.', 'total_profit': 0.0, 'current_epoch': 8, 'is_initial_point': True, 'is_best': False}, {'loss': 2.4731817780991223, 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 mins.', 'total_profit': -0.044050070000000004, 'current_epoch': 9, 'is_initial_point': True, 'is_best': False}, {'loss': -0.2604606005845212, 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 mins.', 'total_profit': 0.00021629, 'current_epoch': 10, 'is_initial_point': True, 'is_best': True}, {'loss': 4.876465945994304, 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 mins.', 'total_profit': -0.07436117, 'current_epoch': 11, 'is_initial_point': True, 'is_best': False}, {'loss': 100000, 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan mins.', 'total_profit': 0, 'current_epoch': 12, 'is_initial_point': True, 'is_best': False}] # noqa: E501 + return [ + { + 'loss': 0.4366182531161519, + 'params_dict': { + 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 + 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501 + 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 + 'total_profit': -0.00125625, + 'current_epoch': 1, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 20.0, + 'params_dict': { + 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501 + 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 + 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.338070047333259}}, + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 + 'total_profit': 6.185e-05, + 'current_epoch': 2, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 14.241196856510731, + 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 + 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501 + 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 + 'total_profit': -0.13639474, + 'current_epoch': 3, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False + }, { + 'loss': 0.22195522184191518, + 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 + 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501 + 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 + 'total_profit': -0.002480140000000001, + 'current_epoch': 5, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 0.545315889154162, + 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 + 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501 + 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 + 'total_profit': -0.0041773, + 'current_epoch': 6, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 4.713497421432944, + 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 + 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501 + 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 + 'total_profit': -0.06339929, + 'current_epoch': 7, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 20.0, # noqa: E501 + 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 + 'total_profit': 0.0, + 'current_epoch': 8, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 2.4731817780991223, + 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 + 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501 + 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 + 'total_profit': -0.044050070000000004, # noqa: E501 + 'current_epoch': 9, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': -0.2604606005845212, # noqa: E501 + 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 + 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501 + 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 + 'total_profit': 0.00021629, + 'current_epoch': 10, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 4.876465945994304, # noqa: E501 + 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 + 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501 + 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 + 'total_profit': -0.07436117, + 'current_epoch': 11, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, + 'current_epoch': 12, + 'is_initial_point': True, + 'is_best': False + } + ] diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 3f6cc8c9a..ad90a86dd 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -250,7 +250,7 @@ tc15 = BTContainer(data=[ BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] ) -# Test 16: Buy, hold for 65 mins, then forcesell using roi=-1 +# Test 16: Buy, hold for 65 min, then forcesell using roi=-1 # Causes negative profit even though sell-reason is ROI. # stop-loss: 10%, ROI: 10% (should not apply), -100% after 65 minutes (limits trade duration) tc16 = BTContainer(data=[ diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 6a8daab8b..89099ff91 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -642,7 +642,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: response_expected = { 'loss': 1.9840569076926293, 'results_explanation': (' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' - '( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). Avg duration 100.0 mins.' + '( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). Avg duration 100.0 min.' ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), 'params_details': {'buy': {'adx-enabled': False, 'adx-value': 0, From 6e778ad710746c22c4ef96888b18946382e92010 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 12 Dec 2019 03:12:28 +0300 Subject: [PATCH 08/61] Seed hyperopt random_state if not passed --- freqtrade/optimize/hyperopt.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ceed704c2..b4eecce2a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -6,6 +6,7 @@ This module contains the hyperopt logic import locale import logging +import random import sys import warnings from collections import OrderedDict @@ -436,7 +437,7 @@ class Hyperopt: acq_optimizer="auto", n_initial_points=INITIAL_POINTS, acq_optimizer_kwargs={'n_jobs': cpu_count}, - random_state=self.config.get('hyperopt_random_state', None), + random_state=self.random_state, ) def fix_optimizer_models_list(self): @@ -475,7 +476,13 @@ class Hyperopt: logger.info(f"Loaded {len(trials)} previous evaluations from disk.") return trials + def _set_random_state(self, random_state: Optional[int]) -> int: + return random_state or random.randint(1, 2**32 - 1) + def start(self) -> None: + self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) + logger.info(f"Using optimizer random state: {self.random_state}") + data, timerange = self.backtesting.load_bt_data() preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) From 330b8cf8a143d684b7b8f65ca74d9e13178693a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 Dec 2019 14:08:44 +0100 Subject: [PATCH 09/61] space before unit ... --- freqtrade/plot/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 57a02dd6b..85089af9c 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -121,7 +121,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) # Create description for sell summarizing the trade desc = trades.apply(lambda row: f"{round(row['profitperc'], 3)}%, {row['sell_reason']}, " - f"{row['duration']}min", + f"{row['duration']} min", axis=1) trade_sells = go.Scatter( x=trades["close_time"], From b69f5afaaf5f0401f63e4b4a3b2fe5c353251211 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2019 06:59:10 +0100 Subject: [PATCH 10/61] Round amount to precision also for dry-runs --- freqtrade/exchange/exchange.py | 7 ++++--- tests/test_freqtradebot.py | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 30868df07..fbe7cd29a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -379,15 +379,16 @@ class Exchange: def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{randint(0, 10**6)}' + _amount = self.symbol_amount_prec(pair, amount) dry_order = { "id": order_id, 'pair': pair, 'price': rate, - 'amount': amount, - "cost": amount * rate, + 'amount': _amount, + "cost": _amount * rate, 'type': ordertype, 'side': side, - 'remaining': amount, + 'remaining': _amount, 'datetime': arrow.utcnow().isoformat(), 'status': "closed" if ordertype == "market" else "open", 'fee': None, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index efab64a6a..5e197da71 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -786,7 +786,10 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - pair = 'NOCLUE/BTC' + pair = 'BLK/BTC' + # Ensure the pair is not in the whitelist! + assert pair not in default_conf['exchange']['pair_whitelist'] + # create open trade not in whitelist Trade.session.add(Trade( pair=pair, From 3bd873f3c6e6b954711cc63d95bb2dfe70a808dc Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Fri, 13 Dec 2019 13:59:18 +0300 Subject: [PATCH 11/61] Add notes on random-state to the docs --- docs/hyperopt.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 9c9e9fdef..16bd4130b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -6,8 +6,12 @@ algorithms included in the `scikit-optimize` package to accomplish this. The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time. +In general, the search for best parameters starts with a few random combinations and then uses Bayesian search with a +ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace +that minimizes the value of the [loss function](#loss-functions). + Hyperopt requires historic data to be available, just as backtesting does. -To learn how to get data for the pairs and exchange you're interrested in, head over to the [Data Downloading](data-download.md) section of the documentation. +To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. !!! Bug Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) @@ -170,10 +174,6 @@ with different value combinations. It will then use the given historical data an buys based on the buy signals generated with the above function and based on the results it will end with telling you which paramter combination produced the best profits. -The search for best parameters starts with a few random combinations and then uses a -regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination -that minimizes the value of the [loss function](#loss-functions). - The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in your custom hyperopt file. @@ -284,6 +284,16 @@ number). You can also enable position stacking in the configuration file by explicitly setting `"position_stacking"=true`. +### Reproducible results + +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with a leading asterisk sign at the Hyperop output. + +The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. + +If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. + +If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyperoptimization results with same random state value used. + ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to create a new strategy. From 014c18ead2d7a2847a74fa1880a5ba9fac405c40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2019 20:27:06 +0100 Subject: [PATCH 12/61] Improve output from show_config when trailing_stop is active --- freqtrade/rpc/telegram.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 51736968b..2e736f11a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -587,12 +587,23 @@ class Telegram(RPC): :return: None """ val = self._rpc_show_config() + if val['trailing_stop']: + sl_info = ( + f"*Initial Stoploss:* `{val['stoploss']}`\n" + f"*Trailing stop positive:* `{val['trailing_stop_positive']}`\n" + f"*Trailing stop offset:* `{val['trailing_stop_positive_offset']}`\n" + f"*Only trail above offset:* `{val['trailing_only_offset_is_reached']}`\n" + ) + + else: + sl_info = f"*Stoploss:* `{val['stoploss']}`\n" + self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n" - f"*{'Trailing ' if val['trailing_stop'] else ''}Stoploss:* `{val['stoploss']}`\n" + f"{sl_info}" f"*Ticker Interval:* `{val['ticker_interval']}`\n" f"*Strategy:* `{val['strategy']}`'" ) From e4cc5c479fe73888a01b77fd4da310b36482ca2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2019 20:27:39 +0100 Subject: [PATCH 13/61] Test new show_config branch --- tests/rpc/test_rpc_telegram.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7c4a8f0d6..2ba1ccf4b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1177,6 +1177,16 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] + assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] + + msg_mock.reset_mock() + freqtradebot.config['trailing_stop'] = True + telegram._show_config(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] + assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] def test_send_msg_buy_notification(default_conf, mocker) -> None: From a48c0ad8680a591f48ddb582a740fa3d3acfdcb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Dec 2019 12:52:33 +0100 Subject: [PATCH 14/61] Use first pair of pairlist to get fee Use this instead of hardcoded ETH/BTC - so backtesting works with exchanges without ETH/BTC pair --- freqtrade/edge/__init__.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index afd20cf61..4bc3023a4 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -80,7 +80,7 @@ class Edge: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee() + self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) def calculate(self) -> bool: pairs = self.config['exchange']['pair_whitelist'] diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 637115dfd..7ef38331e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -65,7 +65,7 @@ class Backtesting: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee() + self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) if self.config.get('runmode') != RunMode.HYPEROPT: self.dataprovider = DataProvider(self.config, self.exchange) From 82ff878e3868c8f08666a8fa08b4b12474184718 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 14 Dec 2019 15:15:20 +0300 Subject: [PATCH 15/61] Fix typo in the docs --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 16bd4130b..f399fe816 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -286,7 +286,7 @@ You can also enable position stacking in the configuration file by explicitly se ### Reproducible results -The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with a leading asterisk sign at the Hyperop output. +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with a leading asterisk sign at the Hyperopt output. The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. From f2266ea9f42184e39597c63d3a64b3cd3b0a97f2 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 14 Dec 2019 15:17:45 +0300 Subject: [PATCH 16/61] Use shorter range for seeded random-state --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b4eecce2a..1d524acf8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -477,7 +477,7 @@ class Hyperopt: return trials def _set_random_state(self, random_state: Optional[int]) -> int: - return random_state or random.randint(1, 2**32 - 1) + return random_state or random.randint(1, 2**16 - 1) def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) From 2275a1539ee989c9b5e2b2666e805472a5c5e8bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Dec 2019 13:22:42 +0100 Subject: [PATCH 17/61] Remove default symbol from get_fee() --- freqtrade/exchange/exchange.py | 2 +- tests/edge/test_edge.py | 2 +- tests/exchange/test_exchange.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fbe7cd29a..860f59fba 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -921,7 +921,7 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, + def get_fee(self, symbol, type='', side='', amount=1, price=1, taker_or_maker='maker') -> float: try: # validate that markets are loaded before trying to get fee diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 001dc9591..bdb986d6d 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -334,7 +334,7 @@ def test_process_expectancy(mocker, edge_conf): edge_conf['edge']['min_trade_number'] = 2 freqtrade = get_patched_freqtradebot(mocker, edge_conf) - def get_fee(): + def get_fee(*args, **kwargs): return 0.001 freqtrade.exchange.get_fee = get_fee diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a21a5f3ac..82b62d5b8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1646,10 +1646,10 @@ def test_get_fee(default_conf, mocker, exchange_name): }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_fee() == 0.025 + assert exchange.get_fee('ETH/BTC') == 0.025 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_fee', 'calculate_fee') + 'get_fee', 'calculate_fee', symbol="ETH/BTC") def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): From f81c49ce6d9734852b3354964543e5793c4adb10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Dec 2019 19:53:20 +0100 Subject: [PATCH 18/61] Fix typo causing a trailing "tic" in /show_config output --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2e736f11a..a3f88003a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -605,7 +605,7 @@ class Telegram(RPC): f"*Minimum ROI:* `{val['minimal_roi']}`\n" f"{sl_info}" f"*Ticker Interval:* `{val['ticker_interval']}`\n" - f"*Strategy:* `{val['strategy']}`'" + f"*Strategy:* `{val['strategy']}`" ) def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: From 26ab1088903d4088881803ba9605b05d58f80b9c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 15 Dec 2019 01:10:09 +0300 Subject: [PATCH 19/61] Fix mypy errors in develop --- 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 4b0288881..064a2f6ba 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -272,7 +272,7 @@ class Backtesting: elif sell.sell_type == (SellType.ROI): roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) if roi is not None: - if roi == -1 and roi_entry % self.timeframe_mins == 0: + if roi == -1 and roi_entry % self.timeframe_min == 0: # When forceselling with ROI=-1, the roi time will always be equal to trade_dur. # If that entry is a multiple of the timeframe (so on candle open) # - we'll use open instead of close @@ -283,7 +283,7 @@ class Backtesting: (1 + trade.fee_open)) / (trade.fee_close - 1) if (trade_dur > 0 and trade_dur == roi_entry - and roi_entry % self.timeframe_mins == 0 + and roi_entry % self.timeframe_min == 0 and sell_row.open > close_rate): # new ROI entry came into effect. # use Open rate if open_rate > calculated sell rate From 931d24b5a8c028b879518921c0c67f79865b9dcf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:22:15 +0100 Subject: [PATCH 20/61] Have dry_run_wallet default to 1000 --- docs/configuration.md | 2 +- freqtrade/constants.py | 5 +++-- freqtrade/exchange/exchange.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5ad1a886e..927432a46 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -47,7 +47,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
***Datatype:*** *String* | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
***Datatype:*** *Boolean* -| `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
***Datatype:*** *Float* +| `dry_run_wallet` | Overrides the default amount of 1000 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
***Datatype:*** *Float* | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Float (as ratio)* diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f5e5969eb..5c7190b41 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -18,7 +18,7 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter'] -DRY_RUN_WALLET = 999.9 +DRY_RUN_WALLET = 1000 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons USERPATH_HYPEROPTS = 'hyperopts' @@ -75,7 +75,7 @@ CONF_SCHEMA = { }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, - 'dry_run_wallet': {'type': 'number'}, + 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, 'process_only_new_candles': {'type': 'boolean'}, 'minimal_roi': { 'type': 'object', @@ -275,6 +275,7 @@ CONF_SCHEMA = { 'stake_currency', 'stake_amount', 'dry_run', + 'dry_run_wallet', 'bid_strategy', 'unfilledtimeout', 'stoploss', diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fbe7cd29a..a148f9dae 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -479,7 +479,7 @@ class Exchange: @retrier def get_balance(self, currency: str) -> float: if self._config['dry_run']: - return constants.DRY_RUN_WALLET + return self._config['dry_run_wallet'] # ccxt exception is already handled by get_balances balances = self.get_balances() From 52b212db64ae6d679f76f6e9c0af382f54b13751 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:37:08 +0100 Subject: [PATCH 21/61] Fix tests after changing dry_run_wallet amount --- tests/exchange/test_exchange.py | 1 + tests/test_configuration.py | 7 ++++--- tests/test_freqtradebot.py | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a21a5f3ac..774ad8cf2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -876,6 +876,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): def test_get_balance_dry_run(default_conf, mocker): default_conf['dry_run'] = True + default_conf['dry_run_wallet'] = 999.9 exchange = get_patched_exchange(mocker, default_conf) assert exchange.get_balance(currency='BTC') == 999.9 diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 89ca74afa..292d53315 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -8,9 +8,9 @@ from pathlib import Path from unittest.mock import MagicMock import pytest -from jsonschema import Draft4Validator, ValidationError, validate +from jsonschema import ValidationError -from freqtrade import OperationalException, constants +from freqtrade import OperationalException from freqtrade.configuration import (Arguments, Configuration, check_exchange, remove_credentials, validate_config_consistency) @@ -718,7 +718,8 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: def test_validate_default_conf(default_conf) -> None: - validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) + # Validate via our validator - we allow setting defaults! + validate_config_schema(default_conf) def test_validate_tsl(default_conf): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5e197da71..a73fd6c61 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -211,6 +211,7 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) + edge_conf['dry_run_wallet'] = 999.9 freqtrade = FreqtradeBot(edge_conf) assert freqtrade._get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 @@ -1338,6 +1339,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, patch_exchange(mocker) patch_edge(mocker) edge_conf['max_open_trades'] = float('inf') + edge_conf['dry_run_wallet'] = 999.9 mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock(return_value={ From fda8f7e30599810a834e2442a484d714e2ba3463 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:38:18 +0100 Subject: [PATCH 22/61] Introuce WalletDry - supporting dry-run wallets --- freqtrade/freqtradebot.py | 13 ++++++++----- freqtrade/rpc/telegram.py | 6 ++++++ freqtrade/wallets.py | 41 ++++++++++++++++++++++++++++++--------- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0595e0d35..df9fd0b17 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -25,7 +25,7 @@ from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellType -from freqtrade.wallets import Wallets +from freqtrade.wallets import Wallets, WalletsDry logger = logging.getLogger(__name__) @@ -62,7 +62,13 @@ class FreqtradeBot: self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange - self.wallets = Wallets(self.config, self.exchange) + persistence.init(self.config.get('db_url', None), + clean_open_orders=self.config.get('dry_run', False)) + + if self.config['dry_run']: + self.wallets = WalletsDry(self.config, self.exchange) + else: + self.wallets = Wallets(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange) # Attach Dataprovider to Strategy baseclass @@ -78,9 +84,6 @@ class FreqtradeBot: self.active_pair_whitelist = self._refresh_whitelist() - persistence.init(self.config.get('db_url', None), - clean_open_orders=self.config.get('dry_run', False)) - # Set initial bot state from config initial_state = self.config.get('initial_state') self.state = State[initial_state.upper()] if initial_state else State.STOPPED diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2e736f11a..4d7857f44 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,7 +331,13 @@ class Telegram(RPC): try: result = self._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) + output = '' + if self._config['dry_run']: + output += ( + f"Simulated balances - starting capital: " + f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" + ) for currency in result['currencies']: if currency['est_stake'] > 0.0001: curr_output = "*{currency}:*\n" \ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index c674b5286..eb2603776 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -28,9 +28,6 @@ class Wallets: def get_free(self, currency) -> float: - if self._config['dry_run']: - return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET) - balance = self._wallets.get(currency) if balance and balance.free: return balance.free @@ -39,9 +36,6 @@ class Wallets: def get_used(self, currency) -> float: - if self._config['dry_run']: - return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET) - balance = self._wallets.get(currency) if balance and balance.used: return balance.used @@ -50,9 +44,6 @@ class Wallets: def get_total(self, currency) -> float: - if self._config['dry_run']: - return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET) - balance = self._wallets.get(currency) if balance and balance.total: return balance.total @@ -75,3 +66,35 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets + + +class WalletsDry(Wallets): + + def __init__(self, config: dict, exchange: Exchange) -> None: + self.start_cap = config['dry_run_wallet'] + super().__init__(config, exchange) + + def update(self) -> None: + """ Update does not do anything in dry-mode...""" + from freqtrade.persistence import Trade + closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() + print(len(closed_trades)) + tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + current_stake = self.start_cap + tot_profit + self._wallets[self._config['stake_currency']] = Wallet( + self._config['stake_currency'], + current_stake, + 0, + current_stake + ) + open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() + + for trade in open_trades: + curr = trade.pair.split('/')[0] + trade.amount + self._wallets[curr] = Wallet( + curr, + trade.amount, + 0, + trade.amount + ) From f0bbc75038134d7debb9aed56d85eb2c2702c6b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:48:35 +0100 Subject: [PATCH 23/61] Combine dry_run wallet into original Wallets class --- freqtrade/freqtradebot.py | 8 ++--- freqtrade/rpc/telegram.py | 2 +- freqtrade/wallets.py | 64 +++++++++++++++++++++------------------ 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index df9fd0b17..a242c11ae 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -25,7 +25,7 @@ from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellType -from freqtrade.wallets import Wallets, WalletsDry +from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) @@ -65,10 +65,8 @@ class FreqtradeBot: persistence.init(self.config.get('db_url', None), clean_open_orders=self.config.get('dry_run', False)) - if self.config['dry_run']: - self.wallets = WalletsDry(self.config, self.exchange) - else: - self.wallets = Wallets(self.config, self.exchange) + self.wallets = Wallets(self.config, self.exchange) + self.dataprovider = DataProvider(self.config, self.exchange) # Attach Dataprovider to Strategy baseclass diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4d7857f44..e36b46ba7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -335,7 +335,7 @@ class Telegram(RPC): output = '' if self._config['dry_run']: output += ( - f"Simulated balances - starting capital: " + f"*Warning:*Simulated balances in Dry Mode.\nStarting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" ) for currency in result['currencies']: diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index eb2603776..f8dd0ee2f 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -4,7 +4,7 @@ import logging from typing import Dict, NamedTuple, Any from freqtrade.exchange import Exchange -from freqtrade import constants +from freqtrade.persistence import Trade logger = logging.getLogger(__name__) @@ -23,6 +23,7 @@ class Wallets: self._config = config self._exchange = exchange self._wallets: Dict[str, Wallet] = {} + self.start_cap = config['dry_run_wallet'] self.update() @@ -50,36 +51,12 @@ class Wallets: else: return 0 - def update(self) -> None: - - balances = self._exchange.get_balances() - - for currency in balances: - self._wallets[currency] = Wallet( - currency, - balances[currency].get('free', None), - balances[currency].get('used', None), - balances[currency].get('total', None) - ) - - logger.info('Wallets synced.') - - def get_all_balances(self) -> Dict[str, Any]: - return self._wallets - - -class WalletsDry(Wallets): - - def __init__(self, config: dict, exchange: Exchange) -> None: - self.start_cap = config['dry_run_wallet'] - super().__init__(config, exchange) - - def update(self) -> None: - """ Update does not do anything in dry-mode...""" - from freqtrade.persistence import Trade + def _update_dry(self) -> None: + """ Update from database in dry-run mode""" closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() - print(len(closed_trades)) + tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + current_stake = self.start_cap + tot_profit self._wallets[self._config['stake_currency']] = Wallet( self._config['stake_currency'], @@ -98,3 +75,32 @@ class WalletsDry(Wallets): 0, trade.amount ) + + def _update_live(self) -> None: + + balances = self._exchange.get_balances() + + for currency in balances: + self._wallets[currency] = Wallet( + currency, + balances[currency].get('free', None), + balances[currency].get('used', None), + balances[currency].get('total', None) + ) + + def update(self) -> None: + if self._config['dry_run']: + self._update_dry() + else: + self._update_live() + logger.info('Wallets synced.') + + def get_all_balances(self) -> Dict[str, Any]: + return self._wallets + + +class WalletsDry(Wallets): + + def __init__(self, config: dict, exchange: Exchange) -> None: + + super().__init__(config, exchange) From 4463d58470aff647fe3769e52c2c90c12588ca98 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:49:56 +0100 Subject: [PATCH 24/61] Add release section about collapsible section --- docs/developer.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index d731f1768..fe37c140e 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -246,6 +246,17 @@ Determine if crucial bugfixes have been made between this commit and the current git log --oneline --no-decorate --no-merges master..new_release ``` +To keep the release-log short, best wrap the full git changelog into a collapsible details secction. + +```markdown +
+Expand full changelog + +... Full git changelog + +
+``` + ### Create github release / tag Once the PR against master is merged (best right after merging): From 5a5741878cecbadae067c8ee0c29b211161a3aeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:26:56 +0100 Subject: [PATCH 25/61] Improve dry-run calculations --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/wallets.py | 7 +++---- tests/test_freqtradebot.py | 30 ++++++++++++++++++++++++++++++ tests/test_integration.py | 1 + 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a242c11ae..5c3ef64b1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -232,8 +232,8 @@ class FreqtradeBot: # Check if stake_amount is fulfilled if available_amount < stake_amount: raise DependencyException( - f"Available balance({available_amount} {self.config['stake_currency']}) is " - f"lower than stake amount({stake_amount} {self.config['stake_currency']})" + f"Available balance ({available_amount} {self.config['stake_currency']}) is " + f"lower than stake amount ({stake_amount} {self.config['stake_currency']})" ) return stake_amount diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f8dd0ee2f..9ee305aab 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -54,21 +54,20 @@ class Wallets: def _update_dry(self) -> None: """ Update from database in dry-run mode""" closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() - + open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + tot_in_trades = sum([trade.stake_amount for trade in open_trades]) - current_stake = self.start_cap + tot_profit + current_stake = self.start_cap + tot_profit - tot_in_trades self._wallets[self._config['stake_currency']] = Wallet( self._config['stake_currency'], current_stake, 0, current_stake ) - open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() for trade in open_trades: curr = trade.pair.split('/')[0] - trade.amount self._wallets[curr] = Wallet( curr, trade.amount, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a73fd6c61..a60ea8c7c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3485,3 +3485,33 @@ def test_process_i_am_alive(default_conf, mocker, caplog): ftbot.process() assert log_has_re(message, caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order): + default_conf['dry_run'] = True + # Initialize to 2 times stake amount + default_conf['dry_run_wallet'] = 0.002 + default_conf['max_open_trades'] = 2 + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + + bot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(bot) + assert bot.wallets.get_free('BTC') == 0.002 + + bot.create_trades() + trades = Trade.query.all() + assert len(trades) == 2 + + bot.config['max_open_trades'] = 3 + with pytest.raises( + DependencyException, + match=r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)"): + bot.create_trades() + diff --git a/tests/test_integration.py b/tests/test_integration.py index 228ed8468..728e96d55 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -71,6 +71,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) + mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1)) freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True From c741b67c3c1be52872ec12273504af0a68c1975f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:39:52 +0100 Subject: [PATCH 26/61] Adjust tests for dry_run wallet simulation --- freqtrade/exchange/exchange.py | 2 +- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 1 + tests/rpc/test_rpc_telegram.py | 6 ++++-- tests/test_freqtradebot.py | 1 - tests/test_wallets.py | 3 ++- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a148f9dae..2401c59b6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -18,7 +18,7 @@ from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError, constants) + OperationalException, TemporaryError) from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 699f2d962..0a8c1cabd 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -398,7 +398,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): get_valid_pair_combination=MagicMock( side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}") ) - + default_conf['dry_run'] = False freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 555fcdc81..ebb70bdf8 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -230,6 +230,7 @@ def test_api_stopbuy(botclient): def test_api_balance(botclient, mocker, rpc_balance): ftbot, client = botclient + ftbot.config['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2ba1ccf4b..b02f11394 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -462,7 +462,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: - + default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', @@ -494,6 +494,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick def test_balance_handle_empty_response(default_conf, update, mocker) -> None: + default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) msg_mock = MagicMock() @@ -533,7 +534,8 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert "Running in Dry Run, balances are not available." in result + assert "*Warning:*Simulated balances in Dry Mode." in result + assert "Starting capital: `1000` BTC" in result def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a60ea8c7c..18f5a461a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3514,4 +3514,3 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order) DependencyException, match=r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)"): bot.create_trades() - diff --git a/tests/test_wallets.py b/tests/test_wallets.py index ae2810a2d..3177edc05 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1,7 +1,8 @@ # pragma pylint: disable=missing-docstring -from tests.conftest import get_patched_freqtradebot from unittest.mock import MagicMock +from tests.conftest import get_patched_freqtradebot + def test_sync_wallet_at_boot(mocker, default_conf): default_conf['dry_run'] = False From 23d467eb0d827f6abd6641b786fe1e2a72da2e9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:41:57 +0100 Subject: [PATCH 27/61] Show simulation note also in restserver --- freqtrade/rpc/rpc.py | 1 + freqtrade/rpc/telegram.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4cebe646e..84b72fe18 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -348,6 +348,7 @@ class RPC: 'total': total, 'symbol': symbol, 'value': value, + 'note': 'Simulated balances' if self._freqtrade.config.get('dry_run', False) else '' } def _rpc_start(self) -> Dict[str, str]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e36b46ba7..c1572bb39 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -335,7 +335,9 @@ class Telegram(RPC): output = '' if self._config['dry_run']: output += ( - f"*Warning:*Simulated balances in Dry Mode.\nStarting capital: " + f"*Warning:*Simulated balances in Dry Mode.\n" + "This mode is still experimental!\n" + "Starting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" ) for currency in result['currencies']: From 56e13c8919319997e99835ba645c7c3c175779ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:55:15 +0100 Subject: [PATCH 28/61] Enhance documentation for dry-run wallet --- docs/configuration.md | 10 ++++++---- freqtrade/wallets.py | 7 ------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 927432a46..f0724abc4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -47,7 +47,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
***Datatype:*** *String* | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
***Datatype:*** *Boolean* -| `dry_run_wallet` | Overrides the default amount of 1000 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
***Datatype:*** *Float* +| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.
*Defaults to `1000`.*
***Datatype:*** *Float* | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Float (as ratio)* @@ -498,8 +498,10 @@ creating trades on the exchange. } ``` -Once you will be happy with your bot performance running in the Dry-run mode, -you can switch it to production mode. +Once you will be happy with your bot performance running in the Dry-run mode, you can switch it to production mode. + +!!! Note + A simulated wallet is available during dry-run mode, and will assume a starting capital of `dry_run_wallet` (defaults to 1000). ## Switch to production mode @@ -529,7 +531,7 @@ you run it in production mode. ``` !!! Note - If you have an exchange API key yet, [see our tutorial](/pre-requisite). + If you have an exchange API key yet, [see our tutorial](installation.md#setup-your-exchange-account). You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange. diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 9ee305aab..4c3a0f657 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -96,10 +96,3 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets - - -class WalletsDry(Wallets): - - def __init__(self, config: dict, exchange: Exchange) -> None: - - super().__init__(config, exchange) From b5b6458f128a13920e5b0d7bf3f1fe1b084ead22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:57:27 +0100 Subject: [PATCH 29/61] Add note about unlimited stake amount --- docs/configuration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index f0724abc4..9490927df 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -149,6 +149,9 @@ In this case a trade amount is calculated as: currency_balance / (max_open_trades - current_open_trades) ``` +!!! Note "When using Dry-Run Mode" + When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value, otherwise it may simulate trades with 100 BTC (or more) at once - which may not correspond to your real available balance. + ### Understand minimal_roi The `minimal_roi` configuration parameter is a JSON object where the key is a duration From ce845ab092e38448cf5f04443d36c004d4feb2c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 11:03:40 +0100 Subject: [PATCH 30/61] Improve docstring for dry-run wallet method --- freqtrade/wallets.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 4c3a0f657..dd706438f 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -52,7 +52,12 @@ class Wallets: return 0 def _update_dry(self) -> None: - """ Update from database in dry-run mode""" + """ + Update from database in dry-run mode + - Apply apply profits of closed trades on top of stake amount + - Subtract currently tied up stake_amount in open trades + - update balances for currencies currently in trades + """ closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() tot_profit = sum([trade.calc_profit() for trade in closed_trades]) From 655672c9575132ef5d143afd4ee38679e9d8b352 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Dec 2019 06:22:54 +0100 Subject: [PATCH 31/61] Enhance documentation Note --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9490927df..2c8f7cea7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -150,7 +150,7 @@ currency_balance / (max_open_trades - current_open_trades) ``` !!! Note "When using Dry-Run Mode" - When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value, otherwise it may simulate trades with 100 BTC (or more) at once - which may not correspond to your real available balance. + When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. ### Understand minimal_roi From e398c37526e3817ea3d452db15098240c7f70e80 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:29:42 +0000 Subject: [PATCH 32/61] Bump pytest from 5.3.1 to 5.3.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.3.1 to 5.3.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.3.1...5.3.2) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f073ece6e..fe5b4e369 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.1.0 mypy==0.750 -pytest==5.3.1 +pytest==5.3.2 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-mock==1.13.0 From 33db37a915ab2f2194483256194963f2021ec56d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:30:04 +0000 Subject: [PATCH 33/61] Bump joblib from 0.14.0 to 0.14.1 Bumps [joblib](https://github.com/joblib/joblib) from 0.14.0 to 0.14.1. - [Release notes](https://github.com/joblib/joblib/releases) - [Changelog](https://github.com/joblib/joblib/blob/master/CHANGES.rst) - [Commits](https://github.com/joblib/joblib/compare/0.14.0...0.14.1) Signed-off-by: dependabot-preview[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 2317cdf3e..b2428e37d 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,4 +6,4 @@ scipy==1.3.3 scikit-learn==0.22 scikit-optimize==0.5.2 filelock==3.0.12 -joblib==0.14.0 +joblib==0.14.1 From c05af1b63c57d5eb2014a2d800a6a6e72e48d506 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:30:27 +0000 Subject: [PATCH 34/61] Bump plotly from 4.3.0 to 4.4.1 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.3.0 to 4.4.1. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.3.0...v4.4.1) Signed-off-by: dependabot-preview[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 87d5553b6..415d4b888 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.3.0 +plotly==4.4.1 From cc41cdbf2277f8fd793317ba9af690ba9f957b9e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:30:46 +0000 Subject: [PATCH 35/61] Bump mkdocs-material from 4.5.1 to 4.6.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 4.5.1 to 4.6.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/4.5.1...4.6.0) Signed-off-by: dependabot-preview[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index ae77c0b06..3e53c15e3 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==4.5.1 +mkdocs-material==4.6.0 mdx_truly_sane_lists==1.2 From 05de60a7febdfe846baa6c0f8714a18dd9ab81cf Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:31:38 +0000 Subject: [PATCH 36/61] Bump cachetools from 3.1.1 to 4.0.0 Bumps [cachetools](https://github.com/tkem/cachetools) from 3.1.1 to 4.0.0. - [Release notes](https://github.com/tkem/cachetools/releases) - [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tkem/cachetools/compare/v3.1.1...v4.0.0) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index f00425e5b..3371537b7 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -4,7 +4,7 @@ ccxt==1.20.46 SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 -cachetools==3.1.1 +cachetools==4.0.0 requests==2.22.0 urllib3==1.25.7 wrapt==1.11.2 From 75e6acd6ed54624c25479a386ccb5ce559354d5e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 09:46:17 +0000 Subject: [PATCH 37/61] Bump ccxt from 1.20.46 to 1.20.84 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.20.46 to 1.20.84. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.20.46...1.20.84) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 3371537b7..a6d9a6f5e 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.20.46 +ccxt==1.20.84 SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 From 2af9ffa7f2cf3f7bdfce4a4b582df667d5bd6cc5 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 21:43:33 +0300 Subject: [PATCH 38/61] Align refresh_backtest_ to each other --- freqtrade/data/history.py | 16 ++++++++-------- freqtrade/utils.py | 2 +- tests/data/test_history.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index ddcae89ab..03aaeb0cc 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -334,12 +334,12 @@ def download_pair_history(datadir: Path, def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str], - dl_path: Path, timerange: Optional[TimeRange] = None, + datadir: Path, timerange: Optional[TimeRange] = None, erase=False) -> List[str]: """ Refresh stored ohlcv data for backtesting and hyperopt operations. - Used by freqtrade download-data - :return: Pairs not available + Used by freqtrade download-data subcommand. + :return: List of pairs that are not available. """ pairs_not_available = [] for pair in pairs: @@ -349,14 +349,14 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes continue for timeframe in timeframes: - dl_file = pair_data_filename(dl_path, pair, timeframe) + dl_file = pair_data_filename(datadir, pair, timeframe) if erase and dl_file.exists(): logger.info( f'Deleting existing data for pair {pair}, interval {timeframe}.') dl_file.unlink() logger.info(f'Downloading pair {pair}, interval {timeframe}.') - download_pair_history(datadir=dl_path, exchange=exchange, + download_pair_history(datadir=datadir, exchange=exchange, pair=pair, timeframe=str(timeframe), timerange=timerange) return pairs_not_available @@ -407,9 +407,9 @@ def download_trades_history(datadir: Path, def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path, timerange: TimeRange, erase=False) -> List[str]: """ - Refresh stored trades data. - Used by freqtrade download-data - :return: Pairs not available + Refresh stored trades data for backtesting and hyperopt operations. + Used by freqtrade download-data subcommand. + :return: List of pairs that are not available. """ pairs_not_available = [] for pair in pairs: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 230fcf268..9e01c7ea6 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -213,7 +213,7 @@ def start_download_data(args: Dict[str, Any]) -> None: else: pairs_not_available = refresh_backtest_ohlcv_data( exchange, pairs=config["pairs"], timeframes=config["timeframes"], - dl_path=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) + datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) except KeyboardInterrupt: sys.exit("SIGINT received, aborting ...") diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 6d89ab7c5..586492703 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -580,7 +580,7 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test ex = get_patched_exchange(mocker, default_conf) timerange = TimeRange.parse_timerange("20190101-20190102") refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"], - timeframes=["1m", "5m"], dl_path=testdatadir, + timeframes=["1m", "5m"], datadir=testdatadir, timerange=timerange, erase=True ) @@ -600,7 +600,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): timerange = TimeRange.parse_timerange("20190101-20190102") unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"], timeframes=["1m", "5m"], - dl_path=testdatadir, + datadir=testdatadir, timerange=timerange, erase=False ) From 4cd45b6535d4ed613718c7c5656701a81891c1ee Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 21:57:03 +0300 Subject: [PATCH 39/61] Rename download_*_history as non-public --- freqtrade/data/history.py | 40 ++++++++++++++--------------- tests/data/test_history.py | 52 +++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 03aaeb0cc..15c5ed451 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -155,11 +155,11 @@ def load_pair_history(pair: str, # The user forced the refresh of pairs if refresh_pairs: - download_pair_history(datadir=datadir, - exchange=exchange, - pair=pair, - timeframe=timeframe, - timerange=timerange) + _download_pair_history(datadir=datadir, + exchange=exchange, + pair=pair, + timeframe=timeframe, + timerange=timerange) pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup) @@ -277,11 +277,11 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, timeframe: str, return (data, since_ms) -def download_pair_history(datadir: Path, - exchange: Optional[Exchange], - pair: str, - timeframe: str = '5m', - timerange: Optional[TimeRange] = None) -> bool: +def _download_pair_history(datadir: Path, + exchange: Optional[Exchange], + pair: str, + timeframe: str = '5m', + timerange: Optional[TimeRange] = None) -> bool: """ Download latest candles from the exchange for the pair and timeframe passed in parameters The data is downloaded starting from the last correct data that @@ -356,16 +356,16 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes dl_file.unlink() logger.info(f'Downloading pair {pair}, interval {timeframe}.') - download_pair_history(datadir=datadir, exchange=exchange, - pair=pair, timeframe=str(timeframe), - timerange=timerange) + _download_pair_history(datadir=datadir, exchange=exchange, + pair=pair, timeframe=str(timeframe), + timerange=timerange) return pairs_not_available -def download_trades_history(datadir: Path, - exchange: Exchange, - pair: str, - timerange: Optional[TimeRange] = None) -> bool: +def _download_trades_history(datadir: Path, + exchange: Exchange, + pair: str, + timerange: Optional[TimeRange] = None) -> bool: """ Download trade history from the exchange. Appends to previously downloaded trades data. @@ -425,9 +425,9 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: dl_file.unlink() logger.info(f'Downloading trades for pair {pair}.') - download_trades_history(datadir=datadir, exchange=exchange, - pair=pair, - timerange=timerange) + _download_trades_history(datadir=datadir, exchange=exchange, + pair=pair, + timerange=timerange) return pairs_not_available diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 586492703..c304aa97d 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -14,9 +14,9 @@ from freqtrade import OperationalException from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.history import (_load_cached_data_for_updating, + _download_pair_history, + _download_trades_history, convert_trades_to_ohlcv, - download_pair_history, - download_trades_history, load_tickerdata_file, pair_data_filename, pair_trades_filename, refresh_backtest_ohlcv_data, @@ -267,12 +267,12 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda assert not file1_1.is_file() assert not file2_1.is_file() - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='MEME/BTC', - timeframe='1m') - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='CFI/BTC', - timeframe='1m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='MEME/BTC', + timeframe='1m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='CFI/BTC', + timeframe='1m') assert not exchange._pairs_last_refresh_time assert file1_1.is_file() assert file2_1.is_file() @@ -284,12 +284,12 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda assert not file1_5.is_file() assert not file2_5.is_file() - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='MEME/BTC', - timeframe='5m') - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='CFI/BTC', - timeframe='5m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='MEME/BTC', + timeframe='5m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='CFI/BTC', + timeframe='5m') assert not exchange._pairs_last_refresh_time assert file1_5.is_file() assert file2_5.is_file() @@ -307,8 +307,8 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) - download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m') - download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m') + _download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m') + _download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m') assert json_dump_mock.call_count == 2 @@ -324,9 +324,9 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, _backup_file(file1_1) _backup_file(file1_5) - assert not download_pair_history(datadir=testdatadir, exchange=exchange, - pair='MEME/BTC', - timeframe='1m') + assert not _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='MEME/BTC', + timeframe='1m') # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) @@ -570,7 +570,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, testdatadir): - dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + dl_mock = mocker.patch('freqtrade.data.history._download_pair_history', MagicMock()) mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) @@ -591,7 +591,7 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): - dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + dl_mock = mocker.patch('freqtrade.data.history._download_pair_history', MagicMock()) ex = get_patched_exchange(mocker, default_conf) mocker.patch( @@ -611,7 +611,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir): - dl_mock = mocker.patch('freqtrade.data.history.download_trades_history', MagicMock()) + dl_mock = mocker.patch('freqtrade.data.history._download_trades_history', MagicMock()) mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) @@ -646,8 +646,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad assert not file1.is_file() - assert download_trades_history(datadir=testdatadir, exchange=exchange, - pair='ETH/BTC') + assert _download_trades_history(datadir=testdatadir, exchange=exchange, + pair='ETH/BTC') assert log_has("New Amount of trades: 5", caplog) assert file1.is_file() @@ -657,8 +657,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', MagicMock(side_effect=ValueError)) - assert not download_trades_history(datadir=testdatadir, exchange=exchange, - pair='ETH/BTC') + assert not _download_trades_history(datadir=testdatadir, exchange=exchange, + pair='ETH/BTC') assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog) From fa968996ed980df63aa139180af0ec5799b74b26 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 22:01:26 +0300 Subject: [PATCH 40/61] Remove useless check --- freqtrade/data/history.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 15c5ed451..5176a9b19 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -295,11 +295,6 @@ def _download_pair_history(datadir: Path, :param timerange: range of time to download :return: bool with success state """ - if not exchange: - raise OperationalException( - "Exchange needs to be initialized when downloading pair history data" - ) - try: logger.info( f'Download history data for pair: "{pair}", timeframe: {timeframe} ' From a6fc743d854e33ddd44fb11cea277ab1a6482f86 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 22:12:26 +0300 Subject: [PATCH 41/61] Align code in _download_*_history() --- freqtrade/data/history.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 5176a9b19..79500c512 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -307,11 +307,12 @@ def _download_pair_history(datadir: Path, logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') # Default since_ms to 30 days if nothing is given - new_data = exchange.get_historic_ohlcv(pair=pair, timeframe=timeframe, - since_ms=since_ms if since_ms - else + new_data = exchange.get_historic_ohlcv(pair=pair, + timeframe=timeframe, + since_ms=since_ms if since_ms else int(arrow.utcnow().shift( - days=-30).float_timestamp) * 1000) + days=-30).float_timestamp) * 1000 + ) data.extend(new_data) logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) @@ -376,11 +377,11 @@ def _download_trades_history(datadir: Path, logger.debug("Current Start: %s", trades[0]['datetime'] if trades else 'None') logger.debug("Current End: %s", trades[-1]['datetime'] if trades else 'None') + # Default since_ms to 30 days if nothing is given new_trades = exchange.get_historic_trades(pair=pair, since=since if since else int(arrow.utcnow().shift( days=-30).float_timestamp) * 1000, - # until=xxx, from_id=from_id, ) trades.extend(new_trades[1]) From 9cea5cd4427cbabd877860d34bb3a8cad32a2149 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Dec 2019 20:38:36 +0100 Subject: [PATCH 42/61] Add documentation about ohlcv_partial_candle --- docs/developer.md | 6 ++++-- docs/exchanges.md | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index fe37c140e..5b07aff03 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -183,17 +183,19 @@ raw = ct.fetch_ohlcv(pair, timeframe=timeframe) # convert to dataframe df1 = parse_ticker_dataframe(raw, timeframe, pair=pair, drop_incomplete=False) -print(df1["date"].tail(1)) +print(df1.tail(1)) print(datetime.utcnow()) ``` ``` output -19 2019-06-08 00:00:00+00:00 + date open high low close volume +499 2019-06-08 00:00:00+00:00 0.000007 0.000007 0.000007 0.000007 26264344.0 2019-06-09 12:30:27.873327 ``` The output will show the last entry from the Exchange as well as the current UTC date. If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above). +Another way is to run this command multiple times in a row and observe if the volume is changing (while the date remains the same). ## Updating example notebooks diff --git a/docs/exchanges.md b/docs/exchanges.md index 5bd283a69..d836b4c32 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -61,3 +61,24 @@ print(res) ```shell $ pip3 install web3 ``` + +### Send incomplete candles to the strategy + +Most exchanges return incomplete candles via their ohlcv / klines interface. +By default, Freqtrade assumes that incomplete candles are returned and removes the last candle assuming it's an incomplete candle. + +Wether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. + +If the exchange does return incomplete candles and you would like to have incomplete candles in your strategy, you can set the following parameter in the configuration file. + +``` json +{ + + "exchange": { + "_ft_has_params": {"ohlcv_partial_candle": false} + } +} +``` + +!!! Warning "Danger of repainting" + Changing this parameter makes the strategy responsible to avoid repainting and handle this accordingly. Doing this is therefore not recommended, and should only be performed by experienced users who are fully aware of the impact this setting has. From 0277cd82ea66cd019138f0f45caaeed3b0afc187 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 23:25:57 +0300 Subject: [PATCH 43/61] Make mypy happy --- freqtrade/data/history.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 79500c512..2c01f9f48 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -295,6 +295,11 @@ def _download_pair_history(datadir: Path, :param timerange: range of time to download :return: bool with success state """ + if not exchange: + raise OperationalException( + "Exchange needs to be initialized when downloading pair history data" + ) + try: logger.info( f'Download history data for pair: "{pair}", timeframe: {timeframe} ' From 707c5668a52f1956a458cf2255b26ce89aea1a7e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 06:11:44 +0100 Subject: [PATCH 44/61] Fix typo --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index d836b4c32..76fa81f4a 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -67,7 +67,7 @@ $ pip3 install web3 Most exchanges return incomplete candles via their ohlcv / klines interface. By default, Freqtrade assumes that incomplete candles are returned and removes the last candle assuming it's an incomplete candle. -Wether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. +Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. If the exchange does return incomplete candles and you would like to have incomplete candles in your strategy, you can set the following parameter in the configuration file. From 0b5354f13db399a21b06275aeb0d6edc3097ee2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 06:58:10 +0100 Subject: [PATCH 45/61] Add required arguments to Trade method --- tests/strategy/test_interface.py | 3 ++ tests/test_freqtradebot.py | 73 +++++++++++++++++++++++--------- tests/test_persistence.py | 33 ++++++++++++--- 3 files changed, 82 insertions(+), 27 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5519b1a34..605622b8f 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -125,6 +125,7 @@ def test_min_roi_reached(default_conf, fee) -> None: trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, @@ -162,6 +163,7 @@ def test_min_roi_reached2(default_conf, fee) -> None: trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, @@ -195,6 +197,7 @@ def test_min_roi_reached3(default_conf, fee) -> None: trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 18f5a461a..9dff73322 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1512,13 +1512,15 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) - trade = Trade() - # Mock session away - Trade.session = MagicMock() - trade.open_order_id = '123' - trade.open_fee = 0.001 + trade = Trade( + open_order_id=123, + fee_open=0.001, + fee_close=0.001, + open_rate=0.01, + open_date=arrow.utcnow().datetime, + amount=11, + ) # Add datetime explicitly since sqlalchemy defaults apply only once written to database - trade.open_date = arrow.utcnow().datetime freqtrade.update_trade_state(trade) # Test amount not modified by fee-logic assert not log_has_re(r'Applying fee to .*', caplog) @@ -1541,7 +1543,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert log_has_re('Found open order for.*', caplog) -def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker): +def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # get_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) @@ -1554,6 +1556,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456", is_open=True, ) @@ -1562,7 +1566,7 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ assert trade.amount == limit_buy_order['amount'] -def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, +def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, fee, limit_buy_order, mocker, caplog): trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) @@ -1577,6 +1581,8 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456", is_open=True, open_date=arrow.utcnow().datetime, @@ -2972,7 +2978,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, assert trade.sell_reason == SellType.STOP_LOSS.value -def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker): +def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) patch_RPCManager(mocker) patch_exchange(mocker) @@ -2982,6 +2988,8 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -2994,7 +3002,7 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca caplog) -def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): +def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, fee): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) patch_RPCManager(mocker) @@ -3005,6 +3013,8 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3017,7 +3027,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): caplog) -def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fee, mocker): trades_for_order[0]['fee']['currency'] = 'ETH' patch_RPCManager(mocker) @@ -3028,6 +3038,8 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3038,7 +3050,8 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo assert freqtrade.get_real_amount(trade, buy_order_fee) == amount -def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee, + fee, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004, 'currency': None} @@ -3052,6 +3065,8 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_ pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3062,7 +3077,7 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_ assert freqtrade.get_real_amount(trade, limit_buy_order) == amount -def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, fee, mocker): trades_for_order[0]['fee']['currency'] = 'BNB' trades_for_order[0]['fee']['cost'] = 0.00094518 @@ -3074,6 +3089,8 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3084,7 +3101,7 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock assert freqtrade.get_real_amount(trade, buy_order_fee) == amount -def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, mocker): +def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, fee, mocker): patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2) @@ -3093,6 +3110,8 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3106,7 +3125,8 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c caplog) -def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, caplog, mocker): +def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, fee, + caplog, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'} @@ -3119,6 +3139,8 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3132,7 +3154,7 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee caplog) -def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, fee, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004} @@ -3144,6 +3166,8 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3154,7 +3178,7 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order assert freqtrade.get_real_amount(trade, limit_buy_order) == amount -def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_fee, fee, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['amount'] = limit_buy_order['amount'] - 0.001 @@ -3167,6 +3191,8 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_ amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3177,7 +3203,7 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_ freqtrade.get_real_amount(trade, limit_buy_order) -def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, buy_order_fee, +def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, buy_order_fee, fee, mocker): # Floats should not be compared directly. limit_buy_order = deepcopy(buy_order_fee) @@ -3191,6 +3217,8 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3202,7 +3230,7 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b abs_tol=MATH_CLOSE_PREC,) -def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, fee, mocker): # Remove "Currency" from fee dict trades_for_order[0]['fee'] = {'cost': 0.008} @@ -3215,6 +3243,9 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3223,7 +3254,7 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, assert freqtrade.get_real_amount(trade, buy_order_fee) == amount -def test_get_real_amount_open_trade(default_conf, mocker): +def test_get_real_amount_open_trade(default_conf, fee, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 12345 @@ -3232,6 +3263,8 @@ def test_get_real_amount_open_trade(default_conf, mocker): amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) order = { diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 231a1d2e2..67c61172c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -136,12 +136,13 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): id=2, pair='ETH/BTC', stake_amount=0.001, + open_rate=0.01, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', ) assert trade.open_order_id is None - assert trade.open_rate is None assert trade.close_profit is None assert trade.close_date is None @@ -173,6 +174,8 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): id=1, pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.01, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -205,6 +208,8 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + open_rate=0.01, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -212,7 +217,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): trade.open_order_id = 'something' trade.update(limit_buy_order) - assert trade.calc_open_trade_price() == 0.0010024999999225068 + assert trade._calc_open_trade_price() == 0.0010024999999225068 trade.update(limit_sell_order) assert trade.calc_close_trade_price() == 0.0010646656050132426 @@ -229,6 +234,8 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + open_rate=0.1, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -244,13 +251,14 @@ def test_update_open_order(limit_buy_order): trade = Trade( pair='ETH/BTC', stake_amount=1.00, + open_rate=0.01, + amount=5, fee_open=0.1, fee_close=0.1, exchange='bittrex', ) assert trade.open_order_id is None - assert trade.open_rate is None assert trade.close_profit is None assert trade.close_date is None @@ -258,7 +266,6 @@ def test_update_open_order(limit_buy_order): trade.update(limit_buy_order) assert trade.open_order_id is None - assert trade.open_rate is None assert trade.close_profit is None assert trade.close_date is None @@ -268,6 +275,8 @@ def test_update_invalid_order(limit_buy_order): trade = Trade( pair='ETH/BTC', stake_amount=1.00, + amount=5, + open_rate=0.001, fee_open=0.1, fee_close=0.1, exchange='bittrex', @@ -282,6 +291,8 @@ def test_calc_open_trade_price(limit_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -290,10 +301,10 @@ def test_calc_open_trade_price(limit_buy_order, fee): trade.update(limit_buy_order) # Buy @ 0.00001099 # Get the open rate price with the standard fee rate - assert trade.calc_open_trade_price() == 0.0010024999999225068 - + assert trade._calc_open_trade_price() == 0.0010024999999225068 + trade.fee_open = 0.003 # Get the open rate price with a custom fee rate - assert trade.calc_open_trade_price(fee=0.003) == 0.001002999999922468 + assert trade._calc_open_trade_price() == 0.001002999999922468 @pytest.mark.usefixtures("init_persistence") @@ -301,6 +312,8 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -324,6 +337,8 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -356,6 +371,8 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -630,6 +647,7 @@ def test_adjust_stop_loss(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -681,6 +699,7 @@ def test_adjust_min_max_rates(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', From 307ade6251a7ec3a5e0a914204c9cf7da07c6866 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 07:02:02 +0100 Subject: [PATCH 46/61] Cache open_trade_price --- freqtrade/persistence.py | 23 ++++++++++++++++------- tests/test_persistence.py | 2 ++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 735c740c3..e1a3ad713 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -86,7 +86,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # Check for latest column - if not has_column(cols, 'stop_loss_pct'): + if not has_column(cols, 'open_trade_price'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -104,6 +104,8 @@ def check_migrate(engine) -> None: sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') ticker_interval = get_column_def(cols, 'ticker_interval', 'null') + open_trade_price = get_column_def(cols, 'open_trade_price', + f'amount * open_rate * (1 + {fee_open})') # Schema migration necessary engine.execute(f"alter table trades rename to {table_back_name}") @@ -121,7 +123,7 @@ def check_migrate(engine) -> None: stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, strategy, - ticker_interval + ticker_interval, open_trade_price ) select id, lower(exchange), case @@ -140,7 +142,8 @@ def check_migrate(engine) -> None: {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, - {strategy} strategy, {ticker_interval} ticker_interval + {strategy} strategy, {ticker_interval} ticker_interval, + {open_trade_price} open_trade_price from {table_back_name} """) @@ -182,6 +185,7 @@ class Trade(_DECL_BASE): fee_close = Column(Float, nullable=False, default=0.0) open_rate = Column(Float) open_rate_requested = Column(Float) + open_trade_price = Column(Float) close_rate = Column(Float) close_rate_requested = Column(Float) close_profit = Column(Float) @@ -210,6 +214,10 @@ class Trade(_DECL_BASE): strategy = Column(String, nullable=True) ticker_interval = Column(Integer, nullable=True) + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.open_trade_price = self._calc_open_trade_price() + def __repr__(self): open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed' @@ -302,6 +310,7 @@ class Trade(_DECL_BASE): # Update open rate and actual amount self.open_rate = Decimal(order['price']) self.amount = Decimal(order['amount']) + self.open_trade_price = self._calc_open_trade_price() logger.info('%s_BUY has been fulfilled for %s.', order_type.upper(), self) self.open_order_id = None elif order_type in ('market', 'limit') and order['side'] == 'sell': @@ -331,7 +340,7 @@ class Trade(_DECL_BASE): self ) - def calc_open_trade_price(self, fee: Optional[float] = None) -> float: + def _calc_open_trade_price(self) -> float: """ Calculate the open_rate including fee. :param fee: fee to use on the open rate (optional). @@ -339,7 +348,7 @@ class Trade(_DECL_BASE): :return: Price in of the open trade incl. Fees """ buy_trade = (Decimal(self.amount) * Decimal(self.open_rate)) - fees = buy_trade * Decimal(fee or self.fee_open) + fees = buy_trade * Decimal(self.fee_open) return float(buy_trade + fees) def calc_close_trade_price(self, rate: Optional[float] = None, @@ -369,7 +378,7 @@ class Trade(_DECL_BASE): If rate is not set self.close_rate will be used :return: profit in stake currency as float """ - open_trade_price = self.calc_open_trade_price() + open_trade_price = self._calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) @@ -386,7 +395,7 @@ class Trade(_DECL_BASE): :param fee: fee to use on the close rate (optional). :return: profit in percentage as float """ - open_trade_price = self.calc_open_trade_price() + open_trade_price = self._calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 67c61172c..2cb79de98 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -498,6 +498,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 + assert trade.open_trade_price == 0.26758131848350003 def test_migrate_new(mocker, default_conf, fee, caplog): @@ -580,6 +581,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration - backup available as trades_bak2", caplog) + assert trade.open_trade_price == 0.26758131848350003 def test_migrate_mid_state(mocker, default_conf, fee, caplog): From 861a7834fc32556b7d59be02c872c66f7a49efb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 07:08:36 +0100 Subject: [PATCH 47/61] Call calc_open_price() whenever necessary --- freqtrade/freqtradebot.py | 2 ++ freqtrade/persistence.py | 17 +++++++++++------ tests/test_persistence.py | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5c3ef64b1..a7ca67dcf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -555,6 +555,7 @@ class FreqtradeBot: order['amount'] = new_amount # Fee was applied, so set to 0 trade.fee_open = 0 + trade.recalc_open_trade_price() except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) @@ -850,6 +851,7 @@ class FreqtradeBot: trade.amount = new_amount # Fee was applied, so set to 0 trade.fee_open = 0 + trade.recalc_open_trade_price() except DependencyException as e: logger.warning("Could not update trade amount: %s", e) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index e1a3ad713..354f713bc 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -216,7 +216,7 @@ class Trade(_DECL_BASE): def __init__(self, **kwargs): super().__init__(**kwargs) - self.open_trade_price = self._calc_open_trade_price() + self.recalc_open_trade_price() def __repr__(self): open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed' @@ -310,7 +310,7 @@ class Trade(_DECL_BASE): # Update open rate and actual amount self.open_rate = Decimal(order['price']) self.amount = Decimal(order['amount']) - self.open_trade_price = self._calc_open_trade_price() + self.recalc_open_trade_price() logger.info('%s_BUY has been fulfilled for %s.', order_type.upper(), self) self.open_order_id = None elif order_type in ('market', 'limit') and order['side'] == 'sell': @@ -351,6 +351,13 @@ class Trade(_DECL_BASE): fees = buy_trade * Decimal(self.fee_open) return float(buy_trade + fees) + def recalc_open_trade_price(self) -> None: + """ + Recalculate open_trade_price. + Must be called whenever open_rate or fee_open is changed. + """ + self.open_trade_price - self._calc_open_trade_price() + def calc_close_trade_price(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: """ @@ -378,12 +385,11 @@ class Trade(_DECL_BASE): If rate is not set self.close_rate will be used :return: profit in stake currency as float """ - open_trade_price = self._calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit = close_trade_price - open_trade_price + profit = close_trade_price - self.open_trade_price return float(f"{profit:.8f}") def calc_profit_percent(self, rate: Optional[float] = None, @@ -395,12 +401,11 @@ class Trade(_DECL_BASE): :param fee: fee to use on the close rate (optional). :return: profit in percentage as float """ - open_trade_price = self._calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit_percent = (close_trade_price / open_trade_price) - 1 + profit_percent = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_percent:.8f}") @staticmethod diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 2cb79de98..06df73cf9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -498,7 +498,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 - assert trade.open_trade_price == 0.26758131848350003 + assert trade.open_trade_price == trade._calc_open_trade_price() def test_migrate_new(mocker, default_conf, fee, caplog): @@ -581,7 +581,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration - backup available as trades_bak2", caplog) - assert trade.open_trade_price == 0.26758131848350003 + assert trade.open_trade_price == trade._calc_open_trade_price() def test_migrate_mid_state(mocker, default_conf, fee, caplog): From 362a40db6f89be6bd0fafb77f5f51b6f55345c45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 07:09:56 +0100 Subject: [PATCH 48/61] Update docstring --- freqtrade/persistence.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 354f713bc..21e1b82ce 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -342,9 +342,7 @@ class Trade(_DECL_BASE): def _calc_open_trade_price(self) -> float: """ - Calculate the open_rate including fee. - :param fee: fee to use on the open rate (optional). - If rate is not set self.fee will be used + Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ buy_trade = (Decimal(self.amount) * Decimal(self.open_rate)) From cbd10309f514862c7bb87eab81be845b7214c134 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 07:13:08 +0100 Subject: [PATCH 49/61] Add mid-state test --- tests/test_persistence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 06df73cf9..0bde2f673 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -641,6 +641,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 + assert trade.open_trade_price == trade._calc_open_trade_price() assert log_has("trying trades_bak0", caplog) assert log_has("Running database migration - backup available as trades_bak0", caplog) From 539b5627fdd67f0e245cce67a9b4428f94d8253f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 08:31:44 +0100 Subject: [PATCH 50/61] Fix typo --- freqtrade/persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 21e1b82ce..61dee0414 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -354,7 +354,7 @@ class Trade(_DECL_BASE): Recalculate open_trade_price. Must be called whenever open_rate or fee_open is changed. """ - self.open_trade_price - self._calc_open_trade_price() + self.open_trade_price = self._calc_open_trade_price() def calc_close_trade_price(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: From a2964afd42c4ccf84ff315e140e0064efd62d6a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 08:53:30 +0100 Subject: [PATCH 51/61] Rename profit_percent to profit_ratio to be consistent --- freqtrade/data/btanalysis.py | 2 +- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/persistence.py | 21 +++++++++++---------- freqtrade/rpc/rpc.py | 8 ++++---- freqtrade/strategy/interface.py | 6 +++--- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/test_freqtradebot.py | 3 ++- tests/test_persistence.py | 12 ++++++------ 9 files changed, 32 insertions(+), 30 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 379c80060..2fc931a9b 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -108,7 +108,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: trades = pd.DataFrame([(t.pair, t.open_date.replace(tzinfo=timezone.utc), t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None, - t.calc_profit(), t.calc_profit_percent(), + t.calc_profit(), t.calc_profit_ratio(), t.open_rate, t.close_rate, t.amount, (round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2) if t.close_date else None), diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a7ca67dcf..8ae027fa2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -950,7 +950,7 @@ class FreqtradeBot: profit_trade = trade.calc_profit(rate=profit_rate) # Use cached ticker here - it was updated seconds ago. current_rate = self.get_sell_rate(trade.pair, False) - profit_percent = trade.calc_profit_percent(profit_rate) + profit_percent = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_percent > 0 else "loss" msg = { diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 064a2f6ba..fc60bd310 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -329,7 +329,7 @@ class Backtesting: closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) return BacktestResult(pair=pair, - profit_percent=trade.calc_profit_percent(rate=closerate), + profit_percent=trade.calc_profit_ratio(rate=closerate), profit_abs=trade.calc_profit(rate=closerate), open_time=buy_row.date, close_time=sell_row.date, @@ -345,7 +345,7 @@ class Backtesting: # no sell condition found - trade stil open at end of backtest period sell_row = partial_ticker[-1] bt_res = BacktestResult(pair=pair, - profit_percent=trade.calc_profit_percent(rate=sell_row.open), + profit_percent=trade.calc_profit_ratio(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open), open_time=buy_row.date, close_time=sell_row.date, diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 61dee0414..10896baaa 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -185,6 +185,7 @@ class Trade(_DECL_BASE): fee_close = Column(Float, nullable=False, default=0.0) open_rate = Column(Float) open_rate_requested = Column(Float) + # open_trade_price - calcuated via _calc_open_trade_price open_trade_price = Column(Float) close_rate = Column(Float) close_rate_requested = Column(Float) @@ -331,7 +332,7 @@ class Trade(_DECL_BASE): and marks trade as closed """ self.close_rate = Decimal(rate) - self.close_profit = self.calc_profit_percent() + self.close_profit = self.calc_profit_ratio() self.close_date = datetime.utcnow() self.is_open = False self.open_order_id = None @@ -361,9 +362,9 @@ class Trade(_DECL_BASE): """ Calculate the close_rate including fee :param fee: fee to use on the close rate (optional). - If rate is not set self.fee will be used + If rate is not set self.fee will be used :param rate: rate to compare with (optional). - If rate is not set self.close_rate will be used + If rate is not set self.close_rate will be used :return: Price in BTC of the open trade """ if rate is None and not self.close_rate: @@ -378,9 +379,9 @@ class Trade(_DECL_BASE): """ Calculate the absolute profit in stake currency between Close and Open trade :param fee: fee to use on the close rate (optional). - If rate is not set self.fee will be used + If rate is not set self.fee will be used :param rate: close rate to compare with (optional). - If rate is not set self.close_rate will be used + If rate is not set self.close_rate will be used :return: profit in stake currency as float """ close_trade_price = self.calc_close_trade_price( @@ -390,14 +391,14 @@ class Trade(_DECL_BASE): profit = close_trade_price - self.open_trade_price return float(f"{profit:.8f}") - def calc_profit_percent(self, rate: Optional[float] = None, - fee: Optional[float] = None) -> float: + def calc_profit_ratio(self, rate: Optional[float] = None, + fee: Optional[float] = None) -> float: """ - Calculates the profit in percentage (including fee). + Calculates the profit as ratio (including fee). :param rate: rate to compare with (optional). - If rate is not set self.close_rate will be used + If rate is not set self.close_rate will be used :param fee: fee to use on the close rate (optional). - :return: profit in percentage as float + :return: profit ratio as float """ close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 84b72fe18..3b4b7570a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -123,7 +123,7 @@ class RPC: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) except DependencyException: current_rate = NAN - current_profit = trade.calc_profit_percent(current_rate) + current_profit = trade.calc_profit_ratio(current_rate) fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' if trade.close_profit else None) trade_dict = trade.to_json() @@ -151,7 +151,7 @@ class RPC: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) except DependencyException: current_rate = NAN - trade_perc = (100 * trade.calc_profit_percent(current_rate)) + trade_perc = (100 * trade.calc_profit_ratio(current_rate)) trade_profit = trade.calc_profit(current_rate) profit_str = f'{trade_perc:.2f}%' if self._fiat_converter: @@ -240,7 +240,7 @@ class RPC: durations.append((trade.close_date - trade.open_date).total_seconds()) if not trade.is_open: - profit_percent = trade.calc_profit_percent() + profit_percent = trade.calc_profit_ratio() profit_closed_coin.append(trade.calc_profit()) profit_closed_perc.append(profit_percent) else: @@ -249,7 +249,7 @@ class RPC: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) except DependencyException: current_rate = NAN - profit_percent = trade.calc_profit_percent(rate=current_rate) + profit_percent = trade.calc_profit_ratio(rate=current_rate) profit_all_coin.append( trade.calc_profit(rate=trade.close_rate or current_rate) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2b3a6194f..985ff37de 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -296,7 +296,7 @@ class IStrategy(ABC): """ # Set current rate to low for backtesting sell current_rate = low or rate - current_profit = trade.calc_profit_percent(current_rate) + current_profit = trade.calc_profit_ratio(current_rate) trade.adjust_min_max_rates(high or current_rate) @@ -311,7 +311,7 @@ class IStrategy(ABC): # Set current rate to high for backtesting sell current_rate = high or rate - current_profit = trade.calc_profit_percent(current_rate) + current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): @@ -360,7 +360,7 @@ class IStrategy(ABC): sl_offset = self.trailing_stop_positive_offset # Make sure current_profit is calculated using high for backtesting. - high_profit = current_profit if not high else trade.calc_profit_percent(high) + high_profit = current_profit if not high else trade.calc_profit_ratio(high) # Don't update stoploss if trailing_only_offset_is_reached is true. if not (self.trailing_only_offset_is_reached and high_profit < sl_offset): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index ebb70bdf8..f1e3421c5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -381,7 +381,7 @@ def test_api_performance(botclient, mocker, ticker, fee): close_rate=0.265441, ) - trade.close_profit = trade.calc_profit_percent() + trade.close_profit = trade.calc_profit_ratio() Trade.session.add(trade) trade = Trade( @@ -396,7 +396,7 @@ def test_api_performance(botclient, mocker, ticker, fee): fee_open=fee.return_value, close_rate=0.391 ) - trade.close_profit = trade.calc_profit_percent() + trade.close_profit = trade.calc_profit_ratio() Trade.session.add(trade) Trade.session.flush() diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9dff73322..341bc021f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1543,7 +1543,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert log_has_re('Found open order for.*', caplog) -def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, mocker): +def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, + mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # get_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0bde2f673..25ad8b6a7 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -226,7 +226,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): assert trade.calc_profit() == 0.00006217 # Profit in percent - assert trade.calc_profit_percent() == 0.06201058 + assert trade.calc_profit_ratio() == 0.06201058 @pytest.mark.usefixtures("init_persistence") @@ -367,7 +367,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): +def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -381,17 +381,17 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): trade.update(limit_buy_order) # Buy @ 0.00001099 # Get percent of profit with a custom rate (Higher than open rate) - assert trade.calc_profit_percent(rate=0.00001234) == 0.11723875 + assert trade.calc_profit_ratio(rate=0.00001234) == 0.11723875 # Get percent of profit with a custom rate (Lower than open rate) - assert trade.calc_profit_percent(rate=0.00000123) == -0.88863828 + assert trade.calc_profit_ratio(rate=0.00000123) == -0.88863828 # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 trade.update(limit_sell_order) - assert trade.calc_profit_percent() == 0.06201058 + assert trade.calc_profit_ratio() == 0.06201058 # Test with a custom fee rate on the close trade - assert trade.calc_profit_percent(fee=0.003) == 0.06147824 + assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 @pytest.mark.usefixtures("init_persistence") From 86de88ed480bfd16ec8c875b79c3d4298b0be849 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 09:36:26 +0100 Subject: [PATCH 52/61] Align usage of history import in test --- tests/data/test_history.py | 100 +++++++++++++++---------------------- 1 file changed, 40 insertions(+), 60 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c304aa97d..708702ab2 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -12,16 +12,17 @@ from pandas import DataFrame from freqtrade import OperationalException from freqtrade.configuration import TimeRange -from freqtrade.data import history -from freqtrade.data.history import (_load_cached_data_for_updating, - _download_pair_history, +from freqtrade.data.history import (_download_pair_history, _download_trades_history, - convert_trades_to_ohlcv, + _load_cached_data_for_updating, + convert_trades_to_ohlcv, get_timeframe, + load_data, load_pair_history, load_tickerdata_file, pair_data_filename, pair_trades_filename, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, - trim_tickerlist) + trim_dataframe, trim_tickerlist, + validate_backtest_data) from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.strategy.default_strategy import DefaultStrategy @@ -64,7 +65,7 @@ def _clean_test_file(file: Path) -> None: def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None: - ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir) + ld = load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir) assert isinstance(ld, DataFrame) assert not log_has( 'Download history data for pair: "UNITTEST/BTC", timeframe: 30m ' @@ -73,7 +74,7 @@ def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> No def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None: - ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir) + ld = load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir) assert isinstance(ld, DataFrame) assert ld.empty assert log_has( @@ -86,7 +87,7 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> N mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history) file = testdatadir / 'UNITTEST_BTC-1m.json' _backup_file(file, copy_file=True) - history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) + load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) assert file.is_file() assert not log_has( 'Download history data for pair: "UNITTEST/BTC", interval: 1m ' @@ -99,10 +100,9 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> ltfmock = mocker.patch('freqtrade.data.history.load_tickerdata_file', MagicMock(return_value=None)) timerange = TimeRange('date', None, 1510639620, 0) - history.load_pair_history(pair='UNITTEST/BTC', timeframe='1m', - datadir=testdatadir, timerange=timerange, - startup_candles=20, - ) + load_pair_history(pair='UNITTEST/BTC', timeframe='1m', + datadir=testdatadir, timerange=timerange, + startup_candles=20,) assert ltfmock.call_count == 1 assert ltfmock.call_args_list[0][1]['timerange'] != timerange @@ -121,9 +121,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, _backup_file(file) # do not download a new pair if refresh_pairs isn't set - history.load_pair_history(datadir=testdatadir, - timeframe='1m', - pair='MEME/BTC') + load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') assert not file.is_file() assert log_has( 'No history data for pair: "MEME/BTC", timeframe: 1m. ' @@ -131,22 +129,16 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, ) # download a new pair if refresh_pairs is set - history.load_pair_history(datadir=testdatadir, - timeframe='1m', - refresh_pairs=True, - exchange=exchange, - pair='MEME/BTC') + load_pair_history(datadir=testdatadir, timeframe='1m', + refresh_pairs=True, exchange=exchange, pair='MEME/BTC') assert file.is_file() assert log_has_re( 'Download history data for pair: "MEME/BTC", timeframe: 1m ' 'and store in .*', caplog ) with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): - history.load_pair_history(datadir=testdatadir, - timeframe='1m', - refresh_pairs=True, - exchange=None, - pair='MEME/BTC') + load_pair_history(datadir=testdatadir, timeframe='1m', + refresh_pairs=True, exchange=None, pair='MEME/BTC') _clean_test_file(file) @@ -351,10 +343,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None: # Make sure we start fresh - test missing data at start start = arrow.get('2018-01-01T00:00:00') end = arrow.get('2018-01-11T00:00:00') - tickerdata = history.load_data(testdatadir, '5m', ['UNITTEST/BTC'], - startup_candles=20, - timerange=TimeRange('date', 'date', - start.timestamp, end.timestamp)) + tickerdata = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20, + timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) assert log_has( 'Using indicator startup period: 20 ...', caplog ) @@ -369,10 +359,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None: caplog.clear() start = arrow.get('2018-01-10T00:00:00') end = arrow.get('2018-02-20T00:00:00') - tickerdata = history.load_data(datadir=testdatadir, timeframe='5m', - pairs=['UNITTEST/BTC'], - timerange=TimeRange('date', 'date', - start.timestamp, end.timestamp)) + tickerdata = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], + timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) # timedifference in 5 minutes td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(tickerdata['UNITTEST/BTC']) @@ -385,7 +373,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: def test_init(default_conf, mocker) -> None: exchange = get_patched_exchange(mocker, default_conf) - assert {} == history.load_data( + assert {} == load_data( datadir='', exchange=exchange, pairs=[], @@ -447,7 +435,7 @@ def test_trim_tickerlist(testdatadir) -> None: def test_trim_dataframe(testdatadir) -> None: - data = history.load_data( + data = load_data( datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'] @@ -458,7 +446,7 @@ def test_trim_dataframe(testdatadir) -> None: # Remove first 30 minutes (1800 s) tr = TimeRange('date', None, min_date + 1800, 0) - data_modify = history.trim_dataframe(data_modify, tr) + data_modify = trim_dataframe(data_modify, tr) assert not data_modify.equals(data) assert len(data_modify) < len(data) assert len(data_modify) == len(data) - 30 @@ -468,7 +456,7 @@ def test_trim_dataframe(testdatadir) -> None: data_modify = data.copy() # Remove last 30 minutes (1800 s) tr = TimeRange(None, 'date', 0, max_date - 1800) - data_modify = history.trim_dataframe(data_modify, tr) + data_modify = trim_dataframe(data_modify, tr) assert not data_modify.equals(data) assert len(data_modify) < len(data) assert len(data_modify) == len(data) - 30 @@ -478,7 +466,7 @@ def test_trim_dataframe(testdatadir) -> None: data_modify = data.copy() # Remove first 25 and last 30 minutes (1800 s) tr = TimeRange('date', 'date', min_date + 1500, max_date - 1800) - data_modify = history.trim_dataframe(data_modify, tr) + data_modify = trim_dataframe(data_modify, tr) assert not data_modify.equals(data) assert len(data_modify) < len(data) assert len(data_modify) == len(data) - 55 @@ -515,13 +503,13 @@ def test_get_timeframe(default_conf, mocker, testdatadir) -> None: strategy = DefaultStrategy(default_conf) data = strategy.tickerdata_to_dataframe( - history.load_data( + load_data( datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'] ) ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = get_timeframe(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' @@ -531,17 +519,17 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) strategy = DefaultStrategy(default_conf) data = strategy.tickerdata_to_dataframe( - history.load_data( + load_data( datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], fill_up_missing=False ) ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = get_timeframe(data) caplog.clear() - assert history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', - min_date, max_date, timeframe_to_minutes('1m')) + assert validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', + min_date, max_date, timeframe_to_minutes('1m')) assert len(caplog.record_tuples) == 1 assert log_has( "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", @@ -554,7 +542,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No timerange = TimeRange('index', 'index', 200, 250) data = strategy.tickerdata_to_dataframe( - history.load_data( + load_data( datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], @@ -562,10 +550,10 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No ) ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = get_timeframe(data) caplog.clear() - assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', - min_date, max_date, timeframe_to_minutes('5m')) + assert not validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', + min_date, max_date, timeframe_to_minutes('5m')) assert len(caplog.record_tuples) == 0 @@ -668,12 +656,8 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): file1 = testdatadir / 'XRP_ETH-1m.json' file5 = testdatadir / 'XRP_ETH-5m.json' # Compare downloaded dataset with converted dataset - dfbak_1m = history.load_pair_history(datadir=testdatadir, - timeframe="1m", - pair=pair) - dfbak_5m = history.load_pair_history(datadir=testdatadir, - timeframe="5m", - pair=pair) + dfbak_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair) + dfbak_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair) _backup_file(file1, copy_file=True) _backup_file(file5) @@ -685,12 +669,8 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog) # Load new data - df_1m = history.load_pair_history(datadir=testdatadir, - timeframe="1m", - pair=pair) - df_5m = history.load_pair_history(datadir=testdatadir, - timeframe="5m", - pair=pair) + df_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair) + df_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair) assert df_1m.equals(dfbak_1m) assert df_5m.equals(dfbak_5m) From e1c0c6af7dcf94fe7dc6a982059b6f1a6ccbd2de Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 10:51:49 +0100 Subject: [PATCH 53/61] fix random-seed to failing one --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6a111944..40a6fd075 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: COVERALLS_SERVICE_NAME: travis-ci TRAVIS: "true" run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc + pytest --random-order --cov=freqtrade --cov-config=.coveragerc --random-order-seed=834267 # Allow failure for coveralls # Fake travis environment to get coveralls working correctly export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" From 2e2f084f662e02eebc6cb083deb1dbeac145e840 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 11:07:59 +0100 Subject: [PATCH 54/61] Try to clear caplog ... --- .github/workflows/ci.yml | 2 +- tests/test_utils.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40a6fd075..f6a111944 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: COVERALLS_SERVICE_NAME: travis-ci TRAVIS: "true" run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc --random-order-seed=834267 + pytest --random-order --cov=freqtrade --cov-config=.coveragerc # Allow failure for coveralls # Fake travis environment to get coveralls working correctly export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" diff --git a/tests/test_utils.py b/tests/test_utils.py index feba1ed59..40ca9ac02 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -444,6 +444,9 @@ def test_create_datadir_failed(caplog): def test_create_datadir(caplog, mocker): + # Ensure that caplog is empty before starting ... + # Should prevent random failures. + caplog.clear() cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock()) csf = mocker.patch("freqtrade.utils.copy_sample_files", MagicMock()) args = [ From 8513a5e2d68721e896e4d3337f8be162847d8c86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 11:35:39 +0100 Subject: [PATCH 55/61] Fix failures in test_main --- .github/workflows/ci.yml | 2 +- tests/test_main.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6a111944..628f169c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,7 @@ jobs: - name: Tests run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc + pytest --random-order --cov=freqtrade --cov-config=.coveragerc --random-order-seed=781055 - name: Backtesting run: | diff --git a/tests/test_main.py b/tests/test_main.py index 4e97c375d..03e6a7ce9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -79,6 +79,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt)) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -98,6 +99,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: MagicMock(side_effect=OperationalException('Oh snap!')) ) patched_configuration_load_config_file(mocker, default_conf) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -120,6 +122,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: OperationalException("Oh snap!")]) mocker.patch('freqtrade.worker.Worker._worker', worker_mock) patched_configuration_load_config_file(mocker, default_conf) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) @@ -143,6 +146,7 @@ def test_reconfigure(mocker, default_conf) -> None: 'freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!')) ) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) From 60f89c8c019f6806b28b044e642f8cdab8c60cc9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 17 Dec 2019 13:43:42 +0300 Subject: [PATCH 56/61] Split refresh from load_data/load_pair_history --- freqtrade/data/history.py | 86 ++++++++++++++++++++---------- freqtrade/edge/__init__.py | 12 ++++- tests/data/test_history.py | 28 +++++++--- tests/edge/test_edge.py | 7 ++- tests/optimize/test_backtesting.py | 4 +- 5 files changed, 97 insertions(+), 40 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 2c01f9f48..62195a005 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -128,39 +128,26 @@ def load_pair_history(pair: str, timeframe: str, datadir: Path, timerange: Optional[TimeRange] = None, - refresh_pairs: bool = False, - exchange: Optional[Exchange] = None, fill_up_missing: bool = True, drop_incomplete: bool = True, startup_candles: int = 0, ) -> DataFrame: """ - Loads cached ticker history for the given pair. + Load cached ticker history for the given pair. + :param pair: Pair to load data for :param timeframe: Ticker timeframe (e.g. "5m") :param datadir: Path to the data storage location. :param timerange: Limit data to be loaded to this timerange - :param refresh_pairs: Refresh pairs from exchange. - (Note: Requires exchange to be passed as well.) - :param exchange: Exchange object (needed when using "refresh_pairs") :param fill_up_missing: Fill missing values with "No action"-candles :param drop_incomplete: Drop last candle assuming it may be incomplete. :param startup_candles: Additional candles to load at the start of the period :return: DataFrame with ohlcv data, or empty DataFrame """ - timerange_startup = deepcopy(timerange) if startup_candles > 0 and timerange_startup: timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) - # The user forced the refresh of pairs - if refresh_pairs: - _download_pair_history(datadir=datadir, - exchange=exchange, - pair=pair, - timeframe=timeframe, - timerange=timerange) - pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup) if pairdata: @@ -177,33 +164,53 @@ def load_pair_history(pair: str, return DataFrame() +def refresh_pair_history(pair: str, + timeframe: str, + datadir: Path, + exchange: Exchange, + timerange: Optional[TimeRange] = None, + startup_candles: int = 0, + ) -> None: + """ + Refresh cached ticker history for the given pair. + + :param pair: Pair to load data for + :param timeframe: Ticker timeframe (e.g. "5m") + :param datadir: Path to the data storage location. + :param timerange: Limit data to be loaded to this timerange + :param exchange: Exchange object + :param startup_candles: Additional candles to load at the start of the period + """ + timerange_startup = deepcopy(timerange) + if startup_candles > 0 and timerange_startup: + timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) + + _download_pair_history(datadir=datadir, + exchange=exchange, + pair=pair, + timeframe=timeframe, + timerange=timerange) + + def load_data(datadir: Path, timeframe: str, pairs: List[str], - refresh_pairs: bool = False, - exchange: Optional[Exchange] = None, timerange: Optional[TimeRange] = None, fill_up_missing: bool = True, startup_candles: int = 0, fail_without_data: bool = False ) -> Dict[str, DataFrame]: """ - Loads ticker history data for a list of pairs + Load ticker history data for a list of pairs. + :param datadir: Path to the data storage location. :param timeframe: Ticker Timeframe (e.g. "5m") :param pairs: List of pairs to load - :param refresh_pairs: Refresh pairs from exchange. - (Note: Requires exchange to be passed as well.) - :param exchange: Exchange object (needed when using "refresh_pairs") :param timerange: Limit data to be loaded to this timerange :param fill_up_missing: Fill missing values with "No action"-candles :param startup_candles: Additional candles to load at the start of the period :param fail_without_data: Raise OperationalException if no data is found. :return: dict(:) - TODO: refresh_pairs is still used by edge to keep the data uptodate. - This should be replaced in the future. Instead, writing the current candles to disk - from dataprovider should be implemented, as this would avoid loading ohlcv data twice. - exchange and refresh_pairs are then not needed here nor in load_pair_history. """ result: Dict[str, DataFrame] = {} if startup_candles > 0 and timerange: @@ -212,8 +219,6 @@ def load_data(datadir: Path, for pair in pairs: hist = load_pair_history(pair=pair, timeframe=timeframe, datadir=datadir, timerange=timerange, - refresh_pairs=refresh_pairs, - exchange=exchange, fill_up_missing=fill_up_missing, startup_candles=startup_candles) if not hist.empty: @@ -224,6 +229,33 @@ def load_data(datadir: Path, return result +def refresh_data(datadir: Path, + timeframe: str, + pairs: List[str], + exchange: Exchange, + timerange: Optional[TimeRange] = None, + startup_candles: int = 0, + ) -> None: + """ + Refresh ticker history data for a list of pairs. + + :param datadir: Path to the data storage location. + :param timeframe: Ticker Timeframe (e.g. "5m") + :param pairs: List of pairs to load + :param exchange: Exchange object + :param timerange: Limit data to be loaded to this timerange + :param startup_candles: Additional candles to load at the start of the period + """ + if startup_candles > 0 and timerange: + logger.info(f'Using indicator startup period: {startup_candles} ...') + + for pair in pairs: + refresh_pair_history(pair=pair, timeframe=timeframe, + datadir=datadir, timerange=timerange, + exchange=exchange, + startup_candles=startup_candles) + + def pair_data_filename(datadir: Path, pair: str, timeframe: str) -> Path: pair_s = pair.replace("/", "_") filename = datadir.joinpath(f'{pair_s}-{timeframe}.json') diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 4bc3023a4..b52b2ffdc 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -94,12 +94,20 @@ class Edge: logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using local backtesting data (using whitelist in given config) ...') + if self._refresh_pairs: + history.refresh_data( + datadir=Path(self.config['datadir']), + pairs=pairs, + exchange=self.exchange, + timeframe=self.strategy.ticker_interval, + timerange=self._timerange, + startup_candles=self.strategy.startup_candle_count, + ) + data = history.load_data( datadir=Path(self.config['datadir']), pairs=pairs, timeframe=self.strategy.ticker_interval, - refresh_pairs=self._refresh_pairs, - exchange=self.exchange, timerange=self._timerange, startup_candles=self.strategy.startup_candle_count, ) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 708702ab2..1b7c2c4a4 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -21,6 +21,7 @@ from freqtrade.data.history import (_download_pair_history, pair_trades_filename, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, + refresh_data, refresh_pair_history, trim_dataframe, trim_tickerlist, validate_backtest_data) from freqtrade.exchange import timeframe_to_minutes @@ -129,16 +130,17 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, ) # download a new pair if refresh_pairs is set - load_pair_history(datadir=testdatadir, timeframe='1m', - refresh_pairs=True, exchange=exchange, pair='MEME/BTC') + refresh_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC', + exchange=exchange) + load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') assert file.is_file() assert log_has_re( 'Download history data for pair: "MEME/BTC", timeframe: 1m ' 'and store in .*', caplog ) with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): - load_pair_history(datadir=testdatadir, timeframe='1m', - refresh_pairs=True, exchange=None, pair='MEME/BTC') + refresh_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC', + exchange=None) _clean_test_file(file) @@ -372,12 +374,24 @@ def test_load_partial_missing(testdatadir, caplog) -> None: def test_init(default_conf, mocker) -> None: - exchange = get_patched_exchange(mocker, default_conf) assert {} == load_data( datadir='', - exchange=exchange, pairs=[], - refresh_pairs=True, + timeframe=default_conf['ticker_interval'] + ) + + +def test_init_with_refresh(default_conf, mocker) -> None: + exchange = get_patched_exchange(mocker, default_conf) + refresh_data( + datadir='', + pairs=[], + timeframe=default_conf['ticker_interval'], + exchange=exchange + ) + assert {} == load_data( + datadir='', + pairs=[], timeframe=default_conf['ticker_interval'] ) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index bdb986d6d..3a866c0a8 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -255,8 +255,8 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): assert edge.calculate() is False -def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, - timerange=None, exchange=None, *args, **kwargs): +def mocked_load_data(datadir, pairs=[], timeframe='0m', + timerange=None, *args, **kwargs): hz = 0.1 base = 0.001 @@ -290,6 +290,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, def test_edge_process_downloaded_data(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -301,6 +302,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf): def test_edge_process_no_data(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -313,6 +315,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): def test_edge_process_no_trades(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) # Return empty mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 5086891a6..38a95be7a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -116,8 +116,8 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: assert len(results) == num_results -def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, - timerange=None, exchange=None, live=False, *args, **kwargs): +def mocked_load_data(datadir, pairs=[], timeframe='0m', + timerange=None, *args, **kwargs): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC", fill_missing=True)} From bbb05b52862dbd04d169a6df010683207545b92b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 11:51:50 +0100 Subject: [PATCH 57/61] Remove fixed random order --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 628f169c2..f6a111944 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,7 @@ jobs: - name: Tests run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc --random-order-seed=781055 + pytest --random-order --cov=freqtrade --cov-config=.coveragerc - name: Backtesting run: | From b2796f99b6c9be25af7a86cf67a2740641879cdd Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 17 Dec 2019 14:06:21 +0300 Subject: [PATCH 58/61] Remove redundant refresh_pair_history --- freqtrade/data/history.py | 35 +++-------------------------------- tests/data/test_history.py | 10 +++++----- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 62195a005..3b16e41a9 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -164,34 +164,6 @@ def load_pair_history(pair: str, return DataFrame() -def refresh_pair_history(pair: str, - timeframe: str, - datadir: Path, - exchange: Exchange, - timerange: Optional[TimeRange] = None, - startup_candles: int = 0, - ) -> None: - """ - Refresh cached ticker history for the given pair. - - :param pair: Pair to load data for - :param timeframe: Ticker timeframe (e.g. "5m") - :param datadir: Path to the data storage location. - :param timerange: Limit data to be loaded to this timerange - :param exchange: Exchange object - :param startup_candles: Additional candles to load at the start of the period - """ - timerange_startup = deepcopy(timerange) - if startup_candles > 0 and timerange_startup: - timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) - - _download_pair_history(datadir=datadir, - exchange=exchange, - pair=pair, - timeframe=timeframe, - timerange=timerange) - - def load_data(datadir: Path, timeframe: str, pairs: List[str], @@ -250,10 +222,9 @@ def refresh_data(datadir: Path, logger.info(f'Using indicator startup period: {startup_candles} ...') for pair in pairs: - refresh_pair_history(pair=pair, timeframe=timeframe, - datadir=datadir, timerange=timerange, - exchange=exchange, - startup_candles=startup_candles) + _download_pair_history(pair=pair, timeframe=timeframe, + datadir=datadir, timerange=timerange, + exchange=exchange) def pair_data_filename(datadir: Path, pair: str, timeframe: str) -> Path: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 1b7c2c4a4..c9b198b39 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -21,7 +21,7 @@ from freqtrade.data.history import (_download_pair_history, pair_trades_filename, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, - refresh_data, refresh_pair_history, + refresh_data, trim_dataframe, trim_tickerlist, validate_backtest_data) from freqtrade.exchange import timeframe_to_minutes @@ -130,8 +130,8 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, ) # download a new pair if refresh_pairs is set - refresh_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC', - exchange=exchange) + refresh_data(datadir=testdatadir, timeframe='1m', pairs=['MEME/BTC'], + exchange=exchange) load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') assert file.is_file() assert log_has_re( @@ -139,8 +139,8 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, 'and store in .*', caplog ) with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): - refresh_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC', - exchange=None) + refresh_data(datadir=testdatadir, timeframe='1m', pairs=['MEME/BTC'], + exchange=None) _clean_test_file(file) From 153738961773fbe307aa557fa2ea257c9fe6ad10 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 17 Dec 2019 18:23:31 +0300 Subject: [PATCH 59/61] Remove startup_candles argument in refresh_data --- freqtrade/data/history.py | 5 ----- freqtrade/edge/__init__.py | 1 - 2 files changed, 6 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 3b16e41a9..64f530b53 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -206,7 +206,6 @@ def refresh_data(datadir: Path, pairs: List[str], exchange: Exchange, timerange: Optional[TimeRange] = None, - startup_candles: int = 0, ) -> None: """ Refresh ticker history data for a list of pairs. @@ -216,11 +215,7 @@ def refresh_data(datadir: Path, :param pairs: List of pairs to load :param exchange: Exchange object :param timerange: Limit data to be loaded to this timerange - :param startup_candles: Additional candles to load at the start of the period """ - if startup_candles > 0 and timerange: - logger.info(f'Using indicator startup period: {startup_candles} ...') - for pair in pairs: _download_pair_history(pair=pair, timeframe=timeframe, datadir=datadir, timerange=timerange, diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index b52b2ffdc..85029905b 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -101,7 +101,6 @@ class Edge: exchange=self.exchange, timeframe=self.strategy.ticker_interval, timerange=self._timerange, - startup_candles=self.strategy.startup_candle_count, ) data = history.load_data( From c5e6a34f25081498908ef0f44fc7366cf59374f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 19:30:04 +0100 Subject: [PATCH 60/61] Remove unnecessary parenteses --- freqtrade/persistence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 10896baaa..993b68bc7 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -346,7 +346,7 @@ class Trade(_DECL_BASE): Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ - buy_trade = (Decimal(self.amount) * Decimal(self.open_rate)) + buy_trade = Decimal(self.amount) * Decimal(self.open_rate) fees = buy_trade * Decimal(self.fee_open) return float(buy_trade + fees) @@ -370,7 +370,7 @@ class Trade(_DECL_BASE): if rate is None and not self.close_rate: return 0.0 - sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate)) + sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) fees = sell_trade * Decimal(fee or self.fee_close) return float(sell_trade - fees) From cf4c3642ce1ea09c758b16a8774cabcea0674ec0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 18 Dec 2019 01:06:03 +0300 Subject: [PATCH 61/61] Minor improvements in data.history --- freqtrade/data/history.py | 20 ++++++++------------ freqtrade/edge/__init__.py | 2 +- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/optimize/hyperopt.py | 6 +++--- tests/data/test_converter.py | 4 ++-- tests/data/test_history.py | 15 +++++---------- tests/optimize/test_backtest_detail.py | 4 ++-- tests/optimize/test_backtesting.py | 20 ++++++++++---------- tests/optimize/test_hyperopt.py | 22 +++++++++++----------- 9 files changed, 44 insertions(+), 53 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 64f530b53..4c5c0521f 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -68,7 +68,7 @@ def trim_dataframe(df: DataFrame, timerange: TimeRange, df_date_col: str = 'date def load_tickerdata_file(datadir: Path, pair: str, timeframe: str, - timerange: Optional[TimeRange] = None) -> Optional[list]: + timerange: Optional[TimeRange] = None) -> List[Dict]: """ Load a pair from file, either .json.gz or .json :return: tickerlist or None if unsuccessful @@ -276,7 +276,7 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, timeframe: str, def _download_pair_history(datadir: Path, - exchange: Optional[Exchange], + exchange: Exchange, pair: str, timeframe: str = '5m', timerange: Optional[TimeRange] = None) -> bool: @@ -293,11 +293,6 @@ def _download_pair_history(datadir: Path, :param timerange: range of time to download :return: bool with success state """ - if not exchange: - raise OperationalException( - "Exchange needs to be initialized when downloading pair history data" - ) - try: logger.info( f'Download history data for pair: "{pair}", timeframe: {timeframe} ' @@ -447,18 +442,19 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str], store_tickerdata_file(datadir, pair, timeframe, data=ohlcv) -def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: +def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: """ - Get the maximum timeframe for the given backtest data + Get the maximum common timerange for the given backtest data. + :param data: dictionary with preprocessed backtesting data :return: tuple containing min_date, max_date """ - timeframe = [ + timeranges = [ (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] + return (min(timeranges, key=operator.itemgetter(0))[0], + max(timeranges, key=operator.itemgetter(1))[1]) def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 85029905b..e56071a98 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -120,7 +120,7 @@ class Edge: preprocessed = self.strategy.tickerdata_to_dataframe(data) # Print timeframe - min_date, max_date = history.get_timeframe(preprocessed) + min_date, max_date = history.get_timerange(preprocessed) logger.info( 'Measuring data from %s up to %s (%s days) ...', min_date.isoformat(), diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fc60bd310..726257cdd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -117,7 +117,7 @@ class Backtesting: fail_without_data=True, ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = history.get_timerange(data) logger.info( 'Loading data from %s up to %s (%s days)..', @@ -481,7 +481,7 @@ class Backtesting: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = history.trim_dataframe(df, timerange) - min_date, max_date = history.get_timeframe(preprocessed) + min_date, max_date = history.get_timerange(preprocessed) logger.info( 'Backtesting with data from %s up to %s (%s days)..', diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cdde2b722..521a4d790 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,7 @@ from joblib import (Parallel, cpu_count, delayed, dump, load, from pandas import DataFrame from freqtrade import OperationalException -from freqtrade.data.history import get_timeframe, trim_dataframe +from freqtrade.data.history import get_timerange, trim_dataframe from freqtrade.misc import plural, round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules @@ -369,7 +369,7 @@ class Hyperopt: processed = load(self.tickerdata_pickle) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) backtesting_results = self.backtesting.backtest( { @@ -490,7 +490,7 @@ class Hyperopt: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange) - min_date, max_date = get_timeframe(data) + min_date, max_date = get_timerange(data) logger.info( 'Hyperopting with data from %s up to %s (%s days)..', diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 8184167b3..414551c95 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -2,7 +2,7 @@ import logging from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data -from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe +from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timerange from tests.conftest import log_has @@ -36,7 +36,7 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog): f"{len(data)} - after: {len(data2)}", caplog) # Test fillup actually fixes invalid backtest data - min_date, max_date = get_timeframe({'UNITTEST/BTC': data}) + min_date, max_date = get_timerange({'UNITTEST/BTC': data}) assert validate_backtest_data(data, 'UNITTEST/BTC', min_date, max_date, 1) assert not validate_backtest_data(data2, 'UNITTEST/BTC', min_date, max_date, 1) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c9b198b39..7b3143db9 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -7,15 +7,13 @@ from shutil import copyfile from unittest.mock import MagicMock, PropertyMock import arrow -import pytest from pandas import DataFrame -from freqtrade import OperationalException from freqtrade.configuration import TimeRange from freqtrade.data.history import (_download_pair_history, _download_trades_history, _load_cached_data_for_updating, - convert_trades_to_ohlcv, get_timeframe, + convert_trades_to_ohlcv, get_timerange, load_data, load_pair_history, load_tickerdata_file, pair_data_filename, pair_trades_filename, @@ -138,9 +136,6 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, 'Download history data for pair: "MEME/BTC", timeframe: 1m ' 'and store in .*', caplog ) - with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): - refresh_data(datadir=testdatadir, timeframe='1m', pairs=['MEME/BTC'], - exchange=None) _clean_test_file(file) @@ -512,7 +507,7 @@ def test_file_dump_json_tofile(testdatadir) -> None: _clean_test_file(file) -def test_get_timeframe(default_conf, mocker, testdatadir) -> None: +def test_get_timerange(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) strategy = DefaultStrategy(default_conf) @@ -523,7 +518,7 @@ def test_get_timeframe(default_conf, mocker, testdatadir) -> None: pairs=['UNITTEST/BTC'] ) ) - min_date, max_date = get_timeframe(data) + min_date, max_date = get_timerange(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' @@ -540,7 +535,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) fill_up_missing=False ) ) - min_date, max_date = get_timeframe(data) + min_date, max_date = get_timerange(data) caplog.clear() assert validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', min_date, max_date, timeframe_to_minutes('1m')) @@ -564,7 +559,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No ) ) - min_date, max_date = get_timeframe(data) + min_date, max_date = get_timerange(data) caplog.clear() assert not validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', min_date, max_date, timeframe_to_minutes('5m')) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index b36c1b9c5..47cb9f353 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import pytest -from freqtrade.data.history import get_timeframe +from freqtrade.data.history import get_timerange from freqtrade.optimize.backtesting import Backtesting from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -380,7 +380,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: pair = "UNITTEST/BTC" # Dummy data as we mock the analyze functions data_processed = {pair: frame.copy()} - min_date, max_date = get_timeframe({pair: frame}) + min_date, max_date = get_timerange({pair: frame}) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 38a95be7a..0ea35e41f 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -16,7 +16,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.data.history import get_timeframe +from freqtrade.data.history import get_timerange from freqtrade.optimize import setup_configuration, start_backtesting from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode @@ -100,7 +100,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: data = load_data_test(contour, testdatadir) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) assert isinstance(processed, dict) results = backtesting.backtest( { @@ -138,7 +138,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC', record= patch_exchange(mocker) backtesting = Backtesting(conf) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) return { 'stake_amount': conf['stake_amount'], 'processed': processed, @@ -458,11 +458,11 @@ def test_generate_text_table_strategyn(default_conf, mocker): def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: - def get_timeframe(input1): + def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) - mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -491,11 +491,11 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: - def get_timeframe(input1): + def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame())) - mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -525,7 +525,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], timerange=timerange) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(data_processed) + min_date, max_date = get_timerange(data_processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], @@ -581,7 +581,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) - data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], timerange=timerange) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], @@ -701,7 +701,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtesting.strategy.advise_sell = _trend_alternate_hold # Override data_processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(data_processed) + min_date, max_date = get_timerange(data_processed) backtest_conf = { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c3afa4911..29b8b5b16 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -251,7 +251,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame)) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -427,7 +427,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -602,7 +602,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: MagicMock(return_value=backtest_result) ) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) ) patch_exchange(mocker) @@ -726,7 +726,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -769,7 +769,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -811,7 +811,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -851,7 +851,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -899,7 +899,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) - mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -930,7 +930,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -977,7 +977,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -1030,7 +1030,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) )